From 2f5680c547b4ee5822f527e16684c96e2543473a Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 20:00:57 +0800 Subject: [PATCH 01/30] =?UTF-8?q?fix(LibTimeSelector)=20style(LibReserve):?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E6=97=B6=E9=97=B4=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E6=96=B9=E6=B3=95=20=5FtimeToMins=20=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=BA=20=5FtimeStrToMins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 之前的实现未严格限制传入参数为整形,导致在转换时间字符串时可能出现类型错误。 - 重命名为 _timeStrToMins 以明确表示该方法仅用于时间字符串转换。并更新相关调用。 - 重命名 __selectSeatTime 中的冗长局部变量,便于理解和维护。 - 删除多余的时间格式转换嗲用 --- src/base/LibTimeSelector.py | 26 ++++++++------ src/operators/LibRenew.py | 6 ++-- src/operators/LibReserve.py | 69 ++++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/base/LibTimeSelector.py b/src/base/LibTimeSelector.py index 27234c5..b8df859 100644 --- a/src/base/LibTimeSelector.py +++ b/src/base/LibTimeSelector.py @@ -9,6 +9,8 @@ See the LICENSE file for details. """ import queue +from datetime import datetime + from base.LibOperator import LibOperator @@ -29,25 +31,33 @@ class LibTimeSelector(LibOperator): super().__init__(input_queue, output_queue) @staticmethod - def _timeToMins( + def _timeStrToMins( time_str: str ) -> int: """ Convert time string "HH:MM" to minutes since midnight. + + Example: + "10:00" -> 600 + "13:30" -> 810 """ hour, minute = map(int, time_str.split(":")) return hour*60 + minute @staticmethod - def _minsToTime( + def _minsToTimeStr( mins: int ) -> str: """ Convert minutes since midnight to time string "HH:MM". + + Example: + 600 -> "10:00" + 810 -> "13:30" """ - hour, minute = divmod(mins, 60) + hour, minute = divmod(int(mins), 60) return f"{hour:02d}:{minute:02d}" @@ -99,11 +109,11 @@ class LibTimeSelector(LibOperator): for time_opt in time_options: # Parse time value based on context if is_reserve: + # Reservation context: parse 'time' attribute time_attr = time_opt.get_attribute("time") if time_attr == "now": - from datetime import datetime now = datetime.now() - time_val = now.hour * 60 + now.minute + time_val = now.hour*60 + now.minute elif time_attr and time_attr.isdigit(): time_val = int(time_attr) else: @@ -114,9 +124,7 @@ class LibTimeSelector(LibOperator): if not (time_attr and time_attr.isdigit()): continue time_val = int(time_attr) - - free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTime(time_val)) - + free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTimeStr(time_val)) actual_diff = time_val - target_time abs_diff = abs(actual_diff) @@ -125,11 +133,9 @@ class LibTimeSelector(LibOperator): (abs_diff == best_time_diff and ((prefer_earlier and actual_diff <= 0) or (not prefer_earlier and actual_diff >= 0)))): - best_time_diff = abs_diff best_actual_diff = actual_diff best_time_opt = time_opt - if best_time_opt is not None: return (best_time_opt, best_time_opt.text.strip(), best_actual_diff, free_times) return (None, None, None, free_times) diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py index cea784c..f6cf153 100644 --- a/src/operators/LibRenew.py +++ b/src/operators/LibRenew.py @@ -91,7 +91,7 @@ class LibRenew(LibTimeSelector): renew_info = reserve_info["renew_time"] max_diff = renew_info["max_diff"] prefer_earlier = renew_info["prefer_early"] - target_renew_mins = self._timeToMins(end_time) + renew_info["expect_duration"]*60 + target_renew_mins = self._timeStrToMins(end_time) + renew_info["expect_duration"]*60 # Validate and adjust target renew time to library closing time if not self.__validateAndAdjustRenewTime(end_time, target_renew_mins): @@ -127,12 +127,12 @@ class LibRenew(LibTimeSelector): """ LIBRARY_CLOSE_TIME = 1410 # 23:30 in minutes if target_renew_mins > LIBRARY_CLOSE_TIME: - actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeToMins(end_time) + actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time) if actual_renew_duration <= 0: self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !") return False self._showTrace( - f"续约时间已调整至闭馆时间 {self._minsToTime(LIBRARY_CLOSE_TIME)}," + f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)}," f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟" ) return True diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index 224eeee..adac708 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -190,10 +190,10 @@ class LibReserve(LibTimeSelector): if reserve_info.get("end_time") is None: reserve_info["end_time"] = {} if "time" not in reserve_info["end_time"]: - end_mins = self._timeToMins(reserve_info["begin_time"]["time"]) + end_mins = self._timeStrToMins(reserve_info["begin_time"]["time"]) end_mins = end_mins + int(reserve_info["expect_duration"]*60) reserve_info["end_time"] = { - "time": self._minsToTime(end_mins), + "time": self._minsToTimeStr(end_mins), "max_diff": 30, "prefer_early": False } @@ -215,8 +215,8 @@ class LibReserve(LibTimeSelector): ): begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] - begin_mins = self._timeToMins(begin_time["time"]) - end_mins = self._timeToMins(end_time["time"]) + begin_mins = self._timeStrToMins(begin_time["time"]) + end_mins = self._timeStrToMins(end_time["time"]) # if end time is earlier than begin_time, exchange them if end_mins < begin_mins: self._showTrace( @@ -225,15 +225,15 @@ class LibReserve(LibTimeSelector): reserve_info["end_time"] = begin_time reserve_info["begin_time"] = end_time begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] - begin_mins = self._timeToMins(begin_time["time"]) - end_mins = self._timeToMins(end_time["time"]) + begin_mins = self._timeStrToMins(begin_time["time"]) + end_mins = self._timeStrToMins(end_time["time"]) # ensure the end time is not later than 23:30 - if end_mins > self._timeToMins("23:30"): + if end_mins > self._timeStrToMins("23:30"): self._showTrace( f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30" ) reserve_info["end_time"]["time"] = "23:30" - end_mins = self._timeToMins("23:30") + end_mins = self._timeStrToMins("23:30") # ensure the duration is not longer than 8 hours if reserve_info["satisfy_duration"]: if reserve_info["expect_duration"] > 8: @@ -250,7 +250,7 @@ class LibReserve(LibTimeSelector): f"{float((end_mins - begin_mins)/60)} 小时 " f"超出最大时长 8 小时, 自动设置为 8 小时" ) - reserve_info["end_time"]["time"] = self._minsToTime(begin_mins + 8*60) + reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60) return True @@ -481,6 +481,9 @@ class LibReserve(LibTimeSelector): """ Select the nearest available time option. + + Returns: + int: The actual selected time value in minutes. """ # Wait for time options to load try: @@ -515,7 +518,7 @@ class LibReserve(LibTimeSelector): ) return target_time self._showTrace( - f"无法选择最近的 {time_type} {self._minsToTime(target_time)}, " + f"无法选择最近的 {time_type} {self._minsToTimeStr(target_time)}, " f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟" ) self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") @@ -530,47 +533,49 @@ class LibReserve(LibTimeSelector): satisfy_duration: bool = True ) -> bool: - """Select seat begin and end time.""" - expect_begin_time = actual_begin_time = begin_time["time"] - expect_end_time = actual_end_time = end_time["time"] - expect_begin_mins = self._timeToMins(expect_begin_time) - actual_begin_mins = expect_begin_mins - expect_end_mins = self._timeToMins(expect_end_time) + """ + Select seat begin and end time. + """ + exp_beg_tm_str = act_beg_tm_str = begin_time["time"] + exp_end_tm_str = act_end_tm_str = end_time["time"] + exp_beg_mins = self._timeStrToMins(exp_beg_tm_str) + act_beg_mins = exp_beg_mins + exp_end_mins = self._timeStrToMins(exp_end_tm_str) + act_end_mins = exp_end_mins # Select begin time - if self.__selectNearestTime( + if act_beg_mins := self.__selectNearestTime( time_id="startTime", time_type="开始时间", - target_time=expect_begin_mins, + target_time=exp_beg_mins, max_time_diff=begin_time["max_diff"], prefer_earlier=begin_time["prefer_early"] - ) == -1: + ) and act_beg_mins == -1: return False - actual_begin_time = self._minsToTime(expect_begin_mins) - actual_begin_mins = self._timeToMins(actual_begin_time) + act_beg_tm_str = self._minsToTimeStr(act_beg_mins) # If 'satisfy_duration' is True, select end time based on actual begin time if satisfy_duration: - expect_end_mins = self.validateAndAdjustEndTime(actual_begin_mins, expct_duration) - expect_end_time = self._minsToTime(expect_end_mins) + exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expct_duration)) + exp_end_tm_str = self._minsToTimeStr(exp_end_mins) self._showTrace( f"需要满足期望预约持续时间: {expct_duration} 小时, " - f"根据开始时间 {actual_begin_time} 计算结束时间: {expect_end_time}" + f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}" ) # Select end time - if self.__selectNearestTime( + if act_end_mins := self.__selectNearestTime( time_id="endTime", time_type="结束时间", - target_time=expect_end_mins, + target_time=exp_end_mins, max_time_diff=end_time["max_diff"], prefer_earlier=end_time["prefer_early"] - ) == -1: + ) and act_end_mins == -1: return False - actual_end_time = self._minsToTime(expect_end_mins) + act_end_tm_str = self._minsToTimeStr(act_end_mins) self._showTrace( - f"期望预约时间段: {expect_begin_time} - {expect_end_time}, " - f"实际预约时间段: {actual_begin_time} - {actual_end_time}" + f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, " + f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}" ) return True @@ -584,8 +589,8 @@ class LibReserve(LibTimeSelector): """ Validate and adjust reserve end time to library closing time if needed. """ - LIBRARY_CLOSE_TIME = self._timeToMins("23:30") - expect_end_mins = begin_mins + duration * 60 + LIBRARY_CLOSE_TIME = self._timeStrToMins("23:30") + expect_end_mins = int(begin_mins + duration*60) if expect_end_mins > LIBRARY_CLOSE_TIME: expect_end_mins = LIBRARY_CLOSE_TIME self._showTrace( From c03eed1d5114f80bc37924372b52b32f23d005f0 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 20:42:31 +0800 Subject: [PATCH 02/30] =?UTF-8?q?fix(LibReserve):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BD=BF=E7=94=A8=E7=9A=84=E6=B5=B7=E8=B1=A1?= =?UTF-8?q?=E8=BF=90=E7=AE=97=E7=AC=A6=E6=9D=A1=E4=BB=B6=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/operators/LibReserve.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index adac708..8734f48 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -544,13 +544,14 @@ class LibReserve(LibTimeSelector): act_end_mins = exp_end_mins # Select begin time - if act_beg_mins := self.__selectNearestTime( + act_beg_mins = self.__selectNearestTime( time_id="startTime", time_type="开始时间", target_time=exp_beg_mins, max_time_diff=begin_time["max_diff"], prefer_earlier=begin_time["prefer_early"] - ) and act_beg_mins == -1: + ) + if act_beg_mins == -1: return False act_beg_tm_str = self._minsToTimeStr(act_beg_mins) @@ -564,13 +565,14 @@ class LibReserve(LibTimeSelector): ) # Select end time - if act_end_mins := self.__selectNearestTime( + act_end_mins = self.__selectNearestTime( time_id="endTime", time_type="结束时间", target_time=exp_end_mins, max_time_diff=end_time["max_diff"], prefer_earlier=end_time["prefer_early"] - ) and act_end_mins == -1: + ) + if act_end_mins == -1: return False act_end_tm_str = self._minsToTimeStr(act_end_mins) self._showTrace( From faa26b489a3b69f7e2c16b589a590758573b9317 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 20:42:42 +0800 Subject: [PATCH 03/30] =?UTF-8?q?fix(LibReserve):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=86=97=E4=BD=99=E7=9A=84=E9=93=BE=E5=BC=8F=E8=B5=8B=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/operators/LibReserve.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index 8734f48..4755d21 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -536,8 +536,11 @@ class LibReserve(LibTimeSelector): """ Select seat begin and end time. """ - exp_beg_tm_str = act_beg_tm_str = begin_time["time"] - exp_end_tm_str = act_end_tm_str = end_time["time"] + exp_beg_tm_str = begin_time["time"] + exp_end_tm_str = end_time["time"] + # Initialize actual time strings for logging + act_beg_tm_str = exp_beg_tm_str + act_end_tm_str = exp_end_tm_str exp_beg_mins = self._timeStrToMins(exp_beg_tm_str) act_beg_mins = exp_beg_mins exp_end_mins = self._timeStrToMins(exp_end_tm_str) From 50ebeb0fab49d583c80e533bf91e291683e5b795 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 20:43:00 +0800 Subject: [PATCH 04/30] =?UTF-8?q?style(LibReserve):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20=5F=5FselectSeatTime=20=E5=8F=82=E6=95=B0=E7=9A=84=E6=8B=BC?= =?UTF-8?q?=E5=86=99=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - expct_duration -> expect_duration --- src/operators/LibReserve.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index 4755d21..f33c1e1 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -529,7 +529,7 @@ class LibReserve(LibTimeSelector): self, begin_time: dict, end_time: dict, - expct_duration: int = 4, + expect_duration: int = 4, satisfy_duration: bool = True ) -> bool: @@ -560,10 +560,10 @@ class LibReserve(LibTimeSelector): # If 'satisfy_duration' is True, select end time based on actual begin time if satisfy_duration: - exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expct_duration)) + exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expect_duration)) exp_end_tm_str = self._minsToTimeStr(exp_end_mins) self._showTrace( - f"需要满足期望预约持续时间: {expct_duration} 小时, " + f"需要满足期望预约持续时间: {expect_duration} 小时, " f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}" ) @@ -645,7 +645,7 @@ class LibReserve(LibTimeSelector): elif not self.__selectSeatTime( begin_time=reserve_info["begin_time"], end_time=reserve_info["end_time"], - expct_duration=reserve_info["expect_duration"], + expect_duration=reserve_info["expect_duration"], satisfy_duration=reserve_info["satisfy_duration"] ): pass From 1d99ca92f223a8115f23dc296fb254f549c5af1e Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 20:46:00 +0800 Subject: [PATCH 05/30] =?UTF-8?q?fix(LibReserve):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E6=AF=94=E8=BE=83=E9=80=BB=E8=BE=91=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=B9=B6=E4=BC=98=E5=8C=96=E6=97=B6=E9=97=B4=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复使用字符串直接比较日期导致的逻辑错误,改用时间戳比较 - 优化时间验证逻辑,支持 satisfy_duration 模式下的开始晚于结束时间时的交换时间处理 - 添加必要的注释说明 place 参数检查的跳过原因和边界情况处理 - 重构变量命名,提高代码可读性(cur_date -> cur_date_str) - 修正字符串引号风格,统一使用单引号 --- src/operators/LibReserve.py | 39 ++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index f33c1e1..45749e0 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -107,6 +107,8 @@ class LibReserve(LibTimeSelector): try: # must contain the required infomation + # key 'place' is no need to check + # because 'place' is only has one possible value '1' or '图书馆' if reserve_info.get("floor") is None: # if existence ? raise ValueError("未指定楼层") if reserve_info["floor"] not in self.__floor_map: # if in the mao ? @@ -133,17 +135,19 @@ class LibReserve(LibTimeSelector): reserve_info: dict ) -> bool: - cur_date = time.strftime("%Y-%m-%d", time.localtime()) + cur_date_str = time.strftime("%Y-%m-%d", time.localtime()) + cur_timestamp = time.mktime(time.strptime(cur_date_str, "%Y-%m-%d")) if reserve_info.get("date") is None: - reserve_info["date"] = cur_date - self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}") + reserve_info["date"] = cur_date_str + self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date_str}") else: - if reserve_info["date"] < cur_date: + res_timestamp = time.mktime(time.strptime(reserve_info["date"], "%Y-%m-%d")) + if res_timestamp < cur_timestamp: self._showTrace( f"预约日期错误 ! :"\ - f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期" + f"{reserve_info['date']} 早于当前日期 {cur_date_str}, 自动设置为当前日期" ) - reserve_info["date"] = cur_date + reserve_info["date"] = cur_date_str return True @@ -190,6 +194,9 @@ class LibReserve(LibTimeSelector): if reserve_info.get("end_time") is None: reserve_info["end_time"] = {} if "time" not in reserve_info["end_time"]: + # here we add the expect duration to the begin time first, + # the edge case that the end time is later than 23:30 will + # be handled in __finalCheck. so no need to concern about it. end_mins = self._timeStrToMins(reserve_info["begin_time"]["time"]) end_mins = end_mins + int(reserve_info["expect_duration"]*60) reserve_info["end_time"] = { @@ -217,23 +224,27 @@ class LibReserve(LibTimeSelector): begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] begin_mins = self._timeStrToMins(begin_time["time"]) end_mins = self._timeStrToMins(end_time["time"]) + # if end time is earlier than begin_time, exchange them - if end_mins < begin_mins: + # except that the user has set the satisfy_duration to True + if end_mins < begin_mins and reserve_info["satisfy_duration"] is False: self._showTrace( f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间" ) - reserve_info["end_time"] = begin_time - reserve_info["begin_time"] = end_time - begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] + reserve_info["end_time"], reserve_info["begin_time"] = begin_time, end_time + begin_time, end_time = end_time, begin_time begin_mins = self._timeStrToMins(begin_time["time"]) end_mins = self._timeStrToMins(end_time["time"]) + # ensure the end time is not later than 23:30 - if end_mins > self._timeStrToMins("23:30"): + max_end_mins = self._timeStrToMins("23:30") + if end_mins > max_end_mins: self._showTrace( f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30" ) reserve_info["end_time"]["time"] = "23:30" - end_mins = self._timeStrToMins("23:30") + end_mins = max_end_mins + # ensure the duration is not longer than 8 hours if reserve_info["satisfy_duration"]: if reserve_info["expect_duration"] > 8: @@ -274,8 +285,8 @@ class LibReserve(LibTimeSelector): self._showTrace( f"预约信息检查完成, 准备预约 " f"{reserve_info['date']} " - f"{reserve_info['begin_time']["time"]} - " - f"{reserve_info['end_time']["time"]} " + f"{reserve_info['begin_time']['time']} - " + f"{reserve_info['end_time']['time']} " f"图书馆 " f"{self.__floor_map[reserve_info['floor']]} " f"{self.__room_map[reserve_info['room']]} " From c26f19b6b318c8f25757bab6f231a8eac0f7dfd8 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 17 Mar 2026 21:37:24 +0800 Subject: [PATCH 06/30] =?UTF-8?q?feat(LogManager):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=8C=81=E4=B9=85=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 LogManager 单例类,支持日志文件按日期滚动 - 创建 CallerInfoFormatter 自定义格式化器,提取真实调用位置 - 为 MsgBase._showTrace 方法添加日志级别参数,集成日志系统 - 新增 initializeLogManager 初始化函数,日志存储于 AppDataLocation/logs/ - 日志输出格式对齐:[时间] - [类名(15)|级别(8)] - [文件:行号(20:4)] - 消息 - 控制台/INFO级别,全量日志 / DEBUG 级别,错误日志 / ERROR级别 - 全量日志保留7天,错误日志保留14天 --- src/Main.py | 14 ++- src/base/MsgBase.py | 12 ++- src/utils/LogManager.py | 191 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 src/utils/LogManager.py diff --git a/src/Main.py b/src/Main.py index 47026b1..7fdc459 100644 --- a/src/Main.py +++ b/src/Main.py @@ -16,7 +16,8 @@ from PySide6.QtWidgets import QApplication from gui.ALMainWindow import ALMainWindow from gui.resources import ALResource -from utils.ConfigManager import instance +from utils.ConfigManager import instance as configInstance +from utils.LogManager import instance as logInstance def initializeConfigManager(): @@ -25,7 +26,15 @@ def initializeConfigManager(): config_dir = os.path.join(app_dir, "config") if not QDir(config_dir).exists(): QDir().mkpath(config_dir) - instance(config_dir) + configInstance(config_dir) + +def initializeLogManager(): + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + log_dir = os.path.join(app_dir, "logs") + if not QDir(log_dir).exists(): + QDir().mkpath(log_dir) + logInstance(log_dir) def main(): @@ -36,6 +45,7 @@ def main(): app.setStyle('Fusion') app.setApplicationName("AutoLibrary") initializeConfigManager() + initializeLogManager() window = ALMainWindow() window.show() sys.exit(app.exec_()) diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py index bd4f07b..44150ee 100644 --- a/src/base/MsgBase.py +++ b/src/base/MsgBase.py @@ -7,9 +7,12 @@ This software is provided "as is", without any warranty of any kind. You may use, modify, and distribute this file under the terms of the MIT License. See the LICENSE file for details. """ +import logging import queue import datetime +from utils.LogManager import getLogger + class MsgBase: """ @@ -38,6 +41,10 @@ class MsgBase: self._class_name = self.__class__.__name__ self._input_queue = input_queue self._output_queue = output_queue + try: + self._logger = getLogger(self._class_name) + except RuntimeError: + self._logger = None def _showMsg( @@ -50,11 +57,14 @@ class MsgBase: def _showTrace( self, - msg: str + msg: str, + level: int = logging.INFO ): timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}") + if self._logger: + self._logger.log(level, msg) def _waitMsg( diff --git a/src/utils/LogManager.py b/src/utils/LogManager.py new file mode 100644 index 0000000..8af4ae9 --- /dev/null +++ b/src/utils/LogManager.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import logging +import os +import threading + +from logging.handlers import TimedRotatingFileHandler +from typing import Optional + + +class CallerInfoFormatter(logging.Formatter): + """ + Custom formatter to extract real caller information. + Skips MsgBase._showTrace to show the actual calling location. + + Format: + - Logger name: left-aligned, max 15 chars + - Level name: left-aligned, max 8 chars + - Filename: left-aligned, max 20 chars + - Line number: left-aligned, max 4 digits + """ + + def __init__( + self, + fmt=None, + datefmt=None, + style='%' + ): + + super().__init__(fmt, datefmt, style) + self.basefmt = fmt + + def format( + self, + record + ): + + depth = 0 + while depth < 10: + record.filename = os.path.basename(record.pathname) + if 'MsgBase.py' not in record.filename and record.funcName != '_showTrace': + break + if not hasattr(record, 'stack'): + record.stack = True + import traceback + record.stack_list = traceback.extract_stack() + depth += 1 + if depth < len(record.stack_list): + frame = record.stack_list[-depth-1] + record.filename = os.path.basename(frame.filename) + record.lineno = frame.lineno + record.funcName = frame.name + record.name = record.name[-15:].ljust(15) + record.levelname = record.levelname.ljust(8) + record.filename = record.filename[-20:].ljust(20) + record.lineno = f"{record.lineno:04d}" + + return super().format(record) + + +class LogManager: + """ + Log Manager Singleton Class + + Args: + log_dir (str): The directory to store log files. + """ + + def __init__( + self, + log_dir: str + ): + + self.__log_dir = os.path.abspath(log_dir) + self.__logger = None + self.__initialized = False + + self.initialize() + + + def initialize( + self + ): + + if self.__initialized: + return + os.makedirs(self.__log_dir, exist_ok=True) + self.__logger = logging.getLogger("AutoLibrary") + self.__logger.setLevel(logging.DEBUG) + self.__logger.handlers.clear() + + formatter = CallerInfoFormatter( + '[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) + self.__logger.addHandler(console_handler) + + all_log_file = os.path.join(self.__log_dir, "all.log") + file_handler_all = TimedRotatingFileHandler( + all_log_file, + when='midnight', + interval=1, + backupCount=7, + encoding='utf-8' + ) + file_handler_all.suffix = "%Y-%m-%d.log" + file_handler_all.setLevel(logging.DEBUG) + file_handler_all.setFormatter(formatter) + self.__logger.addHandler(file_handler_all) + + error_log_file = os.path.join(self.__log_dir, "error.log") + file_handler_error = TimedRotatingFileHandler( + error_log_file, + when='midnight', + interval=1, + backupCount=14, + encoding='utf-8' + ) + file_handler_error.suffix = "%Y-%m-%d.log" + file_handler_error.setLevel(logging.ERROR) + file_handler_error.setFormatter(formatter) + self.__logger.addHandler(file_handler_error) + + self.__initialized = True + + + def getLogger( + self, + name: Optional[str] = None + ) -> logging.Logger: + + if name: + return self.__logger.getChild(name) + return self.__logger + + + def setLevel( + self, + level: int + ): + + if self.__logger: + self.__logger.setLevel(level) + + + def logDir( + self + ) -> str: + + return self.__log_dir + + +# LogManager singleton instance. +_log_manager_instance = None + +# Singleton instance lock. +_instance_lock = threading.Lock() +def instance( + log_dir: str = "" +) -> LogManager: + + global _log_manager_instance + with _instance_lock: + if _log_manager_instance is None: + if not log_dir: + raise ValueError("LogManager initialization requires log_dir parameter") + _log_manager_instance = LogManager(log_dir) + else: + if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir): + raise ValueError("LogManager instance already initialized with a different log directory") + return _log_manager_instance + + +def getLogger( + name: Optional[str] = None +) -> logging.Logger: + + if _log_manager_instance is None: + raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first") + return _log_manager_instance.getLogger(name) From 824b9b886972701878ed82067e7331438db42dc0 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 10:14:27 +0800 Subject: [PATCH 07/30] =?UTF-8?q?fix(ALMainWindow):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20ALMainWindow=20=E7=9A=84=E9=85=8D=E7=BD=AE=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 先前的实现并未考虑到配置窗口更改时的同步问题,本次提交在 每次配置窗口更改并关闭保存时,同步更新 ALMainWindow 中的配置路径 --- src/gui/ALMainWindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index b0fb9d2..218a518 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -298,6 +298,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed) self.__alConfigWidget.deleteLater() self.__alConfigWidget = None + self.__config_paths = ConfigManager.getValidateAutomationConfigPaths() self.setControlButtons(True, None, None) @Slot(dict) From 2d0782c368c2a0692559947bf32574d49c226589 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 10:17:09 +0800 Subject: [PATCH 08/30] =?UTF-8?q?refactor(AppInitializer):=20=E5=B0=86?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=80=BB=E8=BE=91=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=88=B0=20AppInitializer=20=E6=A8=A1=E5=9D=97=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 本次提交将 Main.py 中的 ConfigManager, LogManager 等初始化逻辑提取到 AppInitializer 模块中 - 更改默认的配置文件路径从 config 目录变为 configs 目录,并考虑兼容性问题 --- src/Main.py | 26 +++---------------- src/utils/AppInitializer.py | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 src/utils/AppInitializer.py diff --git a/src/Main.py b/src/Main.py index 7fdc459..5795315 100644 --- a/src/Main.py +++ b/src/Main.py @@ -10,32 +10,15 @@ See the LICENSE file for details. import os import sys -from PySide6.QtCore import QTranslator, QStandardPaths, QDir +from PySide6.QtCore import QTranslator from PySide6.QtWidgets import QApplication from gui.ALMainWindow import ALMainWindow from gui.resources import ALResource -from utils.ConfigManager import instance as configInstance -from utils.LogManager import instance as logInstance +from utils.AppInitializer import initializeApp -def initializeConfigManager(): - - app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) - config_dir = os.path.join(app_dir, "config") - if not QDir(config_dir).exists(): - QDir().mkpath(config_dir) - configInstance(config_dir) - -def initializeLogManager(): - - app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) - log_dir = os.path.join(app_dir, "logs") - if not QDir(log_dir).exists(): - QDir().mkpath(log_dir) - logInstance(log_dir) - def main(): app = QApplication(sys.argv) @@ -44,13 +27,12 @@ def main(): app.installTranslator(translator) app.setStyle('Fusion') app.setApplicationName("AutoLibrary") - initializeConfigManager() - initializeLogManager() + if not initializeApp(): + sys.exit(-1) window = ALMainWindow() window.show() sys.exit(app.exec_()) - if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/utils/AppInitializer.py b/src/utils/AppInitializer.py new file mode 100644 index 0000000..7e717f0 --- /dev/null +++ b/src/utils/AppInitializer.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2025 - 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import os + +from PySide6.QtCore import QStandardPaths, QDir + +from utils.ConfigManager import instance as configInstance +from utils.LogManager import instance as logInstance + + +def initializeConfigManager( +) -> bool: + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + old_config_dir = os.path.join(app_dir, "config") + new_config_dir = os.path.join(app_dir, "configs") + if QDir(old_config_dir).exists(): # old config dir exists + #we rename it to compatible with new version + if not QDir().rename(old_config_dir, new_config_dir): + return False + elif not QDir(new_config_dir).exists(): + if not QDir().mkpath(new_config_dir): + return False + configInstance(new_config_dir) + return True + +def initializeLogManager( +) -> bool: + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + log_dir = os.path.join(app_dir, "logs") + if not QDir(log_dir).exists(): + if not QDir().mkpath(log_dir): + return False + logInstance(log_dir) + return True + +def initializeApp( +) -> bool: + + if not initializeConfigManager(): + return False + if not initializeLogManager(): + return False + return True From 160d6a24289177c621bf3a6f426bb6b9c8da2357 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 11:02:52 +0800 Subject: [PATCH 09/30] =?UTF-8?q?refactor(operators):=20=E4=B8=BA=20=5Fsho?= =?UTF-8?q?wTrace=20=E6=96=B9=E6=B3=95=E6=B7=BB=E5=8A=A0=E5=90=88=E9=80=82?= =?UTF-8?q?=E7=9A=84=20TraceLevel=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Main.py | 1 - src/base/MsgBase.py | 22 +++++++++++++++++ src/gui/ALConfigWidget.py | 1 - src/gui/ALMainWindow.py | 2 +- src/operators/AutoLib.py | 27 ++++++++++++-------- src/operators/LibChecker.py | 10 ++++---- src/operators/LibCheckin.py | 12 ++++----- src/operators/LibLogin.py | 26 +++++++++++++------- src/operators/LibLogout.py | 4 +-- src/operators/LibRenew.py | 21 ++++++++-------- src/operators/LibReserve.py | 49 +++++++++++++++++++++---------------- 11 files changed, 109 insertions(+), 66 deletions(-) diff --git a/src/Main.py b/src/Main.py index 5795315..f1468d8 100644 --- a/src/Main.py +++ b/src/Main.py @@ -7,7 +7,6 @@ This software is provided "as is", without any warranty of any kind. You may use, modify, and distribute this file under the terms of the MIT License. See the LICENSE file for details. """ -import os import sys from PySide6.QtCore import QTranslator diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py index 44150ee..1df0494 100644 --- a/src/base/MsgBase.py +++ b/src/base/MsgBase.py @@ -32,6 +32,18 @@ class MsgBase: implement queue polling to retrieve and process messages. """ + class TraceLevel: + """ + Enum class for trace levels. + + This class provides the trace levels for the logger. + """ + DEBUG = logging.DEBUG + INFO = logging.INFO + WARNING = logging.WARNING + ERROR = logging.ERROR + CRITICAL = logging.CRITICAL + def __init__( self, input_queue: queue.Queue, @@ -67,6 +79,16 @@ class MsgBase: self._logger.log(level, msg) + def _showLog( + self, + msg: str, + level: int = logging.INFO + ): + + if self._logger: + self._logger.log(level, msg) + + def _waitMsg( self, timeout: float = 1.0 diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index 976636a..0e4c8f9 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -8,7 +8,6 @@ You may use, modify, and distribute this file under the terms of the MIT License See the LICENSE file for details. """ import os -import sys from PySide6.QtCore import ( Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 218a518..f033382 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -113,7 +113,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): ): if not QSystemTrayIcon.isSystemTrayAvailable(): - self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标") + self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING) return self.TrayIcon = QSystemTrayIcon(self.icon, self) self.TrayIcon.setToolTip("AutoLibrary") diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py index 651aa09..a1b8ecf 100644 --- a/src/operators/AutoLib.py +++ b/src/operators/AutoLib.py @@ -66,11 +66,14 @@ class AutoLib(MsgBase): case "firefox": driver_options = webdriver.FirefoxOptions() case _: - self._showTrace(f"不支持的浏览器驱动类型: {self.__driver_type} !") + self._showTrace( + f"不支持的浏览器驱动类型: {self.__driver_type} !", + self.TraceLevel.WARNING + ) return False if not web_driver_config: - self._showTrace("未配置浏览器驱动参数 !") + self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR) return False if web_driver_config.get("headless"): driver_options.add_argument("--headless") @@ -110,7 +113,7 @@ class AutoLib(MsgBase): # init browser driver self.__driver_path = web_driver_config.get("driver_path") if not self.__driver_path: - self._showTrace("未配置浏览器驱动路径 !") + self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING) return False self.__driver_path = os.path.abspath(self.__driver_path) try: @@ -128,13 +131,13 @@ class AutoLib(MsgBase): self.__driver = webdriver.Firefox(service=service, options=driver_options) case _: # actually will not happen, beacuse we have checked it at the initlization # of 'driver_options' - raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}") + raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type} !") self.__driver.implicitly_wait(1) self.__driver.execute_script( "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" ) except Exception as e: - self._showTrace(f"浏览器驱动初始化失败: {e}") + self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR) return False self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") return True @@ -145,7 +148,7 @@ class AutoLib(MsgBase): ): if not self.__driver: - self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !") + self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !", self.TraceLevel.WARNING) return self.__lib_checker = LibChecker(self._input_queue, self._output_queue, self.__driver) self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver) @@ -178,7 +181,7 @@ class AutoLib(MsgBase): ) return True except: - self._showTrace(f"登录页面加载失败 !") + self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR) return False @@ -188,7 +191,7 @@ class AutoLib(MsgBase): lib_config = self.__run_config.get("library", None) if not lib_config: - self._showTrace("未配置图书馆参数 !") + self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR) return False url = lib_config.get("host_url") + lib_config.get("login_url") self.__driver.set_page_load_timeout(5) @@ -196,7 +199,9 @@ class AutoLib(MsgBase): self.__driver.get(url) except TimeoutException: self.__driver.execute_script("window.stop();") - self._showTrace(f"图书馆登录页面加载超时 ! 请检查网络环境是否正常") + self._showTrace( + f"图书馆登录页面加载超时 ! 请检查网络环境是否正常", self.TraceLevel.ERROR + ) return False if not self.__waitResponseLoad(): return False @@ -256,6 +261,7 @@ class AutoLib(MsgBase): if can_renew: if self.__lib_renew.renew(username, record, reserve_info): if self.__lib_checker.postRenewCheck(record): + self._showTrace(f"用户 {username} 续约成功 !") result = 0 else: result = 1 @@ -303,7 +309,8 @@ class AutoLib(MsgBase): ) if r == -1: self._showTrace( - f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !" + f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !", + self.TraceLevel.WARNING ) break elif r == 0: diff --git a/src/operators/LibChecker.py b/src/operators/LibChecker.py index 6560efd..0b72f0d 100644 --- a/src/operators/LibChecker.py +++ b/src/operators/LibChecker.py @@ -63,7 +63,7 @@ class LibChecker(LibOperator): EC.presence_of_element_located((By.CLASS_NAME, "myReserveList")) ) except: - self._showTrace("加载预约记录页面失败 !") + self._showTrace("加载预约记录页面失败 !", self.TraceLevel.ERROR) return False return True @@ -174,7 +174,7 @@ class LibChecker(LibOperator): ) return reservations except: - self._showTrace("加载预约记录失败 !") + self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR) return None @@ -197,10 +197,10 @@ class LibChecker(LibOperator): self.__driver.execute_script("arguments[0].click();", more_btn) return True else: - self._showTrace("用户无法加载更多预约记录") + self._showTrace("用户无法加载更多预约记录", self.TraceLevel.WARNING) return False except: - self._showTrace("加载更多预约记录失败 !") + self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR) return False @@ -211,7 +211,7 @@ class LibChecker(LibOperator): ) -> dict: if wanted_date is None: - self._showTrace("日期未指定, 无法检查当前预约状态") + self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING) return None self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......") diff --git a/src/operators/LibCheckin.py b/src/operators/LibCheckin.py index 37cd23c..568c901 100644 --- a/src/operators/LibCheckin.py +++ b/src/operators/LibCheckin.py @@ -51,7 +51,7 @@ class LibCheckin(LibOperator): ) ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK") except: - self._showTrace("签到时发生未知错误 !") + self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) return False result_message = result_message_element.text if "签到成功" in result_message: @@ -109,7 +109,7 @@ class LibCheckin(LibOperator): if result: self._showTrace("签到按钮已启用") else: - self._showTrace("签到按钮启用失败") + self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING) return result @@ -119,24 +119,24 @@ class LibCheckin(LibOperator): ) -> bool: if self.__driver is None: - self._showTrace("未提供有效 WebDriver 实例 !") + self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING) return False try: checkin_btn = WebDriverWait(self.__driver, 2).until( EC.element_to_be_clickable((By.ID, "btnCheckIn")) ) except: - self._showTrace(f"用户 {username} 签到界面加载失败 !") + self._showTrace(f"用户 {username} 签到界面加载失败 !", self.TraceLevel.ERROR) return False if "disabled" in checkin_btn.get_attribute("class"): self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......") if not self.__enableCheckinBtn(): - self._showTrace(f"签到按钮启用失败 !") + self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR) return False checkin_btn.click() if self._waitResponseLoad(): self._showTrace(f"用户 {username} 签到成功 !") return True else: - self._showTrace(f"用户 {username} 签到失败 !") + self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR) return False diff --git a/src/operators/LibLogin.py b/src/operators/LibLogin.py index 31cf2de..3a186c4 100644 --- a/src/operators/LibLogin.py +++ b/src/operators/LibLogin.py @@ -52,7 +52,10 @@ class LibLogin(LibOperator): ) return True except: - self._showTrace(f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准") + self._showTrace( + f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准", + self.TraceLevel.ERROR + ) return False @@ -71,7 +74,7 @@ class LibLogin(LibOperator): password_element.clear() password_element.send_keys(password) except Exception as e: - self._showTrace(f"用户名或密码填写失败 ! : {e}") + self._showTrace(f"用户名或密码填写失败 ! : {e}", self.TraceLevel.ERROR) return False return True @@ -90,10 +93,11 @@ class LibLogin(LibOperator): captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower() self._showTrace(f"识别到验证码为 : '{captcha_text}'") if len(captcha_text) != 4: + self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) raise Exception("识别到的验证码长度不等于 4 个字符 !") return captcha_text except Exception as e: - self._showTrace(f"验证码识别失败 ! : {e}") + self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) return "" @@ -107,10 +111,11 @@ class LibLogin(LibOperator): captcha_text = self._waitMsg(timeout=15) self._showTrace(f"输入的验证码为 : '{captcha_text}'") if len(captcha_text) != 4: + self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) raise Exception("输入的验证码长度不等于 4 个字符 !") return captcha_text except Exception as e: - self._showTrace(f"输入验证码失败 ! : {e}") + self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) return "" @@ -126,7 +131,7 @@ class LibLogin(LibOperator): ).click() return True except Exception as e: - self._showTrace(f"刷新验证码失败 ! : {e}") + self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR) return False @@ -147,7 +152,10 @@ class LibLogin(LibOperator): else: if not self.__refreshCaptcha(): return "" - self._showTrace(f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !") + self._showTrace( + f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !", + self.TraceLevel.WARNING + ) return "" @@ -162,7 +170,7 @@ class LibLogin(LibOperator): captcha_element.send_keys(captcha_text) return True except Exception as e: - self._showTrace(f"验证码填写失败 ! : {e}") + self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR) return False @@ -175,7 +183,7 @@ class LibLogin(LibOperator): ) -> bool: if self.__driver is None: - self._showTrace("未提供有效 WebDriver 实例 !") + self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING) return False # begin login process for attempt in range(max_attempts): @@ -203,5 +211,5 @@ class LibLogin(LibOperator): self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !") return True else: - self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !") + self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !",self.TraceLevel.WARNING) return False diff --git a/src/operators/LibLogout.py b/src/operators/LibLogout.py index c907835..72edf5d 100644 --- a/src/operators/LibLogout.py +++ b/src/operators/LibLogout.py @@ -42,7 +42,7 @@ class LibLogout(LibOperator): ) -> bool: if self.__driver is None: - self._showTrace("未提供有效 WebDriver 实例 !") + self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING) return False try: self.__driver.find_element( @@ -51,5 +51,5 @@ class LibLogout(LibOperator): self._showTrace(f"用户 {username} 注销成功 !") return True except Exception as e: - self._showTrace(f"用户 {username} 注销失败 ! : {e}") + self._showTrace(f"用户 {username} 注销失败 ! : {e}", self.TraceLevel.ERROR) return False diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py index f6cf153..7c42370 100644 --- a/src/operators/LibRenew.py +++ b/src/operators/LibRenew.py @@ -54,7 +54,7 @@ class LibRenew(LibTimeSelector): EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage")) ) except: - self._showTrace("续约时间选择界面加载失败 !") + self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR) return False head_message = head_message.text.strip() if "警告" in head_message: @@ -73,7 +73,7 @@ class LibRenew(LibTimeSelector): EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK")) ) except: - self._showTrace("续约时间选择界面加载失败 !") + self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR) return False return True @@ -99,7 +99,7 @@ class LibRenew(LibTimeSelector): renew_ok_btn = self.__driver.find_element(By.CSS_SELECTOR, "#extendDiv .btnOK") renew_time_opts = self.__driver.find_elements(By.CSS_SELECTOR, "#extendDiv .renewal_List li") if not renew_time_opts: - self._showTrace("当前未查询到可用续约时间 !") + self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING) return False # Find best renewal time option @@ -110,7 +110,8 @@ class LibRenew(LibTimeSelector): return self.__confirmRenewal(best_opt, best_text, actual_diff, record, renew_ok_btn) self._showTrace( "无法选择最近的可用续约时间 ! " - f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !" + f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", + self.TraceLevel.WARNING ) self._showTrace(f"当前可供续约的时间有: {free_times}") return False @@ -129,7 +130,7 @@ class LibRenew(LibTimeSelector): if target_renew_mins > LIBRARY_CLOSE_TIME: actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time) if actual_renew_duration <= 0: - self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !") + self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR) return False self._showTrace( f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)}," @@ -163,7 +164,7 @@ class LibRenew(LibTimeSelector): ok_btn.click() return True except: - self._showTrace("确认续约时发生错误 !") + self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR) return False @@ -175,28 +176,28 @@ class LibRenew(LibTimeSelector): ) -> bool: if self.__driver is None: - self._showTrace("未提供有效 WebDriver 实例 !") + self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING) return False try: renew_btn = WebDriverWait(self.__driver, 2).until( EC.element_to_be_clickable((By.ID, "btnExtend")) ) except: - self._showTrace(f"用户 {username} 续约界面加载失败 !") + self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR) return False if "disabled" in renew_btn.get_attribute("class"): self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试") return False renew_btn.click() if not self.__waitRenewDialog(): - self._showTrace(f"用户 {username} 续约失败 !") + self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR) # After the renewal, the webpage will display a mask overlay, # so we need to refresh the page for subsequent operations. self.__driver.refresh() return False if not self.__selectNearestTime(record, reserve_info): - self._showTrace(f"用户 {username} 续约失败 !") + self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR) self.__driver.refresh() return False if self._waitResponseLoad(): diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index 45749e0..91e23bc 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -72,13 +72,13 @@ class LibReserve(LibTimeSelector): By.CSS_SELECTOR, ".layoutSeat dd" ) if not content_elements: - self._showTrace("未找到预约结果") + self._showTrace("未找到预约结果", self.TraceLevel.WARNING) raise title = title_elements[0].text if title_elements else "" contents = [element.text for element in content_elements if element.text.strip()] for message in contents: if "预约失败" in message or "已有1个有效预约" in message: - self._showTrace(f"预约失败 - {"".join(contents)}") + self._showTrace(f"预约失败 - {"".join(contents)}", self.TraceLevel.ERROR) raise if "预定好了" in title or "预约成功" in title or "操作成功" in title: if len(contents) >= 6: @@ -96,7 +96,7 @@ class LibReserve(LibTimeSelector): ) return True except: - self._showTrace(f"预约结果加载失败 !") + self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR) return False @@ -125,7 +125,8 @@ class LibReserve(LibTimeSelector): except ValueError as e: self._showTrace( f"预约信息错误 ! : {e}, "\ - f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整" + f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整", + self.TraceLevel.ERROR ) return False @@ -145,7 +146,8 @@ class LibReserve(LibTimeSelector): if res_timestamp < cur_timestamp: self._showTrace( f"预约日期错误 ! :"\ - f"{reserve_info['date']} 早于当前日期 {cur_date_str}, 自动设置为当前日期" + f"{reserve_info['date']} 早于当前日期 {cur_date_str}, 自动设置为当前日期", + self.TraceLevel.WARNING ) reserve_info["date"] = cur_date_str return True @@ -229,7 +231,8 @@ class LibReserve(LibTimeSelector): # except that the user has set the satisfy_duration to True if end_mins < begin_mins and reserve_info["satisfy_duration"] is False: self._showTrace( - f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间" + f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间", + self.TraceLevel.WARNING ) reserve_info["end_time"], reserve_info["begin_time"] = begin_time, end_time begin_time, end_time = end_time, begin_time @@ -240,7 +243,8 @@ class LibReserve(LibTimeSelector): max_end_mins = self._timeStrToMins("23:30") if end_mins > max_end_mins: self._showTrace( - f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30" + f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30", + self.TraceLevel.WARNING ) reserve_info["end_time"]["time"] = "23:30" end_mins = max_end_mins @@ -251,7 +255,8 @@ class LibReserve(LibTimeSelector): self._showTrace( f"该用户设置了优先满足时长要求, 但是预约期望持续时间 " f"{reserve_info['expect_duration']} 小时 " - f"超出最大时长 8 小时, 自动设置为 8 小时" + f"超出最大时长 8 小时, 自动设置为 8 小时", + self.TraceLevel.WARNING ) reserve_info["expect_duration"] = 8 else: @@ -259,7 +264,8 @@ class LibReserve(LibTimeSelector): self._showTrace( f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 " f"{float((end_mins - begin_mins)/60)} 小时 " - f"超出最大时长 8 小时, 自动设置为 8 小时" + f"超出最大时长 8 小时, 自动设置为 8 小时", + self.TraceLevel.WARNING ) reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60) return True @@ -429,7 +435,7 @@ class LibReserve(LibTimeSelector): EC.element_to_be_clickable((By.ID, "findRoom")) ).click() except: - self._showTrace("加载房间/区域失败 !") + self._showTrace("加载房间/区域失败 !", self.TraceLevel.ERROR) return False # select room try: @@ -439,7 +445,7 @@ class LibReserve(LibTimeSelector): self._showTrace(f"房间 {display_room} 选择成功 !") return True except: - self._showTrace(f"选择房间失败 ! : {display_room} 不可用") + self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) return False @@ -457,7 +463,7 @@ class LibReserve(LibTimeSelector): EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']")) ) except: - self._showTrace(f"座位加载失败 !") + self._showTrace(f"座位加载失败 !", self.TraceLevel.ERROR) return False try: all_seats = self.__driver.find_elements( @@ -475,9 +481,9 @@ class LibReserve(LibTimeSelector): seat_status = seat_link.get_attribute("title") self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'") return True - self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确") + self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", self.TraceLevel.WARNING) except: - self._showTrace(f"座位选择失败 !") + self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR) return False @@ -504,7 +510,7 @@ class LibReserve(LibTimeSelector): ) ) except: - self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间") + self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR) return -1 # Find best time option @@ -513,7 +519,7 @@ class LibReserve(LibTimeSelector): f"#{time_id} ul li a" ) if not all_time_opts: - self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间") + self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR) return -1 best_opt, best_text, actual_diff, free_times = self._findBestTimeOption( all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True @@ -530,7 +536,7 @@ class LibReserve(LibTimeSelector): return target_time self._showTrace( f"无法选择最近的 {time_type} {self._minsToTimeStr(target_time)}, " - f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟" + f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", self.TraceLevel.WARNING ) self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") return -1 @@ -610,7 +616,8 @@ class LibReserve(LibTimeSelector): if expect_end_mins > LIBRARY_CLOSE_TIME: expect_end_mins = LIBRARY_CLOSE_TIME self._showTrace( - f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30" + f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30", + self.TraceLevel.WARNING ) return expect_end_mins @@ -637,7 +644,7 @@ class LibReserve(LibTimeSelector): EC.presence_of_element_located((By.ID, "seatLayout")) ) except: - self._showTrace(f"加载预约选座页面失败 !") + self._showTrace(f"加载预约选座页面失败 !", self.TraceLevel.ERROR) return False # date, place, floor, room if not self.__selectDate(reserve_info["date"]): @@ -670,11 +677,11 @@ class LibReserve(LibTimeSelector): raise reserve_success = True except: - self._showTrace(f"预约提交失败 !") + self._showTrace(f"预约提交失败 !", self.TraceLevel.ERROR) if not submit_reserve and have_hover_on_page: self.__driver.refresh() if reserve_success: self._showTrace(f"用户 {username} 预约成功 !") else: - self._showTrace(f"用户 {username} 预约失败 !") + self._showTrace(f"用户 {username} 预约失败 !", self.TraceLevel.ERROR) return reserve_success \ No newline at end of file From e48182434484f3c63ddfbb1cc515257f63efe127 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 11:03:44 +0800 Subject: [PATCH 10/30] =?UTF-8?q?refactor(AppInitializer):=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E5=BA=94=E7=94=A8=E7=A8=8B=E5=BA=8F=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E5=85=88=E5=88=9D=E5=A7=8B=E5=8C=96=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8=EF=BC=8C=E5=86=8D=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/AppInitializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/AppInitializer.py b/src/utils/AppInitializer.py index 7e717f0..ecc89b8 100644 --- a/src/utils/AppInitializer.py +++ b/src/utils/AppInitializer.py @@ -45,8 +45,8 @@ def initializeLogManager( def initializeApp( ) -> bool: - if not initializeConfigManager(): - return False if not initializeLogManager(): return False + if not initializeConfigManager(): + return False return True From 02463f087ede811fad07c5c0af104579f3c30f4d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 12:46:37 +0800 Subject: [PATCH 11/30] =?UTF-8?q?feat(MsgBase,=20gui,=20operators):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 _showTrace 方法添加 no_log 参数,支持控制日志写入 - 在主窗口各关键操作点添加日志输出 - 优化错误信息输出策略,分离 trace 和 log 输出 - 改进配置目录初始化过程的日志记录 --- src/base/MsgBase.py | 5 +++-- src/gui/ALMainWindow.py | 11 ++++++++-- src/gui/ALMainWorkers.py | 40 +++++++++++++++++++++++++++---------- src/operators/AutoLib.py | 32 +++++++++++++++++++++-------- src/operators/LibChecker.py | 5 +++-- src/operators/LibCheckin.py | 6 +++--- src/operators/LibLogin.py | 12 +++++------ src/operators/LibRenew.py | 5 +++-- src/operators/LibReserve.py | 10 ++++++++-- src/utils/AppInitializer.py | 6 ++++++ 10 files changed, 94 insertions(+), 38 deletions(-) diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py index 1df0494..d52e193 100644 --- a/src/base/MsgBase.py +++ b/src/base/MsgBase.py @@ -70,12 +70,13 @@ class MsgBase: def _showTrace( self, msg: str, - level: int = logging.INFO + level: int = logging.INFO, + no_log: bool = False ): timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}") - if self._logger: + if self._logger and not no_log: self._logger.log(level, msg) diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index f033382..dd2fbf1 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -59,6 +59,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.connectSignals() self.startMsgPolling() self.startTimerTaskPolling() + self._showLog("主窗口初始化完成") def modifyUi( @@ -186,6 +187,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): if self.__alConfigWidget: self.__alConfigWidget.close() # the config widget is already deleted in the 'self.onConfigWidgetClosed' + self._showLog("主窗口关闭") QMainWindow.closeEvent(self, event) @@ -300,6 +302,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alConfigWidget = None self.__config_paths = ConfigManager.getValidateAutomationConfigPaths() self.setControlButtons(True, None, None) + self._showLog("配置窗口已关闭,配置文件路径已更新") @Slot(dict) def onTimerTaskIsReady( @@ -347,6 +350,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alTimerTaskManageWidget.raise_() self.__alTimerTaskManageWidget.activateWindow() self.TimerTaskManageWidgetButton.setEnabled(False) + self._showLog("打开定时任务管理窗口") @Slot() def onConfigButtonClicked( @@ -360,6 +364,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alConfigWidget.raise_() self.__alConfigWidget.activateWindow() self.ConfigButton.setEnabled(False) + self._showLog("打开配置窗口") @Slot() def onStartButtonClicked( @@ -376,6 +381,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked) self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked) self.__auto_lib_thread.start() + self._showLog("开始手动执行任务") @Slot() def onStopButtonClicked( @@ -383,14 +389,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): ): if self.__auto_lib_thread: - self._showTrace("正在停止操作......") + self._showTrace("正在停止操作......", no_log=True) self.__auto_lib_thread.wait(2000) - self._showTrace("操作已停止") + self._showTrace("操作已停止", no_log=True) self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked) self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked) self.__auto_lib_thread.deleteLater() self.__auto_lib_thread = None self.setControlButtons(None, False, True) + self._showLog("任务已停止") @Slot() def onSendButtonClicked( diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index 4fee821..36e8950 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -44,9 +44,11 @@ class AutoLibWorker(MsgBase, QThread): current_time = time.strftime("%H:%M", time.localtime()) if current_time >= "23:30" or current_time <= "07:30": self._showTrace( - "当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试" + "当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试", + self.TraceLevel.WARNING ) return False + self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO) return True @@ -57,8 +59,12 @@ class AutoLibWorker(MsgBase, QThread): if not all( os.path.exists(path) for path in self.__config_paths.values() ): - self._showTrace("配置文件路径不存在, 请检查配置文件路径是否正确") + self._showTrace( + "配置文件路径不存在, 请检查配置文件路径是否正确", + self.TraceLevel.ERROR + ) return False + self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO) return True @@ -67,22 +73,28 @@ class AutoLibWorker(MsgBase, QThread): ) -> bool: self._showTrace( - f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}" + f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}", + no_log=True ) self.__run_config = JSONReader(self.__config_paths["run"]).data() self._showTrace( - f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}" + f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}", + no_log=True ) self.__user_config = JSONReader(self.__config_paths["user"]).data() if self.__run_config is None or self.__user_config is None: - self._showTrace("配置文件加载失败, 请检查配置文件是否正确") - self._showTrace("配置文件加载失败, 请检查配置文件是否正确") + self._showTrace( + "配置文件加载失败, 请检查配置文件是否正确", + self.TraceLevel.ERROR + ) return False if not self.__user_config.get("groups"): self._showTrace( - "用户配置文件中无有效任务组, 请检查用户配置文件是否正确" + "用户配置文件中无有效任务组, 请检查用户配置文件是否正确", + self.TraceLevel.WARNING ) return False + self._showLog(f"配置文件加载成功, 任务组数量: {len(self.__user_config.get('groups', []))}", self.TraceLevel.INFO) return True @@ -108,14 +120,17 @@ class AutoLibWorker(MsgBase, QThread): groups = self.__user_config.get("groups") for group in groups: if not group["enabled"]: - self._showTrace(f"任务组 {group["name"]} 已跳过") + self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True) continue - self._showTrace(f"正在运行任务组 {group["name"]}") + self._showTrace(f"正在运行任务组 {group["name"]}", no_log=True) auto_lib.run( { "users": group.get("users", []) } ) except Exception as e: - self._showTrace(f"AutoLibrary 运行时发生异常 : {e}") + self._showTrace( + f"AutoLibrary 运行时发生异常 : {e}", + self.TraceLevel.ERROR + ) self.autoLibWorkerFinishedWithError.emit() return if auto_lib: @@ -154,7 +169,10 @@ class TimerTaskWorker(AutoLibWorker): self ): - self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常") + self._showTrace( + f"定时任务 {self.__timer_task['name']} 运行时发生异常", + self.TraceLevel.ERROR + ) self.timerTaskWorkerIsFinished.emit(True, self.__timer_task) @Slot() diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py index a1b8ecf..154030d 100644 --- a/src/operators/AutoLib.py +++ b/src/operators/AutoLib.py @@ -54,7 +54,7 @@ class AutoLib(MsgBase): self ) -> bool: - self._showTrace("正在初始化浏览器驱动......") + self._showTrace("正在初始化浏览器驱动......", no_log=True) web_driver_config = self.__run_config.get("web_driver", None) self.__driver_type = web_driver_config.get("driver_type") @@ -126,7 +126,7 @@ class AutoLib(MsgBase): service = ChromeService(executable_path=self.__driver_path) self.__driver = webdriver.Chrome(service=service, options=driver_options) case "firefox": - self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...") + self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...", no_log=True) service = FirefoxService(executable_path=self.__driver_path) self.__driver = webdriver.Firefox(service=service, options=driver_options) case _: # actually will not happen, beacuse we have checked it at the initlization @@ -245,8 +245,10 @@ class AutoLib(MsgBase): else: self._showTrace(f"用户 {username} 无法预约,已跳过") result = 2 + # checkin - if run_mode["auto_checkin"] and result != 1: + last_result = result + if run_mode["auto_checkin"] and last_result != 1: if self.__lib_checker.canCheckin(): if self.__lib_checkin.checkin(username): result = 0 @@ -255,8 +257,12 @@ class AutoLib(MsgBase): else: self._showTrace(f"用户 {username} 无法签到,已跳过") result = 2 + if last_result == 0: # partly success + result = 0 + # renewal - if run_mode["auto_renewal"] and result != 1: + last_result = result + if run_mode["auto_renewal"] and last_result != 1: can_renew, record = self.__lib_checker.canRenew() if can_renew: if self.__lib_renew.renew(username, record, reserve_info): @@ -264,12 +270,18 @@ class AutoLib(MsgBase): self._showTrace(f"用户 {username} 续约成功 !") result = 0 else: - result = 1 + if result != 1: # partly success + result = 0 + else: + result = 1 else: result = 1 else: self._showTrace(f"用户 {username} 无法续约,已跳过") result = 2 + if last_result == 0: # partly success + result = 0 + # logout if not self.__lib_logout.logout( username @@ -294,7 +306,8 @@ class AutoLib(MsgBase): for user in users: user_counter["current"] += 1 self._showTrace( - f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......" + f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......", + no_log=True ) if not user["enabled"]: self._showTrace(f"用户 {user["username"]} 已跳过") @@ -333,11 +346,14 @@ class AutoLib(MsgBase): if self.__driver: if self.__driver_type.lower() == "firefox": - self._showTrace(f"Firefox 浏览器驱动关闭略慢, 请耐心等待...") + self._showTrace( + f"Firefox 浏览器驱动关闭略慢, 请耐心等待...", + no_log=True + ) self.__driver.quit() self.__driver = None self._showTrace(f"浏览器驱动已关闭") return True else: - self._showTrace(f"浏览器驱动未初始化, 无需关闭") + self._showTrace(f"浏览器驱动未初始化, 无需关闭", no_log=True) return False \ No newline at end of file diff --git a/src/operators/LibChecker.py b/src/operators/LibChecker.py index 0b72f0d..e87a2d9 100644 --- a/src/operators/LibChecker.py +++ b/src/operators/LibChecker.py @@ -213,7 +213,7 @@ class LibChecker(LibOperator): if wanted_date is None: self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING) return None - self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......") + self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......", no_log=True) checked_count = 0 max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked @@ -245,7 +245,8 @@ class LibChecker(LibOperator): self._showTrace( f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, " f"详细信息: {record["date"]} " - f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}" + f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}", + no_log=True ) return record if not self.__showMoreReserveRecords(): diff --git a/src/operators/LibCheckin.py b/src/operators/LibCheckin.py index 568c901..6705ef8 100644 --- a/src/operators/LibCheckin.py +++ b/src/operators/LibCheckin.py @@ -107,7 +107,7 @@ class LibCheckin(LibOperator): result = self.__driver.execute_script(script) time.sleep(0.1) if result: - self._showTrace("签到按钮已启用") + self._showTrace("签到按钮已启用", no_log=True) else: self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING) return result @@ -129,13 +129,13 @@ class LibCheckin(LibOperator): self._showTrace(f"用户 {username} 签到界面加载失败 !", self.TraceLevel.ERROR) return False if "disabled" in checkin_btn.get_attribute("class"): - self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......") + self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......", no_log=True) if not self.__enableCheckinBtn(): self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR) return False checkin_btn.click() if self._waitResponseLoad(): - self._showTrace(f"用户 {username} 签到成功 !") + self._showTrace(f"用户 {username} 签到成功 !", no_log=True) return True else: self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR) diff --git a/src/operators/LibLogin.py b/src/operators/LibLogin.py index 3a186c4..28555d3 100644 --- a/src/operators/LibLogin.py +++ b/src/operators/LibLogin.py @@ -91,7 +91,7 @@ class LibLogin(LibOperator): captcha_img = base64.b64decode(base64_str) captcha_text = self.__ddddocr.classification(captcha_img) captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower() - self._showTrace(f"识别到验证码为 : '{captcha_text}'") + self._showTrace(f"识别到验证码为 : '{captcha_text}'", no_log=True) if len(captcha_text) != 4: self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) raise Exception("识别到的验证码长度不等于 4 个字符 !") @@ -109,7 +109,7 @@ class LibLogin(LibOperator): try: self._showMsg("请输入验证码:") captcha_text = self._waitMsg(timeout=15) - self._showTrace(f"输入的验证码为 : '{captcha_text}'") + self._showTrace(f"输入的验证码为 : '{captcha_text}'", no_log=True) if len(captcha_text) != 4: self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) raise Exception("输入的验证码长度不等于 4 个字符 !") @@ -125,7 +125,7 @@ class LibLogin(LibOperator): # refresh captcha try: - self._showTrace("刷新验证码......") + self._showTrace("刷新验证码......", no_log=True) self.__driver.find_element( By.ID, "loadImgId" ).click() @@ -145,7 +145,7 @@ class LibLogin(LibOperator): if auto_captcha: captcha_text = self.__autoRecognizeCaptcha() else: - self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !") + self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !", no_log=True) captcha_text = self.__manualRecognizeCaptcha() if captcha_text: return captcha_text @@ -187,7 +187,7 @@ class LibLogin(LibOperator): return False # begin login process for attempt in range(max_attempts): - self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......") + self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......", no_log=True) if not self.__fillLogInElements( username, password, @@ -198,7 +198,7 @@ class LibLogin(LibOperator): continue if not self.__fillCaptchaElement(captcha_text): continue - self._showTrace("尝试登录...") + self._showTrace("尝试登录...", no_log=True) try: self.__driver.find_element( By.XPATH, diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py index 7c42370..e09838d 100644 --- a/src/operators/LibRenew.py +++ b/src/operators/LibRenew.py @@ -61,7 +61,7 @@ class LibRenew(LibTimeSelector): result_message = result_message.text.strip() self._showTrace(f"\n"\ f" 续约失败 !\n"\ - f" {result_message}") + f" {result_message}", no_log=True) return False try: WebDriverWait(self.__driver, 2).until( @@ -186,7 +186,8 @@ class LibRenew(LibTimeSelector): self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR) return False if "disabled" in renew_btn.get_attribute("class"): - self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试") + self._showLog(f"用户 {username} 续约按钮不可用, 可能不在场馆内") + self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试", no_log=True) return False renew_btn.click() if not self.__waitRenewDialog(): diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index 91e23bc..55572bd 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -125,9 +125,14 @@ class LibReserve(LibTimeSelector): except ValueError as e: self._showTrace( f"预约信息错误 ! : {e}, "\ - f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整", + f"由于缺少必要的预约信息, 无法开始预约流程", self.TraceLevel.ERROR ) + self._showTrace( + f"预约信息错误 ! : {e}, "\ + f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整", + no_log=True + ) return False @@ -481,7 +486,8 @@ class LibReserve(LibTimeSelector): seat_status = seat_link.get_attribute("title") self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'") return True - self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", self.TraceLevel.WARNING) + self._showLog(f"座位 {seat_id} 在该楼层区域中不存在", self.TraceLevel.WARNING) + self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", no_log=True) except: self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR) return False diff --git a/src/utils/AppInitializer.py b/src/utils/AppInitializer.py index ecc89b8..5dcc19d 100644 --- a/src/utils/AppInitializer.py +++ b/src/utils/AppInitializer.py @@ -18,15 +18,21 @@ from utils.LogManager import instance as logInstance def initializeConfigManager( ) -> bool: + logger = logInstance().getLogger("AppInitializer") + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) old_config_dir = os.path.join(app_dir, "config") new_config_dir = os.path.join(app_dir, "configs") if QDir(old_config_dir).exists(): # old config dir exists #we rename it to compatible with new version + logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir) if not QDir().rename(old_config_dir, new_config_dir): + logger.error("重命名旧配置目录 %s 到 %s 失败", old_config_dir, new_config_dir) return False elif not QDir(new_config_dir).exists(): + logger.info("初始化配置目录 %s", new_config_dir) if not QDir().mkpath(new_config_dir): + logger.error("创建配置目录 %s 失败", new_config_dir) return False configInstance(new_config_dir) return True From 595f43d852ee68663b40f72c625dfbbe4a8a654d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 18 Mar 2026 17:52:02 +0800 Subject: [PATCH 12/30] =?UTF-8?q?optimize(ALTimerTaskHistoryDialog):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=BB=E5=8A=A1=E5=8E=86=E5=8F=B2=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86=E6=A0=87=E9=A2=98=E5=AD=97=E4=BD=93=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALTimerTaskHistoryDialog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py index 99195bb..a39f8e5 100644 --- a/src/gui/ALTimerTaskHistoryDialog.py +++ b/src/gui/ALTimerTaskHistoryDialog.py @@ -47,16 +47,16 @@ class ALTimerTaskHistoryDialog(QDialog): MainLayout = QVBoxLayout(self) InfoLayout = QGridLayout() TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}") - TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 14px;") + TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 12px;") InfoLayout.addWidget(TaskNameLabel, 0, 0) TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}") - TaskUUIDLabel.setStyleSheet("font-size: 10px;") + TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;") InfoLayout.addWidget(TaskUUIDLabel, 1, 0) InfoLayout.setColumnStretch(0, 1) if self.__task_data.get("repeat", False): - RepeatLabel = QLabel("重复任务") - RepeatLabel.setStyleSheet("color: #2294FF; font-weight: bold; font-size: 12px;") + RepeatLabel = QLabel("可重复性任务") + RepeatLabel.setStyleSheet("color: #2294FF; font-size: 12px;") InfoLayout.addWidget(RepeatLabel, 0, 1) MainLayout.addLayout(InfoLayout) self.HistoryTableWidget = QTableWidget() From 30b36b68ddf95cdfbef3cf5ce67f1363cedac6fb Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 19 Mar 2026 11:56:44 +0800 Subject: [PATCH 13/30] =?UTF-8?q?refactor(ALTimerTaskManageWidget):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=A4=8D=E4=BB=BB=E5=8A=A1=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=AE=B0=E5=BD=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 onRepeatTimerTaskIs 方法中日期循环索引错误,使用 %7 正确处理跨周星期计算 - 新增 OUTDATED 状态的专属处理逻辑,补全过期任务的历史记录 - 添加函数返回值并统一枚举比较方式为 ==,提高代码一致性 --- src/gui/ALTimerTaskManageWidget.py | 39 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index 3fa2b65..de53606 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -575,17 +575,37 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): timer_task: dict ) -> dict: + # only these status are valid + valid_statuses = {ALTimerTaskStatus.EXECUTED, ALTimerTaskStatus.ERROR, + ALTimerTaskStatus.OUTDATED} + if status not in valid_statuses: + return timer_task if "history" not in timer_task: timer_task["history"] = [] - executed_time = datetime.now() - duration = (executed_time - timer_task["execute_time"]).total_seconds() - timer_task["history"].append({ - "execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"), - "executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"), - "result": status, - "duration": duration if status is ALTimerTaskStatus.EXECUTED else 0, - "uuid": timer_task["task_uuid"] - }) + if status != ALTimerTaskStatus.OUTDATED: + executed_time = datetime.now() + duration = (executed_time - timer_task["execute_time"]).total_seconds() + timer_task["history"].append({ + "execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"), + "executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"), + "result": status, + "duration": duration, + "uuid": timer_task["task_uuid"] + }) + else: + current_time = datetime.now() + execute_time = timer_task["execute_time"] + execute_weekday = execute_time.weekday() + delta_days = (current_time - execute_time).days + for i in range(delta_days): + if (execute_weekday + i)%7 in timer_task["repeat_days"]: + timer_task["history"].append({ + "execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"), + "executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"), + "result": status, + "duration": 0, + "uuid": timer_task["task_uuid"] + }) next_time = TimerUtils.calculateNextRepeatTime( timer_task["repeat_days"], timer_task["repeat_hour"], @@ -598,6 +618,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): timer_task["executed"] = False else: timer_task["status"] = status + return timer_task @Slot(dict) def onTimerTaskIsExecuted( From e5dea7bcc56131601cf22448f502871ecd7f5c42 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 19 Mar 2026 12:22:32 +0800 Subject: [PATCH 14/30] =?UTF-8?q?refactor(gui):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=AD=97=E6=AE=B5=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 task_uuid 字段重命名为 uuid,添加时间字段 add_time 重命名为 added_time --- src/gui/ALMainWindow.py | 2 +- src/gui/ALTimerTaskAddDialog.py | 5 ++--- src/gui/ALTimerTaskHistoryDialog.py | 2 +- src/gui/ALTimerTaskManageWidget.py | 22 +++++++++++----------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index dd2fbf1..16191d4 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -334,7 +334,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): 1000 ) self._showTrace( - f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}" + f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}" ) if not is_error: self.timerTaskIsExecuted.emit(timer_task) diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index dde52b9..63fec09 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -121,11 +121,11 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): ) task_data = { "name": name, - "task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}", + "uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}", "time_type": self.TimerTypeComboBox.currentText(), "execute_time": execute_time, "silent": silent, - "add_time": added_time, + "added_time": added_time, "status": ALTimerTaskStatus.PENDING, "executed": False, "repeat": self.RepeatCheckBox.isChecked(), @@ -158,7 +158,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): task_data["repeat_minute"], task_data["repeat_second"] ) - return task_data @Slot(int) diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py index a39f8e5..f054b15 100644 --- a/src/gui/ALTimerTaskHistoryDialog.py +++ b/src/gui/ALTimerTaskHistoryDialog.py @@ -49,7 +49,7 @@ class ALTimerTaskHistoryDialog(QDialog): TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}") TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 12px;") InfoLayout.addWidget(TaskNameLabel, 0, 0) - TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}") + TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('uuid', '未命名')}") TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;") InfoLayout.addWidget(TaskUUIDLabel, 1, 0) InfoLayout.setColumnStretch(0, 1) diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index de53606..c809cc9 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -221,7 +221,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK) if timer_tasks and "timer_tasks" in timer_tasks: for task in timer_tasks["timer_tasks"]: - task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S") + task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S") task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S") task["status"] = ALTimerTaskStatus(task["status"]) if "history" in task: @@ -245,7 +245,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): try: for task in timer_tasks: - task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S") + task["added_time"] = task["added_time"].strftime("%Y-%m-%d %H:%M:%S") task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") task["status"] = task["status"].value if "history" in task: @@ -309,7 +309,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ) elif policy == self.SortPolicy.BY_ADD_TIME: self.__timer_tasks.sort( - key = lambda x: x["add_time"], + key = lambda x: x["added_time"], reverse = order is Qt.SortOrder.DescendingOrder ) elif policy == self.SortPolicy.BY_EXECUTE_TIME: @@ -386,7 +386,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): return ( f"任务名称:{timer_task["name"]}\n" - f"添加时间:{timer_task["add_time"]}\n" + f"添加时间:{timer_task["added_time"]}\n" f"当前状态:{timer_task["status"].value}\n" f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n" f"已执行次数:{len(timer_task['history'] if 'history' in timer_task else 0)}" @@ -414,10 +414,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): result = msgbox.exec() if result != QMessageBox.StandardButton.Yes: return - task_uuid = timer_task["task_uuid"] + task_uuid = timer_task["uuid"] self.__timer_tasks = [ x for x in self.__timer_tasks - if x["task_uuid"] != task_uuid + if x["uuid"] != task_uuid ] self.timerTasksChanged.emit() @@ -563,7 +563,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ): for task in self.__timer_tasks: - if task["task_uuid"] == timer_task["task_uuid"]: + if task["uuid"] == timer_task["uuid"]: task["status"] = ALTimerTaskStatus.RUNNING break self.timerTasksChanged.emit() @@ -590,7 +590,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): "executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"), "result": status, "duration": duration, - "uuid": timer_task["task_uuid"] + "uuid": timer_task["uuid"] }) else: current_time = datetime.now() @@ -604,7 +604,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): "executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"), "result": status, "duration": 0, - "uuid": timer_task["task_uuid"] + "uuid": timer_task["uuid"] }) next_time = TimerUtils.calculateNextRepeatTime( timer_task["repeat_days"], @@ -627,7 +627,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ): for task in self.__timer_tasks: - if task["task_uuid"] == timer_task["task_uuid"]: + if task["uuid"] == timer_task["uuid"]: if task.get("repeat", False): self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task) else: @@ -642,7 +642,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ): for task in self.__timer_tasks: - if task["task_uuid"] == timer_task["task_uuid"]: + if task["uuid"] == timer_task["uuid"]: if task.get("repeat", False): self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task) else: From 1cfe2613249072b78ae2aa11fafba6f3b9b848f2 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 19 Mar 2026 12:23:36 +0800 Subject: [PATCH 15/30] =?UTF-8?q?style(ALTimerTaskManageWidget):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=A6=E7=BB=86=E4=BF=A1=E6=81=AF=E7=9A=84?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 “已记录次数” 替代 “已执行次数”,更符合实际含义 --- src/gui/ALTimerTaskManageWidget.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index c809cc9..af04e89 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -378,7 +378,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.__timer_tasks.append(timer_task) self.timerTasksChanged.emit() - @staticmethod def getTimerTaskDetailMessage( timer_task: dict @@ -389,7 +388,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): f"添加时间:{timer_task["added_time"]}\n" f"当前状态:{timer_task["status"].value}\n" f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n" - f"已执行次数:{len(timer_task['history'] if 'history' in timer_task else 0)}" + f"已记录次数:{len(timer_task['history'] if 'history' in timer_task else 0)}" ) @@ -597,7 +596,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): execute_time = timer_task["execute_time"] execute_weekday = execute_time.weekday() delta_days = (current_time - execute_time).days - for i in range(delta_days): + for i in range(delta_days + 1): if (execute_weekday + i)%7 in timer_task["repeat_days"]: timer_task["history"].append({ "execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"), From bf93cc2cbcb5523d835fbb14dbf23c665dff2b3e Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 08:59:09 +0800 Subject: [PATCH 16/30] =?UTF-8?q?style(*):=20=E5=B0=86=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E9=80=97=E5=8F=B7=E6=9B=BF=E6=8D=A2=E4=B8=BA=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E9=80=97=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALMainWindow.py | 2 +- src/gui/ALTimerTaskManageWidget.py | 4 ++-- src/operators/AutoLib.py | 8 ++++---- src/operators/LibChecker.py | 2 +- src/operators/LibRenew.py | 4 ++-- src/utils/AppInitializer.py | 2 +- src/utils/ConfigManager.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 16191d4..e015d0d 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -302,7 +302,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alConfigWidget = None self.__config_paths = ConfigManager.getValidateAutomationConfigPaths() self.setControlButtons(True, None, None) - self._showLog("配置窗口已关闭,配置文件路径已更新") + self._showLog("配置窗口已关闭,配置文件路径已更新") @Slot(dict) def onTimerTaskIsReady( diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index af04e89..22d2298 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -446,7 +446,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): QMessageBox.warning( self, "警告 - AutoLibrary", - f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !" + f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !" ) return # repeat tasks ask before clear @@ -463,7 +463,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) msgbox.setText( - f"存在 {repeat_tasks_count} 个可重复性任务,\n" + f"存在 {repeat_tasks_count} 个可重复性任务,\n" "删除可重复性任务将同时删除所有已执行的记录 !\n" "是否继续 ?" ) diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py index 154030d..8613d2a 100644 --- a/src/operators/AutoLib.py +++ b/src/operators/AutoLib.py @@ -243,7 +243,7 @@ class AutoLib(MsgBase): else: result = 1 else: - self._showTrace(f"用户 {username} 无法预约,已跳过") + self._showTrace(f"用户 {username} 无法预约, 已跳过") result = 2 # checkin @@ -255,7 +255,7 @@ class AutoLib(MsgBase): else: result = 1 else: - self._showTrace(f"用户 {username} 无法签到,已跳过") + self._showTrace(f"用户 {username} 无法签到, 已跳过") result = 2 if last_result == 0: # partly success result = 0 @@ -277,7 +277,7 @@ class AutoLib(MsgBase): else: result = 1 else: - self._showTrace(f"用户 {username} 无法续约,已跳过") + self._showTrace(f"用户 {username} 无法续约, 已跳过") result = 2 if last_result == 0: # partly success result = 0 @@ -322,7 +322,7 @@ class AutoLib(MsgBase): ) if r == -1: self._showTrace( - f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !", + f"用户 {user["username"]} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !", self.TraceLevel.WARNING ) break diff --git a/src/operators/LibChecker.py b/src/operators/LibChecker.py index e87a2d9..e1d4554 100644 --- a/src/operators/LibChecker.py +++ b/src/operators/LibChecker.py @@ -370,7 +370,7 @@ class LibChecker(LibOperator): else: self._showTrace(f"\n"\ f" 续约失败 !\n"\ - f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !" + f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !" ) return False self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py index e09838d..62114a6 100644 --- a/src/operators/LibRenew.py +++ b/src/operators/LibRenew.py @@ -130,10 +130,10 @@ class LibRenew(LibTimeSelector): if target_renew_mins > LIBRARY_CLOSE_TIME: actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time) if actual_renew_duration <= 0: - self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR) + self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR) return False self._showTrace( - f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)}," + f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)}," f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟" ) return True diff --git a/src/utils/AppInitializer.py b/src/utils/AppInitializer.py index 5dcc19d..d19ba1c 100644 --- a/src/utils/AppInitializer.py +++ b/src/utils/AppInitializer.py @@ -25,7 +25,7 @@ def initializeConfigManager( new_config_dir = os.path.join(app_dir, "configs") if QDir(old_config_dir).exists(): # old config dir exists #we rename it to compatible with new version - logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir) + logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir) if not QDir().rename(old_config_dir, new_config_dir): logger.error("重命名旧配置目录 %s 到 %s 失败", old_config_dir, new_config_dir) return False diff --git a/src/utils/ConfigManager.py b/src/utils/ConfigManager.py index 59fb1a3..b45f5f9 100644 --- a/src/utils/ConfigManager.py +++ b/src/utils/ConfigManager.py @@ -240,5 +240,5 @@ def instance( return _config_manager_instance if getBaseConfigDir() != config_dir: raise ValueError( - "ConfigManager 的实例已初始化,不能使用不同的配置目录。") + "ConfigManager 的实例已初始化,不能使用不同的配置目录。") return _config_manager_instance From 706fc889f9c2ba5a93ff4a8ad53bf1aa513d77ad Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 19:19:34 +0800 Subject: [PATCH 17/30] =?UTF-8?q?chore(*):=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 src/boot 目录,用于存放启动时需要初始化的模块 - 新增 src/managers 目录,用于存放项目中的管理模块 - 新增 src/managers/config 目录,用于存放配置管理模块 - 新增 src/managers/log 目录,用于存放日志管理模块 - 新增 src/managers/driver 目录,用于存放浏览器驱动管理模块 - 修改对应文件中 import 导入路径 --- src/Main.py | 2 +- src/base/MsgBase.py | 2 +- src/{utils => boot}/AppInitializer.py | 33 +++++++++++++++---- src/boot/__init__.py | 6 ++++ src/gui/ALConfigWidget.py | 2 +- src/gui/ALMainWindow.py | 2 +- src/gui/ALTimerTaskManageWidget.py | 2 +- src/gui/__init__.py | 18 ++++++++++ src/gui/resources/__init__.py | 3 ++ src/managers/__init__.py | 8 +++++ .../config}/ConfigManager.py | 5 +-- src/managers/config/__init__.py | 6 ++++ src/managers/driver/__init__.py | 8 +++++ src/{utils => managers/log}/LogManager.py | 6 ++-- src/managers/log/__init__.py | 6 ++++ src/utils/__init__.py | 2 +- 16 files changed, 93 insertions(+), 18 deletions(-) rename src/{utils => boot}/AppInitializer.py (68%) create mode 100644 src/boot/__init__.py create mode 100644 src/gui/__init__.py create mode 100644 src/gui/resources/__init__.py create mode 100644 src/managers/__init__.py rename src/{utils => managers/config}/ConfigManager.py (96%) create mode 100644 src/managers/config/__init__.py create mode 100644 src/managers/driver/__init__.py rename src/{utils => managers/log}/LogManager.py (94%) create mode 100644 src/managers/log/__init__.py diff --git a/src/Main.py b/src/Main.py index f1468d8..e27a75a 100644 --- a/src/Main.py +++ b/src/Main.py @@ -15,7 +15,7 @@ from PySide6.QtWidgets import QApplication from gui.ALMainWindow import ALMainWindow from gui.resources import ALResource -from utils.AppInitializer import initializeApp +from boot.AppInitializer import initializeApp def main(): diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py index d52e193..4ccf570 100644 --- a/src/base/MsgBase.py +++ b/src/base/MsgBase.py @@ -11,7 +11,7 @@ import logging import queue import datetime -from utils.LogManager import getLogger +from managers.log.LogManager import getLogger class MsgBase: diff --git a/src/utils/AppInitializer.py b/src/boot/AppInitializer.py similarity index 68% rename from src/utils/AppInitializer.py rename to src/boot/AppInitializer.py index d19ba1c..47091fd 100644 --- a/src/utils/AppInitializer.py +++ b/src/boot/AppInitializer.py @@ -11,10 +11,22 @@ import os from PySide6.QtCore import QStandardPaths, QDir -from utils.ConfigManager import instance as configInstance -from utils.LogManager import instance as logInstance +from managers.log.LogManager import instance as logInstance +from managers.config.ConfigManager import instance as configInstance +from managers.driver.WebDriverManager import instance as webdriverInstance +def initializeLogManager( +) -> bool: + + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) + log_dir = os.path.join(app_dir, "logs") + if not QDir(log_dir).exists(): + if not QDir().mkpath(log_dir): + return False + logInstance(log_dir) + return True + def initializeConfigManager( ) -> bool: @@ -37,15 +49,20 @@ def initializeConfigManager( configInstance(new_config_dir) return True -def initializeLogManager( +def initializeWebDriverManager( ) -> bool: + logger = logInstance().getLogger("AppInitializer") + app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) - log_dir = os.path.join(app_dir, "logs") - if not QDir(log_dir).exists(): - if not QDir().mkpath(log_dir): + driver_dir = os.path.join(app_dir, "drivers") + logger.info("初始化驱动目录 %s", driver_dir) + if not QDir(driver_dir).exists(): + logger.error("创建驱动目录 %s 失败", driver_dir) + if not QDir().mkpath(driver_dir): + logger.error("创建驱动目录 %s 失败", driver_dir) return False - logInstance(log_dir) + webdriverInstance(driver_dir) return True def initializeApp( @@ -55,4 +72,6 @@ def initializeApp( return False if not initializeConfigManager(): return False + if not initializeWebDriverManager(): + return False return True diff --git a/src/boot/__init__.py b/src/boot/__init__.py new file mode 100644 index 0000000..393e4ca --- /dev/null +++ b/src/boot/__init__.py @@ -0,0 +1,6 @@ +""" + Boot module for the AutoLibrary project. + + Here are the classes and modules in this package: + - AppInitializer: Application initializer class. +""" \ No newline at end of file diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index 0e4c8f9..b0768ea 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -20,7 +20,7 @@ from PySide6.QtGui import ( QCloseEvent, QAction ) -import utils.ConfigManager as ConfigManager +import managers.config.ConfigManager as ConfigManager from utils.JSONReader import JSONReader from utils.JSONWriter import JSONWriter diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index e015d0d..f1744b5 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -19,7 +19,7 @@ from PySide6.QtGui import ( QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices ) -import utils.ConfigManager as ConfigManager +import managers.config.ConfigManager as ConfigManager from base.MsgBase import MsgBase diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index 22d2298..74060a6 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -25,7 +25,7 @@ from PySide6.QtGui import ( QCloseEvent ) -import utils.ConfigManager as ConfigManager +import managers.config.ConfigManager as ConfigManager import utils.TimerUtils as TimerUtils from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..c0cf4b0 --- /dev/null +++ b/src/gui/__init__.py @@ -0,0 +1,18 @@ +""" + GUI module for the AutoLibrary project. + + Here are the classes and modules in this package: + - ALMainWindow: Main window class. + - ALAboutDialog: About dialog class. + - ALConfigWidget: Configuration widget class. + - ALSeatFrame: Seat frame class. + - ALSeatMapView: Seat map view class. + - ALSeatMapTable: Seat map table class. + - ALSeatMapSelectDialog: Seat map select dialog class. + - ALTimerTaskAddDialog: Timer task add dialog class. + - ALTimerTaskHistoryDialog: Timer task history dialog class. + - ALTimerTaskManageWidget: Timer task manage widget class. + - ALUserTreeWidget: User tree widget class. + - ALMainWorkers: Main workers class. + - ALVersionInfo: Version info class. +""" \ No newline at end of file diff --git a/src/gui/resources/__init__.py b/src/gui/resources/__init__.py new file mode 100644 index 0000000..482953b --- /dev/null +++ b/src/gui/resources/__init__.py @@ -0,0 +1,3 @@ +""" + GUI resources module for the AutoLibrary project. +""" \ No newline at end of file diff --git a/src/managers/__init__.py b/src/managers/__init__.py new file mode 100644 index 0000000..6665757 --- /dev/null +++ b/src/managers/__init__.py @@ -0,0 +1,8 @@ +""" + Managers module for the AutoLibrary project. + + Here are the classes and modules in this package: + - ConfigManager: Config manager for managing configuration files. + - LogManager: Log manager for logging. + - WebDriverManager: Web driver manager for managing web drivers. +""" \ No newline at end of file diff --git a/src/utils/ConfigManager.py b/src/managers/config/ConfigManager.py similarity index 96% rename from src/utils/ConfigManager.py rename to src/managers/config/ConfigManager.py index b45f5f9..247ec42 100644 --- a/src/utils/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -234,11 +234,12 @@ def instance( global _config_manager_instance with _instance_lock: if _config_manager_instance is None: + if not config_dir: + raise ValueError("ConfigManager 需要配置目录参数") _config_manager_instance = ConfigManager(config_dir) else: if config_dir == "": return _config_manager_instance if getBaseConfigDir() != config_dir: - raise ValueError( - "ConfigManager 的实例已初始化,不能使用不同的配置目录。") + raise ValueError("ConfigManager 的实例已初始化,不能使用不同的配置目录。") return _config_manager_instance diff --git a/src/managers/config/__init__.py b/src/managers/config/__init__.py new file mode 100644 index 0000000..1c1c60e --- /dev/null +++ b/src/managers/config/__init__.py @@ -0,0 +1,6 @@ +""" + Config managers module for the AutoLibrary project. + + Here are the classes and modules in this package: + - ConfigManager: Config manager for managing configuration files. +""" \ No newline at end of file diff --git a/src/managers/driver/__init__.py b/src/managers/driver/__init__.py new file mode 100644 index 0000000..7e0b908 --- /dev/null +++ b/src/managers/driver/__init__.py @@ -0,0 +1,8 @@ +""" + Driver managers module for the AutoLibrary project. + + Here are the classes and modules in this package: + - WebBrowserDetector: Web browser detector class. + - WebDriverDownloader: Web driver downloader class. + - WebDriverManager: Web driver manager class. +""" \ No newline at end of file diff --git a/src/utils/LogManager.py b/src/managers/log/LogManager.py similarity index 94% rename from src/utils/LogManager.py rename to src/managers/log/LogManager.py index 8af4ae9..a931ada 100644 --- a/src/utils/LogManager.py +++ b/src/managers/log/LogManager.py @@ -174,11 +174,11 @@ def instance( with _instance_lock: if _log_manager_instance is None: if not log_dir: - raise ValueError("LogManager initialization requires log_dir parameter") + raise ValueError("LogManager 需要日志目录参数") _log_manager_instance = LogManager(log_dir) else: if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir): - raise ValueError("LogManager instance already initialized with a different log directory") + raise ValueError("LogManager 的实例已初始化,不能使用不同的日志目录") return _log_manager_instance @@ -187,5 +187,5 @@ def getLogger( ) -> logging.Logger: if _log_manager_instance is None: - raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first") + raise RuntimeError("LogManager 未初始化,请先调用 LogManager.instance(log_dir) 初始化") return _log_manager_instance.getLogger(name) diff --git a/src/managers/log/__init__.py b/src/managers/log/__init__.py new file mode 100644 index 0000000..bdf450a --- /dev/null +++ b/src/managers/log/__init__.py @@ -0,0 +1,6 @@ +""" + Log managers module for the AutoLibrary project. + + Here are the classes and modules in this package: + - LogManager: Log manager for logging. +""" \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py index a05bee0..4d0a056 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -2,7 +2,7 @@ Utils module for the AutoLibrary project. Here are the classes and modules in this package: - - ConfigManager: Configuration manager class for the AutoLibrary project. + - TimerUtils: Timer utils class for the AutoLibrary project. - JSONReader: JSON reader class for the AutoLibrary project. - JSONWriter: JSON writer class for the AutoLibrary project. """ \ No newline at end of file From 571af554d24d890827eb3cc5dc1186678b081095 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 19:20:01 +0800 Subject: [PATCH 18/30] =?UTF-8?q?chore(Main.py):=20=E4=BD=BF=E7=94=A8=20ex?= =?UTF-8?q?ec()=20=E6=9B=BF=E6=8D=A2=20exec=5F()=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chore(Main.py): 使用 exec() 替换 exec_() 方法 --- src/Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.py b/src/Main.py index e27a75a..7f79f81 100644 --- a/src/Main.py +++ b/src/Main.py @@ -30,7 +30,7 @@ def main(): sys.exit(-1) window = ALMainWindow() window.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) if __name__ == "__main__": From 95aa2bb518442694913cc6aa5b112030404c108d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 19:20:43 +0800 Subject: [PATCH 19/30] =?UTF-8?q?feat(WebDriverManager):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=B5=8F=E8=A7=88=E5=99=A8=E7=AE=A1=E7=90=86=E7=B1=BB?= =?UTF-8?q?=20WebDriverManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增浏览器管理类,支持下载和管理浏览器驱动 --- src/managers/driver/WebBrowserDetector.py | 166 ++++++++ src/managers/driver/WebDriverDownloader.py | 443 +++++++++++++++++++++ src/managers/driver/WebDriverManager.py | 403 +++++++++++++++++++ 3 files changed, 1012 insertions(+) create mode 100644 src/managers/driver/WebBrowserDetector.py create mode 100644 src/managers/driver/WebDriverDownloader.py create mode 100644 src/managers/driver/WebDriverManager.py diff --git a/src/managers/driver/WebBrowserDetector.py b/src/managers/driver/WebBrowserDetector.py new file mode 100644 index 0000000..1da85d8 --- /dev/null +++ b/src/managers/driver/WebBrowserDetector.py @@ -0,0 +1,166 @@ +import platform +import installed_browsers + +from pathlib import Path +from enum import Enum +from dataclasses import dataclass + + +class WebBrowserType(Enum): + """ + Web browser type + """ + + CHROME = "chrome" + FIREFOX = "firefox" + EDGE = "edge" + + +class WebBrowserArch(Enum): + """ + Web browser architecture + """ + + WINX86_32 = 0 + WINX86_64 = 1 + WINARM = 2 + + LINUXX86_32 = 3 + LINUXX86_64 = 4 + LINUXARM = 5 + + MACX86_64 = 6 + MACARM = 7 + +@dataclass +class WebBrowserInfo: + """ + Web browser information + + Attributes: + browser_arch (WebBrowserArch): Web browser architecture + browser_type (WebBrowserType): Web browser type + browser_version (str): Web browser version + browser_path (Path): Web browser executable file path + """ + + browser_arch: WebBrowserArch + browser_type: WebBrowserType + browser_version: str + browser_path: Path + + +class WebBrowserArchDetector: + """ + Web browser architecture detector + """ + + def __init__( + self + ): + + pass + + + def detect( + self + ) -> WebBrowserArch: + """ + Detect system architecture + + Returns: + WebBrowserArch: System architecture + """ + + system = platform.system() + machine = platform.machine().lower() + if system == "Windows": + if machine in ["amd64", "x86_64"]: + return WebBrowserArch.WINX86_64 + elif machine in ["i386", "i686", "x86"]: + return WebBrowserArch.WINX86_32 + elif machine in ["arm64", "aarch64"]: + return WebBrowserArch.WINARM + else: + return WebBrowserArch.WINX86_64 + elif system == "Darwin": + if machine in ["arm64", "aarch64"]: + return WebBrowserArch.MACARM + else: + return WebBrowserArch.MACX86_64 + elif system == "Linux": + if machine in ["amd64", "x86_64"]: + return WebBrowserArch.LINUXX86_64 + elif machine in ["i386", "i686", "x86"]: + return WebBrowserArch.LINUXX86_32 + elif machine in ["arm64", "aarch64"]: + return WebBrowserArch.LINUXARM + elif machine.startswith("arm"): + return WebBrowserArch.LINUXARM + else: + return WebBrowserArch.LINUXX86_64 + raise ValueError(f"不支持的系统架构 : {system} {machine}") + + +class WebBrowserDetector: + """ + Web browser detector + """ + + def __init__( + self + ): + + self.browser_arch = WebBrowserArchDetector().detect() + self.browser_infos : list[WebBrowserInfo] = [] + + + def detect( + self + ) -> list[WebBrowserInfo]: + + """ + Detect installed web browsers on the system. + + Returns: + list[WebBrowserInfo]: List of detected browser information objects. + """ + + self.browser_infos = [] + try: + all_browsers = installed_browsers.browsers() + except Exception as e: + self.browser_infos = [] + return self.browser_infos + + # Mapping from internal library name to our enum + type_map = { + 'chrome': WebBrowserType.CHROME, + 'firefox': WebBrowserType.FIREFOX, + 'msedge': WebBrowserType.EDGE, + } + for browser in all_browsers: + internal_name = browser.get('name', '').lower() + if internal_name not in type_map: + continue # Not one of the browsers we care about + version = browser.get('version') + if not version: + # Skip browsers with no version info (unlikely, but defensive) + continue + exe_path = browser.get('location') + if not exe_path: + continue + try: + path = Path(exe_path) + if not path.is_file(): + continue + except Exception: + continue # Invalid path + info = WebBrowserInfo( + browser_arch=self.browser_arch, # Use system architecture as fallback + browser_type=type_map[internal_name], + browser_version=version, + browser_path=path, + ) + self.browser_infos.append(info) + return self.browser_infos \ No newline at end of file diff --git a/src/managers/driver/WebDriverDownloader.py b/src/managers/driver/WebDriverDownloader.py new file mode 100644 index 0000000..de3bf74 --- /dev/null +++ b/src/managers/driver/WebDriverDownloader.py @@ -0,0 +1,443 @@ +import os +import time +import shutil +import requests +import zipfile +import tarfile + +from enum import Enum +from pathlib import Path +from typing import Optional, Callable + + +class WebDriverType(Enum): + """ + Web driver type + """ + + CHROME = "chrome" + FIREFOX = "firefox" + EDGE = "edge" + + +class WebDriverArch(Enum): + """ + Web driver architecture + """ + + class Chrome(Enum): + """ + Chrome web driver architecture + """ + + WINX86_32 = "win32" + WINX86_64 = "win64" + + # LINUX86_32 : no support for linux 32bit + LINUX86_64 = "linux64" + # LINUXARM : no support for linux arm64 + + MACX86_64 = "mac-x64" + MACARM = "mac-arm64" + + class Firefox(Enum): + """ + Firefox web driver architecture + """ + + WINX86_32 = "win32" + WINX86_64 = "win64" + WINARM = "win-aarch64" + + LINUXX86_32 = "linux32" + LINUXX86_64 = "linux64" + LINUXARM = "linux-aarch64" + + MACX86_64 = "macos" + MACARM = "macos-aarch64" + + class Edge(Enum): + """ + Edge web driver architecture + """ + + WINX86_32 = "win32" + WINX86_64 = "win64" + WINARM = "arm64" + + # LINUX86_32 : no support for linux 32bit + LINUXX86_64 = "linux64" + # LINUXARM : no support for linux arm64 + + MACX86_64 = "mac64" + MACARM = "mac64_m1" + + +class WebDriverName: + """ + Web driver name + """ + + def __init__( + self, + driver_type: WebDriverType + ): + + self.driver_type = driver_type + + + def __str__( + self + ) -> str: + + match self.driver_type: + case WebDriverType.CHROME: + return "chromedriver" + case WebDriverType.FIREFOX: + return "geckodriver" + case WebDriverType.EDGE: + return "msedgedriver" + case _: + raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}") + + +class WebDriverExecName: + """ + Web driver executable file name + """ + + def __init__( + self, + driver_type: WebDriverType, + arch: WebDriverArch + ): + + self.driver_type = driver_type + self.arch = arch + + + def __str__( + self + ) -> str: + + is_win = True if self.arch is WebDriverArch.Chrome.WINX86_32 or\ + self.arch is WebDriverArch.Chrome.WINX86_64 or\ + self.arch is WebDriverArch.Firefox.WINX86_32 or\ + self.arch is WebDriverArch.Firefox.WINX86_64 or\ + self.arch is WebDriverArch.Edge.WINX86_32 or\ + self.arch is WebDriverArch.Edge.WINX86_64 else False + match self.driver_type: + case WebDriverType.CHROME: + return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "") + case WebDriverType.FIREFOX: + return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "") + case WebDriverType.EDGE: + return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "") + case _: + raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}") + + +class WebDriverFileName: + """\ + Web driver compressed file name + """ + + def __init__( + self, + version: str, + driver_type: WebDriverType, + arch: WebDriverArch + ): + + self.version = version + self.driver_type = driver_type + self.arch = arch + + def __str__( + self + ) -> str: + + match self.driver_type: + case WebDriverType.CHROME: + return f"{WebDriverName(self.driver_type)}-{self.arch.value}.zip" + case WebDriverType.FIREFOX: + if self.arch is WebDriverArch.Firefox.WINX86_32 or\ + self.arch is WebDriverArch.Firefox.WINX86_64: + suffix = "zip" + else: + suffix = "tar.gz" + return f"{WebDriverName(self.driver_type)}-v{self.version}-{self.arch.value}.{suffix}" + case WebDriverType.EDGE: + return f"edgedriver_{self.arch.value}.zip" # Edge web driver file name is different + case _: + raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}") + + +class WebDriverURL: + """ + Web driver download URL + """ + + def __init__( + self, + version: str, + driver_type: WebDriverType, + arch: WebDriverArch + ): + + self.version = version + self.driver_type = driver_type + self.arch = arch + self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch)) + + + def __str__( + self + ) -> str: + + match self.driver_type: + case WebDriverType.CHROME: + return f"https://storage.googleapis.com/chrome-for-testing-public/"\ + f"{self.version}/"\ + f"{self.arch.value}/"\ + f"{self.file_name}" + case WebDriverType.FIREFOX: + return f"https://github.com/mozilla/geckodriver/releases/download/"\ + f"v{self.version}/"\ + f"{self.file_name}" + case WebDriverType.EDGE: + return f"https://msedgedriver.microsoft.com/"\ + f"{self.version}/"\ + f"{self.file_name}" + case _: + raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}") + + +class WebDriverDownloader: + """ + Base class for WebDriver downloaders + + Args: + driver_type (WebDriverType): Web driver type + version (str): WebDriver version + arch (WebDriverArch): WebDriver architecture + download_dir (str): Download directory + """ + + def __init__( + self, + driver_type: WebDriverType, + driver_version: str, + driver_arch: WebDriverArch, + download_dir: str + ): + + self.driver_type = driver_type + self.arch = driver_arch + self.version = driver_version + self.download_url = str(WebDriverURL(self.version, self.driver_type, self.arch)) + self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value + self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True) + self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch)) + + + def download( + self, + progress_callback: Optional[Callable[[int, int, float, str], None]] = None + ) -> Optional[Path]: + + try: + # downlaod file : 0% - 98% + if not self._download(progress_callback): + return None + # verify file : 98% - 99% + if not self._verify(progress_callback): + return None + # extract file : 99% - 100% + driver_path = self._extract(progress_callback) + if not driver_path: + return None + return driver_path + except Exception: + return None + + + def _download( + self, + progress_callback: Optional[Callable[[int, int, float, str], None]] = None, + max_retries: int = 3 + ) -> bool: + + CHUNK_SIZE = 8192*8 # 64KB chunk + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept-Encoding': 'gzip, deflate' + } + + for attempt in range(max_retries): + try: + # resume download if file exists + if self.download_path.exists(): + downloaded_size = self.download_path.stat().st_size + headers_ = headers.copy() + headers_['Range'] = f"bytes={downloaded_size}-" + mode = 'ab' + else: + downloaded_size = 0 + headers_ = headers + mode = 'wb' + # get response + response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=120) + if response.status_code not in [200, 206]: + if self.download_path.exists(): + self.download_path.unlink() + downloaded_size = 0 + mode = 'wb' + response = requests.get(str(self.download_url), headers=headers, stream=True) + response.raise_for_status() + # get total size + total_size = int(response.headers.get('Content-Length', 0)) + if response.status_code == 206: # Partial Content - server supports Range + total_size += downloaded_size + # download file with progress callback and speed calculation + start_time = time.time() + last_time = start_time + last_size = downloaded_size + last_progress = 0.0 + with open(self.download_path, mode) as f: + for chunk in response.iter_content(CHUNK_SIZE): + if not chunk: + continue + f.write(chunk) + downloaded_size += len(chunk) + if not progress_callback or total_size == 0: + continue + current_time = time.time() + current_progress = (downloaded_size/total_size)*98.0 + if current_progress - last_progress >= 1.0 or current_progress == 98.0: + elapsed = current_time - last_time + if elapsed > 0: + speed = (downloaded_size - last_size)/elapsed/1024.0 # KB/s + else: + speed = 0.0 + progress_callback(current_progress, 100, speed, "下载中...") + last_progress = current_progress + last_size = downloaded_size + last_time = current_time + if total_size > 0 and self.download_path.stat().st_size < total_size: + raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节") + return True + except Exception as e: + if attempt < max_retries - 1: + progress_callback(0, 100, 0.0, "准备重试...") + time.sleep(1) + continue + raise e + + + def _verify( + self, + progress_callback: Optional[Callable[[int, int, float, str], None]] = None + ) -> bool: + + progress_callback(98, 100, 0.0, "验证完成") + return True + + + def _extract( + self, + progress_callback: Optional[Callable[[int, int, float, str], None]] = None + ) -> Optional[Path]: + + try: + progress_callback(98, 100, 0.0, "解压中...") + file_path_str = str(self.download_path) + if file_path_str.endswith('.tar.gz'): + with tarfile.open(self.download_path, 'r:gz') as tar_ref: + tar_ref.extractall(self.download_dir) + else: + with zipfile.ZipFile(self.download_path, 'r') as zip_ref: + zip_ref.extractall(self.download_dir) + driver_file = None + for root, _, files in os.walk(self.download_dir): + for file in files: + expected_name = str(WebDriverExecName(self.driver_type, self.arch)) + if file == str(expected_name): + src_path = Path(root, file) + dst_path = self.download_dir/file + src_path.rename(dst_path) + driver_file = dst_path + break + if driver_file: + break + if not driver_file: + raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}") + progress_callback(100, 100, 0.0, "解压完成") + self.download_path.unlink() + self._cleanup(driver_file) + return driver_file + except Exception: + return None + + + def _cleanup( + self, + driver_file: Path + ) -> None: + + for item in self.download_dir.iterdir(): + if item != driver_file: + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + + +class ChromeDriverDownloader(WebDriverDownloader): + """ + Chrome web driver downloader + + Only support version higher than 114 + """ + + def __init__( + self, + version: str, + arch: WebDriverArch, + download_dir: str + ): + + super().__init__(WebDriverType.CHROME, version, arch, download_dir) + + +class FirefoxDriverDownloader(WebDriverDownloader): + """ + Firefox web driver downloader + + This class do not resolve version mapping, + only support driver version higher than 0.17.0 + """ + + def __init__( + self, + version: str, + arch: WebDriverArch, + download_dir: str + ): + + super().__init__(WebDriverType.FIREFOX, version, arch, download_dir) + + +class EdgeDriverDownloader(WebDriverDownloader): + """ + Edge web driver downloader + """ + + def __init__( + self, + version: str, + arch: WebDriverArch, + download_dir: str + ): + + super().__init__(WebDriverType.EDGE, version, arch, download_dir) diff --git a/src/managers/driver/WebDriverManager.py b/src/managers/driver/WebDriverManager.py new file mode 100644 index 0000000..afd44fe --- /dev/null +++ b/src/managers/driver/WebDriverManager.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import os +import threading +import packaging.version as ver + +from enum import Enum +from pathlib import Path +from typing import Optional, Callable + +from managers.driver.WebBrowserDetector import ( + WebBrowserType, WebBrowserArch, WebBrowserInfo, WebBrowserDetector +) +from managers.driver.WebDriverDownloader import ( + WebDriverArch, WebDriverType, + ChromeDriverDownloader, FirefoxDriverDownloader, EdgeDriverDownloader +) + + +class DriverStatus(Enum): + """ + Web driver status. + """ + + NOT_INSTALLED = 0 + INSTALLED = 1 + DOWNLOADING = 2 + ERROR = 3 + + +class WebDriverInfo: + """ + Web driver information. + + Attributes: + browser_info (WebBrowserInfo): Web browser information + driver_type (WebDriverType): Web driver type + driver_version (str): Web driver version + driver_path (Optional[Path]): Web driver executable file path + driver_status (DriverStatus): Web driver status + """ + + def __init__( + self, + browser_info: WebBrowserInfo + ): + + self.browser_info = browser_info + self.driver_type = WebDriverType(browser_info.browser_type.value) + self.driver_version = "" + self.driver_path: Optional[Path] = None + self.driver_status = DriverStatus.NOT_INSTALLED + + +class WebDriverManager: + """ + Web Driver Manager Singleton Class + + Args: + driver_dir (str): The directory to store web drivers. + """ + + def __init__( + self, + driver_dir: str + ): + + self.__driver_dir = os.path.abspath(driver_dir) + self.__browser_detector = WebBrowserDetector() + self.__driver_infos: list[WebDriverInfo] = [] + self.__initialized = False + self.__lock = threading.Lock() + + self.initialize() + + + def initialize( + self + ): + + if self.__initialized: + return + os.makedirs(self.__driver_dir, exist_ok=True) + self._detectBrowsers() + self._checkDriverStatus() + self.__initialized = True + + + def _detectBrowsers( + self + ): + + with self.__lock: + browser_infos = self.__browser_detector.detect() + self.__driver_infos = [WebDriverInfo(info) for info in browser_infos] + + + def _checkDriverStatus( + self + ): + + with self.__lock: + for driver_info in self.__driver_infos: + driver_arch = self._mapWebBrowserArch( + driver_info.browser_info.browser_type, + driver_info.browser_info.browser_arch + ) + driver_path = self._getDriverPath( + driver_info.driver_type, + driver_arch + ) + if driver_path and driver_path.exists() and driver_path.is_file(): + driver_info.driver_path = driver_path + driver_info.driver_status = DriverStatus.INSTALLED + try: + driver_info.driver_version = self._getDriverVersion( + driver_info.driver_type, + driver_info.driver_info.browser_version + ) + except Exception: + driver_info.driver_status = DriverStatus.ERROR + + + def _mapWebBrowserArch( + self, + browser_type: WebBrowserType, + browser_arch: WebBrowserArch + ) -> WebDriverArch: + + if browser_type == WebBrowserType.CHROME: + if browser_arch == WebBrowserArch.WINX86_32: + return WebDriverArch.Chrome.WINX86_32 + elif browser_arch == WebBrowserArch.WINX86_64: + return WebDriverArch.Chrome.WINX86_64 + elif browser_arch == WebBrowserArch.WINARM: + raise ValueError("Chrome 不支持 Windows ARM 架构") + elif browser_arch == WebBrowserArch.LINUXX86_32: + raise ValueError("Chrome 不支持 Linux x86_32 架构") + elif browser_arch == WebBrowserArch.LINUXX86_64: + return WebDriverArch.Chrome.LINUXX86_64 + elif browser_arch == WebBrowserArch.LINUXARM: + raise ValueError("Chrome 不支持 Linux ARM 架构") + elif browser_arch == WebBrowserArch.MACX86_64: + return WebDriverArch.Chrome.MACX86_64 + elif browser_arch == WebBrowserArch.MACARM: + return WebDriverArch.Chrome.MACARM + else: + raise ValueError(f"不支持的 Chrome 浏览器架构 : {browser_arch}") + elif browser_type == WebBrowserType.FIREFOX: + if browser_arch == WebBrowserArch.WINX86_32: + return WebDriverArch.Firefox.WINX86_32 + elif browser_arch == WebBrowserArch.WINX86_64: + return WebDriverArch.Firefox.WINX86_64 + elif browser_arch == WebBrowserArch.WINARM: + return WebDriverArch.Firefox.WINARM + elif browser_arch == WebBrowserArch.LINUXX86_32: + return WebDriverArch.Firefox.LINUXX86_32 + elif browser_arch == WebBrowserArch.LINUXX86_64: + return WebDriverArch.Firefox.LINUXX86_64 + elif browser_arch == WebBrowserArch.LINUXARM: + return WebDriverArch.Firefox.LINUXARM + elif browser_arch == WebBrowserArch.MACX86_64: + return WebDriverArch.Firefox.MACX86_64 + elif browser_arch == WebBrowserArch.MACARM: + return WebDriverArch.Firefox.MACARM + else: + raise ValueError(f"不支持的 Firefox 浏览器架构 : {browser_arch}") + elif browser_type == WebBrowserType.EDGE: + if browser_arch == WebBrowserArch.WINX86_32: + return WebDriverArch.Edge.WINX86_32 + elif browser_arch == WebBrowserArch.WINX86_64: + return WebDriverArch.Edge.WINX86_64 + elif browser_arch == WebBrowserArch.WINARM: + return WebDriverArch.Edge.WINARM + elif browser_arch == WebBrowserArch.LINUXX86_32: + raise ValueError("Edge 不支持 Linux x86_32 架构") + elif browser_arch == WebBrowserArch.LINUXX86_64: + return WebDriverArch.Edge.LINUXX86_64 + elif browser_arch == WebBrowserArch.LINUXARM: + raise ValueError("Edge 不支持 Linux ARM 架构") + elif browser_arch == WebBrowserArch.MACX86_64: + return WebDriverArch.Edge.MACX86_64 + elif browser_arch == WebBrowserArch.MACARM: + return WebDriverArch.Edge.MACARM + else: + raise ValueError(f"不支持的 Edge 浏览器架构 : {browser_arch}") + else: + raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}") + + + def _mapFirefoxDriverVersion( + self, + version: str + ) -> str: + + version_mapping = [ + (ver.Version("128.0"), ver.Version("999.0"), "0.36.0"), + (ver.Version("115.0"), ver.Version("127.0"), "0.35.0"), + (ver.Version("91.0"), ver.Version("114.0"), "0.34.0"), + (ver.Version("91.0"), ver.Version("120.0"), "0.33.0"), + (ver.Version("91.0"), ver.Version("120.0"), "0.32.0"), + (ver.Version("91.0"), ver.Version("120.0"), "0.31.0"), + (ver.Version("78.0"), ver.Version("90.0"), "0.30.0"), + (ver.Version("60.0"), ver.Version("90.0"), "0.29.0"), + (ver.Version("60.0"), ver.Version("90.0"), "0.28.0"), + (ver.Version("60.0"), ver.Version("90.0"), "0.27.0"), + (ver.Version("57.0"), ver.Version("90.0"), "0.26.0"), + (ver.Version("55.0"), ver.Version("62.0"), "0.25.0"), + (ver.Version("55.0"), ver.Version("62.0"), "0.24.0"), + (ver.Version("57.0"), ver.Version("79.0"), "0.23.0"), + (ver.Version("57.0"), ver.Version("79.0"), "0.22.0"), + (ver.Version("57.0"), ver.Version("79.0"), "0.21.0"), + (ver.Version("55.0"), ver.Version("62.0"), "0.20.0"), + (ver.Version("55.0"), ver.Version("62.0"), "0.19.0"), + (ver.Version("53.0"), ver.Version("62.0"), "0.18.0"), + (ver.Version("52.0"), ver.Version("62.0"), "0.17.0"), + ] + + try: + firefox_version = ver.Version(version) + for min_ver, max_ver, gecko_ver in version_mapping: + if min_ver <= firefox_version <= max_ver: + return gecko_ver + raise ValueError( + f"不支持的 Firefox 版本 : {version}" + f"Firefox 版本 52 及以上受支持" + ) + except Exception as e: + raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e + + + def _getDriverPath( + self, + driver_type: WebDriverType, + driver_arch: WebDriverArch + ) -> Optional[Path]: + + if driver_type == WebDriverType.CHROME: + driver_name = "chromedriver" + elif driver_type == WebDriverType.FIREFOX: + driver_name = "geckodriver" + elif driver_type == WebDriverType.EDGE: + driver_name = "msedgedriver" + else: + return None + is_win = driver_arch in [ + WebDriverArch.Chrome.WINX86_32, + WebDriverArch.Chrome.WINX86_64, + WebDriverArch.Firefox.WINX86_32, + WebDriverArch.Firefox.WINX86_64, + WebDriverArch.Edge.WINX86_32, + WebDriverArch.Edge.WINX86_64, + ] + exe_name = f"{driver_name}.exe" if is_win else driver_name + driver_dir = Path(self.__driver_dir) / driver_type.value / driver_arch.value + driver_path = driver_dir / exe_name + if driver_path.exists() and driver_path.is_file(): + return driver_path + return None + + + def _getDriverVersion( + self, + driver_type: WebDriverType, + browser_version: str + ) -> str: + + if driver_type == WebDriverType.FIREFOX: + return self._mapFirefoxDriverVersion(browser_version) + return browser_version + + + def refresh( + self + ): + + with self.__lock: + self._detectBrowsers() + self._checkDriverStatus() + + + def getDriverInfos( + self + ) -> list[WebDriverInfo]: + + with self.__lock: + return self.__driver_infos.copy() + + + def getDriverInfo( + self, + driver_type: WebDriverType + ) -> Optional[WebDriverInfo]: + + with self.__lock: + for driver_info in self.__driver_infos: + if driver_info.driver_type == driver_type: + return driver_info + return None + + + def getDriverPath( + self, + driver_type: WebDriverType + ) -> Optional[Path]: + + driver_info = self.getDriverInfo(driver_type) + if driver_info and driver_info.driver_status == DriverStatus.INSTALLED: + return driver_info.driver_path + return None + + + def installDriver( + self, + driver_type: WebDriverType, + progress_callback: Optional[Callable[[int, int, float, str], None]] = None + ) -> Optional[Path]: + + with self.__lock: + driver_info = self.getDriverInfo(driver_type) + if not driver_info: + raise ValueError(f"未找到类型为 {driver_type} 的浏览器") + if driver_info.driver_status == DriverStatus.DOWNLOADING: + raise ValueError(f"{driver_type} 驱动正在下载中") + driver_info.driver_status = DriverStatus.DOWNLOADING + try: + driver_arch = self._mapWebBrowserArch( + driver_info.browser_info.browser_type, + driver_info.browser_info.browser_arch + ) + browser_version = driver_info.browser_info.browser_version + driver_version = self._getDriverVersion(driver_type, browser_version) + downloader = None + if driver_type == WebDriverType.CHROME: + downloader = ChromeDriverDownloader( + version=driver_version, + arch=driver_arch, + download_dir=self.__driver_dir + ) + elif driver_type == WebDriverType.FIREFOX: + downloader = FirefoxDriverDownloader( + version=driver_version, + arch=driver_arch, + download_dir=self.__driver_dir + ) + elif driver_type == WebDriverType.EDGE: + downloader = EdgeDriverDownloader( + version=driver_version, + arch=driver_arch, + download_dir=self.__driver_dir + ) + if downloader is None: + raise ValueError(f"不支持的 Web Driver 类型 : {driver_type}") + + driver_path = downloader.download(progress_callback=progress_callback) + with self.__lock: + if driver_path: + driver_info.driver_path = driver_path + driver_info.driver_version = driver_version + driver_info.driver_status = DriverStatus.INSTALLED + else: + driver_info.driver_status = DriverStatus.ERROR + return driver_path + except Exception as e: + with self.__lock: + driver_info.driver_status = DriverStatus.ERROR + raise + + + def driverDir( + self + ) -> str: + + return self.__driver_dir + + +# WebDriverManager singleton instance. +_webdriver_manager_instance = None + +# Singleton instance lock. +_instance_lock = threading.Lock() + +def instance( + driver_dir: str = "" +) -> WebDriverManager: + + global _webdriver_manager_instance + with _instance_lock: + if _webdriver_manager_instance is None: + if not driver_dir: + raise ValueError("WebDriverManager 需要驱动目录参数") + _webdriver_manager_instance = WebDriverManager(driver_dir) + else: + if driver_dir and _webdriver_manager_instance.driverDir() != os.path.abspath(driver_dir): + raise ValueError("WebDriverManager 的实例已初始化,不能使用不同的驱动目录") + return _webdriver_manager_instance From 6b2bf4863eac189bb34edf0304f8a6e54c0fecb4 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 19:21:56 +0800 Subject: [PATCH 20/30] =?UTF-8?q?chore(*):=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=BE=9D=E8=B5=96=EF=BC=8C=E5=B9=B6=E7=94=B1=E6=AD=A4?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=20CI/CD=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新项目依赖 --- .github/workflows/build-test.yml | 40 -- .github/workflows/build.yml | 40 -- Pipfile | 15 - Pipfile.lock | 630 ------------------------------- requirement.txt | Bin 1498 -> 1328 bytes 5 files changed, 725 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 31da669..773123f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -55,46 +55,6 @@ jobs: python -m pip install --upgrade pip pip install -r requirement.txt - - name: Solve ddddocr compatibility and copy model files - run: | - $ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))" - Write-Host "ddddocr package location: $ddddocrPath" - - $initFile = Join-Path $ddddocrPath "__init__.py" - if (Test-Path $initFile) { - Write-Host "Fixing ddddocr compatibility in: $initFile" - (Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile - Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS" - } else { - Write-Error "✗ ddddocr __init__.py not found" - exit 1 - } - - if (-not (Test-Path "models")) { - New-Item -ItemType Directory -Path "models" | Out-Null - Write-Host "✓ Created models directory" - } - - $onnxSource = Join-Path $ddddocrPath "common.onnx" - $onnxDest = "models/common.onnx" - if (Test-Path $onnxSource) { - Copy-Item $onnxSource $onnxDest -Force - Write-Host "✓ Copied ONNX model from: $onnxSource" - Write-Host "✓ ONNX model copied to: $onnxDest" - } else { - Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource" - exit 1 - } - - if (Test-Path $onnxDest) { - $fileSize = (Get-Item $onnxDest).Length / 1MB - Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)" - } else { - Write-Error "✗ Failed to copy model file" - exit 1 - } - shell: pwsh - - name: Compile Qt Resource files run: | cd batchs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cea2c05..02b826f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,46 +91,6 @@ jobs: python -m pip install --upgrade pip pip install -r requirement.txt - - name: Solve ddddocr compatibility and copy model files - run: | - $ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))" - Write-Host "ddddocr package location: $ddddocrPath" - - $initFile = Join-Path $ddddocrPath "__init__.py" - if (Test-Path $initFile) { - Write-Host "Fixing ddddocr compatibility in: $initFile" - (Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile - Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS" - } else { - Write-Error "✗ ddddocr __init__.py not found" - exit 1 - } - - if (-not (Test-Path "models")) { - New-Item -ItemType Directory -Path "models" | Out-Null - Write-Host "✓ Created models directory" - } - - $onnxSource = Join-Path $ddddocrPath "common.onnx" - $onnxDest = "models/common.onnx" - if (Test-Path $onnxSource) { - Copy-Item $onnxSource $onnxDest -Force - Write-Host "✓ Copied ONNX model from: $onnxSource" - Write-Host "✓ ONNX model copied to: $onnxDest" - } else { - Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource" - exit 1 - } - - if (Test-Path $onnxDest) { - $fileSize = (Get-Item $onnxDest).Length / 1MB - Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)" - } else { - Write-Error "✗ Failed to copy model file" - exit 1 - } - shell: pwsh - - name: Compile Qt Resource files run: | cd batchs diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 8b55232..0000000 --- a/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -ddddocr = "*" -selenium = "*" -pyinstaller = "*" -pyside6 = "*" - -[dev-packages] - -[requires] -python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 0bb9f2f..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,630 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "26dffc26812d5328611959b95713a7ed65e20c08c60089b54283b0f406dd08e4" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.13" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "altgraph": { - "hashes": [ - "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", - "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff" - ], - "version": "==0.17.4" - }, - "attrs": { - "hashes": [ - "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", - "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" - ], - "markers": "python_version >= '3.9'", - "version": "==25.4.0" - }, - "certifi": { - "hashes": [ - "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", - "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" - ], - "markers": "python_version >= '3.7'", - "version": "==2025.10.5" - }, - "cffi": { - "hashes": [ - "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", - "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", - "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", - "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", - "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", - "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", - "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", - "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", - "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", - "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", - "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", - "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", - "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", - "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", - "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", - "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", - "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", - "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", - "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", - "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", - "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", - "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", - "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", - "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", - "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", - "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", - "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", - "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", - "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", - "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", - "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", - "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", - "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", - "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", - "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", - "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", - "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", - "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", - "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", - "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", - "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", - "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", - "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", - "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", - "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", - "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", - "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", - "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", - "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", - "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", - "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", - "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", - "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", - "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", - "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", - "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", - "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", - "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", - "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", - "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", - "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", - "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", - "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", - "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", - "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", - "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", - "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", - "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", - "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", - "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", - "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", - "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", - "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", - "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", - "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", - "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", - "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", - "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", - "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", - "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", - "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", - "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", - "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", - "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" - ], - "markers": "python_version >= '3.9'", - "version": "==2.0.0" - }, - "coloredlogs": { - "hashes": [ - "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", - "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==15.0.1" - }, - "ddddocr": { - "hashes": [ - "sha256:5991594d481d33ba0b136022e910f578d6d5b0ca536b44886591359622ab0c70", - "sha256:7c44b58ba7d7566d785c65b8526ec5b78efacd121e993dea4fda5f7966897428" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.0.6" - }, - "flatbuffers": { - "hashes": [ - "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", - "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12" - ], - "version": "==25.9.23" - }, - "h11": { - "hashes": [ - "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", - "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" - ], - "markers": "python_version >= '3.8'", - "version": "==0.16.0" - }, - "humanfriendly": { - "hashes": [ - "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", - "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==10.0" - }, - "idna": { - "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" - ], - "markers": "python_version >= '3.8'", - "version": "==3.11" - }, - "mpmath": { - "hashes": [ - "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", - "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" - ], - "version": "==1.3.0" - }, - "numpy": { - "hashes": [ - "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", - "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", - "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", - "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", - "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", - "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", - "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", - "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", - "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", - "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", - "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", - "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", - "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", - "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", - "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", - "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", - "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", - "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", - "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", - "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", - "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", - "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", - "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", - "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", - "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", - "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", - "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", - "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", - "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", - "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", - "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", - "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", - "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", - "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", - "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", - "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", - "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", - "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", - "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", - "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", - "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", - "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", - "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", - "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", - "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", - "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", - "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", - "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", - "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", - "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", - "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", - "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", - "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", - "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", - "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", - "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", - "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", - "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", - "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", - "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", - "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", - "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", - "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", - "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", - "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", - "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", - "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", - "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", - "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", - "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", - "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", - "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", - "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", - "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb" - ], - "markers": "python_version >= '3.11'", - "version": "==2.3.4" - }, - "onnxruntime": { - "hashes": [ - "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", - "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", - "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", - "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", - "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", - "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", - "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", - "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", - "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", - "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", - "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", - "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", - "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", - "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", - "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", - "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", - "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", - "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", - "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", - "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", - "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", - "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145" - ], - "markers": "python_version >= '3.10'", - "version": "==1.23.2" - }, - "outcome": { - "hashes": [ - "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", - "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.0.post0" - }, - "packaging": { - "hashes": [ - "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", - "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" - ], - "markers": "python_version >= '3.8'", - "version": "==25.0" - }, - "pefile": { - "hashes": [ - "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", - "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2023.2.7" - }, - "pillow": { - "hashes": [ - "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", - "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", - "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", - "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", - "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", - "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", - "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", - "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", - "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", - "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", - "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", - "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", - "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", - "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", - "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", - "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", - "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", - "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", - "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", - "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", - "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", - "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", - "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", - "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", - "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", - "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", - "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", - "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", - "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", - "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", - "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", - "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", - "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", - "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", - "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", - "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", - "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", - "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", - "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", - "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", - "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", - "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", - "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", - "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", - "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", - "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", - "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", - "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", - "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", - "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", - "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", - "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", - "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", - "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", - "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", - "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", - "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", - "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", - "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", - "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", - "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", - "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", - "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", - "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", - "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", - "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", - "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", - "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", - "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", - "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", - "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", - "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", - "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", - "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", - "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", - "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", - "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", - "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", - "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", - "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", - "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", - "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", - "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", - "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", - "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", - "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", - "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", - "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", - "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", - "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", - "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1" - ], - "markers": "python_version >= '3.10'", - "version": "==12.0.0" - }, - "protobuf": { - "hashes": [ - "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", - "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", - "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", - "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", - "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", - "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9", - "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3", - "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", - "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", - "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298" - ], - "markers": "python_version >= '3.9'", - "version": "==6.33.0" - }, - "pycparser": { - "hashes": [ - "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", - "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" - ], - "markers": "python_version >= '3.8'", - "version": "==2.23" - }, - "pyinstaller": { - "hashes": [ - "sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058", - "sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef", - "sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8", - "sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851", - "sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a", - "sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3", - "sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0", - "sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0", - "sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41", - "sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454", - "sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64", - "sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5" - ], - "index": "pypi", - "markers": "python_version < '3.15' and python_version >= '3.8'", - "version": "==6.16.0" - }, - "pyinstaller-hooks-contrib": { - "hashes": [ - "sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6", - "sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038" - ], - "markers": "python_version >= '3.8'", - "version": "==2025.9" - }, - "pyreadline3": { - "hashes": [ - "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", - "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6" - ], - "markers": "python_version >= '3.8'", - "version": "==3.5.4" - }, - "pyside6": { - "hashes": [ - "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339", - "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f", - "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820", - "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01", - "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0" - ], - "index": "pypi", - "markers": "python_version < '3.14' and python_version >= '3.9'", - "version": "==6.10.0" - }, - "pyside6-addons": { - "hashes": [ - "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464", - "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0", - "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c", - "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1", - "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859" - ], - "markers": "python_version < '3.14' and python_version >= '3.9'", - "version": "==6.10.0" - }, - "pyside6-essentials": { - "hashes": [ - "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a", - "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8", - "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1", - "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998", - "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe" - ], - "markers": "python_version < '3.14' and python_version >= '3.9'", - "version": "==6.10.0" - }, - "pysocks": { - "hashes": [ - "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299", - "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", - "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.7.1" - }, - "pywin32-ctypes": { - "hashes": [ - "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", - "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" - ], - "markers": "python_version >= '3.6'", - "version": "==0.2.3" - }, - "selenium": { - "hashes": [ - "sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c", - "sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd" - ], - "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==4.38.0" - }, - "setuptools": { - "hashes": [ - "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", - "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" - ], - "markers": "python_version >= '3.9'", - "version": "==80.9.0" - }, - "shiboken6": { - "hashes": [ - "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717", - "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543", - "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148", - "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61", - "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234" - ], - "markers": "python_version < '3.14' and python_version >= '3.9'", - "version": "==6.10.0" - }, - "sniffio": { - "hashes": [ - "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", - "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, - "sympy": { - "hashes": [ - "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", - "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5" - ], - "markers": "python_version >= '3.9'", - "version": "==1.14.0" - }, - "trio": { - "hashes": [ - "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", - "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5" - ], - "markers": "python_version >= '3.10'", - "version": "==0.32.0" - }, - "trio-websocket": { - "hashes": [ - "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", - "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.2" - }, - "typing-extensions": { - "hashes": [ - "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", - "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" - ], - "markers": "python_version >= '3.9'", - "version": "==4.15.0" - }, - "urllib3": { - "extras": [ - "socks" - ], - "hashes": [ - "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", - "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" - ], - "markers": "python_version >= '3.9'", - "version": "==2.5.0" - }, - "websocket-client": { - "hashes": [ - "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", - "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef" - ], - "markers": "python_version >= '3.9'", - "version": "==1.9.0" - }, - "wsproto": { - "hashes": [ - "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", - "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==1.2.0" - } - }, - "develop": {} -} diff --git a/requirement.txt b/requirement.txt index 5ee4f335b61b92661f9b19f5faf4bfeba0377152..4ed330fbd8911bddaa15088ffcfeb5cc2734227d 100644 GIT binary patch delta 416 zcmXw#&q~8U5XNVl&@TU{0?b}HKzp~V@1@XL(=6@t1d|-QKtLdF%4}` zirx<8G{xx_wFzbPNGVRDHfWC#z4Eo;RiMwHt}RB>p!{y~iQ3f}01JRbK7sfe$Po}z zdO%%Zm6;GIjY#-XC7yr{zo?)uSxnpHLs6hVrz^S#qo5V2V^ni%NBXoiC8Pm=sB`No z?@~_{oOI0YHUrRCC%Yx r21aV<`~OlQ$xkM{arT^;G@rU{=x6S;GW_GNtt-P%-i0r3MV9pk>1|El delta 597 zcmZuu%Syvg5S=6~V#Sq$NG&yJ7e!i=+}a1?LR<>=8=`$;Y)Ts!U5E=2*KyYVgWyN# zH@Ff1z@4ttGdC%?kb#>abLN~gGmp8~rTw38p8|@gPZRQKOanSVI8-H<8hC58OgU5{ z#Du20>d~H5;x&6w5?A2y;nq19AxFk>;MIT~7RI^NHbP6?$rXW0Wjp{AL`N#hIhckI~jcbAYdi zdH4^0tx}~1@0$6nh&$Xi*B9!@6e$kP;WRDSCA29AjMZ!VMkFDd1819OoUC3@ruPD?P#J7JN#gk!TN0786LMYxBBS1 Qll{yJe`M-8;Z3-H0i%^`Gynhq From 9a3abc365c14db515529cc90951b1e49733151cd Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 19:26:17 +0800 Subject: [PATCH 21/30] =?UTF-8?q?fix(requirement.txt):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=BC=BA=E5=A4=B1=E7=9A=84=E4=BE=9D=E8=B5=96=E9=A1=B9?= =?UTF-8?q?=20pyinstaller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirement.txt | Bin 1328 -> 1600 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirement.txt b/requirement.txt index 4ed330fbd8911bddaa15088ffcfeb5cc2734227d..30bbe30e4e1455481388986e9b53729e45ce76d3 100644 GIT binary patch delta 300 zcmZvYJr06E5QU%65j}&kF|oiOK*WMGXkulFib^C%0As8?f(0kAvej7FdNTFx#KOXC zcCy*|^1a!;8qaF;{R$AHzzP`xq*%k}>Y|Mv2CRM5VBn@XT0O)P5#dk}i;x9}h{tWg zEjwCtOmQHSAg5A^6`cw1igL)>73F`@e%4Qw4`XaLg8%>k delta 28 kcmX@Wvw@50|G$lDCQOqXn35(3F&~@k#^SYk2TKPd0H(YO*8l(j From c8e202dc8c16cde6770d68a9a2268310ff7fad08 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 20:09:30 +0800 Subject: [PATCH 22/30] =?UTF-8?q?ci(workflows):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=96=87=E4=BB=B6=E5=A4=8D=E5=88=B6=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-test.yml | 32 ++++++++++++++++++++++++++++++- .github/workflows/build.yml | 32 ++++++++++++++++++++++++++++++- requirement.txt | Bin 1600 -> 1600 bytes 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 773123f..809148a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -55,6 +55,36 @@ jobs: python -m pip install --upgrade pip pip install -r requirement.txt + - name: Copy ddddocr model files + run: | + $ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))" + Write-Host "ddddocr package location: $ddddocrPath" + + if (-not (Test-Path "models")) { + New-Item -ItemType Directory -Path "models" | Out-Null + Write-Host "✓ Created models directory" + } + + $onnxSource = Join-Path $ddddocrPath "common_old.onnx" + $onnxDest = "models/common_old.onnx" + if (Test-Path $onnxSource) { + Copy-Item $onnxSource $onnxDest -Force + Write-Host "✓ Copied ONNX model from: $onnxSource" + Write-Host "✓ ONNX model copied to: $onnxDest" + } else { + Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource" + exit 1 + } + + if (Test-Path $onnxDest) { + $fileSize = (Get-Item $onnxDest).Length / 1MB + Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)" + } else { + Write-Error "✗ Failed to copy model file" + exit 1 + } + shell: pwsh + - name: Compile Qt Resource files run: | cd batchs @@ -84,7 +114,7 @@ jobs: " pathex=[]," " binaries=[]," " datas=[" - " ('models\\common.onnx', 'ddddocr')," + " ('models\\common_old.onnx', 'ddddocr')," " ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons')," " ]," " hiddenimports=[]," diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 02b826f..f54a2ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,6 +91,36 @@ jobs: python -m pip install --upgrade pip pip install -r requirement.txt + - name: Copy ddddocr model files + run: | + $ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))" + Write-Host "ddddocr package location: $ddddocrPath" + + if (-not (Test-Path "models")) { + New-Item -ItemType Directory -Path "models" | Out-Null + Write-Host "✓ Created models directory" + } + + $onnxSource = Join-Path $ddddocrPath "common_old.onnx" + $onnxDest = "models/common_old.onnx" + if (Test-Path $onnxSource) { + Copy-Item $onnxSource $onnxDest -Force + Write-Host "✓ Copied ONNX model from: $onnxSource" + Write-Host "✓ ONNX model copied to: $onnxDest" + } else { + Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource" + exit 1 + } + + if (Test-Path $onnxDest) { + $fileSize = (Get-Item $onnxDest).Length / 1MB + Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)" + } else { + Write-Error "✗ Failed to copy model file" + exit 1 + } + shell: pwsh + - name: Compile Qt Resource files run: | cd batchs @@ -120,7 +150,7 @@ jobs: " pathex=[]," " binaries=[]," " datas=[" - " ('models\\common.onnx', 'ddddocr')," + " ('models\\common_old.onnx', 'ddddocr')," " ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons')," " ]," " hiddenimports=[]," diff --git a/requirement.txt b/requirement.txt index 30bbe30e4e1455481388986e9b53729e45ce76d3..5a95907538c96df1dbdd5a9dc4c01af2384d5537 100644 GIT binary patch delta 16 XcmX@WbAV?<6AQC3gT>}nmfeg1FZTsC delta 16 XcmX@WbAV?<6AQBmgW={@mfeg1FVzJv From e40c7f4f3eda4f8531af254d80708749307c7dc1 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 20 Mar 2026 20:57:24 +0800 Subject: [PATCH 23/30] =?UTF-8?q?chore(*):=20=E9=99=8D=E4=BD=8E=20ddddocr?= =?UTF-8?q?=20=E7=89=88=E6=9C=AC=E4=BB=A5=E9=81=BF=E5=85=8D=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84=E6=89=93=E5=8C=85=E4=BD=93=E7=A7=AF?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=97=B6=E5=9B=9E=E6=BB=9A=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-test.yml | 18 ++++++++++++++---- .github/workflows/build.yml | 18 ++++++++++++++---- requirement.txt | Bin 1600 -> 1600 bytes 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 809148a..31da669 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -55,18 +55,28 @@ jobs: python -m pip install --upgrade pip pip install -r requirement.txt - - name: Copy ddddocr model files + - name: Solve ddddocr compatibility and copy model files run: | $ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))" Write-Host "ddddocr package location: $ddddocrPath" + $initFile = Join-Path $ddddocrPath "__init__.py" + if (Test-Path $initFile) { + Write-Host "Fixing ddddocr compatibility in: $initFile" + (Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile + Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS" + } else { + Write-Error "✗ ddddocr __init__.py not found" + exit 1 + } + if (-not (Test-Path "models")) { New-Item -ItemType Directory -Path "models" | Out-Null Write-Host "✓ Created models directory" } - $onnxSource = Join-Path $ddddocrPath "common_old.onnx" - $onnxDest = "models/common_old.onnx" + $onnxSource = Join-Path $ddddocrPath "common.onnx" + $onnxDest = "models/common.onnx" if (Test-Path $onnxSource) { Copy-Item $onnxSource $onnxDest -Force Write-Host "✓ Copied ONNX model from: $onnxSource" @@ -114,7 +124,7 @@ jobs: " pathex=[]," " binaries=[]," " datas=[" - " ('models\\common_old.onnx', 'ddddocr')," + " ('models\\common.onnx', 'ddddocr')," " ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons')," " ]," " hiddenimports=[]," diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f54a2ea..cea2c05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,18 +91,28 @@ jobs: python -m pip install --upgrade pip pip install -r requirement.txt - - name: Copy ddddocr model files + - name: Solve ddddocr compatibility and copy model files run: | $ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))" Write-Host "ddddocr package location: $ddddocrPath" + $initFile = Join-Path $ddddocrPath "__init__.py" + if (Test-Path $initFile) { + Write-Host "Fixing ddddocr compatibility in: $initFile" + (Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile + Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS" + } else { + Write-Error "✗ ddddocr __init__.py not found" + exit 1 + } + if (-not (Test-Path "models")) { New-Item -ItemType Directory -Path "models" | Out-Null Write-Host "✓ Created models directory" } - $onnxSource = Join-Path $ddddocrPath "common_old.onnx" - $onnxDest = "models/common_old.onnx" + $onnxSource = Join-Path $ddddocrPath "common.onnx" + $onnxDest = "models/common.onnx" if (Test-Path $onnxSource) { Copy-Item $onnxSource $onnxDest -Force Write-Host "✓ Copied ONNX model from: $onnxSource" @@ -150,7 +160,7 @@ jobs: " pathex=[]," " binaries=[]," " datas=[" - " ('models\\common_old.onnx', 'ddddocr')," + " ('models\\common.onnx', 'ddddocr')," " ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons')," " ]," " hiddenimports=[]," diff --git a/requirement.txt b/requirement.txt index 5a95907538c96df1dbdd5a9dc4c01af2384d5537..711bb84d5b5e510cf3e3e8218c6743d04bbe3c7b 100644 GIT binary patch delta 16 XcmX@WbAV^UB~}9lJqELl*E3lGG57_F delta 16 XcmX@WbAV^UB~~*AJqE*#*E3lGG8F}j From 84cff6acc3199c6ac7efb00c47c414e12f085de2 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 00:54:49 +0800 Subject: [PATCH 24/30] =?UTF-8?q?feat(WebDriverManager):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8B=E8=BD=BD=E5=8F=96=E6=B6=88=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E5=96=84=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/managers/driver/WebDriverDownloader.py | 32 +++- src/managers/driver/WebDriverManager.py | 202 ++++++++++++++------- 2 files changed, 157 insertions(+), 77 deletions(-) diff --git a/src/managers/driver/WebDriverDownloader.py b/src/managers/driver/WebDriverDownloader.py index de3bf74..a3d9003 100644 --- a/src/managers/driver/WebDriverDownloader.py +++ b/src/managers/driver/WebDriverDownloader.py @@ -1,6 +1,7 @@ import os import time import shutil +import threading import requests import zipfile import tarfile @@ -243,29 +244,33 @@ class WebDriverDownloader: def download( self, - progress_callback: Optional[Callable[[int, int, float, str], None]] = None + progress_callback: Optional[Callable[[float, int, float, str], None]] = None, + cancel_event: Optional[threading.Event] = None ) -> Optional[Path]: try: # downlaod file : 0% - 98% - if not self._download(progress_callback): + if not self._download(progress_callback, cancel_event=cancel_event): return None # verify file : 98% - 99% if not self._verify(progress_callback): + progress_callback(0, 100, 0.0, "验证失败") return None # extract file : 99% - 100% driver_path = self._extract(progress_callback) if not driver_path: + progress_callback(0, 100, 0.0, "解压失败") return None return driver_path - except Exception: - return None + except Exception as e: + raise e def _download( self, - progress_callback: Optional[Callable[[int, int, float, str], None]] = None, - max_retries: int = 3 + progress_callback: Optional[Callable[[float, int, float, str], None]] = None, + max_retries: int = 3, + cancel_event: Optional[threading.Event] = None ) -> bool: CHUNK_SIZE = 8192*8 # 64KB chunk @@ -276,6 +281,8 @@ class WebDriverDownloader: for attempt in range(max_retries): try: + if cancel_event and cancel_event.is_set(): + return False # resume download if file exists if self.download_path.exists(): downloaded_size = self.download_path.stat().st_size @@ -287,7 +294,7 @@ class WebDriverDownloader: headers_ = headers mode = 'wb' # get response - response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=120) + response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=10) if response.status_code not in [200, 206]: if self.download_path.exists(): self.download_path.unlink() @@ -306,6 +313,9 @@ class WebDriverDownloader: last_progress = 0.0 with open(self.download_path, mode) as f: for chunk in response.iter_content(CHUNK_SIZE): + if cancel_event and cancel_event.is_set(): + response.close() + return False if not chunk: continue f.write(chunk) @@ -328,8 +338,10 @@ class WebDriverDownloader: raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节") return True except Exception as e: + if cancel_event and cancel_event.is_set(): + return False if attempt < max_retries - 1: - progress_callback(0, 100, 0.0, "准备重试...") + progress_callback(0, 100, 0.0, f"第 {attempt+1} 次重试...") time.sleep(1) continue raise e @@ -337,7 +349,7 @@ class WebDriverDownloader: def _verify( self, - progress_callback: Optional[Callable[[int, int, float, str], None]] = None + progress_callback: Optional[Callable[[float, int, float, str], None]] = None ) -> bool: progress_callback(98, 100, 0.0, "验证完成") @@ -346,7 +358,7 @@ class WebDriverDownloader: def _extract( self, - progress_callback: Optional[Callable[[int, int, float, str], None]] = None + progress_callback: Optional[Callable[[float, int, float, str], None]] = None ) -> Optional[Path]: try: diff --git a/src/managers/driver/WebDriverManager.py b/src/managers/driver/WebDriverManager.py index afd44fe..3e70bd8 100644 --- a/src/managers/driver/WebDriverManager.py +++ b/src/managers/driver/WebDriverManager.py @@ -40,21 +40,22 @@ class WebDriverInfo: Web driver information. Attributes: - browser_info (WebBrowserInfo): Web browser information driver_type (WebDriverType): Web driver type + driver_arch (WebDriverArch): Web driver architecture driver_version (str): Web driver version + browser_version (str): Web browser version driver_path (Optional[Path]): Web driver executable file path driver_status (DriverStatus): Web driver status """ def __init__( - self, - browser_info: WebBrowserInfo + self ): - self.browser_info = browser_info - self.driver_type = WebDriverType(browser_info.browser_type.value) + self.driver_type = None + self.driver_arch = None self.driver_version = "" + self.browser_version = "" self.driver_path: Optional[Path] = None self.driver_status = DriverStatus.NOT_INSTALLED @@ -99,7 +100,10 @@ class WebDriverManager: with self.__lock: browser_infos = self.__browser_detector.detect() - self.__driver_infos = [WebDriverInfo(info) for info in browser_infos] + self.__driver_infos = [ + self._getDriverInfo(info) + for info in browser_infos + ] def _checkDriverStatus( @@ -108,27 +112,28 @@ class WebDriverManager: with self.__lock: for driver_info in self.__driver_infos: - driver_arch = self._mapWebBrowserArch( - driver_info.browser_info.browser_type, - driver_info.browser_info.browser_arch - ) - driver_path = self._getDriverPath( - driver_info.driver_type, - driver_arch - ) + driver_path = self._getDriverPath(driver_info) if driver_path and driver_path.exists() and driver_path.is_file(): driver_info.driver_path = driver_path driver_info.driver_status = DriverStatus.INSTALLED - try: - driver_info.driver_version = self._getDriverVersion( - driver_info.driver_type, - driver_info.driver_info.browser_version - ) - except Exception: - driver_info.driver_status = DriverStatus.ERROR - def _mapWebBrowserArch( + def _mapWebBrowserTypeToDriver( + self, + browser_type: WebBrowserType + ) -> WebDriverType: + + if browser_type == WebBrowserType.CHROME: + return WebDriverType.CHROME + elif browser_type == WebBrowserType.FIREFOX: + return WebDriverType.FIREFOX + elif browser_type == WebBrowserType.EDGE: + return WebDriverType.EDGE + else: + raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}") + + + def _mapWebBrowserArchToDriver( self, browser_type: WebBrowserType, browser_arch: WebBrowserArch @@ -236,12 +241,30 @@ class WebDriverManager: raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e + def _getDriverInfo( + self, + browser_info: WebBrowserInfo + ) -> WebDriverInfo: + + driver_info = WebDriverInfo() + driver_info.driver_type = self._mapWebBrowserTypeToDriver(browser_info.browser_type) + driver_info.driver_arch = self._mapWebBrowserArchToDriver(browser_info.browser_type, browser_info.browser_arch) + if browser_info.browser_type == WebBrowserType.FIREFOX: + driver_info.driver_version = self._mapFirefoxDriverVersion(browser_info.browser_version) + else: + driver_info.driver_version = browser_info.browser_version + driver_info.browser_version = browser_info.browser_version + return driver_info + + def _getDriverPath( self, - driver_type: WebDriverType, - driver_arch: WebDriverArch + driver_info: WebDriverInfo ) -> Optional[Path]: + driver_type = driver_info.driver_type + driver_arch = driver_info.driver_arch + driver_version = driver_info.driver_version if driver_type == WebDriverType.CHROME: driver_name = "chromedriver" elif driver_type == WebDriverType.FIREFOX: @@ -259,31 +282,17 @@ class WebDriverManager: WebDriverArch.Edge.WINX86_64, ] exe_name = f"{driver_name}.exe" if is_win else driver_name - driver_dir = Path(self.__driver_dir) / driver_type.value / driver_arch.value - driver_path = driver_dir / exe_name - if driver_path.exists() and driver_path.is_file(): - return driver_path - return None - - - def _getDriverVersion( - self, - driver_type: WebDriverType, - browser_version: str - ) -> str: - - if driver_type == WebDriverType.FIREFOX: - return self._mapFirefoxDriverVersion(browser_version) - return browser_version + driver_dir = Path(self.__driver_dir)/driver_type.value/driver_version/driver_arch.value + driver_path = driver_dir/exe_name + return driver_path def refresh( self ): - with self.__lock: - self._detectBrowsers() - self._checkDriverStatus() + self._detectBrowsers() + self._checkDriverStatus() def getDriverInfos( @@ -297,21 +306,21 @@ class WebDriverManager: def getDriverInfo( self, driver_type: WebDriverType - ) -> Optional[WebDriverInfo]: + ) -> list[WebDriverInfo]: with self.__lock: - for driver_info in self.__driver_infos: - if driver_info.driver_type == driver_type: - return driver_info - return None + return [ + info + for info in self.__driver_infos + if info.driver_type == driver_type + ] def getDriverPath( self, - driver_type: WebDriverType + driver_info: WebDriverInfo ) -> Optional[Path]: - driver_info = self.getDriverInfo(driver_type) if driver_info and driver_info.driver_status == DriverStatus.INSTALLED: return driver_info.driver_path return None @@ -319,24 +328,28 @@ class WebDriverManager: def installDriver( self, - driver_type: WebDriverType, - progress_callback: Optional[Callable[[int, int, float, str], None]] = None + driver_info: WebDriverInfo, + progress_callback: Optional[Callable[[float, int, float, str], None]] = None, + cancel_event: Optional[threading.Event] = None ) -> Optional[Path]: with self.__lock: - driver_info = self.getDriverInfo(driver_type) if not driver_info: - raise ValueError(f"未找到类型为 {driver_type} 的浏览器") - if driver_info.driver_status == DriverStatus.DOWNLOADING: - raise ValueError(f"{driver_type} 驱动正在下载中") - driver_info.driver_status = DriverStatus.DOWNLOADING + if progress_callback: + progress_callback(0, 0, 0, "未找到浏览器信息") + else: + raise ValueError("未找到浏览器信息") + if driver_info and driver_info.driver_status == DriverStatus.DOWNLOADING: + if progress_callback: + progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动正在下载中") + else: + raise ValueError(f"{driver_info.driver_type} 驱动正在下载中") try: - driver_arch = self._mapWebBrowserArch( - driver_info.browser_info.browser_type, - driver_info.browser_info.browser_arch - ) - browser_version = driver_info.browser_info.browser_version - driver_version = self._getDriverVersion(driver_type, browser_version) + if not driver_info: + raise ValueError("未找到浏览器信息") + driver_arch = driver_info.driver_arch + driver_type = driver_info.driver_type + driver_version = driver_info.driver_version downloader = None if driver_type == WebDriverType.CHROME: downloader = ChromeDriverDownloader( @@ -357,9 +370,13 @@ class WebDriverManager: download_dir=self.__driver_dir ) if downloader is None: - raise ValueError(f"不支持的 Web Driver 类型 : {driver_type}") - - driver_path = downloader.download(progress_callback=progress_callback) + if progress_callback: + progress_callback(0, 0, 0, f"不支持的 Web Driver 类型") + else: + raise ValueError(f"不支持的 Web Driver 类型") + with self.__lock: + driver_info.driver_status = DriverStatus.DOWNLOADING + driver_path = downloader.download(progress_callback=progress_callback, cancel_event=cancel_event) with self.__lock: if driver_path: driver_info.driver_path = driver_path @@ -369,6 +386,57 @@ class WebDriverManager: driver_info.driver_status = DriverStatus.ERROR return driver_path except Exception as e: + with self.__lock: + driver_info.driver_status = DriverStatus.ERROR + raise e + + + def cancelDriverDownload( + self, + driver_info: WebDriverInfo + ) -> bool: + + import shutil + + try: + driver_path = self._getDriverPath(driver_info) + if driver_path: + download_dir = driver_path.parent + if download_dir.exists(): + shutil.rmtree(download_dir, ignore_errors=True) + with self.__lock: + driver_info.driver_path = None + driver_info.driver_status = DriverStatus.NOT_INSTALLED + return True + except Exception: + return False + + + def uninstallDriver( + self, + driver_info: WebDriverInfo, + progress_callback: Optional[Callable[[int, int, float, str], None]] = None + ) -> bool: + + with self.__lock: + if not driver_info: + if progress_callback: + progress_callback(0, 0, 0, "未找到浏览器信息") + else: + raise ValueError("未找到浏览器信息") + if driver_info.driver_status != DriverStatus.INSTALLED: + if progress_callback: + progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动未安装") + else: + raise ValueError(f"{driver_info.driver_type} 驱动未安装") + try: + driver_path = driver_info.driver_path + driver_path.unlink() + with self.__lock: + driver_info.driver_path = None + driver_info.driver_status = DriverStatus.NOT_INSTALLED + return True + except Exception: with self.__lock: driver_info.driver_status = DriverStatus.ERROR raise @@ -399,5 +467,5 @@ def instance( _webdriver_manager_instance = WebDriverManager(driver_dir) else: if driver_dir and _webdriver_manager_instance.driverDir() != os.path.abspath(driver_dir): - raise ValueError("WebDriverManager 的实例已初始化,不能使用不同的驱动目录") + raise ValueError("WebDriverManager 的实例已初始化, 不能使用不同的驱动目录") return _webdriver_manager_instance From afa1d390519e68b5617648e2686c4c1d5b4c219e Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 00:55:02 +0800 Subject: [PATCH 25/30] =?UTF-8?q?feat(gui):=20=E6=96=B0=E5=A2=9E=20ALStatu?= =?UTF-8?q?sLabel=20=E7=8A=B6=E6=80=81=E6=A0=87=E7=AD=BE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=92=8C=E6=B5=8F=E8=A7=88=E5=99=A8=E9=A9=B1=E5=8A=A8=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=AF=B9=E8=AF=9D=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALStatusLabel.py | 246 +++++++++++++ src/gui/ALWebDriverDownloadDialog.py | 518 +++++++++++++++++++++++++++ 2 files changed, 764 insertions(+) create mode 100644 src/gui/ALStatusLabel.py create mode 100644 src/gui/ALWebDriverDownloadDialog.py diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py new file mode 100644 index 0000000..7b01648 --- /dev/null +++ b/src/gui/ALStatusLabel.py @@ -0,0 +1,246 @@ + +from enum import Enum + +from PySide6.QtWidgets import ( + QLabel +) +from PySide6.QtCore import ( + Qt, Property, QPropertyAnimation, QEasingCurve +) +from PySide6.QtGui import ( + QPainter, QColor, QConicalGradient, QPalette +) + + +class ALStatusLabel(QLabel): + + class Status(Enum): + """ + Enum class for representing the status of ALStatusLabel. + """ + + WAITING = 0 + RUNNING = 1 + SUCCESS = 2 + WARNING = 3 + FAILURE = 4 + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self.__status = self.Status.WAITING + self.__icon_angle = 0 + + self.setupUi() + + + def setupUi( + self + ): + + self.setFixedSize(36, 36) + self.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.RunningAnimation = QPropertyAnimation(self, b"iconAngle") + self.RunningAnimation.setDuration(1000) + self.RunningAnimation.setStartValue(0) + self.RunningAnimation.setEndValue(-360) + self.RunningAnimation.setLoopCount(-1) + self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear) + + + def isDarkMode( + self + ) -> bool: + + return self.palette().color(QPalette.ColorRole.Window).value() < 128 + + + def getMarkColor( + self + ) -> QColor: + + return QColor("#FFFFFF") if self.isDarkMode() else QColor("#454545") + + @Property(Status) + def status( + self + ) -> Status: + + return self.__status + + @Property(int) + def iconAngle( + self + ) -> int: + + return self.__icon_angle + + @status.setter + def status( + self, + status: Status + ): + + if status not in self.Status: + raise ValueError(f"Invalid (class)Status[enum.Enum] value: {status}") + self.__status = status + if self.__status == self.Status.RUNNING: + self.RunningAnimation.start() + else: + self.RunningAnimation.stop() + self.update() + + @iconAngle.setter + def iconAngle( + self, + value: int + ): + + self.__icon_angle = value + self.update() + + + def paintEvent( + self, + event + ): + + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + center_x = self.width()/2 + center_y = self.height()/2 + radius = min(center_x, center_y) - 3 + match self.__status: + case self.Status.WAITING: + pen = painter.pen() + pen.setWidth(2) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + pen.setColor(QColor("#969696")) # grey + painter.setPen(pen) + painter.drawEllipse( + int(center_x - radius), + int(center_y - radius), + int(radius*2), + int(radius*2) + ) + case self.Status.RUNNING: + gradient = QConicalGradient(center_x, center_y, self.__icon_angle) + gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF")) + gradient.setColorAt(1.0, QColor("#2294FF00")) + pen = painter.pen() + pen.setWidth(3) + pen.setBrush(gradient) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + painter.setPen(pen) + painter.drawEllipse( + int(center_x - radius), + int(center_y - radius), + int(radius*2), + int(radius*2) + ) + case self.Status.SUCCESS: + # draw the success green circle + pen = painter.pen() + pen.setWidth(2) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green + painter.setPen(pen) + painter.drawEllipse( + int(center_x - radius), + int(center_y - radius), + int(radius*2), + int(radius*2) + ) + # draw the success check mark '✓' + painter.setPen(Qt.PenStyle.SolidLine) + pen = painter.pen() + pen.setWidth(3) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + # white when dark mode, black when light mode + pen.setColor(self.getMarkColor()) + painter.setPen(pen) + mark_size = radius/2 + mark_path = [ + (center_x - mark_size, center_y), + (center_x - mark_size/3, center_y + mark_size/2), + (center_x + mark_size, center_y - mark_size/2) + ] + painter.drawLine( + int(mark_path[0][0]),int(mark_path[0][1]), + int(mark_path[1][0]),int(mark_path[1][1]) + ) + painter.drawLine( + int(mark_path[1][0]),int(mark_path[1][1]), + int(mark_path[2][0]),int(mark_path[2][1]) + ) + case self.Status.WARNING: + # draw the warning orange circle + pen = painter.pen() + pen.setWidth(2) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + pen.setColor(QColor("#FF9800")) # orange + painter.setPen(pen) + painter.drawEllipse( + int(center_x - radius), + int(center_y - radius), + int(radius*2), + int(radius*2) + ) + # draw the warning exclamation mark '!' + painter.setPen(Qt.PenStyle.SolidLine) + pen = painter.pen() + pen.setWidth(3) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + # white when dark mode, black when light mode + pen.setColor(self.getMarkColor()) + painter.setPen(pen) + painter.drawLine( + int(center_x), int(center_y - radius/2), + int(center_x), int(center_y + radius/6) + ) + painter.drawPoint( + int(center_x), int(center_y + radius/2) + ) + case self.Status.FAILURE: + # draw the failure red circle + pen = painter.pen() + pen.setWidth(2) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + pen.setColor(QColor("#DC0000")) # red + painter.setPen(pen) + painter.drawEllipse( + int(center_x - radius), + int(center_y - radius), + int(radius*2), + int(radius*2) + ) + # draw the failure cross mark '✗' + painter.setPen(Qt.PenStyle.SolidLine) + pen = painter.pen() + pen.setWidth(3) + pen.setBrush(Qt.BrushStyle.NoBrush) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + # white when dark mode, black when light mode + pen.setColor(self.getMarkColor()) + painter.setPen(pen) + mark_size = radius/3 + painter.drawLine( + int(center_x - mark_size), int(center_y - mark_size), + int(center_x + mark_size), int(center_y + mark_size) + ) + painter.drawLine( + int(center_x + mark_size), int(center_y - mark_size), + int(center_x - mark_size), int(center_y + mark_size) + ) + painter.end() + super().paintEvent(event) diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py new file mode 100644 index 0000000..6f34702 --- /dev/null +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" +import threading +from typing import Optional + +from PySide6.QtCore import ( + Qt, Slot, QThread, Signal +) +from PySide6.QtWidgets import ( + QDialog, QLabel, QComboBox, QProgressBar, + QPushButton, QVBoxLayout, QHBoxLayout, + QMessageBox, QFrame, QLineEdit +) +from PySide6.QtGui import ( + QCloseEvent +) + +from managers.driver.WebDriverManager import ( + instance as webdriver_manager_instance, + WebDriverManager, WebDriverInfo, WebDriverType +) +from gui.ALStatusLabel import ALStatusLabel + + +class DownloadWorker(QThread): + """ + Worker thread for downloading web drivers. + """ + + progress = Signal(float, int, float, str) + finished = Signal(object, str) + error = Signal(str) + cancelled = Signal() + + def __init__( + self, + driver_manager: WebDriverManager, + driver_info: WebDriverInfo + ): + super().__init__() + self.__driver_manager = driver_manager + self.__driver_info = driver_info + self.__driver_path = None + self.__cancelled = False + self.__cancel_event = threading.Event() + + def cancel( + self + ): + """ + Cancel the download operation. + """ + + self.__cancelled = True + self.__cancel_event.set() + + def run( + self + ): + try: + if self.__cancelled: + self.cancelled.emit() + return + self.__driver_path = self.__driver_manager.installDriver( + self.__driver_info, + progress_callback=self.onProgress, + cancel_event=self.__cancel_event + ) + if self.__cancelled: + self.cancelled.emit() + return + if self.__driver_path: + self.finished.emit(self.__driver_path, "") + else: + self.error.emit("下载失败: 未返回有效路径") + except Exception as e: + if not self.__cancelled: + self.error.emit(f"下载失败: {str(e)}") + + def onProgress( + self, + downloaded: float, + total: int, + speed: float, + message: str + ): + + if self.__cancel_event.is_set(): + self.__cancelled = True + if not self.__cancelled: + self.progress.emit(downloaded, total, speed, message) + + +class ALWebDriverDownloadDialog(QDialog): + + def __init__( + self, + parent: Optional[QDialog] = None, + driver_dir: str = "" + ): + """ + Web driver download dialog. + + Args: + parent: Parent widget. + driver_dir: Driver directory path. + """ + + super().__init__(parent) + + self.__driver_dir = driver_dir + self.__driver_manager: Optional[WebDriverManager] = None + self.__confirmed = False + self.__selected_driver_info: Optional[WebDriverInfo] = None + self.__driver_infos: list[WebDriverInfo] = [] + self.__download_thread: Optional[DownloadWorker] = None + + self.setupUi() + self.connectSignals() + self.initializeDriverManager() + self.refreshDriverList() + + def showEvent( + self, + event + ): + + result = super().showEvent(event) + if self.parent(): + screen_rect = self.screen().geometry() + target_pos = self.parent().geometry().center() + target_pos.setX(target_pos.x() - self.width()//2) + target_pos.setY(target_pos.y() - self.height()//2) + if target_pos.x() < 0: + target_pos.setX(0) + if target_pos.x() + self.width() > screen_rect.width(): + target_pos.setX(screen_rect.width() - self.width()) + if target_pos.y() < 0: + target_pos.setY(0) + if target_pos.y() + self.height() > screen_rect.height(): + target_pos.setY(screen_rect.height() - self.height()) + self.move(target_pos) + return result + + def setupUi( + self + ): + + self.setModal(True) + self.setMaximumHeight(240) + self.setMinimumHeight(240) + self.setWindowTitle("浏览器驱动下载 - AutoLibrary") + + self.MainLayout = QVBoxLayout(self) + self.MainLayout.setContentsMargins(5, 5, 5, 5) + self.MainLayout.setSpacing(5) + + self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:") + self.MainLayout.addWidget(self.BrowserCountLabel) + + self.DriverInfoLayout = QHBoxLayout() + self.DriverInfoLayout.setSpacing(5) + self.DriverComboBox = QComboBox() + self.DriverInfoLayout.addWidget(self.DriverComboBox) + self.StatusLabel = ALStatusLabel() + self.StatusLabel.setFixedSize(32, 32) + self.DriverInfoLayout.addWidget(self.StatusLabel) + self.MainLayout.addLayout(self.DriverInfoLayout) + + self.DetailLayout = QVBoxLayout() + self.DetailLayout.setSpacing(5) + self.DetailLayout.setContentsMargins(5, 5, 5, 5) + self.BrowserTypeLabel = QLabel("类型:") + self.DetailLayout.addWidget(self.BrowserTypeLabel) + self.VersionLabel = QLabel("版本:") + self.DetailLayout.addWidget(self.VersionLabel) + self.PathLabel = QLineEdit() + self.PathLabel.setReadOnly(True) + self.PathLabel.setText("路径:未安装") + self.DetailLayout.addWidget(self.PathLabel) + self.MainLayout.addLayout(self.DetailLayout) + + self.Line = QFrame() + self.Line.setFrameShape(QFrame.Shape.HLine) + self.Line.setFrameShadow(QFrame.Shadow.Sunken) + self.MainLayout.addWidget(self.Line) + self.ProgressBar = QProgressBar() + self.ProgressBar.setValue(0) + self.ProgressBar.setTextVisible(False) + self.MainLayout.addWidget(self.ProgressBar) + self.ProgressText = QLabel("") + self.ProgressText.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.MainLayout.addWidget(self.ProgressText) + self.ControlLayout = QHBoxLayout() + self.ControlLayout.setSpacing(8) + self.ControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight) + self.RefreshButton = QPushButton("刷新") + self.RefreshButton.setFixedSize(80, 25) + self.DownloadButton = QPushButton("下载驱动") + self.DownloadButton.setFixedSize(80, 25) + self.DeleteButton = QPushButton("删除驱动") + self.DeleteButton.setFixedSize(80, 25) + self.CancelButton = QPushButton("取消") + self.CancelButton.setFixedSize(80, 25) + self.ConfirmButton = QPushButton("确认") + self.ConfirmButton.setFixedSize(80, 25) + self.ConfirmButton.setEnabled(False) + + self.ControlLayout.addWidget(self.RefreshButton) + self.ControlLayout.addWidget(self.DownloadButton) + self.ControlLayout.addWidget(self.DeleteButton) + self.ControlLayout.addWidget(self.CancelButton) + self.ControlLayout.addWidget(self.ConfirmButton) + self.MainLayout.addLayout(self.ControlLayout) + + + def connectSignals( + self + ): + + self.RefreshButton.clicked.connect(self.onRefreshButtonClicked) + self.DownloadButton.clicked.connect(self.onDownloadButtonClicked) + self.DeleteButton.clicked.connect(self.onDeleteButtonClicked) + self.CancelButton.clicked.connect(self.onCancelButtonClicked) + self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) + self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged) + + + def initializeDriverManager( + self + ): + + try: + self.__driver_manager = webdriver_manager_instance(self.__driver_dir) + except ValueError as e: + QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}") + self.reject() + + + def refreshDriverList( + self + ): + + if not self.__driver_manager: + return + self.__driver_manager.refresh() + self.__driver_infos = self.__driver_manager.getDriverInfos() + self.DriverComboBox.clear() + for driver_info in self.__driver_infos: + display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}" + self.DriverComboBox.addItem(display_text) + count = len(self.__driver_infos) + self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:") + if self.__driver_infos: + self.onDriverComboBoxChanged(0) + + + def onDriverComboBoxChanged( + self, + index: int + ): + + if not self.__driver_infos or index < 0 or index >= len(self.__driver_infos): + return + driver_info = self.__driver_infos[index] + self.updateDriverInfoDisplay(driver_info) + self.updateButtonStates(driver_info) + + + @Slot() + def onRefreshButtonClicked( + self + ): + + self.refreshDriverList() + + + @Slot() + def onDeleteButtonClicked( + self + ): + + index = self.DriverComboBox.currentIndex() + if index < 0 or index >= len(self.__driver_infos): + return + driver_info = self.__driver_infos[index] + if driver_info.driver_status.name != "INSTALLED": + QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除") + return + reply = QMessageBox.question( + self, + "确认删除 - AutoLibrary", + f"确定要删除 {driver_info.driver_type.value} 驱动吗 ?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.No: + return + try: + self.__driver_manager.uninstallDriver(driver_info) + self.refreshDriverList() + QMessageBox.information(self, "删除成功 - AutoLibrary", "驱动已成功删除") + except Exception as e: + QMessageBox.critical(self, "删除失败 - AutoLibrary", f"删除驱动时出错:\n{str(e)}") + + @Slot() + def onDownloadButtonClicked( + self + ): + + index = self.DriverComboBox.currentIndex() + if index < 0 or index >= len(self.__driver_infos): + return + driver_info = self.__driver_infos[index] + if driver_info.driver_status.name == "INSTALLED": + return + self.StatusLabel.status = ALStatusLabel.Status.RUNNING + self.DownloadButton.setEnabled(False) + self.RefreshButton.setEnabled(False) + self.DriverComboBox.setEnabled(False) + self.ProgressBar.setValue(0) + self.ProgressText.setText("正在下载驱动...") + self.__download_thread = DownloadWorker(self.__driver_manager, driver_info) + self.__download_thread.progress.connect(self.onDownloadProgress) + self.__download_thread.finished.connect(self.onDownloadFinished) + self.__download_thread.error.connect(self.onDownloadError) + self.__download_thread.cancelled.connect(self.onDownloadCancelled) + self.__download_thread.start() + + @Slot() + def onDownloadProgress( + self, + downloaded: float, + total: int, + speed: float, + message: str + ): + + progress = downloaded + self.ProgressBar.setValue(progress) + if speed >= 1024: + speed_text = f"{speed/1024:.1f} MB/s" + else: + speed_text = f"{speed:.1f} KB/s" + progress_text = f"{message}... {downloaded:.1f}% - {speed_text}" + self.ProgressText.setText(progress_text) + + @Slot() + def onDownloadFinished( + self + ): + + self.ProgressBar.setValue(100) + self.ProgressText.setText("下载完成 !") + self.StatusLabel.status = ALStatusLabel.Status.SUCCESS + index = self.DriverComboBox.currentIndex() + if 0 <= index < len(self.__driver_infos): + driver_info = self.__driver_infos[index] + self.updateDriverInfoDisplay(driver_info) + self.__download_thread = None + self.ConfirmButton.setEnabled(True) + self.DownloadButton.setEnabled(False) + self.RefreshButton.setEnabled(True) + self.DriverComboBox.setEnabled(True) + self.DeleteButton.setEnabled(True) + + @Slot() + def onDownloadError( + self, + error_message: str + ): + + self.StatusLabel.status = ALStatusLabel.Status.FAILURE + QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message) + self.DownloadButton.setEnabled(True) + self.RefreshButton.setEnabled(True) + self.DriverComboBox.setEnabled(True) + self.CancelButton.setEnabled(True) + + + @Slot() + def onDownloadCancelled( + self + ): + + if self.__download_thread: + self.__download_thread.wait(3000) + self.__download_thread = None + index = self.DriverComboBox.currentIndex() + if 0 <= index < len(self.__driver_infos): + driver_info = self.__driver_infos[index] + self.__driver_manager.cancelDriverDownload(driver_info) + self.updateDriverInfoDisplay(driver_info) + self.ProgressText.setText("下载已取消") + self.ProgressBar.setValue(0) + self.StatusLabel.status = ALStatusLabel.Status.WAITING + self.DownloadButton.setEnabled(True) + self.RefreshButton.setEnabled(True) + self.DriverComboBox.setEnabled(True) + self.CancelButton.setEnabled(True) + self.DeleteButton.setEnabled(False) + + + @Slot() + def onConfirmButtonClicked( + self + ): + + index = self.DriverComboBox.currentIndex() + if index < 0 or index >= len(self.__driver_infos): + return + driver_info = self.__driver_infos[index] + if driver_info.driver_status.name != "INSTALLED": + return + self.__selected_driver_info = driver_info + self.__confirmed = True + self.accept() + + + @Slot() + def onCancelButtonClicked( + self + ): + + if self.__download_thread: + reply = QMessageBox.question( + self, + "确认取消 - AutoLibrary", + "正在下载中, 确定要取消下载吗 ?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.Yes: + self.__download_thread.cancel() + else: + self.__confirmed = False + self.__selected_driver_info = None + self.reject() + + + def closeEvent( + self, + event: QCloseEvent + ): + + if self.__download_thread and self.__download_thread.isRunning(): + reply = QMessageBox.question( + self, + "确认关闭 - AutoLibrary", + "驱动正在下载中, 确定要取消并关闭对话框吗 ?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.No: + event.ignore() + return + self.__download_thread.cancel() + self.__download_thread.wait(5000) + if not self.__confirmed: + self.__selected_driver_info = None + event.accept() + super().closeEvent(event) + + + def getSelectedDriverInfo( + self + ) -> Optional[WebDriverInfo]: + + return self.__selected_driver_info + + + def updateDriverInfoDisplay( + self, + driver_info: WebDriverInfo + ): + + if driver_info.driver_type == WebDriverType.CHROME: + driver_type = "Google Chrome" + elif driver_info.driver_type == WebDriverType.FIREFOX: + driver_type = "Mozilla Firefox" + elif driver_info.driver_type == WebDriverType.EDGE: + driver_type = "Microsoft Edge" + else: + driver_type = "未知" + self.BrowserTypeLabel.setText(f"类型:{driver_type}") + self.VersionLabel.setText(f"版本:{driver_info.driver_version}") + if driver_info.driver_path: + self.PathLabel.setText(str(driver_info.driver_path)) + else: + self.PathLabel.setText("未安装") + match driver_info.driver_status.name: + case "NOT_INSTALLED": + self.StatusLabel.status = ALStatusLabel.Status.WAITING + case "INSTALLED": + self.StatusLabel.status = ALStatusLabel.Status.SUCCESS + case "DOWNLOADING": + self.StatusLabel.status = ALStatusLabel.Status.RUNNING + case "ERROR": + self.StatusLabel.status = ALStatusLabel.Status.FAILURE + + + def updateButtonStates( + self, + driver_info: WebDriverInfo + ): + + if driver_info.driver_status.name == "INSTALLED": + self.DownloadButton.setEnabled(False) + self.DeleteButton.setEnabled(True) + self.ConfirmButton.setEnabled(True) + else: + self.DeleteButton.setEnabled(False) + self.DownloadButton.setEnabled(True) + self.ConfirmButton.setEnabled(False) From aef28b6d5e3c81aaa001bc116158559e7d2c8c3c Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 00:55:09 +0800 Subject: [PATCH 26/30] =?UTF-8?q?feat(ALConfigWidget):=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E6=B5=8F=E8=A7=88=E5=99=A8=E9=A9=B1=E5=8A=A8=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD=E5=88=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALConfigWidget.py | 17 ++ src/gui/resources/ui/ALConfigWidget.ui | 353 ++++++++++++++----------- 2 files changed, 209 insertions(+), 161 deletions(-) diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index b0768ea..97f190e 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -29,6 +29,7 @@ from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog from gui.ALSeatMapTable import ALSeatMapTable from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType +from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog class ALConfigWidget(QWidget, Ui_ALConfigWidget): @@ -80,6 +81,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.AddUserButton.clicked.connect(self.onAddUserButtonClicked) self.DelUserButton.clicked.connect(self.onDelUserButtonClicked) self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked) + self.AutoDownloadWebDriverButton.clicked.connect(self.onAutoDownloadWebDriverButtonClicked) self.BrowseCurrentRunConfigButton.clicked.connect(self.onBrowseCurrentRunConfigButtonClicked) self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked) self.BrowseExportRunConfigButton.clicked.connect(self.onBrowseExportRunConfigButtonClicked) @@ -948,6 +950,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): if browser_driver_path: self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path)) + + @Slot() + def onAutoDownloadWebDriverButtonClicked( + self + ): + + dialog = ALWebDriverDownloadDialog(self) + dialog.show() + dialog.exec_() + selected_driver_info = dialog.getSelectedDriverInfo() + if selected_driver_info and selected_driver_info.driver_path: + self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value) + self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path))) + + @Slot() def onBrowseCurrentRunConfigButtonClicked( self diff --git a/src/gui/resources/ui/ALConfigWidget.ui b/src/gui/resources/ui/ALConfigWidget.ui index ea41727..a272576 100644 --- a/src/gui/resources/ui/ALConfigWidget.ui +++ b/src/gui/resources/ui/ALConfigWidget.ui @@ -1233,12 +1233,31 @@ - - - - 浏览器设置 + + + + + 0 + 270 + - + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + 0 + + + + + + + 运行模式 + + 5 @@ -1255,162 +1274,59 @@ 3 - + - 80 + 100 25 - 80 + 100 25 - 浏览器类型: + 自动预约 - + - 80 + 100 25 - 80 - 25 - - - - <html><head/><body><p>脚本运行使用的浏览器类型</p></body></html> - - - <html><head/><body><p><br/></p></body></html> - - - edge - - - 0 - - - 3 - - - 3 - - - - edge - - - - - chrome - - - - - firefox - - - - - - - - - 80 - 25 - - - - - 80 + 100 25 - 驱动路径: + 自动签到 - - - 5 - - - - - - 250 - 25 - - - - - 300 - 25 - - - - <html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html> - - - <html><head/><body><p><br/></p></body></html> - - - - - - - - 35 - 25 - - - - - 35 - 25 - - - - ... - - - - - - - + - 0 + 100 25 - 16777215 + 100 25 - - <html><head/><body><p>运行时不显示浏览器</p></body></html> - - - <html><head/><body><p><br/></p></body></html> - - 无头模式 + 自动续约 @@ -1529,15 +1445,12 @@ - - + + - 运行模式 + 浏览器设置 - - - 5 - + 3 @@ -1550,85 +1463,203 @@ 3 - - + + 5 + + + - 100 + 80 25 - 100 + 80 25 - 自动预约 + 浏览器类型: - - + + - 100 + 80 25 - 100 + 80 + 25 + + + + <html><head/><body><p>脚本运行使用的浏览器类型</p></body></html> + + + <html><head/><body><p><br/></p></body></html> + + + edge + + + 0 + + + 3 + + + 3 + + + + edge + + + + + chrome + + + + + firefox + + + + + + + + + 175 + 25 + + + + + 175 25 - 自动签到 + 驱动路径: - - + + + + 5 + + + + + + 250 + 25 + + + + + 300 + 25 + + + + <html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html> + + + <html><head/><body><p><br/></p></body></html> + + + + + + + + 35 + 25 + + + + + 35 + 25 + + + + ... + + + + + + + - 100 + 0 25 - 100 + 16777215 25 + + <html><head/><body><p>运行时不显示浏览器</p></body></html> + + + <html><head/><body><p><br/></p></body></html> + - 自动续约 + 无头模式 + + + + + + + + 0 + 0 + + + + + 120 + 25 + + + + + 120 + 25 + + + + Qt::LayoutDirection::LeftToRight + + + 自动下载驱动 + + + - - - - - 0 - 270 - - - - QFrame::Shape::NoFrame - - - QFrame::Shadow::Plain - - - 0 - - - From 62c1ecdb077729d1b5d80060d15edad793c116e8 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 00:55:17 +0800 Subject: [PATCH 27/30] =?UTF-8?q?fix(LogManager):=20=E4=BF=AE=E5=A4=8D=20C?= =?UTF-8?q?allerInfoFormatter=20=E4=B8=AD=20lineno=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/managers/log/LogManager.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/managers/log/LogManager.py b/src/managers/log/LogManager.py index a931ada..28a860c 100644 --- a/src/managers/log/LogManager.py +++ b/src/managers/log/LogManager.py @@ -55,12 +55,17 @@ class CallerInfoFormatter(logging.Formatter): if depth < len(record.stack_list): frame = record.stack_list[-depth-1] record.filename = os.path.basename(frame.filename) - record.lineno = frame.lineno + record.lineno = int(frame.lineno) record.funcName = frame.name record.name = record.name[-15:].ljust(15) record.levelname = record.levelname.ljust(8) record.filename = record.filename[-20:].ljust(20) - record.lineno = f"{record.lineno:04d}" + # Ensure lineno is always integer before formatting + try: + lineno_int = int(record.lineno) + except (ValueError, TypeError): + lineno_int = 0 + record.lineno = f"{lineno_int:04d}" return super().format(record) @@ -178,7 +183,7 @@ def instance( _log_manager_instance = LogManager(log_dir) else: if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir): - raise ValueError("LogManager 的实例已初始化,不能使用不同的日志目录") + raise ValueError("LogManager 的实例已初始化, 不能使用不同的日志目录") return _log_manager_instance @@ -187,5 +192,5 @@ def getLogger( ) -> logging.Logger: if _log_manager_instance is None: - raise RuntimeError("LogManager 未初始化,请先调用 LogManager.instance(log_dir) 初始化") + raise RuntimeError("LogManager 未初始化, 请先调用 LogManager.instance(log_dir) 初始化") return _log_manager_instance.getLogger(name) From 4924f4b031ec8f153ee5af4746257babcf062b0c Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 01:52:20 +0800 Subject: [PATCH 28/30] =?UTF-8?q?fix(WebDriverDownloader):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=B8=8B=E8=BD=BD=E9=80=9F=E5=BA=A6=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=B9=B6=E6=94=B9=E7=94=A8=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E9=97=B4=E9=9A=94=E8=A7=A6=E5=8F=91=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将回调触发条件由进度变化量改为固定时间间隔(0.5s), 避免突发数据导致速度虚高 - 修正 total_size == 0 为 total_size <= 0, 完善边界判断 - 重命名变量提升可读性(last_time/last_size -> last_callback_time/last_callback_size) --- src/managers/driver/WebDriverDownloader.py | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/managers/driver/WebDriverDownloader.py b/src/managers/driver/WebDriverDownloader.py index a3d9003..c4e3279 100644 --- a/src/managers/driver/WebDriverDownloader.py +++ b/src/managers/driver/WebDriverDownloader.py @@ -306,13 +306,12 @@ class WebDriverDownloader: total_size = int(response.headers.get('Content-Length', 0)) if response.status_code == 206: # Partial Content - server supports Range total_size += downloaded_size - # download file with progress callback and speed calculation - start_time = time.time() - last_time = start_time - last_size = downloaded_size - last_progress = 0.0 + last_callback_time = time.time() + last_callback_size = downloaded_size + callback_interval = 0.1 with open(self.download_path, mode) as f: for chunk in response.iter_content(CHUNK_SIZE): + current_time = time.time() if cancel_event and cancel_event.is_set(): response.close() return False @@ -320,20 +319,18 @@ class WebDriverDownloader: continue f.write(chunk) downloaded_size += len(chunk) - if not progress_callback or total_size == 0: + if not progress_callback or total_size <= 0: continue - current_time = time.time() current_progress = (downloaded_size/total_size)*98.0 - if current_progress - last_progress >= 1.0 or current_progress == 98.0: - elapsed = current_time - last_time + if current_time - last_callback_time >= callback_interval or current_progress >= 98.0: + elapsed = current_time - last_callback_time if elapsed > 0: - speed = (downloaded_size - last_size)/elapsed/1024.0 # KB/s + speed = (downloaded_size - last_callback_size)/(elapsed*1024.0) else: speed = 0.0 progress_callback(current_progress, 100, speed, "下载中...") - last_progress = current_progress - last_size = downloaded_size - last_time = current_time + last_callback_time = current_time + last_callback_size = downloaded_size if total_size > 0 and self.download_path.stat().st_size < total_size: raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节") return True From 5c393595d779b63b2143bd7d4ead54d906366777 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 01:53:22 +0800 Subject: [PATCH 29/30] =?UTF-8?q?fix(ALWebDriverDownloadDialog):=20?= =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=E4=BF=A1=E5=8F=B7=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E4=B8=8E=20QThread=20=E5=86=85=E7=BD=AE=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E5=B9=B6=E6=94=B9=E8=BF=9B=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALWebDriverDownloadDialog.py | 52 +++++++++++++++++++--------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py index 6f34702..0891603 100644 --- a/src/gui/ALWebDriverDownloadDialog.py +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -35,9 +35,9 @@ class DownloadWorker(QThread): """ progress = Signal(float, int, float, str) - finished = Signal(object, str) - error = Signal(str) - cancelled = Signal() + downloadFinished = Signal(object, str) + downloadError = Signal(str) + downloadCancelled = Signal() def __init__( self, @@ -66,7 +66,7 @@ class DownloadWorker(QThread): ): try: if self.__cancelled: - self.cancelled.emit() + self.downloadCancelled.emit() return self.__driver_path = self.__driver_manager.installDriver( self.__driver_info, @@ -74,15 +74,15 @@ class DownloadWorker(QThread): cancel_event=self.__cancel_event ) if self.__cancelled: - self.cancelled.emit() + self.downloadCancelled.emit() return if self.__driver_path: - self.finished.emit(self.__driver_path, "") + self.downloadFinished.emit(self.__driver_path, "") else: - self.error.emit("下载失败: 未返回有效路径") + self.downloadError.emit("下载失败: 未返回有效路径") except Exception as e: if not self.__cancelled: - self.error.emit(f"下载失败: {str(e)}") + self.downloadError.emit(f"下载失败: {str(e)}") def onProgress( self, @@ -97,6 +97,20 @@ class DownloadWorker(QThread): if not self.__cancelled: self.progress.emit(downloaded, total, speed, message) + def stop( + self + ): + """ + Cancel and wait for the thread to finish. + Must only be called from the main thread. + """ + + self.cancel() + if not self.isFinished(): + if not self.wait(5000): + self.terminate() + self.wait() + class ALWebDriverDownloadDialog(QDialog): @@ -328,9 +342,10 @@ class ALWebDriverDownloadDialog(QDialog): self.ProgressText.setText("正在下载驱动...") self.__download_thread = DownloadWorker(self.__driver_manager, driver_info) self.__download_thread.progress.connect(self.onDownloadProgress) - self.__download_thread.finished.connect(self.onDownloadFinished) - self.__download_thread.error.connect(self.onDownloadError) - self.__download_thread.cancelled.connect(self.onDownloadCancelled) + self.__download_thread.downloadFinished.connect(self.onDownloadFinished) + self.__download_thread.downloadError.connect(self.onDownloadError) + self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled) + self.__download_thread.finished.connect(self.__onThreadFinished) self.__download_thread.start() @Slot() @@ -363,7 +378,6 @@ class ALWebDriverDownloadDialog(QDialog): if 0 <= index < len(self.__driver_infos): driver_info = self.__driver_infos[index] self.updateDriverInfoDisplay(driver_info) - self.__download_thread = None self.ConfirmButton.setEnabled(True) self.DownloadButton.setEnabled(False) self.RefreshButton.setEnabled(True) @@ -389,9 +403,6 @@ class ALWebDriverDownloadDialog(QDialog): self ): - if self.__download_thread: - self.__download_thread.wait(3000) - self.__download_thread = None index = self.DriverComboBox.currentIndex() if 0 <= index < len(self.__driver_infos): driver_info = self.__driver_infos[index] @@ -458,13 +469,20 @@ class ALWebDriverDownloadDialog(QDialog): if reply == QMessageBox.StandardButton.No: event.ignore() return - self.__download_thread.cancel() - self.__download_thread.wait(5000) + self.__download_thread.stop() if not self.__confirmed: self.__selected_driver_info = None event.accept() super().closeEvent(event) + def __onThreadFinished( + self + ): + + if self.__download_thread: + self.__download_thread.deleteLater() + self.__download_thread = None + def getSelectedDriverInfo( self From 2c90008fcd3d556573dce42e567545fb9123e560 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 21 Mar 2026 17:22:25 +0800 Subject: [PATCH 30/30] =?UTF-8?q?refactor(WebDriverManager,=20ALWebDriverD?= =?UTF-8?q?ownloadDialog):=20=E9=87=8D=E5=91=BD=E5=90=8D=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9E=9A=E4=B8=BE=E5=B9=B6=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E7=8A=B6=E6=80=81=E6=84=9F=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALWebDriverDownloadDialog.py | 124 ++++++++++++++++-------- src/managers/driver/WebDriverManager.py | 26 ++--- 2 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py index 0891603..59571c5 100644 --- a/src/gui/ALWebDriverDownloadDialog.py +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -24,7 +24,8 @@ from PySide6.QtGui import ( from managers.driver.WebDriverManager import ( instance as webdriver_manager_instance, - WebDriverManager, WebDriverInfo, WebDriverType + WebDriverManager, WebDriverInfo, WebDriverType, + WebDriverStatus ) from gui.ALStatusLabel import ALStatusLabel @@ -141,6 +142,7 @@ class ALWebDriverDownloadDialog(QDialog): self.initializeDriverManager() self.refreshDriverList() + def showEvent( self, event @@ -163,6 +165,7 @@ class ALWebDriverDownloadDialog(QDialog): self.move(target_pos) return result + def setupUi( self ): @@ -267,13 +270,16 @@ class ALWebDriverDownloadDialog(QDialog): self.__driver_manager.refresh() self.__driver_infos = self.__driver_manager.getDriverInfos() self.DriverComboBox.clear() - for driver_info in self.__driver_infos: + installed_idx = 0 + for i, driver_info in enumerate(self.__driver_infos): + if driver_info.driver_status == WebDriverStatus.INSTALLED: + installed_idx = i # get the installed driver index display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}" self.DriverComboBox.addItem(display_text) count = len(self.__driver_infos) self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:") if self.__driver_infos: - self.onDriverComboBoxChanged(0) + self.DriverComboBox.setCurrentIndex(installed_idx) def onDriverComboBoxChanged( @@ -285,6 +291,7 @@ class ALWebDriverDownloadDialog(QDialog): return driver_info = self.__driver_infos[index] self.updateDriverInfoDisplay(driver_info) + self.updateProgressBarStates(driver_info) self.updateButtonStates(driver_info) @@ -328,18 +335,21 @@ class ALWebDriverDownloadDialog(QDialog): self ): + self.DriverComboBox.setEnabled(False) index = self.DriverComboBox.currentIndex() if index < 0 or index >= len(self.__driver_infos): return driver_info = self.__driver_infos[index] - if driver_info.driver_status.name == "INSTALLED": + if driver_info.driver_status == WebDriverStatus.INSTALLED: return - self.StatusLabel.status = ALStatusLabel.Status.RUNNING - self.DownloadButton.setEnabled(False) - self.RefreshButton.setEnabled(False) - self.DriverComboBox.setEnabled(False) - self.ProgressBar.setValue(0) - self.ProgressText.setText("正在下载驱动...") + driver_info.driver_status = WebDriverStatus.DOWNLOADING # we set this only to update + # the display, and we will set to not installed in the download thread + self.updateDriverInfoDisplay(driver_info) + self.updateProgressBarStates(driver_info) + self.ProgressText.setText("准备开始下载...") + self.updateButtonStates(driver_info) + # set to not installed + driver_info.driver_status = WebDriverStatus.NOT_INSTALLED self.__download_thread = DownloadWorker(self.__driver_manager, driver_info) self.__download_thread.progress.connect(self.onDownloadProgress) self.__download_thread.downloadFinished.connect(self.onDownloadFinished) @@ -371,18 +381,14 @@ class ALWebDriverDownloadDialog(QDialog): self ): - self.ProgressBar.setValue(100) - self.ProgressText.setText("下载完成 !") - self.StatusLabel.status = ALStatusLabel.Status.SUCCESS + self.DriverComboBox.setEnabled(True) index = self.DriverComboBox.currentIndex() if 0 <= index < len(self.__driver_infos): driver_info = self.__driver_infos[index] + driver_info.driver_status = WebDriverStatus.INSTALLED self.updateDriverInfoDisplay(driver_info) - self.ConfirmButton.setEnabled(True) - self.DownloadButton.setEnabled(False) - self.RefreshButton.setEnabled(True) - self.DriverComboBox.setEnabled(True) - self.DeleteButton.setEnabled(True) + self.updateProgressBarStates(driver_info) + self.updateButtonStates(driver_info) @Slot() def onDownloadError( @@ -390,12 +396,15 @@ class ALWebDriverDownloadDialog(QDialog): error_message: str ): - self.StatusLabel.status = ALStatusLabel.Status.FAILURE - QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message) - self.DownloadButton.setEnabled(True) - self.RefreshButton.setEnabled(True) self.DriverComboBox.setEnabled(True) - self.CancelButton.setEnabled(True) + index = self.DriverComboBox.currentIndex() + if 0 <= index < len(self.__driver_infos): + driver_info = self.__driver_infos[index] + driver_info.driver_status = WebDriverStatus.ERROR + self.updateDriverInfoDisplay(driver_info) + self.updateProgressBarStates(driver_info) + self.updateButtonStates(driver_info) + QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message) @Slot() @@ -403,19 +412,16 @@ class ALWebDriverDownloadDialog(QDialog): self ): + self.DriverComboBox.setEnabled(True) index = self.DriverComboBox.currentIndex() if 0 <= index < len(self.__driver_infos): driver_info = self.__driver_infos[index] self.__driver_manager.cancelDriverDownload(driver_info) + driver_info.driver_status = WebDriverStatus.NOT_INSTALLED self.updateDriverInfoDisplay(driver_info) - self.ProgressText.setText("下载已取消") - self.ProgressBar.setValue(0) - self.StatusLabel.status = ALStatusLabel.Status.WAITING - self.DownloadButton.setEnabled(True) - self.RefreshButton.setEnabled(True) - self.DriverComboBox.setEnabled(True) - self.CancelButton.setEnabled(True) - self.DeleteButton.setEnabled(False) + self.updateProgressBarStates(driver_info) + self.updateButtonStates(driver_info) + self.ProgressText.setText("下载已取消") @Slot() @@ -427,7 +433,7 @@ class ALWebDriverDownloadDialog(QDialog): if index < 0 or index >= len(self.__driver_infos): return driver_info = self.__driver_infos[index] - if driver_info.driver_status.name != "INSTALLED": + if driver_info.driver_status != WebDriverStatus.INSTALLED: return self.__selected_driver_info = driver_info self.__confirmed = True @@ -510,27 +516,61 @@ class ALWebDriverDownloadDialog(QDialog): self.PathLabel.setText(str(driver_info.driver_path)) else: self.PathLabel.setText("未安装") - match driver_info.driver_status.name: - case "NOT_INSTALLED": + match driver_info.driver_status: + case WebDriverStatus.NOT_INSTALLED: self.StatusLabel.status = ALStatusLabel.Status.WAITING - case "INSTALLED": + case WebDriverStatus.INSTALLED: self.StatusLabel.status = ALStatusLabel.Status.SUCCESS - case "DOWNLOADING": + case WebDriverStatus.DOWNLOADING: self.StatusLabel.status = ALStatusLabel.Status.RUNNING - case "ERROR": + case WebDriverStatus.ERROR: self.StatusLabel.status = ALStatusLabel.Status.FAILURE + def updateProgressBarStates( + self, + driver_info: WebDriverInfo + ): + + if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED: + self.ProgressBar.setValue(0) + self.ProgressText.setText("未安装") + elif driver_info.driver_status == WebDriverStatus.INSTALLED: + self.ProgressBar.setValue(100) + self.ProgressText.setText("已安装") + elif driver_info.driver_status == WebDriverStatus.DOWNLOADING: + pass # update by worker thread + elif driver_info.driver_status == WebDriverStatus.ERROR: + self.ProgressBar.setValue(0) + self.ProgressText.setText("下载失败") + + def updateButtonStates( self, driver_info: WebDriverInfo ): - if driver_info.driver_status.name == "INSTALLED": - self.DownloadButton.setEnabled(False) - self.DeleteButton.setEnabled(True) - self.ConfirmButton.setEnabled(True) - else: + if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED: + self.RefreshButton.setEnabled(True) self.DeleteButton.setEnabled(False) self.DownloadButton.setEnabled(True) + self.CancelButton.setEnabled(True) + self.ConfirmButton.setEnabled(False) + elif driver_info.driver_status == WebDriverStatus.INSTALLED: + self.RefreshButton.setEnabled(True) + self.DownloadButton.setEnabled(False) + self.DeleteButton.setEnabled(True) + self.CancelButton.setEnabled(True) + self.ConfirmButton.setEnabled(True) + elif driver_info.driver_status == WebDriverStatus.DOWNLOADING: + self.RefreshButton.setEnabled(False) + self.DownloadButton.setEnabled(False) + self.DeleteButton.setEnabled(False) + self.CancelButton.setEnabled(True) + self.ConfirmButton.setEnabled(False) + elif driver_info.driver_status == WebDriverStatus.ERROR: + self.RefreshButton.setEnabled(True) + self.DownloadButton.setEnabled(True) + self.DeleteButton.setEnabled(False) + self.CancelButton.setEnabled(True) self.ConfirmButton.setEnabled(False) diff --git a/src/managers/driver/WebDriverManager.py b/src/managers/driver/WebDriverManager.py index 3e70bd8..7846560 100644 --- a/src/managers/driver/WebDriverManager.py +++ b/src/managers/driver/WebDriverManager.py @@ -24,7 +24,7 @@ from managers.driver.WebDriverDownloader import ( ) -class DriverStatus(Enum): +class WebDriverStatus(Enum): """ Web driver status. """ @@ -57,7 +57,7 @@ class WebDriverInfo: self.driver_version = "" self.browser_version = "" self.driver_path: Optional[Path] = None - self.driver_status = DriverStatus.NOT_INSTALLED + self.driver_status = WebDriverStatus.NOT_INSTALLED class WebDriverManager: @@ -115,7 +115,7 @@ class WebDriverManager: driver_path = self._getDriverPath(driver_info) if driver_path and driver_path.exists() and driver_path.is_file(): driver_info.driver_path = driver_path - driver_info.driver_status = DriverStatus.INSTALLED + driver_info.driver_status = WebDriverStatus.INSTALLED def _mapWebBrowserTypeToDriver( @@ -321,7 +321,7 @@ class WebDriverManager: driver_info: WebDriverInfo ) -> Optional[Path]: - if driver_info and driver_info.driver_status == DriverStatus.INSTALLED: + if driver_info and driver_info.driver_status == WebDriverStatus.INSTALLED: return driver_info.driver_path return None @@ -339,7 +339,7 @@ class WebDriverManager: progress_callback(0, 0, 0, "未找到浏览器信息") else: raise ValueError("未找到浏览器信息") - if driver_info and driver_info.driver_status == DriverStatus.DOWNLOADING: + if driver_info and driver_info.driver_status == WebDriverStatus.DOWNLOADING: if progress_callback: progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动正在下载中") else: @@ -375,19 +375,19 @@ class WebDriverManager: else: raise ValueError(f"不支持的 Web Driver 类型") with self.__lock: - driver_info.driver_status = DriverStatus.DOWNLOADING + driver_info.driver_status = WebDriverStatus.DOWNLOADING driver_path = downloader.download(progress_callback=progress_callback, cancel_event=cancel_event) with self.__lock: if driver_path: driver_info.driver_path = driver_path driver_info.driver_version = driver_version - driver_info.driver_status = DriverStatus.INSTALLED + driver_info.driver_status = WebDriverStatus.INSTALLED else: - driver_info.driver_status = DriverStatus.ERROR + driver_info.driver_status = WebDriverStatus.ERROR return driver_path except Exception as e: with self.__lock: - driver_info.driver_status = DriverStatus.ERROR + driver_info.driver_status = WebDriverStatus.ERROR raise e @@ -406,7 +406,7 @@ class WebDriverManager: shutil.rmtree(download_dir, ignore_errors=True) with self.__lock: driver_info.driver_path = None - driver_info.driver_status = DriverStatus.NOT_INSTALLED + driver_info.driver_status = WebDriverStatus.NOT_INSTALLED return True except Exception: return False @@ -424,7 +424,7 @@ class WebDriverManager: progress_callback(0, 0, 0, "未找到浏览器信息") else: raise ValueError("未找到浏览器信息") - if driver_info.driver_status != DriverStatus.INSTALLED: + if driver_info.driver_status != WebDriverStatus.INSTALLED: if progress_callback: progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动未安装") else: @@ -434,11 +434,11 @@ class WebDriverManager: driver_path.unlink() with self.__lock: driver_info.driver_path = None - driver_info.driver_status = DriverStatus.NOT_INSTALLED + driver_info.driver_status = WebDriverStatus.NOT_INSTALLED return True except Exception: with self.__lock: - driver_info.driver_status = DriverStatus.ERROR + driver_info.driver_status = WebDriverStatus.ERROR raise