From bbd97970a6adcd3a112056f41a42de29f0c98b58 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 10 May 2026 15:32:26 +0800 Subject: [PATCH 01/49] =?UTF-8?q?refactor(modules):=20=E5=B0=86=20AutoScri?= =?UTF-8?q?ptEngine=20=E7=A7=BB=E8=87=B3=20dsl/=EF=BC=8CConfigUtils=20?= =?UTF-8?q?=E7=A7=BB=E8=87=B3=20managers/config/=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=8D=95=E4=B8=80=E8=81=8C=E8=B4=A3=E5=92=8C=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=80=92=E7=BD=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/{utils => dsl}/AutoScriptEngine.py | 0 src/dsl/__init__.py | 9 +++++++++ src/gui/ALAutoScriptOrchDialog.py | 2 +- src/gui/ALConfigWidget.py | 2 +- src/gui/ALMainWindow.py | 2 +- src/gui/ALMainWorkers.py | 2 +- src/{utils => managers/config}/ConfigUtils.py | 2 +- src/utils/__init__.py | 4 +--- 8 files changed, 15 insertions(+), 8 deletions(-) rename src/{utils => dsl}/AutoScriptEngine.py (100%) create mode 100644 src/dsl/__init__.py rename src/{utils => managers/config}/ConfigUtils.py (98%) diff --git a/src/utils/AutoScriptEngine.py b/src/dsl/AutoScriptEngine.py similarity index 100% rename from src/utils/AutoScriptEngine.py rename to src/dsl/AutoScriptEngine.py diff --git a/src/dsl/__init__.py b/src/dsl/__init__.py new file mode 100644 index 0000000..878d740 --- /dev/null +++ b/src/dsl/__init__.py @@ -0,0 +1,9 @@ +""" + DSL module for the AutoLibrary project. + + Contains the AutoScript DSL engine and related components + for preprocessing user reservation data in timer tasks. + + Classes: + - AutoScriptEngine: AutoScript script engine class. +""" diff --git a/src/gui/ALAutoScriptOrchDialog.py b/src/gui/ALAutoScriptOrchDialog.py index d4b7e78..e244763 100644 --- a/src/gui/ALAutoScriptOrchDialog.py +++ b/src/gui/ALAutoScriptOrchDialog.py @@ -17,7 +17,7 @@ from PySide6.QtWidgets import ( QGroupBox, QSizePolicy ) -from utils.AutoScriptEngine import AutoScriptEngine +from dsl.AutoScriptEngine import AutoScriptEngine VARIABLE_META = AutoScriptEngine.VARIABLE_META diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index 9a203e8..d97d9b5 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -24,7 +24,7 @@ import managers.config.ConfigManager as ConfigManager from utils.JSONReader import JSONReader from utils.JSONWriter import JSONWriter -from utils.ConfigUtils import ConfigUtils +from managers.config.ConfigUtils import ConfigUtils from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 38d4402..3974b34 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -20,7 +20,7 @@ from PySide6.QtGui import ( ) from base.MsgBase import MsgBase -from utils.ConfigUtils import ConfigUtils +from managers.config.ConfigUtils import ConfigUtils from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow from gui.resources import ALResource diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index 9bc1c9f..f6003d3 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -18,7 +18,7 @@ from PySide6.QtCore import ( from base.MsgBase import MsgBase from operators.AutoLib import AutoLib from utils.JSONReader import JSONReader -from utils.AutoScriptEngine import AutoScriptEngine +from dsl.AutoScriptEngine import AutoScriptEngine class AutoLibWorker(MsgBase, QThread): diff --git a/src/utils/ConfigUtils.py b/src/managers/config/ConfigUtils.py similarity index 98% rename from src/utils/ConfigUtils.py rename to src/managers/config/ConfigUtils.py index bab72ec..8d399aa 100644 --- a/src/utils/ConfigUtils.py +++ b/src/managers/config/ConfigUtils.py @@ -43,4 +43,4 @@ class ConfigUtils: data = {"current": index, "paths": paths} auto_config[f"{cfg_type}_path"] = data cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation", auto_config) - return config_paths \ No newline at end of file + return config_paths diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 8c4cdf0..b84955c 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -5,6 +5,4 @@ - TimerUtils: Timer utils class for the AutoLibrary project. - JSONReader: JSON reader class for the AutoLibrary project. - JSONWriter: JSON writer class for the AutoLibrary project. - - ConfigUtils: Config utils class for the AutoLibrary project. - - AutoScriptEngine: AutoScript script engine class for the AutoLibrary project. -""" \ No newline at end of file +""" From 14c6db3384ea31a37dff58788d660b98e75a93c7 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 10 May 2026 16:14:20 +0800 Subject: [PATCH 02/49] =?UTF-8?q?refactor(config):=20=E5=BC=95=E5=85=A5=20?= =?UTF-8?q?ConfigPath=20=E5=80=BC=E5=AF=B9=E8=B1=A1=E6=B6=88=E9=99=A4=20Co?= =?UTF-8?q?nfigType/ConfigKey=20=E7=9A=84=E6=B6=88=E8=B4=B9=E8=80=85=20API?= =?UTF-8?q?=20=E5=86=97=E4=BD=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALConfigWidget.py | 11 +-- src/gui/ALTimerTaskManageWidget.py | 10 ++- src/interfaces/ConfigProvider.py | 117 +++++++++++++++++++++++++++ src/interfaces/__init__.py | 11 +++ src/managers/config/ConfigManager.py | 33 +++----- src/managers/config/ConfigUtils.py | 6 +- 6 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 src/interfaces/ConfigProvider.py create mode 100644 src/interfaces/__init__.py diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index d97d9b5..052b580 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -24,6 +24,7 @@ import managers.config.ConfigManager as ConfigManager from utils.JSONReader import JSONReader from utils.JSONWriter import JSONWriter +from interfaces.ConfigProvider import ConfigProvider, CfgKey from managers.config.ConfigUtils import ConfigUtils from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget @@ -43,7 +44,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ): super().__init__(parent) - self.__cfg_mgr = ConfigManager.instance() + self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.__config_data = {"run": {}, "user": {}} @@ -985,13 +986,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.setRunConfigToWidget(data) self.__config_paths["run"] = run_config_path self.CurrentRunConfigEdit.setText(run_config_path) - paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.run_path.paths", []) + paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, []) if run_config_path not in paths: paths.append(run_config_path) index = len(paths) - 1 else: index = paths.index(run_config_path) - self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths}) + self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.ROOT, {"current": index, "paths": paths}) else: QMessageBox.warning( self, @@ -1020,13 +1021,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.setUsersToTreeWidget(data) self.__config_paths["user"] = user_config_path self.CurrentUserConfigEdit.setText(user_config_path) - paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.user_path.paths", []) + paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.USER_PATH.PATHS, []) if user_config_path not in paths: paths.append(user_config_path) index = len(paths) - 1 else: index = paths.index(user_config_path) - self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths}) + self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.USER_PATH.ROOT, {"current": index, "paths": paths}) else: QMessageBox.warning( self, diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index c603903..4aef755 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -26,7 +26,9 @@ from PySide6.QtGui import ( ) import managers.config.ConfigManager as ConfigManager + from utils.TimerUtils import TimerUtils +from interfaces.ConfigProvider import ConfigProvider, CfgKey from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus @@ -190,7 +192,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ): super().__init__(parent) - self.__cfg_mgr = ConfigManager.instance() + self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__timer_tasks = [] self.__check_timer = None self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME @@ -244,7 +246,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ) -> list: try: - timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK) + timer_tasks = self.__cfg_mgr.get(CfgKey.TIMERTASK.ROOT) if timer_tasks and "timer_tasks" in timer_tasks: for task in timer_tasks["timer_tasks"]: task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S") @@ -277,7 +279,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): if "repeat_history" in task: for item in task["repeat_history"]: item["result"] = item["result"].value - self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks }) + self.__cfg_mgr.set(CfgKey.TIMERTASK.ROOT, { "timer_tasks": timer_tasks }) return True except Exception as e: QMessageBox.warning( @@ -437,7 +439,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n" f"已记录次数:{history_count}" ) - + def deleteTask( self, diff --git a/src/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py new file mode 100644 index 0000000..bf5ebb3 --- /dev/null +++ b/src/interfaces/ConfigProvider.py @@ -0,0 +1,117 @@ +# -*- 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. +""" +from dataclasses import dataclass +from enum import Enum +from typing import Any, Optional, Protocol + + +class ConfigType(Enum): + """ + Config type enum. Values represent the default filename. + """ + GLOBAL = "autolibrary.json" + BULLETIN = "bulletin.json" + TIMERTASK = "timer_task.json" + + +@dataclass(frozen=True) +class ConfigPath: + """ + A typed configuration path that carries both the config file + and the dot-separated key in a single object. + + Consumers pass this directly to ConfigProvider.get/set, + eliminating the need to import ConfigType separately. + """ + config_type: ConfigType + key: str = "" + + +class CfgKey: + """ + Type-safe hierarchical configuration key constants. + + Each leaf is a ConfigPath that can be passed directly to + ``ConfigProvider.get()`` or ``ConfigProvider.set()``. + + Usage:: + + CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS + # -> ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths") + + config.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, []) + config.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, value) + """ + + class GLOBAL: + class AUTOMATION: + ROOT = ConfigPath(ConfigType.GLOBAL, "automation") + + class RUN_PATH: + ROOT = ConfigPath(ConfigType.GLOBAL, "automation.run_path") + CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.run_path.current") + PATHS = ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths") + + class USER_PATH: + ROOT = ConfigPath(ConfigType.GLOBAL, "automation.user_path") + CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current") + PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths") + + class TIMERTASK: + ROOT = ConfigPath(ConfigType.TIMERTASK, "") + TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks") + + class BULLETIN: + ROOT = ConfigPath(ConfigType.BULLETIN, "") + BULLETIN = ConfigPath(ConfigType.BULLETIN, "bulletin") + LAST_SYNC_TIME = ConfigPath(ConfigType.BULLETIN, "last_sync_time") + + +class ConfigProvider(Protocol): + """ + Abstract interface for configuration storage access. + + Concrete implementations (e.g. ConfigManager) conform to + this protocol structurally rather than through explicit + inheritance. + """ + + def get( + self, + key: ConfigPath, + default: Optional[Any] = None + ) -> Any: + """ + Retrieve a configuration value. + + Args: + key: A ConfigPath object specifying which config file + and key to read from. + default: Fallback value if the key is not found. + + Returns: + The configuration value at the given key path. + """ + ... + + def set( + self, + key: ConfigPath, + value: Any = None + ) -> None: + """ + Set a configuration value and persist to disk. + + Args: + key: A ConfigPath object specifying which config file + and key to write to. + value: The value to store. + """ + ... diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py new file mode 100644 index 0000000..2df5c1b --- /dev/null +++ b/src/interfaces/__init__.py @@ -0,0 +1,11 @@ +""" + Interfaces module for the AutoLibrary project. + + Defines abstract interfaces (Protocols) and shared type definitions + used across layers to decouple consumers from concrete implementations. + + Key components: + - ConfigProvider: Abstract interface for configuration access. + - ConfigType: Enumeration of configuration file types. + - ConfigKey: Type-safe hierarchical key constants for config lookups. +""" diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py index b415442..1dc108f 100644 --- a/src/managers/config/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -10,26 +10,17 @@ See the LICENSE file for details. import os import threading -from enum import Enum from typing import Any, Optional from utils.JSONReader import JSONReader from utils.JSONWriter import JSONWriter +from interfaces.ConfigProvider import ConfigType, ConfigPath # This config manager class only responsible for global and other # unconfigurable config files. -class ConfigType(Enum): - """ - Config type class. Values represent the default filename. - """ - GLOBAL = "autolibrary.json" # Global config file. - BULLETIN = "bulletin.json" # Bulletin board config file. - TIMERTASK = "timer_task.json" # Timer task config file. - - class ConfigTemplate: """ Config template class. @@ -120,16 +111,15 @@ class ConfigManager: def get( self, - config_type: ConfigType, - key: str = "", + key: ConfigPath, default: Optional[Any] = None ) -> Any: with self.__config_lock: - config_data = self.__config_data[config_type.value] - if key == "": + config_data = self.__config_data[key.config_type.value] + if key.key == "": return config_data - keys = key.split('.') + keys = key.key.split('.') for k in keys[:-1]: config_data = config_data.get(k, None) if config_data is None: @@ -139,24 +129,23 @@ class ConfigManager: def set( self, - config_type: ConfigType, - key: str = "", + key: ConfigPath, value: Any = None ): with self.__config_lock: - root_data = self.__config_data[config_type.value] - if key == "": - self.__config_data[config_type.value] = value + root_data = self.__config_data[key.config_type.value] + if key.key == "": + self.__config_data[key.config_type.value] = value else: - keys = key.split('.') + keys = key.key.split('.') config_data = root_data for k in keys[:-1]: if k not in config_data: config_data[k] = {} config_data = config_data[k] config_data[keys[-1]] = value - self.save(config_type) + self.save(key.config_type) def save( diff --git a/src/managers/config/ConfigUtils.py b/src/managers/config/ConfigUtils.py index 8d399aa..7f5c68e 100644 --- a/src/managers/config/ConfigUtils.py +++ b/src/managers/config/ConfigUtils.py @@ -11,6 +11,8 @@ import os import managers.config.ConfigManager as ConfigManager +from interfaces.ConfigProvider import CfgKey + class ConfigUtils: """ Config utilities class. @@ -29,7 +31,7 @@ class ConfigUtils: cfg_mgr = ConfigManager.instance() # config manager instance config_paths = {"run": "", "user": ""} - auto_config = cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation", {}) + auto_config = cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.ROOT, {}) for cfg_type in ["run", "user"]: paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", []) index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0) @@ -42,5 +44,5 @@ class ConfigUtils: config_paths[cfg_type] = paths[index] data = {"current": index, "paths": paths} auto_config[f"{cfg_type}_path"] = data - cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation", auto_config) + cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.ROOT, auto_config) return config_paths From 500ddd41c59fdee7d97662442305f5664a06bccc Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 12 May 2026 11:49:43 +0800 Subject: [PATCH 03/49] =?UTF-8?q?refactor(autoscript):=20=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=20dsl=20=E5=8C=85=E4=B8=BA=20autoscript=20=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASEngine.py | 573 +++++++++++++++++++++++++++++++++++ src/autoscript/ASObject.py | 251 +++++++++++++++ src/autoscript/ASOperator.py | 185 +++++++++++ src/autoscript/__init__.py | 55 ++++ src/dsl/AutoScriptEngine.py | 386 ----------------------- src/dsl/__init__.py | 9 - 6 files changed, 1064 insertions(+), 395 deletions(-) create mode 100644 src/autoscript/ASEngine.py create mode 100644 src/autoscript/ASObject.py create mode 100644 src/autoscript/ASOperator.py create mode 100644 src/autoscript/__init__.py delete mode 100644 src/dsl/AutoScriptEngine.py delete mode 100644 src/dsl/__init__.py diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py new file mode 100644 index 0000000..9e62490 --- /dev/null +++ b/src/autoscript/ASEngine.py @@ -0,0 +1,573 @@ +# -*- 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 re +from datetime import datetime, timedelta, date, time + +from .ASObject import ASObject, _META_VARS, _inferType +from .ASOperator import ASOperator + + +__all__ = ["execute", "addTargetVar"] + + +# Engine state +# User-registered target variables (bound to target_data paths) +_TARGET_VARS = {} +# Free-form script variables (not bound to target_data) +_SCRIPT_VARS = {} +# Name -> ASObject lookup map built from _META_VARS, _TARGET_VARS, and display names +_FIELD_MAP = {} +# Current line number for error reporting +_CUR_LINE = 0 + + +def _errPos( + message: str +) -> str: + """ + Format an error message with the current script line number. + + Args: + message (str): The error description. + + Returns: + str: A formatted error string like "AutoScript syntax error(line X): message". + """ + return f"AutoScript 语法错误(第{_CUR_LINE}行): {message}" + + +def _findConditionBegin( + upper_line: str +) -> int: + """ + Find the position of the opening parenthesis that starts a condition. + + Args: + upper_line (str): The uppercased IF / ELSE IF line. + + Returns: + int: Index of '(' or -1 if not found. + """ + return upper_line.find("(") + + +def _findConditionEnd( + upper_line: str, + start_pos: int +) -> int: + """ + Find the matching closing parenthesis for a condition expression. + + Handles nested parentheses and optionally strips a trailing "THEN" keyword. + + Args: + upper_line (str): The uppercased IF / ELSE IF line. + start_pos (int): Index of the opening '('. + + Returns: + int: Index of the matching ')' or -1 if unbalanced. + """ + + line = upper_line.rstrip() + if line.endswith(" THEN"): + line = line[:-5].rstrip() + depth = 1 + for i in range(start_pos + 1, len(line)): + ch = line[i] + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + return i + return -1 + + +def _splitTopLevel( + text: str, + delimiter: str +) -> list: + """ + Split a condition expression by a delimiter (.AND. / .OR.), respecting parentheses. + + Only splits at the top nesting level; delimiters inside parentheses are ignored. + + Args: + text (str): The condition expression to split. + delimiter (str): The delimiter string, e.g. ".OR." or ".AND.". + + Returns: + list: A list of sub-expression strings (stripped of leading/trailing whitespace). + """ + + parts = [] + depth = 0 + buf = "" + i = 0 + text_upper = text.upper() + delim_upper = delimiter.upper() + dlen = len(delim_upper) + while i < len(text): + if text[i] == "(": + depth += 1 + buf += text[i] + elif text[i] == ")": + depth -= 1 + buf += text[i] + elif depth == 0 and text_upper[i:i + dlen] == delim_upper: + parts.append(buf) + buf = "" + i += dlen + continue + else: + buf += text[i] + i += 1 + if buf.strip(): + parts.append(buf) + return parts + + +def _buildFieldMap(): + """ + Rebuild the _FIELD_MAP lookup from _META_VARS and _TARGET_VARS. + + Each variable is registered under both its canonical name (uppercased) + and its display_name (if present), so that scripts can refer to either. + """ + + _FIELD_MAP.clear() + for ch_name, obj in _META_VARS.items(): + _FIELD_MAP[obj.name.upper()] = obj + _FIELD_MAP[ch_name.upper().strip()] = obj + for obj in _TARGET_VARS.values(): + _FIELD_MAP[obj.name.upper()] = obj + if obj.display_name: + _FIELD_MAP[obj.display_name.upper().strip()] = obj + + +def _resolveFieldObj( + field_name: str +): + """ + Resolve a field name to its ASObject by looking up _FIELD_MAP then _SCRIPT_VARS. + + Unlike getting a raw value, this returns the ASObject instance itself, + preserving type information for operations and comparisons. + + Args: + field_name (str): The field name (case-insensitive). + + Returns: + ASObject or None: The resolved ASObject, or None if not found. + """ + + upper_name = field_name.upper().strip() + obj = _FIELD_MAP.get(upper_name) + if obj: + return obj + obj = _SCRIPT_VARS.get(upper_name) + if obj: + return obj + return None + + +def _resolveValue( + value_str: str, + target_data: dict +): + """ + Parse and resolve a value string from a script into a Python object. + + Supports the following literal forms: + - TIME(hh:mm) + - DATE(yyyy-mm-dd) + - .TRUE. / .FALSE. + - Single/double quoted strings (with escaped single quotes) + - CURRENT_DATE + N / CURRENT_TIME + N (relative offsets) + - Numeric literals (int / float) + - Field references (resolved via _resolveFieldObj) + + Args: + value_str (str): The raw value string from the script. + target_data (dict): The application data dict. + + Returns: + The resolved Python value. + """ + + s = value_str.strip() + m = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE) + if m: + return time(int(m.group(1)), int(m.group(2))) + m = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE) + if m: + return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) + up = s.upper() + if up == ".TRUE.": + return True + if up == ".FALSE.": + return False + if s.startswith("'") and s.endswith("'"): + return s[1:-1].replace("''", "'") + if s.startswith('"') and s.endswith('"'): + return s[1:-1] + m = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE) + if m: + days = int(m.group(1)) + return datetime.now().date() + timedelta(days=days) + m = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE) + if m: + hours = int(m.group(1)) + return (datetime.now() + timedelta(hours=hours)).time() + try: + return int(s) + except ValueError: + pass + try: + return float(s) + except ValueError: + pass + obj = _resolveFieldObj(s) + if obj: + return obj.getValue(target_data) + return "" + + +def _resolveAsObject( + expr: str, + target_data: dict +) -> ASObject: + """ + Resolve a value expression to an ASObject. + + - If the expression is a registered field name, returns its ASObject directly. + - If the expression is a literal (number, string, DATE(), TIME(), bool), + creates a temporary ASObject with the inferred type. + + This is the key function that ensures all internal operations work + with typed ASObject instances rather than raw Python values. + + Args: + expr (str): The raw expression string from the script. + target_data (dict): The application data dict. + + Returns: + ASObject: A registered or temporary ASObject representing the expression value. + """ + + s = expr.strip() + obj = _resolveFieldObj(s) + if obj is not None: + return obj + value = _resolveValue(s, target_data) + inferred = _inferType(value, s) + return ASObject._makeTemp(value, inferred) + + +def _evaluateCondition( + condition_str: str, + target_data: dict +) -> bool: + """ + Evaluate a condition expression and return a boolean result. + + Supports: + - Boolean literals: .TRUE., .FALSE. + - .AND. / .OR. operators (lowest precedence) + - Parenthesised sub-expressions + - Comparison operators: .EQ., .NEQ., .BGT., .BLT., .BGE., .BLE. + + All operands are resolved as ASObject instances (via _resolveAsObject) + and comparisons are delegated to ASOperator.compare(). + + Args: + condition_str (str): The raw condition expression from the script. + target_data (dict): The application data dict. + + Returns: + bool: The evaluation result. + + Raises: + ValueError: If the expression contains unrecognised tokens or type-mismatched comparisons. + """ + + s = condition_str.strip() + if not s: + return False + or_parts = _splitTopLevel(s, ".OR.") + if len(or_parts) > 1: + return any( + _evaluateCondition(p.strip(), target_data) + for p in or_parts + ) + and_parts = _splitTopLevel(s, ".AND.") + if len(and_parts) > 1: + return all( + _evaluateCondition(p.strip(), target_data) + for p in and_parts + ) + s = s.strip() + if s.startswith("(") and s.endswith(")"): + return _evaluateCondition(s[1:-1], target_data) + up = s.upper() + if up == ".TRUE.": + return True + if up == ".FALSE.": + return False + for op in ASOperator._COMPARE: + idx = up.find(op.upper()) + if idx < 0: + continue + left_raw = s[:idx].strip() + right_raw = s[idx + len(op):].strip() + left_obj = _resolveAsObject(left_raw, target_data) + right_obj = _resolveAsObject(right_raw, target_data) + return ASOperator.compare(left_obj, right_obj, op, target_data) + raise ValueError( + _errPos(f"无法识别的条件表达式 '{condition_str}'") + ) + + +def _executeSet( + line: str, + target_data: dict +): + """ + Execute a SET statement to assign a value to a field or script variable. + + Parses the line as "SET field_name = value_expr", resolves the value, + and assigns it. If the target field does not exist, a new script variable + is created with an inferred type. + + Args: + line (str): The raw SET line from the script. + target_data (dict): The application data dict. + + Raises: + ValueError: If the value string contains unexpected extra tokens. + """ + + rest = line[3:].strip() + eq_idx = rest.find("=") + if eq_idx < 0: + return + field_name = rest[:eq_idx].strip() + value_str = rest[eq_idx + 1:].strip() + if not field_name: + return + resolved = _resolveValue(value_str, target_data) + stripped = value_str.strip() + if resolved == "" and stripped not in ("''", '""') and len(stripped.split()) > 1: + raise ValueError(_errPos(f"SET 值中存在多余内容 '{stripped}'")) + upper_name = field_name.upper().strip() + obj = _FIELD_MAP.get(upper_name) + if not obj: + obj = _SCRIPT_VARS.get(upper_name) + if obj: + obj.setValue(resolved, target_data) + return + inferred_type = _inferType(resolved, stripped) + new_var = ASObject( + upper_name, + inferred_type, + read_only=False, + is_config=False, + default_value=resolved + ) + _SCRIPT_VARS[upper_name] = new_var + + +def _executeOperation( + line: str, + target_data: dict +): + """ + Execute a field operation statement: "FIELD .ADD. N" or "FIELD .SUB. N". + + Resolves the left side as a registered ASObject and the right side + as a temporary numeric ASObject, then delegates to ASOperator.apply(). + + Args: + line (str): The raw operation line from the script (e.g. "RESERVE_DATE .ADD. 1"). + target_data (dict): The application data dict. + + Raises: + ValueError: If the field is unknown, the operand is invalid, + or the type does not support the operation. + """ + + parts = line.split() + if len(parts) < 3: + return + if len(parts) > 3: + raise ValueError( + _errPos(f"操作语句中存在多余内容 '{' '.join(parts[3:])}'") + ) + field_name = parts[0].upper().strip() + op = parts[1].upper().strip() + raw_value = parts[2].strip() + target = _resolveFieldObj(field_name) + if target is None: + raise ValueError(_errPos(f"未知字段 '{field_name}'")) + operand = _resolveAsObject(raw_value, target_data) + ASOperator.apply(target, operand, op, target_data) + + +def _assertInIf( + if_stack: list, + line: str +): + """ + Assert that an executable statement is inside an IF block. + + Args: + if_stack (list): The current IF nesting stack. + line (str): The statement line (used for error message). + + Raises: + ValueError: If if_stack is empty (statement is outside any IF block). + """ + + if not if_stack: + raise ValueError(_errPos(f"可执行语句必须位于 IF 块内: {line}")) + + +def addTargetVar( + name: str, + var_type: str, + key_path: list, + display_name: str = None +): + """ + Register a new target variable bound to a path in the application data dict. + + Once registered, the variable can be read, written, and operated on in scripts + using its canonical name or display_name. + + Args: + name (str): The canonical variable name (e.g. "RESERVE_DATE"). + var_type (str): The type ("Int", "Float", "Boolean", "Date", "Time", "String"). + key_path (list): The nested path into target_data, e.g. ["reserve_info", "date"]. + display_name (str): An optional Chinese alias for use in script conditions. + + Example: + >>> addTargetVar("MY_FIELD", "String", ["custom", "field"], display_name="自定义字段") + """ + + upper_name = name.upper().strip() + obj = ASObject( + upper_name, + var_type, + is_config=True, + key_path=key_path, + display_name=display_name + ) + _TARGET_VARS[upper_name] = obj + + +def execute( + script_text: str, + target_data: dict +): + """ + Execute an AutoScript on the given target data. + + Parses the script line by line, maintaining an IF nesting stack to control + which blocks are active. Supports IF / ELSE IF / ELSE / END IF control flow, + SET assignments, PASS no-ops, and .ADD. / .SUB. field operations. + + Args: + script_text (str): The AutoScript source code. + target_data (dict): The application data dict to read from / write to. + + Raises: + ValueError: On syntax errors, unbalanced IF/END IF, unknown fields, etc. + + Example: + >>> data = {"reserve_info": {"date": "2026-05-01"}} + >>> execute( + ... "IF(.TRUE.)\\n" + ... " RESERVE_DATE .ADD. 1\\n" + ... "END IF", + ... data + ... ) + >>> data["reserve_info"]["date"] + '2026-05-02' + """ + + global _CUR_LINE + + _buildFieldMap() + if not script_text or not script_text.strip(): + return + lines = [l.strip() for l in script_text.split("\n") if l.strip()] + if not lines: + return + if_stack = [] + for _CUR_LINE, line in enumerate(lines, 1): + upper_line = line.upper().strip() + if upper_line.startswith("IF"): + paren_open = _findConditionBegin(upper_line) + if paren_open < 0: + raise ValueError(_errPos("IF 缺少左括号")) + cond_end = _findConditionEnd(upper_line, paren_open) + if cond_end < 0: + raise ValueError(_errPos("IF 缺少右括号")) + remaining = upper_line[cond_end + 1:].strip() + if remaining and remaining.upper() != "THEN": + raise ValueError(_errPos(f"IF 条件后存在多余内容 '{remaining}'")) + condition_str = line[paren_open + 1:cond_end].strip() + matched = _evaluateCondition(condition_str, target_data) + if_stack.append([matched, matched]) + elif upper_line.startswith("ELSE IF"): + if not if_stack: + raise ValueError(_errPos("ELSE IF 前缺少 IF")) + paren_open = _findConditionBegin(upper_line) + if paren_open < 0: + raise ValueError(_errPos("ELSE IF 缺少左括号")) + cond_end = _findConditionEnd(upper_line, paren_open) + if cond_end < 0: + raise ValueError(_errPos("ELSE IF 缺少右括号")) + remaining = upper_line[cond_end + 1:].strip() + if remaining and remaining.upper() != "THEN": + raise ValueError(_errPos(f"ELSE IF 条件后存在多余内容 '{remaining}'")) + _, branch_matched = if_stack[-1] + if not branch_matched: + condition_str = line[paren_open + 1:cond_end].strip() + matched = _evaluateCondition(condition_str, target_data) + if_stack[-1] = [matched, matched] + else: + if_stack[-1][0] = False + elif upper_line == "ELSE": + if not if_stack: + raise ValueError(_errPos("ELSE 前缺少 IF")) + _, branch_matched = if_stack[-1] + if not branch_matched: + if_stack[-1] = [True, True] + else: + if_stack[-1][0] = False + elif upper_line in ("ENDIF", "END IF"): + if not if_stack: + raise ValueError(_errPos("ENDIF / END IF 前缺少 IF")) + if_stack.pop() + elif upper_line.startswith("SET "): + _assertInIf(if_stack, line) + if all(ctx[0] for ctx in if_stack): + _executeSet(line, target_data) + elif upper_line == "PASS": + continue + else: + _assertInIf(if_stack, line) + if all(ctx[0] for ctx in if_stack): + _executeOperation(line, target_data) + if if_stack: + raise ValueError( + "AutoScript 语法错误: IF 与 ENDIF / END IF 不匹配" + ) diff --git a/src/autoscript/ASObject.py b/src/autoscript/ASObject.py new file mode 100644 index 0000000..082c0f4 --- /dev/null +++ b/src/autoscript/ASObject.py @@ -0,0 +1,251 @@ +# -*- 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 re +from datetime import datetime, date, time + + +__all__ = ["ASObject", "_META_VARS", "_inferType"] + + +# Default values for each supported type when no value is present +_TYPE_DEFAULTS = { + "Int": 0, + "Float": 0.0, + "Boolean": False, + "Date": None, + "Time": None, + "String": "", +} + + +class ASObject: + """ + Represents a variable object used throughout the AutoScript engine. + + An ASObject can be a meta variable (read-only, e.g. CURRENT_DATE), + a target variable (bound to a config data dict via key_path), + or a script variable (free-form, stored internally). + + Args: + name (str): The canonical name of the variable (case-insensitive). + var_type (str): One of "Int", "Float", "Boolean", "Date", "Time", "String". + read_only (bool): Whether the variable is read-only (default: False). + is_config (bool): Whether the variable maps to a target_data path (default: False). + key_path (list): The nested key path into target_data, e.g. ["reserve_info", "date"]. + default_value: The fallback value when no target_data value is found. + display_name (str): An alias for use in script conditions and assignments. + + Example: + >>> obj = ASObject("MY_DATE", "Date", is_config=True, + ... key_path=["reserve_info", "date"], + ... display_name="预约日期") + >>> obj.getValue({"reserve_info": {"date": "2026-05-01"}}) + datetime.date(2026, 5, 1) + """ + + _TEMP_COUNTER = 0 + + def __init__( + self, + name: str, + var_type: str, *, + read_only: bool = False, + is_config: bool = False, + key_path: list = None, + default_value=None, + display_name: str = None + ): + + self.name = name + self.var_type = var_type + self.read_only = read_only + self.is_config = is_config + self.key_path = key_path or [] + self._value = default_value + self.display_name = display_name + + @classmethod + def _makeTemp( + cls, + value, + inferred_type: str + ): + """ + Create a temporary unnamed ASObject from a literal value. + + Temporary objects are used for inline script literals (e.g. 42, + 'hello', DATE(2026-01-01)) so they can participate in typed + operations alongside registered variables. + + Args: + value: The resolved Python value. + inferred_type (str): The AutoScript type name. + + Returns: + ASObject: A temporary, non-config, read-write ASObject. + """ + + cls._TEMP_COUNTER += 1 + return cls( + f"__TMP_{cls._TEMP_COUNTER}", + inferred_type, + read_only=False, + is_config=False, + default_value=value + ) + + + def _typeEmpty( + self + ): + """ + Return the type-appropriate empty / default value. + """ + + return _TYPE_DEFAULTS.get(self.var_type, "") + + + def getValue( + self, + target_data: dict = None + ): + """ + Retrieve the current value of this variable. + + For read-only variables (CURRENT_DATE, CURRENT_TIME), returns the + live datetime. For config variables, traverses the key_path into + target_data and parses Date/Time strings. Otherwise returns the + internal _value. + + Args: + target_data (dict): The application data dict (required for config vars). + + Returns: + The resolved value, or a type-appropriate default if missing. + """ + + if self.read_only: + if self.name == "CURRENT_DATE": + return datetime.now().date() + if self.name == "CURRENT_TIME": + return datetime.now().time() + return self._value + if self.is_config and target_data is not None and self.key_path: + d = target_data + for key in self.key_path[:-1]: + d = d.get(key, {}) + if not isinstance(d, dict): + return self._typeEmpty() + raw = d.get(self.key_path[-1]) + if raw is None: + return self._typeEmpty() + if self.var_type == "Date" and isinstance(raw, str): + try: + return datetime.strptime(raw, "%Y-%m-%d").date() + except ValueError: + return self._typeEmpty() + if self.var_type == "Time" and isinstance(raw, str): + try: + return datetime.strptime(raw, "%H:%M").time() + except ValueError: + return self._typeEmpty() + return raw + return self._value + + + def setValue( + self, + value, + target_data: dict = None + ): + """ + Assign a new value to this variable, with type coercion. + + Performs coercion for Boolean (string -> bool), Int, and Float types. + For config variables, dates/times are converted back to strings before + writing into target_data. + + Args: + value: The value to assign. + target_data (dict): The application data dict (required for config vars). + + Raises: + ValueError: If the variable is read-only or value cannot be coerced. + """ + + if self.read_only: + raise ValueError(f"不能修改只读变量 '{self.name}'") + if self.var_type == "Boolean" and not isinstance(value, bool): + value = (str(value).upper() == "TRUE") + if self.var_type == "Int" and not isinstance(value, int): + try: + value = int(value) + except (ValueError, TypeError): + raise ValueError(f"无法将值 '{value}' 转换为 Int 类型") + if self.var_type == "Float" and not isinstance(value, float): + try: + value = float(value) + except (ValueError, TypeError): + raise ValueError(f"无法将值 '{value}' 转换为 Float 类型") + if self.is_config: + if self.var_type == "Date" and isinstance(value, date): + value = value.strftime("%Y-%m-%d") + if self.var_type == "Time" and isinstance(value, time): + value = value.strftime("%H:%M") + if self.is_config and target_data is not None and self.key_path: + d = target_data + for key in self.key_path[:-1]: + d = d.setdefault(key, {}) + d[self.key_path[-1]] = value + else: + self._value = value + + +# Built-in read-only meta variables available to all scripts +_META_VARS = { + "CURRENT_DATE": ASObject("CURRENT_DATE", "Date", read_only=True, display_name="当前日期"), + "CURRENT_TIME": ASObject("CURRENT_TIME", "Time", read_only=True, display_name="当前时间"), +} + + +def _inferType( + value, + raw_expr: str = None +) -> str: + """ + Infer the ASObject type string from a Python value or raw expression. + + When the Python type is ambiguous (e.g. int can be Int or a component + of Date), the raw_expr is used as a hint. + + Args: + value: The resolved Python value. + raw_expr (str): The original expression string from the script (optional). + + Returns: + str: One of "Boolean", "Int", "Float", "Date", "Time", "String". + """ + + if isinstance(value, bool): + return "Boolean" + if isinstance(value, int): + return "Int" + if isinstance(value, float): + return "Float" + if isinstance(value, date): + return "Date" + if isinstance(value, time): + return "Time" + if raw_expr: + if re.match(r"^DATE\(\d{4}-\d{2}-\d{2}\)$", raw_expr, re.IGNORECASE): + return "Date" + if re.match(r"^TIME\(\d{1,2}:\d{2}\)$", raw_expr, re.IGNORECASE): + return "Time" + return "String" diff --git a/src/autoscript/ASOperator.py b/src/autoscript/ASOperator.py new file mode 100644 index 0000000..bc9cf1b --- /dev/null +++ b/src/autoscript/ASOperator.py @@ -0,0 +1,185 @@ +# -*- 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. +""" +from datetime import datetime, timedelta, date, time + +from .ASObject import ASObject + + +__all__ = ["ASOperator"] + + +class ASOperator: + """ + Centralised type-safe operations for AutoScript engine types. + + All arithmetic (ADD / SUB) and comparison operators are routed through + this class, which dispatches to the correct Python-level logic based on + the ASObject's var_type. This keeps type-specific branching in one + place instead of scattering it across the engine. + + Args: + op (str): One of ".ADD.", ".SUB.", ".EQ.", ".NEQ.", ".BGT.", + ".BLT.", ".BGE.", ".BLE.". + + Example: + >>> obj = ASObject("X", "Int", default_value=10) + >>> ASOperator.apply(obj, ASObject._makeTemp(5, "Int"), ".ADD.", None) + >>> obj.getValue() + 15 + >>> ASOperator.compare( + ... obj, + ... ASObject._makeTemp(15, "Int"), + ... ".EQ.", None + ... ) + True + """ + + _COMPARE = { + ".EQ." : lambda a, b: a == b, + ".NEQ.": lambda a, b: a != b, + ".BGT.": lambda a, b: a > b, + ".BLT.": lambda a, b: a < b, + ".BGE.": lambda a, b: a >= b, + ".BLE.": lambda a, b: a <= b, + } + _ARITH_TYPES = {"Date", "Time", "Int", "Float"} + + @staticmethod + def apply( + target: ASObject, + operand: ASObject, + op: str, + target_data: dict + ): + """ + Apply ADD or SUB to a target ASObject, modifying it in place. + + Args: + target (ASObject): The variable to modify. + operand (ASObject): The operand (numeric value for Date/Time/Int/Float). + op (str): ".ADD." or ".SUB.". + target_data (dict): Application data dict (passed through to getValue/setValue). + + Raises: + ValueError: If the type does not support the operation or values are invalid. + """ + + tp = target.var_type + op_tp = operand.var_type + if tp not in ASOperator._ARITH_TYPES: + raise ValueError(f"'{tp}' 类型字段不支持操作运算") + if op_tp not in ("Int", "Float"): + raise ValueError(f"操作数类型 '{op_tp}' 不能用于运算,需要数值类型 (Int / Float)") + if tp in ("Date", "Time") and op_tp != "Int": + raise ValueError(f"'{tp}' 类型的加减法操作数必须为 Int 类型,不允许 Float") + target_val = target.getValue(target_data) + if target_val is None: + raise ValueError(f"'{target.name}' 的值为空,无法进行运算") + if op == ".ADD.": + ASOperator._arithAdd(target, target_val, operand, target_data) + elif op == ".SUB.": + ASOperator._arithSub(target, target_val, operand, target_data) + else: + raise ValueError(f"不支持的操作 '{op}'") + + @staticmethod + def _arithAdd( + target: ASObject, + target_val, + operand: ASObject, + target_data: dict + ): + """Dispatch ADD per type.""" + + tp = target.var_type + raw_op = operand._value + + if tp == "Date": + if not isinstance(target_val, date): + raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期") + new_val = target_val + timedelta(days=int(raw_op)) + elif tp == "Time": + if not isinstance(target_val, time): + raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间") + dt = datetime.combine(datetime.today(), target_val) + timedelta(hours=int(raw_op)) + new_val = dt.time() + elif tp == "Int": + new_val = int(target_val) + int(raw_op) + elif tp == "Float": + new_val = float(target_val) + float(raw_op) + else: + raise ValueError(f"'{tp}' 类型不支持 ADD 操作") + target.setValue(new_val, target_data) + + @staticmethod + def _arithSub( + target: ASObject, + target_val, + operand: ASObject, + target_data: dict + ): + + """Dispatch SUB per type.""" + + tp = target.var_type + raw_op = operand._value + + if tp == "Date": + if not isinstance(target_val, date): + raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期") + new_val = target_val - timedelta(days=int(raw_op)) + elif tp == "Time": + if not isinstance(target_val, time): + raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间") + dt = datetime.combine(datetime.today(), target_val) - timedelta(hours=int(raw_op)) + new_val = dt.time() + elif tp == "Int": + new_val = int(target_val) - int(raw_op) + elif tp == "Float": + new_val = float(target_val) - float(raw_op) + else: + raise ValueError(f"'{tp}' 类型不支持 SUB 操作") + target.setValue(new_val, target_data) + + @staticmethod + def compare( + left: ASObject, + right: ASObject, + op: str, + target_data: dict + ) -> bool: + """ + Compare two ASObjects using the given comparison operator. + + Args: + left (ASObject): Left-hand side. + right (ASObject): Right-hand side. + op (str): One of ".EQ.", ".NEQ.", ".BGT.", ".BLT.", ".BGE.", ".BLE.". + target_data (dict): Application data dict. + + Returns: + bool: The comparison result. + + Raises: + ValueError: If the types are incompatible for comparison. + """ + + cmp_func = ASOperator._COMPARE.get(op) + if cmp_func is None: + raise ValueError(f"未知的比较操作 '{op}'") + left_val = left.getValue(target_data) + right_val = right.getValue(target_data) + try: + return cmp_func(left_val, right_val) + except TypeError: + raise ValueError( + f"无法比较 '{left.name}' ({left.var_type}) " + f"与 '{right.name}' ({right.var_type})" + ) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py new file mode 100644 index 0000000..a2a6f1e --- /dev/null +++ b/src/autoscript/__init__.py @@ -0,0 +1,55 @@ +""" + AutoScript module for the AutoLibrary project. + + A lightweight scripting DSL for preprocessing user reservation data + in repeatable timer tasks. Supports IF/ELSE IF/ELSE/END IF control + flow, SET assignments, .ADD./.SUB. operations, and rich comparisons. + + Public API: + - execute(script_text, target_data): Execute an AutoScript. + - addTargetVar(name, var_type, key_path, display_name): Register a variable. + - registerDefaultTargetVars(): Register all built-in target variables. + - META_VARS: dict of built-in read-only meta variables. + - ALL_VARIABLES: dict of all available variables (display_name -> (name, type)). +""" + +from autoscript.ASEngine import execute, addTargetVar +from autoscript.ASObject import _META_VARS as META_VARS + +__all__ = [ + "execute", "addTargetVar", "registerDefaultTargetVars", + "META_VARS", "ALL_VARIABLES", +] + +# All variables available to scripts (display_name -> (name, type)). +# This mirrors the old AutoScriptEngine.VARIABLE_META for backward +# compatibility in the UI orchestration dialog. +ALL_VARIABLES: dict = { + "用户名": ("USERNAME", "String"), + "用户启用": ("USER_ENABLE", "Boolean"), + "预约日期": ("RESERVE_DATE", "Date"), + "预约开始时间": ("RESERVE_BEGIN_TIME", "Time"), + "预约结束时间": ("RESERVE_END_TIME", "Time"), + "当前时间": ("CURRENT_TIME", "Time"), + "当前日期": ("CURRENT_DATE", "Date"), +} + +# Key paths into target_data dict for each target variable. +# (name, type, key_path, display_name) +_TARGET_VAR_DEFS = [ + ("USERNAME", "String",["username"], "用户名"), + ("USER_ENABLE", "Boolean",["enabled"], "用户启用"), + ("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"), + ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), + ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), +] + + +def registerDefaultTargetVars() -> None: + """ + Register all built-in target variables with the engine. + This must be called before any script execution. + Calling multiple times is idempotent (re-registers same keys). + """ + for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + addTargetVar(name, var_type, key_path, display_name) diff --git a/src/dsl/AutoScriptEngine.py b/src/dsl/AutoScriptEngine.py deleted file mode 100644 index 7504ea6..0000000 --- a/src/dsl/AutoScriptEngine.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- 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 re -from datetime import datetime, timedelta - - -class AutoScriptEngine: - """ - AutoScript script engine. - - Parses and executes AutoScript — a lightweight scripting DSL - used in repeatable timer tasks to preprocess user reservation - data before automation runs. - - Supports IF/ELSE IF/ELSE/END IF control flow, SET assignments, - .ADD./.SUB. operations on Date/Time fields, and rich comparison - operators (.EQ. .NEQ. .BGT. .BLT. .BGE. .BLE.). - - Examples: - >>> engine = AutoScriptEngine - >>> user = { - ... "username": "test", - ... "enabled": True, - ... "reserve_info": {"date": "2026-05-07"} - ... } - >>> engine.execute( - ... 'IF(CURRENT_TIME .BGT. TIME(19:00))\\n' - ... ' RESERVE_DATE .ADD. 1\\n' - ... 'END IF', - ... user - ... ) - """ - COMPARE_OPS = { # compare operators - ".EQ." : lambda a, b: a == b, - ".NEQ.": lambda a, b: a != b, - ".BGT.": lambda a, b: a > b, - ".BLT.": lambda a, b: a < b, - ".BGE.": lambda a, b: a >= b, - ".BLE.": lambda a, b: a <= b, - } - VARIABLE_META = { # variable metadata - "预约开始时间": ("RESERVE_BEGIN_TIME", "Time"), - "预约结束时间": ("RESERVE_END_TIME", "Time"), - "预约日期": ("RESERVE_DATE", "Date"), - "用户名": ("USERNAME", "String"), - "用户启用": ("USER_ENABLE", "Boolean"), - "当前时间": ("CURRENT_TIME", "Time"), - "当前日期": ("CURRENT_DATE", "Date"), - } - _FIELD_TYPE_MAP = {meta[0]: meta[1] for meta in VARIABLE_META.values()} - - @staticmethod - def execute( - script_text: str, - user_data: dict - ): - """ - Execute an AutoScript against the given user data. - - The script is parsed line-by-line. All modifications are - applied directly to ``user_data`` in-place. - - Args: - script_text (str): Raw AutoScript source code. - user_data (dict): User data dictionary to read from and - write to. Must conform to the standard user profile - structure (username, enabled, reserve_info, etc.). - - Raises: - ValueError: On any syntax or type error encountered - during parsing or execution. - """ - - if not script_text or not script_text.strip(): - return - lines = [l.strip() for l in script_text.split("\n") if l.strip()] - if not lines: - return - if_stack = [] - - for line in lines: - upper_line = line.upper().strip() - if upper_line.startswith("IF("): - cond_end = _findConditionEnd(upper_line) - if cond_end < 0: - raise ValueError("AutoScript 语法错误: IF 缺少右括号") - condition_str = line[3:cond_end].strip() - matched = AutoScriptEngine._evaluateCondition( - condition_str, user_data - ) - if_stack.append([matched, matched]) - elif upper_line.startswith("ELSE IF("): - if not if_stack: - raise ValueError("AutoScript 语法错误: ELSE IF 前缺少 IF") - cond_end = _findConditionEnd(upper_line) - if cond_end < 0: - raise ValueError("AutoScript 语法错误: ELSE IF 缺少右括号") - condition_str = line[8:cond_end].strip() - _, has_matched = if_stack[-1] - if not has_matched: - matched = AutoScriptEngine._evaluateCondition( - condition_str, user_data - ) - if_stack[-1] = [matched, matched] - else: - if_stack[-1][0] = False - elif upper_line == "ELSE": - if not if_stack: - raise ValueError("AutoScript 语法错误: ELSE 前缺少 IF") - _, has_matched = if_stack[-1] - if not has_matched: - if_stack[-1] = [True, True] - else: - if_stack[-1][0] = False - elif upper_line in ("ENDIF", "END IF"): - if not if_stack: - raise ValueError("AutoScript 语法错误: ENDIF/END IF 前缺少 IF") - if_stack.pop() - elif upper_line.startswith("SET "): - should_execute = ( - all(ctx[0] for ctx in if_stack) if if_stack else True - ) - if should_execute: - AutoScriptEngine._executeSet(line, user_data) - elif upper_line == "PASS": - continue - else: - should_execute = ( - all(ctx[0] for ctx in if_stack) if if_stack else True - ) - if should_execute: - AutoScriptEngine._executeOperation(line, user_data) - if if_stack: - raise ValueError("AutoScript 语法错误: IF 与 ENDIF/END IF 不匹配") - - @staticmethod - def _resolveField( - field_name: str, - user_data: dict - ): - - upper_name = field_name.upper().strip() - if upper_name == "CURRENT_DATE": - return datetime.now().strftime("%Y-%m-%d") - elif upper_name == "CURRENT_TIME": - return datetime.now().strftime("%H:%M") - elif upper_name == "USERNAME": - return user_data.get("username", "") - elif upper_name == "USER_ENABLE": - return user_data.get("enabled", False) - elif upper_name == "RESERVE_DATE": - return user_data.get("reserve_info", {}).get("date", "") - elif upper_name == "RESERVE_BEGIN_TIME": - return ( - user_data - .get("reserve_info", {}) - .get("begin_time", {}) - .get("time", "") - ) - elif upper_name == "RESERVE_END_TIME": - return ( - user_data - .get("reserve_info", {}) - .get("end_time", {}) - .get("time", "") - ) - return "" - - @staticmethod - def _resolveValue( - value_str: str, - user_data: dict - ): - - s = value_str.strip() - time_match = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE) - if time_match: - h, m = time_match.group(1), time_match.group(2) - return f"{int(h):02d}:{int(m):02d}" - date_match = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE) - if date_match: - y, mo, d = date_match.group(1), date_match.group(2), date_match.group(3) - return f"{int(y):04d}-{int(mo):02d}-{int(d):02d}" - if s.upper() == ".TRUE.": - return True - if s.upper() == ".FALSE.": - return False - if s.startswith("'") and s.endswith("'"): - inner = s[1:-1].replace("''", "'") - return inner - if s.startswith('"') and s.endswith('"'): - return s[1:-1] - relDate = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE) - if relDate: - days = int(relDate.group(1)) - return (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d") - relTime = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE) - if relTime: - hours = int(relTime.group(1)) - return (datetime.now() + timedelta(hours=hours)).strftime("%H:%M") - try: - return int(s) - except ValueError: - pass - try: - return float(s) - except ValueError: - pass - resolved = AutoScriptEngine._resolveField(s, user_data) - return resolved - - @staticmethod - def _setField( - field_name: str, - value: str, - user_data: dict - ): - upper_name = field_name.upper().strip() - if upper_name == "RESERVE_DATE": - user_data.setdefault("reserve_info", {})["date"] = value - elif upper_name == "RESERVE_BEGIN_TIME": - ri = user_data.setdefault("reserve_info", {}) - ri.setdefault("begin_time", {})["time"] = value - elif upper_name == "RESERVE_END_TIME": - ri = user_data.setdefault("reserve_info", {}) - ri.setdefault("end_time", {})["time"] = value - elif upper_name == "USERNAME": - user_data["username"] = value - elif upper_name == "USER_ENABLE": - if isinstance(value, bool): - user_data["enabled"] = value - else: - user_data["enabled"] = (str(value).upper() == "TRUE") - - @staticmethod - def _evaluateCondition( - condition_str: str, - user_data: dict - ) -> bool: - - for op, cmp_func in AutoScriptEngine.COMPARE_OPS.items(): - if op not in condition_str.upper(): - continue - idx = condition_str.upper().find(op) - parts = [condition_str[:idx], condition_str[idx + len(op):]] - if len(parts) != 2: - continue - field_name = parts[0].strip() - value_str = parts[1].strip() - left_val = AutoScriptEngine._resolveField(field_name, user_data) - right_val = AutoScriptEngine._resolveValue(value_str, user_data) - try: - return cmp_func(left_val, right_val) - except TypeError: - raise ValueError( - f"AutoScript 语法错误: 无法比较 " - f"'{field_name}' ({type(left_val).__name__}) " - f"与 '{value_str}' ({type(right_val).__name__})" - ) - return False - - @staticmethod - def _executeSet( - line: str, - user_data: dict - ): - rest = line[3:].strip() - eq_idx = rest.find("=") - if eq_idx < 0: - return - field_name = rest[:eq_idx].strip() - value_str = rest[eq_idx + 1:].strip() - if not field_name: - return - resolved = AutoScriptEngine._resolveValue(value_str, user_data) - AutoScriptEngine._setField(field_name, resolved, user_data) - - @staticmethod - def _executeOperation( - line: str, - user_data: dict - ): - - parts = line.split() - if len(parts) < 3: - return - field_name = parts[0].upper().strip() - op = parts[1].upper().strip() - raw_value = parts[2].strip() - field_type = AutoScriptEngine._FIELD_TYPE_MAP.get(field_name) - if not field_type: - raise ValueError( - f"AutoScript 语法错误: 未知字段 '{field_name}'" - ) - try: - num_value = float(raw_value) if "." in raw_value else int(raw_value) - except (ValueError, TypeError): - raise ValueError( - f"AutoScript 语法错误: 无效操作数 '{raw_value}'" - ) - if field_type == "Date": - date_str = AutoScriptEngine._resolveField(field_name, user_data) - if not date_str: - return - try: - date_obj = datetime.strptime(date_str, "%Y-%m-%d") - except (ValueError, TypeError): - return - if op == ".ADD.": - date_obj += timedelta(days=num_value) - elif op == ".SUB.": - date_obj -= timedelta(days=num_value) - else: - raise ValueError( - f"AutoScript 语法错误: Date 类型不支持操作 '{op}'" - ) - AutoScriptEngine._setField( - field_name, date_obj.strftime("%Y-%m-%d"), user_data - ) - elif field_type == "Time": - time_str = AutoScriptEngine._resolveField(field_name, user_data) - if not time_str: - return - try: - time_obj = datetime.strptime(time_str, "%H:%M") - except (ValueError, TypeError): - return - if op == ".ADD.": - time_obj += timedelta(hours=num_value) - elif op == ".SUB.": - time_obj -= timedelta(hours=num_value) - else: - raise ValueError( - f"AutoScript 语法错误: Time 类型不支持操作 '{op}'" - ) - AutoScriptEngine._setField( - field_name, time_obj.strftime("%H:%M"), user_data - ) - elif field_type in ("String", "Boolean"): - raise ValueError( - f"AutoScript 语法错误: '{field_type}' 类型字段不支持操作运算" - ) - else: - raise ValueError( - f"AutoScript 语法错误: 未知字段类型 '{field_type}'" - ) - - -def _findConditionEnd( - upper_line: str -) -> int: - """ - Find the index of the closing parenthesis that matches the - opening parenthesis in a condition expression, handling nested - parentheses and optional ``THEN`` keyword. - - Args: - upper_line (str): The uppercased line text containing the - condition, e.g. ``"IF(A .BGT. B) THEN"``. - - Returns: - int: Index of the matching ``)``, or ``-1`` if no match - is found. - """ - - line = upper_line.rstrip() - if line.endswith(" THEN"): - line = line[:-5].rstrip() - paren_depth = 0 - start_found = False - for i, ch in enumerate(line): - if ch == "(": - paren_depth += 1 - start_found = True - elif ch == ")": - paren_depth -= 1 - if start_found and paren_depth == 0: - return i - return -1 diff --git a/src/dsl/__init__.py b/src/dsl/__init__.py deleted file mode 100644 index 878d740..0000000 --- a/src/dsl/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" - DSL module for the AutoLibrary project. - - Contains the AutoScript DSL engine and related components - for preprocessing user reservation data in timer tasks. - - Classes: - - AutoScriptEngine: AutoScript script engine class. -""" From 9bdc9a3de9c550842191c0c297e0cfd1c1ddfcae Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 17 May 2026 01:33:22 +0800 Subject: [PATCH 04/49] =?UTF-8?q?refactor(autoscript):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20ASTokenizer=20=E5=92=8C=20NodeVisitor=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E8=A7=A3=E6=9E=90=E4=B8=8E=E6=89=A7=E8=A1=8C=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASEngine.py | 344 ++++++++++++------------ src/autoscript/ASObject.py | 14 +- src/autoscript/ASOperator.py | 103 ++++---- src/autoscript/ASTokenizer.py | 478 ++++++++++++++++++++++++++++++++++ src/autoscript/__init__.py | 57 +++- 5 files changed, 771 insertions(+), 225 deletions(-) create mode 100644 src/autoscript/ASTokenizer.py diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index 9e62490..8eaed3d 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -8,10 +8,29 @@ You may use, modify, and distribute this file under the terms of the MIT License See the LICENSE file for details. """ import re -from datetime import datetime, timedelta, date, time +from datetime import ( + datetime, + timedelta, + date, + time +) -from .ASObject import ASObject, _META_VARS, _inferType +from .ASObject import ( + ASObject, + _META_VARS, + _inferType +) from .ASOperator import ASOperator +from .ASTokenizer import ( + ASTokenizer, + NodeVisitor, + Script, + IfNode, + SetNode, + OpNode, + PassNode, + UnrecogNode +) __all__ = ["execute", "addTargetVar"] @@ -24,70 +43,30 @@ _TARGET_VARS = {} _SCRIPT_VARS = {} # Name -> ASObject lookup map built from _META_VARS, _TARGET_VARS, and display names _FIELD_MAP = {} -# Current line number for error reporting -_CUR_LINE = 0 def _errPos( + line: int, message: str ) -> str: """ - Format an error message with the current script line number. + Format an error message with a script line number. Args: + line (int): The script line number where the error occurred. message (str): The error description. Returns: str: A formatted error string like "AutoScript syntax error(line X): message". """ - return f"AutoScript 语法错误(第{_CUR_LINE}行): {message}" + return f"AutoScript 语法错误(第{line}行): {message}" -def _findConditionBegin( - upper_line: str -) -> int: - """ - Find the position of the opening parenthesis that starts a condition. - - Args: - upper_line (str): The uppercased IF / ELSE IF line. - - Returns: - int: Index of '(' or -1 if not found. - """ - return upper_line.find("(") - - -def _findConditionEnd( - upper_line: str, - start_pos: int -) -> int: - """ - Find the matching closing parenthesis for a condition expression. - - Handles nested parentheses and optionally strips a trailing "THEN" keyword. - - Args: - upper_line (str): The uppercased IF / ELSE IF line. - start_pos (int): Index of the opening '('. - - Returns: - int: Index of the matching ')' or -1 if unbalanced. - """ - - line = upper_line.rstrip() - if line.endswith(" THEN"): - line = line[:-5].rstrip() - depth = 1 - for i in range(start_pos + 1, len(line)): - ch = line[i] - if ch == "(": - depth += 1 - elif ch == ")": - depth -= 1 - if depth == 0: - return i - return -1 +# Pre-compiled regex patterns for value resolution +_RE_TIME = re.compile(r"^TIME\((\d{1,2}):(\d{2})\)$", re.IGNORECASE) +_RE_DATE = re.compile(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", re.IGNORECASE) +_RE_CUR_DATE_OFFSET = re.compile(r"^CURRENT_DATE\s*\+\s*(\d+)$", re.IGNORECASE) +_RE_CUR_TIME_OFFSET = re.compile(r"^CURRENT_TIME\s*\+\s*(\d+)$", re.IGNORECASE) def _splitTopLevel( @@ -203,10 +182,10 @@ def _resolveValue( """ s = value_str.strip() - m = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE) + m = _RE_TIME.match(s) if m: return time(int(m.group(1)), int(m.group(2))) - m = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE) + m = _RE_DATE.match(s) if m: return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) up = s.upper() @@ -218,11 +197,11 @@ def _resolveValue( return s[1:-1].replace("''", "'") if s.startswith('"') and s.endswith('"'): return s[1:-1] - m = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE) + m = _RE_CUR_DATE_OFFSET.match(s) if m: days = int(m.group(1)) return datetime.now().date() + timedelta(days=days) - m = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE) + m = _RE_CUR_TIME_OFFSET.match(s) if m: hours = int(m.group(1)) return (datetime.now() + timedelta(hours=hours)).time() @@ -273,7 +252,8 @@ def _resolveAsObject( def _evaluateCondition( condition_str: str, - target_data: dict + target_data: dict, + line: int = 0 ) -> bool: """ Evaluate a condition expression and return a boolean result. @@ -304,18 +284,18 @@ def _evaluateCondition( or_parts = _splitTopLevel(s, ".OR.") if len(or_parts) > 1: return any( - _evaluateCondition(p.strip(), target_data) + _evaluateCondition(p.strip(), target_data, line) for p in or_parts ) and_parts = _splitTopLevel(s, ".AND.") if len(and_parts) > 1: return all( - _evaluateCondition(p.strip(), target_data) + _evaluateCondition(p.strip(), target_data, line) for p in and_parts ) s = s.strip() if s.startswith("(") and s.endswith(")"): - return _evaluateCondition(s[1:-1], target_data) + return _evaluateCondition(s[1:-1], target_data, line) up = s.upper() if up == ".TRUE.": return True @@ -331,13 +311,14 @@ def _evaluateCondition( right_obj = _resolveAsObject(right_raw, target_data) return ASOperator.compare(left_obj, right_obj, op, target_data) raise ValueError( - _errPos(f"无法识别的条件表达式 '{condition_str}'") + _errPos(line, f"无法识别的条件表达式 '{condition_str}'") ) def _executeSet( - line: str, - target_data: dict + line_text: str, + target_data: dict, + line: int = 0 ): """ Execute a SET statement to assign a value to a field or script variable. @@ -354,7 +335,7 @@ def _executeSet( ValueError: If the value string contains unexpected extra tokens. """ - rest = line[3:].strip() + rest = line_text[3:].strip() eq_idx = rest.find("=") if eq_idx < 0: return @@ -365,7 +346,7 @@ def _executeSet( resolved = _resolveValue(value_str, target_data) stripped = value_str.strip() if resolved == "" and stripped not in ("''", '""') and len(stripped.split()) > 1: - raise ValueError(_errPos(f"SET 值中存在多余内容 '{stripped}'")) + raise ValueError(_errPos(line, f"SET 值中存在多余内容 '{stripped}'")) upper_name = field_name.upper().strip() obj = _FIELD_MAP.get(upper_name) if not obj: @@ -385,8 +366,9 @@ def _executeSet( def _executeOperation( - line: str, - target_data: dict + line_text: str, + target_data: dict, + line: int = 0 ): """ Execute a field operation statement: "FIELD .ADD. N" or "FIELD .SUB. N". @@ -403,42 +385,23 @@ def _executeOperation( or the type does not support the operation. """ - parts = line.split() + parts = line_text.split() if len(parts) < 3: return if len(parts) > 3: raise ValueError( - _errPos(f"操作语句中存在多余内容 '{' '.join(parts[3:])}'") + _errPos(line, f"操作语句中存在多余内容 '{' '.join(parts[3:])}'") ) field_name = parts[0].upper().strip() op = parts[1].upper().strip() raw_value = parts[2].strip() target = _resolveFieldObj(field_name) if target is None: - raise ValueError(_errPos(f"未知字段 '{field_name}'")) + raise ValueError(_errPos(line, f"未知字段 '{field_name}'")) operand = _resolveAsObject(raw_value, target_data) ASOperator.apply(target, operand, op, target_data) -def _assertInIf( - if_stack: list, - line: str -): - """ - Assert that an executable statement is inside an IF block. - - Args: - if_stack (list): The current IF nesting stack. - line (str): The statement line (used for error message). - - Raises: - ValueError: If if_stack is empty (statement is outside any IF block). - """ - - if not if_stack: - raise ValueError(_errPos(f"可执行语句必须位于 IF 块内: {line}")) - - def addTargetVar( name: str, var_type: str, @@ -472,6 +435,125 @@ def addTargetVar( _TARGET_VARS[upper_name] = obj +class _EngineExecutor(NodeVisitor): + """ + AST visitor that executes AutoScript against target_data. + Walks the AST and dispatches SET / ADD / SUB operations + via visitScript / visitIf / visitSet / visitOp / visitPass / visitUnrecog. + """ + + def __init__( + self, + target_data: dict + ): + + super().__init__() + self._target_data = target_data + self._cur_line = 0 + + @property + def _line(self) -> int: + """Return current line number for _errPos calls.""" + + return self._cur_line + + def _incLine( + self + ): + + self._cur_line += 1 + + def visitScript( + self, + _node: Script + ): + + for child in _node.body: + child.accept(self) + + def visitIf( + self, + _node: IfNode + ): + + self._incLine() + if not _node.closed: + raise ValueError(_errPos(self._line, "IF 与 ENDIF / END IF 不匹配")) + matched = _evaluateCondition(_node.condition, self._target_data, self._line) + if matched: + for child in _node.body: + child.accept(self) + else: + executed = False + for elif_node in _node.elif_branches: + self._incLine() + if _evaluateCondition(elif_node.condition, self._target_data, self._line): + for child in elif_node.body: + child.accept(self) + executed = True + break + if not executed and _node.else_body: + self._incLine() + for child in _node.else_body: + child.accept(self) + + def visitSet( + self, + _node: SetNode + ): + + self._incLine() + full_line = f"SET {_node.target} = {_node.value}" + _executeSet(full_line, self._target_data, self._line) + + def visitOp( + self, + _node: OpNode + ): + + self._incLine() + op_upper = _node.op_type.upper() + full_line = f"{_node.target} .{op_upper}. {_node.value}" + _executeOperation(full_line, self._target_data, self._line) + + def visitPass( + self, + _node: PassNode + ): + + self._incLine() + + def visitUnrecog( + self, + _node: UnrecogNode + ): + + self._incLine() + upper = _node.raw_line.upper().strip() + if upper.startswith("IF"): + paren_open = upper.find("(") + if paren_open < 0: + raise ValueError(_errPos(self._line, "IF 缺少左括号")) + depth = 1 + for ci in range(paren_open + 1, len(upper)): + if upper[ci] == "(": + depth += 1 + elif upper[ci] == ")": + depth -= 1 + if depth == 0: + remaining = upper[ci + 1:].strip() + if remaining and remaining != "THEN": + raise ValueError(_errPos(self._line, f"IF 条件后存在多余内容 '{remaining}'")) + break + if depth > 0: + raise ValueError(_errPos(self._line, "IF 缺少右括号")) + elif upper.startswith("ELSE IF"): + paren_open = upper.find("(") + if paren_open < 0: + raise ValueError(_errPos(self._line, "ELSE IF 缺少左括号")) + _executeOperation(_node.raw_line, self._target_data, self._line) + + def execute( script_text: str, target_data: dict @@ -479,9 +561,9 @@ def execute( """ Execute an AutoScript on the given target data. - Parses the script line by line, maintaining an IF nesting stack to control - which blocks are active. Supports IF / ELSE IF / ELSE / END IF control flow, - SET assignments, PASS no-ops, and .ADD. / .SUB. field operations. + Parses the script into an AST via ASTokenizer.parse(), + then walks the tree with a visitor to evaluate conditions + and dispatch SET / ADD / SUB operations. Args: script_text (str): The AutoScript source code. @@ -489,85 +571,13 @@ def execute( Raises: ValueError: On syntax errors, unbalanced IF/END IF, unknown fields, etc. - - Example: - >>> data = {"reserve_info": {"date": "2026-05-01"}} - >>> execute( - ... "IF(.TRUE.)\\n" - ... " RESERVE_DATE .ADD. 1\\n" - ... "END IF", - ... data - ... ) - >>> data["reserve_info"]["date"] - '2026-05-02' """ - global _CUR_LINE - _buildFieldMap() if not script_text or not script_text.strip(): return - lines = [l.strip() for l in script_text.split("\n") if l.strip()] - if not lines: + ast = ASTokenizer.parse(script_text) + if not ast.body: return - if_stack = [] - for _CUR_LINE, line in enumerate(lines, 1): - upper_line = line.upper().strip() - if upper_line.startswith("IF"): - paren_open = _findConditionBegin(upper_line) - if paren_open < 0: - raise ValueError(_errPos("IF 缺少左括号")) - cond_end = _findConditionEnd(upper_line, paren_open) - if cond_end < 0: - raise ValueError(_errPos("IF 缺少右括号")) - remaining = upper_line[cond_end + 1:].strip() - if remaining and remaining.upper() != "THEN": - raise ValueError(_errPos(f"IF 条件后存在多余内容 '{remaining}'")) - condition_str = line[paren_open + 1:cond_end].strip() - matched = _evaluateCondition(condition_str, target_data) - if_stack.append([matched, matched]) - elif upper_line.startswith("ELSE IF"): - if not if_stack: - raise ValueError(_errPos("ELSE IF 前缺少 IF")) - paren_open = _findConditionBegin(upper_line) - if paren_open < 0: - raise ValueError(_errPos("ELSE IF 缺少左括号")) - cond_end = _findConditionEnd(upper_line, paren_open) - if cond_end < 0: - raise ValueError(_errPos("ELSE IF 缺少右括号")) - remaining = upper_line[cond_end + 1:].strip() - if remaining and remaining.upper() != "THEN": - raise ValueError(_errPos(f"ELSE IF 条件后存在多余内容 '{remaining}'")) - _, branch_matched = if_stack[-1] - if not branch_matched: - condition_str = line[paren_open + 1:cond_end].strip() - matched = _evaluateCondition(condition_str, target_data) - if_stack[-1] = [matched, matched] - else: - if_stack[-1][0] = False - elif upper_line == "ELSE": - if not if_stack: - raise ValueError(_errPos("ELSE 前缺少 IF")) - _, branch_matched = if_stack[-1] - if not branch_matched: - if_stack[-1] = [True, True] - else: - if_stack[-1][0] = False - elif upper_line in ("ENDIF", "END IF"): - if not if_stack: - raise ValueError(_errPos("ENDIF / END IF 前缺少 IF")) - if_stack.pop() - elif upper_line.startswith("SET "): - _assertInIf(if_stack, line) - if all(ctx[0] for ctx in if_stack): - _executeSet(line, target_data) - elif upper_line == "PASS": - continue - else: - _assertInIf(if_stack, line) - if all(ctx[0] for ctx in if_stack): - _executeOperation(line, target_data) - if if_stack: - raise ValueError( - "AutoScript 语法错误: IF 与 ENDIF / END IF 不匹配" - ) + executor = _EngineExecutor(target_data) + ast.accept(executor) diff --git a/src/autoscript/ASObject.py b/src/autoscript/ASObject.py index 082c0f4..af16bca 100644 --- a/src/autoscript/ASObject.py +++ b/src/autoscript/ASObject.py @@ -8,10 +8,18 @@ You may use, modify, and distribute this file under the terms of the MIT License See the LICENSE file for details. """ import re -from datetime import datetime, date, time +from datetime import ( + datetime, + date, + time +) -__all__ = ["ASObject", "_META_VARS", "_inferType"] +__all__ = [ + "ASObject", + "_META_VARS", + "_inferType" +] # Default values for each supported type when no value is present @@ -21,7 +29,7 @@ _TYPE_DEFAULTS = { "Boolean": False, "Date": None, "Time": None, - "String": "", + "String": "" } diff --git a/src/autoscript/ASOperator.py b/src/autoscript/ASOperator.py index bc9cf1b..65f55fb 100644 --- a/src/autoscript/ASOperator.py +++ b/src/autoscript/ASOperator.py @@ -7,7 +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. """ -from datetime import datetime, timedelta, date, time +from datetime import ( + datetime, + timedelta, + date, + time +) from .ASObject import ASObject @@ -51,8 +56,9 @@ class ASOperator: } _ARITH_TYPES = {"Date", "Time", "Int", "Float"} - @staticmethod + @classmethod def apply( + cls, target: ASObject, operand: ASObject, op: str, @@ -73,7 +79,7 @@ class ASOperator: tp = target.var_type op_tp = operand.var_type - if tp not in ASOperator._ARITH_TYPES: + if tp not in cls._ARITH_TYPES: raise ValueError(f"'{tp}' 类型字段不支持操作运算") if op_tp not in ("Int", "Float"): raise ValueError(f"操作数类型 '{op_tp}' 不能用于运算,需要数值类型 (Int / Float)") @@ -83,73 +89,70 @@ class ASOperator: if target_val is None: raise ValueError(f"'{target.name}' 的值为空,无法进行运算") if op == ".ADD.": - ASOperator._arithAdd(target, target_val, operand, target_data) + cls._arithAdd(target, target_val, operand, target_data) elif op == ".SUB.": - ASOperator._arithSub(target, target_val, operand, target_data) + cls._arithSub(target, target_val, operand, target_data) else: raise ValueError(f"不支持的操作 '{op}'") - @staticmethod + @classmethod + def _arithBinary( + cls, + target: ASObject, + target_val, + operand: ASObject, + target_data: dict, + sign: int + ): + """Apply ADD (sign=1) or SUB (sign=-1) per type.""" + + tp = target.var_type + raw_op = operand._value + op_name = "ADD" if sign == 1 else "SUB" + + if tp == "Date": + if not isinstance(target_val, date): + raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期") + new_val = target_val + timedelta(days=int(raw_op)) * sign + elif tp == "Time": + if not isinstance(target_val, time): + raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间") + delta = timedelta(hours=int(raw_op)) * sign + dt = datetime.combine(datetime.today(), target_val) + delta + new_val = dt.time() + elif tp == "Int": + new_val = int(target_val) + int(raw_op) * sign + elif tp == "Float": + new_val = float(target_val) + float(raw_op) * sign + else: + raise ValueError(f"'{tp}' 类型不支持 {op_name} 操作") + target.setValue(new_val, target_data) + + @classmethod def _arithAdd( + cls, target: ASObject, target_val, operand: ASObject, target_data: dict ): """Dispatch ADD per type.""" + cls._arithBinary(target, target_val, operand, target_data, 1) - tp = target.var_type - raw_op = operand._value - - if tp == "Date": - if not isinstance(target_val, date): - raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期") - new_val = target_val + timedelta(days=int(raw_op)) - elif tp == "Time": - if not isinstance(target_val, time): - raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间") - dt = datetime.combine(datetime.today(), target_val) + timedelta(hours=int(raw_op)) - new_val = dt.time() - elif tp == "Int": - new_val = int(target_val) + int(raw_op) - elif tp == "Float": - new_val = float(target_val) + float(raw_op) - else: - raise ValueError(f"'{tp}' 类型不支持 ADD 操作") - target.setValue(new_val, target_data) - - @staticmethod + @classmethod def _arithSub( + cls, target: ASObject, target_val, operand: ASObject, target_data: dict ): - """Dispatch SUB per type.""" + cls._arithBinary(target, target_val, operand, target_data, -1) - tp = target.var_type - raw_op = operand._value - - if tp == "Date": - if not isinstance(target_val, date): - raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期") - new_val = target_val - timedelta(days=int(raw_op)) - elif tp == "Time": - if not isinstance(target_val, time): - raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间") - dt = datetime.combine(datetime.today(), target_val) - timedelta(hours=int(raw_op)) - new_val = dt.time() - elif tp == "Int": - new_val = int(target_val) - int(raw_op) - elif tp == "Float": - new_val = float(target_val) - float(raw_op) - else: - raise ValueError(f"'{tp}' 类型不支持 SUB 操作") - target.setValue(new_val, target_data) - - @staticmethod + @classmethod def compare( + cls, left: ASObject, right: ASObject, op: str, @@ -171,7 +174,7 @@ class ASOperator: ValueError: If the types are incompatible for comparison. """ - cmp_func = ASOperator._COMPARE.get(op) + cmp_func = cls._COMPARE.get(op) if cmp_func is None: raise ValueError(f"未知的比较操作 '{op}'") left_val = left.getValue(target_data) diff --git a/src/autoscript/ASTokenizer.py b/src/autoscript/ASTokenizer.py new file mode 100644 index 0000000..aa0723e --- /dev/null +++ b/src/autoscript/ASTokenizer.py @@ -0,0 +1,478 @@ +# -*- 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 re + + +__all__ = [ + "ASTokenizer", + "Stmt", + "Script", + "IfNode", + "ElifNode", + "SetNode", + "OpNode", + "PassNode", + "UnrecogNode", + "NodeVisitor", + "LineStrategy" +] + + +# Token kind constants +K_IF = "IF" +K_ELSE_IF = "ELSE IF" +K_ELSE = "ELSE" +K_ENDIF = "ENDIF" +K_SET = "SET" +K_ADD = "ADD" +K_SUB = "SUB" +K_PASS = "PASS" + +# Op-type constants +OP_SET = "set" +OP_ADD = "add" +OP_SUB = "sub" + +# Compiled line patterns +_RE_IF = re.compile(r"^IF\((.+)\)(?:\s+THEN\s*)?$", re.IGNORECASE) +_RE_ELSE_IF = re.compile(r"^ELSE\s+IF\((.+)\)(?:\s+THEN\s*)?$", re.IGNORECASE) +_RE_ELSE = re.compile(r"^ELSE\s*$", re.IGNORECASE) +_RE_ENDIF = re.compile(r"^(ENDIF|END IF)$", re.IGNORECASE) +_RE_SET = re.compile(r"^SET\s+(\w+)\s*=\s*(.+)$", re.IGNORECASE) +_RE_ADD = re.compile(r"^(\w+)\s+\.ADD\.\s+(\d+)$", re.IGNORECASE) +_RE_SUB = re.compile(r"^(\w+)\s+\.SUB\.\s+(\d+)$", re.IGNORECASE) +_RE_PASS = re.compile(r"^\s*PASS\s*$", re.IGNORECASE) + + +class Script: + """ + Root AST node for an entire AutoScript. + Contains an ordered list of top-level statement nodes. + """ + + def __init__( + self, + body: list = None + ): + + self.body = body or [] + + def accept( + self, + visitor + ): + + return visitor.visitScript(self) + + +class IfNode: + """ + IF conditional block with optional ELSE IF / ELSE branches. + + Attributes: + condition (str): Raw condition expression. + body (list): Statements executed when condition is true. + elif_branches (list[ElifNode]): ELSE IF branches in order. + else_body (list): Statements executed for the ELSE branch. + closed (bool): Whether this IF has a matching ENDIF token. + """ + + def __init__( + self, + condition: str = "", + body: list = None, + elif_branches: list = None, + else_body: list = None, + closed: bool = True + ): + + self.condition = condition + self.body = body or [] + self.elif_branches = elif_branches or [] + self.else_body = else_body or [] + self.closed = closed + + def accept( + self, + visitor + ): + + return visitor.visitIf(self) + + +class ElifNode: + """ + ELSE IF branch within an IfNode. + """ + + def __init__( + self, + condition: str = "", + body: list = None + ): + + self.condition = condition + self.body = body or [] + + +class SetNode: + """ + SET assignment statement. + """ + + def __init__( + self, + target: str = "", + value: str = "" + ): + + self.target = target + self.value = value + + def accept( + self, + visitor + ): + + return visitor.visitSet(self) + + +class OpNode: + """ + .ADD. / .SUB. operation statement. + """ + + def __init__( + self, + op_type: str = "", + target: str = "", + value: str = "" + ): + + self.op_type = op_type + self.target = target + self.value = value + + def accept( + self, + visitor + ): + + return visitor.visitOp(self) + + +class PassNode: + """ + PASS no-op statement. + """ + + def accept( + self, + visitor + ): + + return visitor.visitPass(self) + + +class UnrecogNode: + """ + Unrecognised line preserved for downstream error reporting. + """ + + def __init__( + self, + raw_line: str = "" + ): + + self.raw_line = raw_line + + def accept( + self, + visitor + ): + + return visitor.visitUnrecog(self) + + +class NodeVisitor: + """ + Base visitor for the AutoScript AST. + + Subclass and override visit* methods to implement + custom traversal logic. Default walks tree depth-first. + """ + + def visitScript( + self, + _node: Script + ): + + for child in _node.body: + child.accept(self) + + def visitIf( + self, + _node: IfNode + ): + + for child in _node.body: + child.accept(self) + for elif_node in _node.elif_branches: + for child in elif_node.body: + child.accept(self) + for child in _node.else_body: + child.accept(self) + + def visitSet( + self, + _node: SetNode + ): + + pass + + def visitOp( + self, + _node: OpNode + ): + + pass + + def visitPass( + self, + _node: PassNode + ): + + pass + + def visitUnrecog( + self, + _node: UnrecogNode + ): + + pass + + +class LineStrategy: + """ + Encapsulates a regex pattern and its data-extraction handler. + Used by the tokenizer to classify a single line. + """ + + def __init__( + self, + pattern, + handler + ): + + self.pattern = pattern + self.handler = handler + + def match( + self, + line: str + ): + + m = self.pattern.match(line) + if m: + return self.handler(m) + return None + + +# Strategy instances — one per recognised AutoScript syntax form +_LINE_STRATEGIES = [ + LineStrategy(_RE_IF, lambda m: (K_IF, m.group(1))), + LineStrategy(_RE_ELSE_IF, lambda m: (K_ELSE_IF, m.group(1))), + LineStrategy(_RE_ELSE, lambda m: (K_ELSE, None)), + LineStrategy(_RE_ENDIF, lambda m: (K_ENDIF, None)), + LineStrategy(_RE_SET, lambda m: (K_SET, (m.group(1).strip(), m.group(2).strip()))), + LineStrategy(_RE_ADD, lambda m: (K_ADD, (m.group(1).strip(), m.group(2).strip()))), + LineStrategy(_RE_SUB, lambda m: (K_SUB, (m.group(1).strip(), m.group(2).strip()))), + LineStrategy(_RE_PASS, lambda m: (K_PASS, None)), +] + + +class Stmt: + """ + Flat statement container, backward-compatible with the original + tokenize() output and the orchestration dialog's _classifyLine. + """ + + def __init__( + self, + kind: str | None = None, + condition: str | None = None, + target: str | None = None, + value: str | None = None, + op_type: str | None = None, + raw_line: str = "" + ): + + self.kind = kind + self.condition = condition + self.target = target + self.value = value + self.op_type = op_type + self.raw_line = raw_line + + +class ASTokenizer: + """ + Tokenizer / parser for the AutoScript DSL. + + Provides three entry points: + - classifyLine(line) — single-line classifier. + - tokenize(script) — flat Stmt list. + - parse(script) — structured AST (Script root). + """ + + @classmethod + def _matchLine( + cls, + stripped: str + ): + + for strategy in _LINE_STRATEGIES: + result = strategy.match(stripped) + if result: + return result + return (None, None) + + @classmethod + def classifyLine( + cls, + stripped: str + ): + + kind, data = cls._matchLine(stripped) + if kind is None or kind == K_PASS: + return None + return (kind, data) + + @classmethod + def tokenize( + cls, + script: str + ) -> list: + + statements = [] + for raw_line in script.split("\n"): + stripped = raw_line.strip() + if not stripped: + continue + kind, data = cls._matchLine(stripped) + stmt = Stmt(kind=kind, raw_line=stripped) + + if kind == K_IF or kind == K_ELSE_IF: + stmt.condition = data + elif kind == K_SET: + stmt.target, stmt.value = data + stmt.op_type = OP_SET + elif kind == K_ADD: + stmt.target, stmt.value = data + stmt.op_type = OP_ADD + elif kind == K_SUB: + stmt.target, stmt.value = data + stmt.op_type = OP_SUB + statements.append(stmt) + return statements + + @classmethod + def parse( + cls, + script: str + ) -> Script: + + tokens = cls.tokenize(script) + body = [] + i = 0 + while i < len(tokens): + tok = tokens[i] + kind = tok.kind + + if kind == K_IF: + node, consumed = cls._parseIfBlock(tokens, i) + body.append(node) + i += consumed + elif kind in (K_ELSE_IF, K_ELSE, K_ENDIF): + i += 1 + elif kind == K_SET: + body.append(SetNode(target=tok.target, value=tok.value)) + i += 1 + elif kind in (K_ADD, K_SUB): + body.append(OpNode( + op_type=tok.op_type, + target=tok.target, + value=tok.value + )) + i += 1 + elif kind == K_PASS: + body.append(PassNode()) + i += 1 + else: + body.append(UnrecogNode(raw_line=tok.raw_line)) + i += 1 + return Script(body=body) + + @classmethod + def _parseIfBlock( + cls, + tokens: list, + start: int + ): + + first = tokens[start] + node = IfNode(condition=first.condition or "") + body = [] + elif_branches = [] + else_body = [] + current_target = body + i = start + 1 + + while i < len(tokens): + tok = tokens[i] + kind = tok.kind + if kind == K_IF: + sub_node, consumed = cls._parseIfBlock(tokens, i) + current_target.append(sub_node) + i += consumed + elif kind == K_ELSE_IF: + elif_branches.append(ElifNode(condition=tok.condition or "")) + current_target = elif_branches[-1].body + i += 1 + elif kind == K_ELSE: + else_body = [] + current_target = else_body + i += 1 + elif kind == K_ENDIF: + node.body = body + node.elif_branches = elif_branches + node.else_body = else_body + return (node, i - start + 1) + elif kind == K_SET: + current_target.append(SetNode(target=tok.target, value=tok.value)) + i += 1 + elif kind in (K_ADD, K_SUB): + current_target.append(OpNode( + op_type=tok.op_type, + target=tok.target, + value=tok.value + )) + i += 1 + elif kind == K_PASS: + current_target.append(PassNode()) + i += 1 + else: + current_target.append(UnrecogNode(raw_line=tok.raw_line)) + i += 1 + node.body = body + node.elif_branches = elif_branches + node.else_body = else_body + node.closed = False + return (node, i - start) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index a2a6f1e..6c6ae75 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -11,14 +11,37 @@ - registerDefaultTargetVars(): Register all built-in target variables. - META_VARS: dict of built-in read-only meta variables. - ALL_VARIABLES: dict of all available variables (display_name -> (name, type)). + - ASTokenizer: Unified tokenizer for the orchestration dialog and engine. """ - -from autoscript.ASEngine import execute, addTargetVar +from autoscript.ASTokenizer import ( + ASTokenizer, + Stmt, + ElifNode, + Script, + IfNode, + SetNode, + OpNode, +) +from autoscript.ASEngine import ( + execute, + addTargetVar, +) from autoscript.ASObject import _META_VARS as META_VARS __all__ = [ - "execute", "addTargetVar", "registerDefaultTargetVars", - "META_VARS", "ALL_VARIABLES", + "execute", + "addTargetVar", + "registerDefaultTargetVars", + "buildMockTargetData", + "META_VARS", + "ALL_VARIABLES", + "ASTokenizer", + "Stmt", + "Script", + "IfNode", + "SetNode", + "OpNode", + "ElifNode" ] # All variables available to scripts (display_name -> (name, type)). @@ -43,9 +66,33 @@ _TARGET_VAR_DEFS = [ ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), ] +_MOCK_TYPE_VALUES = { + "String": "__mock__", + "Boolean": True, + "Date": "2099-01-01", + "Time": "00:00", + "Int": 0, + "Float": 0.0, +} -def registerDefaultTargetVars() -> None: +def buildMockTargetData( +) -> dict: + """ + Build a target_data dict filled with type-appropriate mock values + for all registered target variables. + """ + data = {} + for _, var_type, key_path, _ in _TARGET_VAR_DEFS: + d = data + for key in key_path[:-1]: + d = d.setdefault(key, {}) + d[key_path[-1]] = _MOCK_TYPE_VALUES.get(var_type, "") + return data + + +def registerDefaultTargetVars( +) -> None: """ Register all built-in target variables with the engine. This must be called before any script execution. From 2843300cf9d8ef2e4c08c26813ec44985958be23 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 17 May 2026 01:48:25 +0800 Subject: [PATCH 05/49] =?UTF-8?q?refactor(autoscript):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E8=A7=82=E5=AF=9F=E8=80=85=E6=A8=A1=E5=BC=8F=E8=A7=A3?= =?UTF-8?q?=E8=80=A6=E8=A7=A3=E6=9E=90=E4=B8=8E=E9=A2=84=E6=A3=80=E6=9F=A5?= =?UTF-8?q?/=E7=BC=96=E6=8E=92=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASObserver.py | 78 ++++++++++++++++++++ src/autoscript/ASTokenizer.py | 135 +++++++++++++++++++++++++++------- src/autoscript/__init__.py | 17 +---- 3 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 src/autoscript/ASObserver.py diff --git a/src/autoscript/ASObserver.py b/src/autoscript/ASObserver.py new file mode 100644 index 0000000..7fa3138 --- /dev/null +++ b/src/autoscript/ASObserver.py @@ -0,0 +1,78 @@ +# -*- 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. +""" + + +class ParsingObserver: + """ + Base observer for AutoScript parsing events. + + Subclass and override the relevant methods to react to + tokenization / parsing events produced by ASTokenizer. + This is the core abstraction that lets pre-check and + orchestration modules subscribe to the same parsing pipeline. + """ + + def onParseStart( + self, + script_text: str + ): + """ + Called when tokenization of a new script begins. + + Args: + script_text (str): The full script text being parsed. + """ + pass + + def onTokenParsed( + self, + kind: str | None, + data, + line_num: int, + raw_line: str + ): + """ + Called after each script line has been classified as a token. + + Args: + kind (str | None): Token kind (K_IF, K_ELSE_IF, K_ELSE, + K_ENDIF, K_SET, K_ADD, K_SUB) or + None if unrecognised. + data: Token payload — condition string for IF/ELSE IF, + (target, value) tuple for SET/ADD/SUB, + None for ELSE/ENDIF/unrecognised. + line_num (int): 1-based line number. + raw_line (str): The stripped raw line text. + """ + pass + + def onParseComplete( + self, + statements: list + ): + """ + Called when flat tokenization is complete. + + Args: + statements (list[Stmt]): The list of parsed Stmt objects. + """ + pass + + def onASTReady( + self, + ast + ): + """ + Called when full AST construction is complete (after parse()). + + Args: + ast (Script): The root Script AST node. + """ + pass diff --git a/src/autoscript/ASTokenizer.py b/src/autoscript/ASTokenizer.py index aa0723e..bdd2809 100644 --- a/src/autoscript/ASTokenizer.py +++ b/src/autoscript/ASTokenizer.py @@ -326,12 +326,27 @@ class ASTokenizer: """ Tokenizer / parser for the AutoScript DSL. - Provides three entry points: + Main class-level entry points (engine-facing): - classifyLine(line) — single-line classifier. - tokenize(script) — flat Stmt list. - parse(script) — structured AST (Script root). + + Observer-enabled API (used by pre-check & orchestration): + >>> obs = ScriptPrecheckObserver() + >>> stmts = ASTokenizer.tokenizeWithObservers(script, [obs]) """ + @classmethod + def _notifyObservers( + cls, + observers: list, + method: str, + *args + ): + + for obs in observers: + getattr(obs, method)(*args) + @classmethod def _matchLine( cls, @@ -345,18 +360,29 @@ class ASTokenizer: return (None, None) @classmethod - def classifyLine( + def _buildStmt( cls, - stripped: str - ): + stripped: str, + kind: str | None, + data + ) -> Stmt: - kind, data = cls._matchLine(stripped) - if kind is None or kind == K_PASS: - return None - return (kind, data) + stmt = Stmt(kind=kind, raw_line=stripped) + if kind == K_IF or kind == K_ELSE_IF: + stmt.condition = data + elif kind == K_SET: + stmt.target, stmt.value = data + stmt.op_type = OP_SET + elif kind == K_ADD: + stmt.target, stmt.value = data + stmt.op_type = OP_ADD + elif kind == K_SUB: + stmt.target, stmt.value = data + stmt.op_type = OP_SUB + return stmt @classmethod - def tokenize( + def _tokenizeImpl( cls, script: str ) -> list: @@ -367,29 +393,15 @@ class ASTokenizer: if not stripped: continue kind, data = cls._matchLine(stripped) - stmt = Stmt(kind=kind, raw_line=stripped) - - if kind == K_IF or kind == K_ELSE_IF: - stmt.condition = data - elif kind == K_SET: - stmt.target, stmt.value = data - stmt.op_type = OP_SET - elif kind == K_ADD: - stmt.target, stmt.value = data - stmt.op_type = OP_ADD - elif kind == K_SUB: - stmt.target, stmt.value = data - stmt.op_type = OP_SUB - statements.append(stmt) + statements.append(cls._buildStmt(stripped, kind, data)) return statements @classmethod - def parse( + def _parseTokens( cls, - script: str + tokens: list ) -> Script: - tokens = cls.tokenize(script) body = [] i = 0 while i < len(tokens): @@ -420,6 +432,77 @@ class ASTokenizer: i += 1 return Script(body=body) + @classmethod + def classifyLine( + cls, + stripped: str + ): + + kind, data = cls._matchLine(stripped) + if kind is None or kind == K_PASS: + return None + return (kind, data) + + @classmethod + def tokenize( + cls, + script: str + ) -> list: + + return cls._tokenizeImpl(script) + + @classmethod + def parse( + cls, + script: str + ) -> Script: + + return cls._parseTokens(cls._tokenizeImpl(script)) + + @classmethod + def tokenizeWithObservers( + cls, + script: str, + observers: list + ) -> list: + """ + Tokenize and notify observers for each classified line. + + Fires onParseStart, onTokenParsed, and onParseComplete + events to each observer. This is the single tokenization + pipeline shared by pre-check and orchestration modules. + """ + + cls._notifyObservers(observers, "onParseStart", script) + statements = [] + for i, raw_line in enumerate(script.split("\n"), 1): + stripped = raw_line.strip() + if not stripped: + continue + kind, data = cls._matchLine(stripped) + cls._notifyObservers(observers, "onTokenParsed", kind, data, i, stripped) + statements.append(cls._buildStmt(stripped, kind, data)) + cls._notifyObservers(observers, "onParseComplete", statements) + return statements + + @classmethod + def parseWithObservers( + cls, + script: str, + observers: list + ) -> Script: + """ + Parse and notify observers throughout the pipeline. + + Calls tokenizeWithObservers (which fires per-token events), + then builds the AST and fires onASTReady. + """ + + tokens = cls.tokenizeWithObservers(script, observers) + ast = cls._parseTokens(tokens) + cls._notifyObservers(observers, "onASTReady", ast) + return ast + @classmethod def _parseIfBlock( cls, diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index 6c6ae75..c761e6d 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -1,17 +1,6 @@ """ AutoScript module for the AutoLibrary project. - - A lightweight scripting DSL for preprocessing user reservation data - in repeatable timer tasks. Supports IF/ELSE IF/ELSE/END IF control - flow, SET assignments, .ADD./.SUB. operations, and rich comparisons. - - Public API: - - execute(script_text, target_data): Execute an AutoScript. - - addTargetVar(name, var_type, key_path, display_name): Register a variable. - - registerDefaultTargetVars(): Register all built-in target variables. - - META_VARS: dict of built-in read-only meta variables. - - ALL_VARIABLES: dict of all available variables (display_name -> (name, type)). - - ASTokenizer: Unified tokenizer for the orchestration dialog and engine. + A lightweight scripting DSL for preprocessing user reservation data. """ from autoscript.ASTokenizer import ( ASTokenizer, @@ -27,6 +16,7 @@ from autoscript.ASEngine import ( addTargetVar, ) from autoscript.ASObject import _META_VARS as META_VARS +from autoscript.ASObserver import ParsingObserver __all__ = [ "execute", @@ -41,7 +31,8 @@ __all__ = [ "IfNode", "SetNode", "OpNode", - "ElifNode" + "ElifNode", + "ParsingObserver", ] # All variables available to scripts (display_name -> (name, type)). From 33c0f4414c14896a36cea83b749aac4094464012 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 17 May 2026 02:58:47 +0800 Subject: [PATCH 06/49] =?UTF-8?q?fix(autoscript):=20=E4=B8=BA=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=B7=BB=E5=8A=A0=E8=A1=8C=E5=8F=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E5=85=85=E7=B1=BB=E5=9E=8B=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASEngine.py | 21 +++++++++++++++------ src/autoscript/ASOperator.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index 8eaed3d..f73657b 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -307,9 +307,12 @@ def _evaluateCondition( continue left_raw = s[:idx].strip() right_raw = s[idx + len(op):].strip() - left_obj = _resolveAsObject(left_raw, target_data) - right_obj = _resolveAsObject(right_raw, target_data) - return ASOperator.compare(left_obj, right_obj, op, target_data) + try: + left_obj = _resolveAsObject(left_raw, target_data) + right_obj = _resolveAsObject(right_raw, target_data) + return ASOperator.compare(left_obj, right_obj, op, target_data) + except ValueError as e: + raise ValueError(_errPos(line, str(e))) raise ValueError( _errPos(line, f"无法识别的条件表达式 '{condition_str}'") ) @@ -352,7 +355,10 @@ def _executeSet( if not obj: obj = _SCRIPT_VARS.get(upper_name) if obj: - obj.setValue(resolved, target_data) + try: + obj.setValue(resolved, target_data) + except ValueError as e: + raise ValueError(_errPos(line, str(e))) return inferred_type = _inferType(resolved, stripped) new_var = ASObject( @@ -398,8 +404,11 @@ def _executeOperation( target = _resolveFieldObj(field_name) if target is None: raise ValueError(_errPos(line, f"未知字段 '{field_name}'")) - operand = _resolveAsObject(raw_value, target_data) - ASOperator.apply(target, operand, op, target_data) + try: + operand = _resolveAsObject(raw_value, target_data) + ASOperator.apply(target, operand, op, target_data) + except ValueError as e: + raise ValueError(_errPos(line, str(e))) def addTargetVar( diff --git a/src/autoscript/ASOperator.py b/src/autoscript/ASOperator.py index 65f55fb..49facd7 100644 --- a/src/autoscript/ASOperator.py +++ b/src/autoscript/ASOperator.py @@ -55,6 +55,14 @@ class ASOperator: ".BLE.": lambda a, b: a <= b, } _ARITH_TYPES = {"Date", "Time", "Int", "Float"} + # Comparison-compatible type groups + _COMPATIBLE_GROUPS = [ + {"String"}, + {"Boolean"}, + {"Int", "Float"}, + {"Date"}, + {"Time"}, + ] @classmethod def apply( @@ -177,6 +185,18 @@ class ASOperator: cmp_func = cls._COMPARE.get(op) if cmp_func is None: raise ValueError(f"未知的比较操作 '{op}'") + left_tp = left.var_type + right_tp = right.var_type + if left_tp != right_tp: + same_group = any( + left_tp in g and right_tp in g + for g in cls._COMPATIBLE_GROUPS + ) + if not same_group: + raise ValueError( + f"类型不兼容: 无法将 '{left.name}' ({left_tp}) " + f"与 '{right.name}' ({right_tp}) 进行比较" + ) left_val = left.getValue(target_data) right_val = right.getValue(target_data) try: From 6cf182c8c86154af54e97dd5c4d51e0a7e1dfee5 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 11:15:35 +0800 Subject: [PATCH 07/49] =?UTF-8?q?refactor(gui):=20=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E8=BF=81=E7=A7=BB=E8=87=B3=E6=96=B0=E5=8C=85?= =?UTF-8?q?=E5=B9=B6=E7=A7=BB=E9=99=A4=E6=97=A7=E7=9A=84=E9=A2=84=E8=A7=88?= =?UTF-8?q?/=E7=BC=96=E6=8E=92=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/ALAutoScriptEditDialog.py | 390 ++++++++ src/gui/ALAutoScriptOrchDialog.py | 884 ------------------ src/gui/ALAutoScriptOrchDialog/__init__.py | 3 + src/gui/ALAutoScriptOrchDialog/_blocks.py | 272 ++++++ src/gui/ALAutoScriptOrchDialog/_dialog.py | 296 ++++++ src/gui/ALAutoScriptOrchDialog/_helpers.py | 688 ++++++++++++++ .../ALAutoScriptOrchDialog/_orchestrate.py | 107 +++ src/gui/ALAutoScriptOrchDialog/_precheck.py | 163 ++++ src/gui/ALAutoScriptOrchDialog/_widgets.py | 534 +++++++++++ src/gui/ALAutoScriptPrevDialog.py | 226 ----- src/gui/ALMainWorkers.py | 5 +- src/gui/ALTimerTaskAddDialog.py | 22 +- 12 files changed, 2468 insertions(+), 1122 deletions(-) create mode 100644 src/gui/ALAutoScriptEditDialog.py delete mode 100644 src/gui/ALAutoScriptOrchDialog.py create mode 100644 src/gui/ALAutoScriptOrchDialog/__init__.py create mode 100644 src/gui/ALAutoScriptOrchDialog/_blocks.py create mode 100644 src/gui/ALAutoScriptOrchDialog/_dialog.py create mode 100644 src/gui/ALAutoScriptOrchDialog/_helpers.py create mode 100644 src/gui/ALAutoScriptOrchDialog/_orchestrate.py create mode 100644 src/gui/ALAutoScriptOrchDialog/_precheck.py create mode 100644 src/gui/ALAutoScriptOrchDialog/_widgets.py delete mode 100644 src/gui/ALAutoScriptPrevDialog.py diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py new file mode 100644 index 0000000..2cbfdef --- /dev/null +++ b/src/gui/ALAutoScriptEditDialog.py @@ -0,0 +1,390 @@ +# -*- 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. +""" +from PySide6.QtCore import Qt, Slot +from PySide6.QtGui import ( + QColor, + QFont, + QSyntaxHighlighter, + QTextCharFormat, +) +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QDialogButtonBox, + QGridLayout, + QHBoxLayout, + QLabel, + QPlainTextEdit, + QPushButton, + + QStyle, + QTabWidget, + QVBoxLayout, + QWidget, +) + +from autoscript import ALL_VARIABLES + + +class ALScriptHighlighter(QSyntaxHighlighter): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self._rules = [] + + keywordFmt = QTextCharFormat() + keywordFmt.setForeground(QColor("#007ACC")) + keywordFmt.setFontWeight(QFont.Weight.Bold) + for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF", + "SET", "PASS", "THEN"]: + pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b" + self._rules.append((pattern, keywordFmt)) + opFmt = QTextCharFormat() + opFmt.setForeground(QColor("#AF00DB")) + opFmt.setFontWeight(QFont.Weight.Normal) + for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.", + r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\.", + r"\.AND\.", r"\.OR\."]: + self._rules.append((op, opFmt)) + literalFmt = QTextCharFormat() + literalFmt.setForeground(QColor("#AF00DB")) + literalFmt.setFontWeight(QFont.Weight.Bold) + for lit in [".TRUE.", ".FALSE."]: + self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt)) + funcFmt = QTextCharFormat() + funcFmt.setForeground(QColor("#795E26")) + funcFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b(?:DATE|TIME)\b", funcFmt)) + varFmt = QTextCharFormat() + varFmt.setForeground(QColor("#267F99")) + varFmt.setFontWeight(QFont.Weight.Normal) + var_names = [name for _, (name, _) in ALL_VARIABLES.items()] + for var in var_names: + self._rules.append((r"\b" + var + r"\b", varFmt)) + strFmt = QTextCharFormat() + strFmt.setForeground(QColor("#A31515")) + strFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"'[^']*'", strFmt)) + numFmt = QTextCharFormat() + numFmt.setForeground(QColor("#098658")) + numFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b\d+\b", numFmt)) + commentFmt = QTextCharFormat() + commentFmt.setForeground(QColor("#008000")) + commentFmt.setFontItalic(True) + self._rules.append((r"//[^\n]*", commentFmt)) + + + def highlightBlock( + self, + text + ): + + import re + for pattern, fmt in self._rules: + for match in re.finditer(pattern, text, re.IGNORECASE): + start = match.start() + length = match.end() - match.start() + self.setFormat(start, length, fmt) + + +class ALAutoScriptEditDialog(QDialog): + + def __init__( + self, + parent = None, + script: str = "" + ): + + super().__init__(parent) + self._fontSize = 19 + + self.modifyUi() + self.connectSignals() + + self._textEdit.setPlainText(script) + self._highlighter = ALScriptHighlighter( + self._textEdit.document() + ) + + + def modifyUi( + self + ): + + self.setWindowTitle("AutoScript 编辑 - AutoLibrary") + self.setMinimumSize(640, 600) + layout = QVBoxLayout(self) + layout.setSpacing(4) + layout.setContentsMargins(4, 4, 4, 4) + toolbarLayout = QHBoxLayout() + self._zoomInBtn = QPushButton("+") + self._zoomInBtn.setFixedSize(25, 25) + self._zoomOutBtn = QPushButton("-") + self._zoomOutBtn.setFixedSize(25, 25) + self._zoomResetBtn = QPushButton( + QApplication.style().standardIcon( + QStyle.StandardPixmap.SP_BrowserReload + ), "" + ) + self._zoomResetBtn.setFixedSize(25, 25) + self._zoomResetBtn.setToolTip("重置缩放") + self._zoomLabel = QLabel(f"{self._fontSize}px") + self._zoomLabel.setFixedHeight(25) + toolbarLayout.addWidget(self._zoomInBtn) + toolbarLayout.addWidget(self._zoomOutBtn) + toolbarLayout.addWidget(self._zoomResetBtn) + toolbarLayout.addWidget(self._zoomLabel) + toolbarLayout.addStretch() + self._copyBtn = QPushButton( + QApplication.style().standardIcon( + QStyle.StandardPixmap.SP_FileDialogDetailedView + ), "" + ) + self._copyBtn.setFixedSize(25, 25) + self._copyBtn.setToolTip("复制脚本") + toolbarLayout.addWidget(self._copyBtn) + layout.addLayout(toolbarLayout) + self._textEdit = QPlainTextEdit(self) + self._textEdit.setLineWrapMode( + QPlainTextEdit.LineWrapMode.NoWrap + ) + self._textEdit.setStyleSheet( + "QPlainTextEdit {" + " font-family: 'Courier New', 'Consolas', monospace;" + f" font-size: {self._fontSize}px;" + "}" + ) + layout.addWidget(self._textEdit) + + self._createButtonPanel(layout) + + self._btnBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel + ) + self._btnBox.button( + QDialogButtonBox.StandardButton.Ok + ).setText("保存") + self._btnBox.button( + QDialogButtonBox.StandardButton.Cancel + ).setText("取消") + layout.addWidget(self._btnBox) + + def _createButtonPanel( + self, + parent_layout + ): + + + tab_widget = QTabWidget() + tab_widget.setMaximumHeight(200) + basic_widget = QWidget() + basic_layout = QGridLayout(basic_widget) + basic_layout.setSpacing(4) + basic_layout.setContentsMargins(4, 4, 4, 4) + basic_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + control_buttons = [ + ("IF", "IF()\n \nEND IF"), + ("ELSE IF", "ELSE IF()\n "), + ("ELSE", "ELSE"), + ("END IF", "END IF"), + ("PASS", "PASS"), + ] + self._addButtonsToGrid(basic_layout, control_buttons, 0, 0, 5) + + assign_buttons = [ + ("SET", "SET = "), + ] + self._addButtonsToGrid(basic_layout, assign_buttons, 0, 5, 1) + + func_buttons = [ + ("DATE()", "DATE()"), + ("TIME()", "TIME()"), + ] + self._addButtonsToGrid(basic_layout, func_buttons, 1, 0, 2) + + tab_widget.addTab(basic_widget, "基本语法") + operator_widget = QWidget() + operator_layout = QGridLayout(operator_widget) + operator_layout.setSpacing(4) + operator_layout.setContentsMargins(4, 4, 4, 4) + operator_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + arithmetic_buttons = [ + (".ADD.", ".ADD."), + (".SUB.", ".SUB."), + ] + self._addButtonsToGrid(operator_layout, arithmetic_buttons, 0, 0, 2) + compare_buttons = [ + (".EQ.", ".EQ."), + (".NEQ.", ".NEQ."), + (".BGT.", ".BGT."), + (".BLT.", ".BLT."), + (".BGE.", ".BGE."), + (".BLE.", ".BLE."), + ] + self._addButtonsToGrid(operator_layout, compare_buttons, 1, 0, 6) + logic_buttons = [ + (".AND.", ".AND."), + (".OR.", ".OR."), + ] + self._addButtonsToGrid(operator_layout, logic_buttons, 2, 0, 2) + tab_widget.addTab(operator_widget, "运算符") + literal_widget = QWidget() + literal_layout = QGridLayout(literal_widget) + literal_layout.setSpacing(4) + literal_layout.setContentsMargins(4, 4, 4, 4) + literal_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + bool_buttons = [ + (".TRUE.", ".TRUE."), + (".FALSE.", ".FALSE."), + ] + self._addButtonsToGrid(literal_layout, bool_buttons, 0, 0, 2) + hint_buttons = [ + ("字符串", "'文本'"), + ("数字", "123"), + ("注释", "// 注释"), + ] + self._addButtonsToGrid(literal_layout, hint_buttons, 1, 0, 3) + tab_widget.addTab(literal_widget, "字面量") + var_widget = QWidget() + var_layout = QGridLayout(var_widget) + var_layout.setSpacing(4) + var_layout.setContentsMargins(4, 4, 4, 4) + var_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + var_buttons = [ + (display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() + ] + + self._addButtonsToGrid(var_layout, var_buttons, 0, 0, 5) + tab_widget.addTab(var_widget, "变量") + parent_layout.addWidget(tab_widget) + + def _addButtonsToGrid( + self, + grid_layout, + buttons, + start_row, + start_col, + max_columns + ): + + col = start_col + row = start_row + + for btn_text, template in buttons: + btn = QPushButton(btn_text) + btn.setProperty("template", template) + btn.clicked.connect(self._insertTemplate) + btn.setFixedWidth(100) + btn.setFixedHeight(30) + btn.setToolTip(f"插入: {template}") + grid_layout.addWidget(btn, row, col) + + col += 1 + if col >= start_col + max_columns: + col = start_col + row += 1 + + @Slot() + def _insertTemplate( + self + ): + + btn = self.sender() + if not isinstance(btn, QPushButton): + return + template = btn.property("template") + if not template: + return + cursor = self._textEdit.textCursor() + cursor.insertText(template) + + def connectSignals( + self + ): + + self._btnBox.accepted.connect(self.accept) + self._btnBox.rejected.connect(self.reject) + self._zoomInBtn.clicked.connect(self.onZoomIn) + self._zoomOutBtn.clicked.connect(self.onZoomOut) + self._zoomResetBtn.clicked.connect(self.onZoomReset) + self._copyBtn.clicked.connect(self.onCopy) + + + def getScript( + self + ) -> str: + + return self._textEdit.toPlainText() + + + def updateFontSize( + self + ): + + font = self._textEdit.font() + font.setPointSize(self._fontSize) + self._textEdit.setFont(font) + self._textEdit.setStyleSheet( + "QPlainTextEdit {" + " font-family: 'Courier New', 'Consolas', monospace;" + f" font-size: {self._fontSize}px;" + "}" + ) + self._zoomLabel.setText(f"{self._fontSize}px") + + + @Slot() + def onZoomIn( + self + ): + + self._fontSize = min(self._fontSize + 2, 40) + self.updateFontSize() + + + @Slot() + def onZoomOut( + self + ): + + self._fontSize = max(self._fontSize - 2, 8) + self.updateFontSize() + + + @Slot() + def onZoomReset( + self + ): + + self._fontSize = 13 + self.updateFontSize() + + + @Slot() + def onCopy( + self + ): + + clipboard = QApplication.clipboard() + clipboard.setText(self._textEdit.toPlainText()) + original = self._copyBtn.text() + self._copyBtn.setText("已复制") + self._copyBtn.setEnabled(False) + from PySide6.QtCore import QTimer + QTimer.singleShot(2000, lambda: ( + self._copyBtn.setText(original), + self._copyBtn.setEnabled(True) + )) diff --git a/src/gui/ALAutoScriptOrchDialog.py b/src/gui/ALAutoScriptOrchDialog.py deleted file mode 100644 index e244763..0000000 --- a/src/gui/ALAutoScriptOrchDialog.py +++ /dev/null @@ -1,884 +0,0 @@ -# -*- 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. -""" - -from PySide6.QtCore import QTime, QDate, Slot -from PySide6.QtWidgets import ( - QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QComboBox, QPushButton, QScrollArea, QTimeEdit, - QDateEdit, QLineEdit, QSpinBox, QDoubleSpinBox, - QStackedWidget, QFrame, QDialogButtonBox, - QGroupBox, QSizePolicy -) - -from dsl.AutoScriptEngine import AutoScriptEngine - - -VARIABLE_META = AutoScriptEngine.VARIABLE_META -_VAR_COMBO_ITEMS = [ - (display, varname, vartype) - for display, (varname, vartype) in VARIABLE_META.items() -] -_VAR_COMBO_ITEMS_SET = [ - (display, varname, vartype) - for display, (varname, vartype) in VARIABLE_META.items() - if not varname.startswith("CURRENT_") -] -OP_ITEMS = [ - ("等于", ".EQ."), - ("不等于", ".NEQ."), - ("大于", ".BGT."), - ("小于", ".BLT."), - ("大于等于", ".BGE."), - ("小于等于", ".BLE."), -] - - -def _makeVarCombo( -) -> QComboBox: - - cb = QComboBox() - for display, varname, vartype in _VAR_COMBO_ITEMS: - cb.addItem(display, (varname, vartype)) - cb.setMinimumWidth(120) - cb.setFixedHeight(25) - return cb - - -def _makeSetVarCombo( -) -> QComboBox: - - cb = QComboBox() - for display, varname, vartype in _VAR_COMBO_ITEMS_SET: - cb.addItem(display, (varname, vartype)) - cb.setMinimumWidth(120) - cb.setFixedHeight(25) - return cb - - -def _makeOpCombo( -) -> QComboBox: - - cb = QComboBox() - for display, op in OP_ITEMS: - cb.addItem(display, op) - cb.setMinimumWidth(80) - cb.setFixedHeight(25) - return cb - - -def _makeValueWidget( - data_type: str -) -> QWidget: - - if data_type == "Time": - w = QTimeEdit() - w.setDisplayFormat("HH:mm") - w.setMinimumWidth(100) - w.setFixedHeight(25) - elif data_type == "Date": - w = QDateEdit() - w.setDisplayFormat("yyyy-MM-dd") - w.setCalendarPopup(True) - w.setMinimumWidth(130) - w.setFixedHeight(25) - elif data_type == "Integer": - w = QSpinBox() - w.setMinimum(-999999) - w.setMaximum(999999) - w.setMinimumWidth(100) - w.setFixedHeight(25) - elif data_type == "Float": - w = QDoubleSpinBox() - w.setMinimum(-999999) - w.setMaximum(999999) - w.setDecimals(2) - w.setMinimumWidth(100) - w.setFixedHeight(25) - elif data_type == "Boolean": - w = QComboBox() - w.addItem(".TRUE.", ".TRUE.") - w.addItem(".FALSE.", ".FALSE.") - w.setMinimumWidth(100) - w.setFixedHeight(25) - else: - w = QLineEdit() - w.setPlaceholderText("输入值") - w.setMinimumWidth(120) - w.setFixedHeight(25) - return w - - -def _makeActionValueWidget( - data_type: str -) -> QWidget: - - if data_type == "Date": - w = QComboBox() - w.addItem("今天", "today") - w.addItem("明天", "tomorrow") - w.setFixedHeight(25) - w.setMinimumWidth(100) - w._is_date_action = True - return w - - if data_type == "Time": - container = QWidget() - layout = QHBoxLayout(container) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(2) - modeCombo = QComboBox() - modeCombo.addItem("固定时间", "fixed") - modeCombo.addItem("相对当前", "relative") - modeCombo.setFixedHeight(25) - stack = QStackedWidget() - timeEdit = QTimeEdit() - timeEdit.setDisplayFormat("HH:mm") - timeEdit.setFixedHeight(25) - spinBox = QSpinBox() - spinBox.setRange(0, 23) - spinBox.setSuffix("小时") - spinBox.setFixedHeight(25) - stack.addWidget(timeEdit) - stack.addWidget(spinBox) - modeCombo.currentIndexChanged.connect( - lambda i: stack.setCurrentIndex(i) - ) - layout.addWidget(modeCombo) - layout.addWidget(stack) - container._modeCombo = modeCombo - container._timeEdit = timeEdit - container._spinBox = spinBox - container._isActionTime = True - return container - - return _makeValueWidget(data_type) - - -def _getValueFromWidget( - w: QWidget -) -> str: - - if getattr(w, '_isActionTime', False): - if w._modeCombo.currentData() == "fixed": - return w._timeEdit.time().toString("HH:mm") - else: - return f"+{w._spinBox.value()}" - if isinstance(w, QTimeEdit): - return w.time().toString("HH:mm") - if isinstance(w, QDateEdit): - return w.date().toString("yyyy-MM-dd") - if isinstance(w, QComboBox): - return w.currentText() - if isinstance(w, QSpinBox): - return str(w.value()) - if isinstance(w, QDoubleSpinBox): - return str(w.value()) - if isinstance(w, QLineEdit): - return w.text() - return "" - - -def _encodeValueStr( - raw_value: str, - data_type: str -) -> str: - - if data_type == "Time": - if raw_value.startswith("+"): - return raw_value - return f"TIME({raw_value})" - elif data_type == "Date": - if raw_value == "今天": - return "CURRENT_DATE" - elif raw_value == "明天": - return "CURRENT_DATE + 1" - return f"DATE({raw_value})" - elif data_type == "Boolean": - return raw_value - elif data_type == "String": - escaped = raw_value.replace("'", "''") - return f"'{escaped}'" - else: - return raw_value - - -def _setWidgetValue( - w: QWidget, - vartype: str, - expr: str -): - - import re - s = expr.strip() - - if getattr(w, '_isActionTime', False): - timeMatch = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE) - if timeMatch: - w._modeCombo.setCurrentIndex(0) - parts = timeMatch.group(1).split(":") - w._timeEdit.setTime(QTime(int(parts[0]), int(parts[1]))) - return - relMatch = re.match(r"^\+(\d+)$", s) - if relMatch: - w._modeCombo.setCurrentIndex(1) - w._spinBox.setValue(int(relMatch.group(1))) - return - return - if getattr(w, '_is_date_action', False) and isinstance(w, QComboBox): - if s.upper() in ("CURRENT_DATE", "TODAY"): - w.setCurrentIndex(0) - elif s.upper() in ("CURRENT_DATE + 1", "TOMORROW"): - w.setCurrentIndex(1) - else: - dateMatch = re.match( - r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE - ) - if dateMatch: - from datetime import datetime, timedelta - dateStr = dateMatch.group(1) - today = datetime.now().strftime("%Y-%m-%d") - tomorrow = ( - datetime.now() + timedelta(days=1) - ).strftime("%Y-%m-%d") - if dateStr == today: - w.setCurrentIndex(0) - elif dateStr == tomorrow: - w.setCurrentIndex(1) - return - if vartype == "Time": - m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE) - if m and isinstance(w, QTimeEdit): - parts = m.group(1).split(":") - w.setTime(QTime(int(parts[0]), int(parts[1]))) - elif vartype == "Date": - m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE) - if m and isinstance(w, QDateEdit): - parts = m.group(1).split("-") - w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) - elif vartype == "Boolean" and isinstance(w, QComboBox): - for i in range(w.count()): - if w.itemData(i) == s.upper(): - w.setCurrentIndex(i) - break - elif vartype == "Integer" and isinstance(w, QSpinBox): - try: - w.setValue(int(s)) - except ValueError: - pass - elif vartype == "Float" and isinstance(w, QDoubleSpinBox): - try: - w.setValue(float(s)) - except ValueError: - pass - elif isinstance(w, QLineEdit): - inner = s - if (inner.startswith("'") and inner.endswith("'")) or \ - (inner.startswith('"') and inner.endswith('"')): - inner = inner[1:-1].replace("''", "'") - w.setText(inner) - - -class ActionStepFrame(QFrame): - - def __init__( - self, - parent=None - ): - super().__init__(parent) - - self.setupUi() - self.connectSignals() - self.onTargetChanged(0) - - - def setupUi( - self - ): - - self.setFrameShape(QFrame.Shape.StyledPanel) - self.setFrameShadow(QFrame.Shadow.Raised) - self.setFixedHeight(35) - - layout = QHBoxLayout(self) - layout.setContentsMargins(2, 2, 2, 2) - layout.setSpacing(4) - - self.targetCombo = _makeSetVarCombo() - self.valueWidgetStack = QStackedWidget() - self.valueWidgetStack.setFixedHeight(25) - self.initValueStack() - - setLabel = QLabel("设置") - setLabel.setFixedHeight(25) - layout.addWidget(setLabel) - layout.addWidget(self.targetCombo) - toLabel = QLabel("为") - toLabel.setFixedHeight(25) - layout.addWidget(toLabel) - layout.addWidget(self.valueWidgetStack) - - self.deleteBtn = QPushButton("×") - self.deleteBtn.setFixedSize(24, 25) - self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.deleteBtn) - - - def connectSignals( - self - ): - - self.targetCombo.currentIndexChanged.connect(self.onTargetChanged) - - - def initValueStack( - self - ): - - self._valueWidgets = {} - for _, _, vartype in _VAR_COMBO_ITEMS: - if vartype not in self._valueWidgets: - w = _makeActionValueWidget(vartype) - self._valueWidgets[vartype] = w - self.valueWidgetStack.addWidget(w) - self.valueWidgetStack.setCurrentWidget( - self._valueWidgets.get("String", self.valueWidgetStack.widget(0)) - ) - - - def getTarget( - self - ) -> str: - - data = self.targetCombo.currentData() - return data[0] if data else "" - - - def getTargetType( - self - ) -> str: - - data = self.targetCombo.currentData() - return data[1] if data else "String" - - - def getValueRaw( - self - ) -> str: - - currentType = self.getTargetType() - w = self._valueWidgets.get(currentType) - if w: - return _getValueFromWidget(w) - return "" - - - def toScriptLine( - self - ) -> str: - - target = self.getTarget() - if not target: - return "" - rawVal = self.getValueRaw() - targetType = self.getTargetType() - encoded = _encodeValueStr(rawVal, targetType) - if targetType == "Time" and rawVal.startswith("+"): - hours = rawVal[1:] - return f" {target} .ADD. {hours}" - return f" SET {target} = {encoded}" - - - def loadFromScript( - self, - targetVar: str, - valueExpr: str - ): - - for idx in range(self.targetCombo.count()): - data = self.targetCombo.itemData(idx) - if data and data[0] == targetVar: - self.targetCombo.setCurrentIndex(idx) - break - self.setValueFromExpr(valueExpr) - - - def setValueFromExpr( - self, - expr: str - ): - - targetType = self.getTargetType() - w = self._valueWidgets.get(targetType) - if not w: - return - _setWidgetValue(w, targetType, expr) - - @Slot(int) - def onTargetChanged( - self, - idx - ): - if idx < 0: - return - data = self.targetCombo.itemData(idx) - if data: - _, vartype = data - w = self._valueWidgets.get(vartype) - if w: - self.valueWidgetStack.setCurrentWidget(w) - - -class ConditionalBlock(QGroupBox): - - def __init__( - self, - blockIndex: int, - parent=None - ): - super().__init__(parent) - - self.blockIndex = blockIndex - self._actionWidgets = [] - - self.setupUi() - self.connectSignals() - self.onOperandChanged(0) - - - def setupUi( - self - ): - - self.setStyleSheet( - "QGroupBox { font-weight: bold; border: 1px solid #ccc; " - "margin-top: 5px; padding-top: 5px; }" - ) - self.setSizePolicy( - QSizePolicy.Policy.Preferred, - QSizePolicy.Policy.Fixed - ) - - mainLayout = QVBoxLayout(self) - mainLayout.setSpacing(4) - mainLayout.setContentsMargins(5, 5, 5, 5) - - headerLayout = QHBoxLayout() - self.typeCombo = QComboBox() - self.typeCombo.addItem("IF", "IF") - self.typeCombo.addItem("ELSE IF", "ELSE IF") - self.typeCombo.addItem("ELSE", "ELSE") - if self.blockIndex == 0: - self.typeCombo.setEnabled(False) - typeLabel = QLabel("类型:") - typeLabel.setFixedHeight(25) - headerLayout.addWidget(typeLabel) - headerLayout.addWidget(self.typeCombo) - headerLayout.addStretch() - self.deleteBlockBtn = QPushButton("删除此块") - self.deleteBlockBtn.setStyleSheet("color: red;") - self.deleteBlockBtn.setFixedHeight(25) - headerLayout.addWidget(self.deleteBlockBtn) - mainLayout.addLayout(headerLayout) - - self.conditionWidget = QWidget() - self.conditionWidget.setFixedHeight(60) - condLayout = QHBoxLayout(self.conditionWidget) - condLayout.setContentsMargins(0, 0, 0, 0) - ifLabel = QLabel("如果") - ifLabel.setFixedHeight(25) - condLayout.addWidget(ifLabel) - self.operandCombo = _makeVarCombo() - condLayout.addWidget(self.operandCombo) - self.opCombo = _makeOpCombo() - condLayout.addWidget(self.opCombo) - - self.condValueStack = QStackedWidget() - self.condValueStack.setFixedHeight(25) - self._condValueWidgets = {} - for vartype in ["Time", "Date", "String", "Integer", "Float", "Boolean"]: - w = _makeValueWidget(vartype) - self._condValueWidgets[vartype] = w - self.condValueStack.addWidget(w) - self.condValueStack.setCurrentWidget(self._condValueWidgets.get("String")) - condLayout.addWidget(self.condValueStack) - mainLayout.addWidget(self.conditionWidget) - - self.actionLabel = QLabel("执行步骤:") - self.actionLabel.setFixedHeight(25) - mainLayout.addWidget(self.actionLabel) - - self.actionsLayout = QVBoxLayout() - self.actionsLayout.setSpacing(2) - mainLayout.addLayout(self.actionsLayout) - - self.addActionBtn = QPushButton("+ 添加执行步骤") - self.addActionBtn.setFixedHeight(25) - mainLayout.addWidget(self.addActionBtn) - - - def connectSignals( - self - ): - - self.operandCombo.currentIndexChanged.connect(self.onOperandChanged) - self.typeCombo.currentIndexChanged.connect(self.onTypeChanged) - self.addActionBtn.clicked.connect(self.addActionStep) - - - def removeActionStep( - self, - step: ActionStepFrame - ): - - if step in self._actionWidgets: - self._actionWidgets.remove(step) - self.actionsLayout.removeWidget(step) - step.hide() - step.deleteLater() - - - def getBlockType( - self - ) -> str: - - return self.typeCombo.currentData() - - - def toScriptLines( - self - ) -> list: - - blockType = self.getBlockType() - lines = [] - - if blockType in ("IF", "ELSE IF"): - operand = self.operandCombo.currentData() - operandName = operand[0] if operand else "" - operandType = operand[1] if operand else "String" - opSym = self.opCombo.currentData() - rawVal = _getValueFromWidget( - self._condValueWidgets.get(operandType, QLineEdit()) - ) - encodedVal = _encodeValueStr(rawVal, operandType) - if blockType == "IF": - lines.append(f"IF({operandName} {opSym} {encodedVal}) THEN") - else: - lines.append(f"ELSE IF({operandName} {opSym} {encodedVal}) THEN") - else: - lines.append("ELSE") - for step in self._actionWidgets: - scriptLine = step.toScriptLine() - if scriptLine: - lines.append(scriptLine) - - return lines - - - def getConditionSummary( - self - ) -> str: - - bt = self.getBlockType() - if bt == "ELSE": - return "ELSE" - operandData = self.operandCombo.currentData() - if not operandData: - return bt - operandDisplay = self.operandCombo.currentText() - opDisplay = self.opCombo.currentText() - rawVal = self.getConditionRawValuePreview() - return f"{bt} ({operandDisplay} {opDisplay} {rawVal})" - - - def getConditionRawValuePreview( - self - ) -> str: - - data = self.operandCombo.currentData() - if not data: - return "" - _, vartype = data - w = self._condValueWidgets.get(vartype) - if w: - return _getValueFromWidget(w) - return "" - - - def countActionSteps( - self - ) -> int: - - return len(self._actionWidgets) - - @Slot(int) - def onOperandChanged( - self, - idx - ): - if idx < 0: - return - data = self.operandCombo.itemData(idx) - if data: - _, vartype = data - w = self._condValueWidgets.get(vartype) - if w: - self.condValueStack.setCurrentWidget(w) - - @Slot(int) - def onTypeChanged( - self, - idx - ): - isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") - self.conditionWidget.setVisible(isCond) - self.actionLabel.setText("执行步骤:" if isCond else "ELSE 执行步骤:") - - @Slot() - def addActionStep( - self - ): - - step = ActionStepFrame(self) - step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step)) - self._actionWidgets.append(step) - self.actionsLayout.addWidget(step) - - -class ALAutoScriptOrchDialog(QDialog): - - def __init__( - self, - parent=None, - existingScript: str = "" - ): - super().__init__(parent) - self._blocks: list[ConditionalBlock] = [] - - self.modifyUi() - self.connectSignals() - - if existingScript and existingScript.strip(): - self.loadFromScript(existingScript) - else: - self.addBlock() - self._scrollLayout.addStretch() - - - def modifyUi( - self - ): - - self.setWindowTitle("AutoScript 指令编排 - AutoLibrary") - self.setMinimumSize(420, 400) - self.setModal(True) - mainLayout = QVBoxLayout(self) - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.Shape.NoFrame) - scrollContent = QWidget() - self._scrollLayout = QVBoxLayout(scrollContent) - self._scrollLayout.setSpacing(5) - scroll.setWidget(scrollContent) - mainLayout.addWidget(scroll) - addBlockLayout = QHBoxLayout() - self.addBlockBtn = QPushButton("+ 添加判断块") - self.addBlockBtn.setFixedHeight(25) - addBlockLayout.addStretch() - addBlockLayout.addWidget(self.addBlockBtn) - addBlockLayout.addStretch() - mainLayout.addLayout(addBlockLayout) - self.btnBox = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | - QDialogButtonBox.StandardButton.Cancel - ) - self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") - self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") - mainLayout.addWidget(self.btnBox) - - - def connectSignals( - self - ): - - self.btnBox.accepted.connect(self.accept) - self.btnBox.rejected.connect(self.reject) - self.addBlockBtn.clicked.connect(self.addBlock) - - - def removeBlock( - self, - block: ConditionalBlock - ): - - if block in self._blocks: - self._blocks.remove(block) - self._scrollLayout.removeWidget(block) - block.hide() - block.deleteLater() - - - def getScript( - self - ) -> str: - - parts = [] - for i, block in enumerate(self._blocks): - blockType = block.getBlockType() - if blockType == "IF" and i > 0: - parts.append("ENDIF") - lines = block.toScriptLines() - parts.extend(lines) - if self._blocks and self._blocks[0].getBlockType() == "IF": - parts.append("ENDIF") - return "\n".join(parts) - - - def getScriptPreview( - self - ) -> str: - - s = self.getScript() - if len(s) > 10: - return s[:7] + "..." - return s - - - def loadFromScript( - self, - script: str - ): - - import re - lines = [l.strip() for l in script.split("\n") if l.strip()] - if not lines: - self.addBlock() - return - - currentBlock = None - currentBlockType = None - actionsBuffer = [] - - def flushBlock(): - nonlocal currentBlock, currentBlockType, actionsBuffer - if currentBlock is None: - return - typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2} - idx = typeIdxMap.get(currentBlockType, 0) - currentBlock.typeCombo.setCurrentIndex(idx) - currentBlock.onTypeChanged(idx) - for oldStep in list(currentBlock._actionWidgets): - currentBlock.removeActionStep(oldStep) - for target, valueExpr in actionsBuffer: - currentBlock.addActionStep() - step = currentBlock._actionWidgets[-1] - step.loadFromScript(target, valueExpr) - self._blocks.clear() - while self._scrollLayout.count(): - item = self._scrollLayout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - for line in lines: - upper = line.upper() - ifMatch = re.match(r"^IF\((.+)\)\s*THEN\s*$", upper) - if ifMatch: - flushBlock() - currentBlockType = "IF" - actionsBuffer = [] - self.addBlock() - currentBlock = self._blocks[-1] - self.parseConditionToBlock(currentBlock, ifMatch.group(1)) - continue - elifIfMatch = re.match(r"^ELSE\s+IF\((.+)\)\s*THEN\s*$", upper) - if elifIfMatch: - flushBlock() - currentBlockType = "ELSE IF" - actionsBuffer = [] - self.addBlock() - currentBlock = self._blocks[-1] - self.parseConditionToBlock(currentBlock, elifIfMatch.group(1)) - continue - if upper == "ELSE": - flushBlock() - currentBlockType = "ELSE" - actionsBuffer = [] - self.addBlock() - currentBlock = self._blocks[-1] - currentBlock.conditionWidget.setVisible(False) - continue - setMatch = re.match(r"^SET\s+(\w+)\s*=\s*(.+)$", line, re.IGNORECASE) - if setMatch: - target = setMatch.group(1).strip() - valueExpr = setMatch.group(2).strip() - actionsBuffer.append((target, valueExpr)) - continue - addMatch = re.match(r"^(\w+)\s+\.ADD\.\s+(\d+)$", line, re.IGNORECASE) - if addMatch: - target = addMatch.group(1).strip() - hours = addMatch.group(2).strip() - actionsBuffer.append((target, f"+{hours}")) - continue - if upper in ("ENDIF", "END IF"): - flushBlock() - currentBlock = None - currentBlockType = None - actionsBuffer = [] - continue - flushBlock() - if not self._blocks: - self.addBlock() - - - def parseConditionToBlock( - self, - block: ConditionalBlock, - condStr: str - ): - - condStr = condStr.strip() - for _, opSym in OP_ITEMS: - idx = condStr.upper().find(opSym) - if idx >= 0: - leftPart = condStr[:idx].strip() - rightPart = condStr[idx + len(opSym):].strip() - for ci in range(block.operandCombo.count()): - data = block.operandCombo.itemData(ci) - if data and data[0] == leftPart: - block.operandCombo.setCurrentIndex(ci) - break - for oi in range(block.opCombo.count()): - if block.opCombo.itemData(oi) == opSym: - block.opCombo.setCurrentIndex(oi) - break - opData = block.operandCombo.currentData() - vartype = opData[1] if opData else "String" - w = block._condValueWidgets.get(vartype) - if w: - _setWidgetValue(w, vartype, rightPart) - return - - @Slot() - def addBlock( - self - ): - - block = ConditionalBlock(len(self._blocks), self) - block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block)) - self._blocks.append(block) - block.addActionStep() - if self._scrollLayout.count() > 0: - lastItem = self._scrollLayout.itemAt( - self._scrollLayout.count() - 1 - ) - if lastItem and lastItem.spacerItem(): - self._scrollLayout.insertWidget( - self._scrollLayout.count() - 1, block - ) - return - self._scrollLayout.addWidget(block) \ No newline at end of file diff --git a/src/gui/ALAutoScriptOrchDialog/__init__.py b/src/gui/ALAutoScriptOrchDialog/__init__.py new file mode 100644 index 0000000..38ca871 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/__init__.py @@ -0,0 +1,3 @@ +from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog + +__all__ = ["ALAutoScriptOrchDialog"] diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py new file mode 100644 index 0000000..4422e80 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -0,0 +1,272 @@ +""" +Conditional block widget for the AutoScript orchestration dialog. +""" +from PySide6.QtCore import Slot +from PySide6.QtWidgets import ( + QComboBox, + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from gui.ALAutoScriptOrchDialog._widgets import ( + ActionStepFrame, + ConditionRowFrame, +) + + +class ConditionalBlock(QGroupBox): + + def __init__( + self, + blockIndex: int, + varMgr = None, + parent = None + ): + + super().__init__(parent) + self.blockIndex = blockIndex + self._varMgr = varMgr + self._actionWidgets = [] + self._conditionRows = [] + + self.setupUi() + self.connectSignals() + self.addInitialConditionRow() + + + def setupUi( + self + ): + + self.setUpdatesEnabled(False) + self.setStyleSheet( + "QGroupBox { font-weight: bold; border: 1px solid #ccc; " + "margin-top: 5px; padding-top: 5px; }" + ) + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + mainLayout = QVBoxLayout(self) + mainLayout.setSpacing(6) + mainLayout.setContentsMargins(8, 8, 8, 8) + + headerLayout = QHBoxLayout() + headerLayout.setSpacing(8) + self.typeCombo = QComboBox(self) + self.typeCombo.addItem("IF", "IF") + self.typeCombo.addItem("ELSE IF", "ELSE IF") + self.typeCombo.addItem("ELSE", "ELSE") + self.typeCombo.setFixedHeight(25) + if self.blockIndex == 0: + self.typeCombo.setEnabled(False) + headerLayout.addWidget(QLabel("类型:", self)) + headerLayout.addWidget(self.typeCombo) + headerLayout.addStretch() + self.deleteBlockBtn = QPushButton("删除此块", self) + self.deleteBlockBtn.setStyleSheet("color: red;") + self.deleteBlockBtn.setFixedHeight(25) + headerLayout.addWidget(self.deleteBlockBtn) + mainLayout.addLayout(headerLayout) + + self.conditionWidget = QWidget(self) + self.conditionWidget.setSizePolicy( + QSizePolicy.Preferred, QSizePolicy.Preferred + ) + condLayout = QVBoxLayout(self.conditionWidget) + condLayout.setContentsMargins(4, 4, 4, 4) + condLayout.setSpacing(6) + + self._condRowsLayout = QVBoxLayout() + self._condRowsLayout.setSpacing(4) + condLayout.addLayout(self._condRowsLayout) + self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget) + self.addCondBtn.setFixedHeight(25) + condLayout.addWidget(self.addCondBtn) + mainLayout.addWidget(self.conditionWidget) + self.actionLabel = QLabel("执行步骤:", self) + self.actionLabel.setFixedHeight(25) + mainLayout.addWidget(self.actionLabel) + self._actionsLayout = QVBoxLayout() + self._actionsLayout.setSpacing(4) + mainLayout.addLayout(self._actionsLayout) + self.addActionBtn = QPushButton("+ 添加执行步骤", self) + self.addActionBtn.setFixedHeight(25) + mainLayout.addWidget(self.addActionBtn) + self.setUpdatesEnabled(True) + + + def connectSignals( + self + ): + + self.typeCombo.currentIndexChanged.connect(self.onTypeChanged) + self.addCondBtn.clicked.connect(self.addConditionRow) + self.addActionBtn.clicked.connect(self.addActionStep) + + + def addInitialConditionRow( + self + ): + + row = ConditionRowFrame( + self._varMgr, self.blockIndex, + isFirst=True, parent=self + ) + self._conditionRows.append(row) + self._condRowsLayout.addWidget(row) + + + def addConditionRow( + self + ): + + row = ConditionRowFrame( + self._varMgr, self.blockIndex, + isFirst=False, parent=self + ) + row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row)) + self._conditionRows.append(row) + self._condRowsLayout.addWidget(row) + + + def removeConditionRow( + self, + row: ConditionRowFrame + ): + + if row in self._conditionRows and len(self._conditionRows) > 1: + self._conditionRows.remove(row) + self._condRowsLayout.removeWidget(row) + row.hide() + row.deleteLater() + + + def addActionStep( + self + ): + + step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self) + step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step)) + self._actionWidgets.append(step) + self._actionsLayout.addWidget(step) + + + def removeActionStep( + self, + step: ActionStepFrame + ): + + if step in self._actionWidgets: + self._actionWidgets.remove(step) + self._actionsLayout.removeWidget(step) + step.hide() + step.deleteLater() + + @Slot(int) + def onTypeChanged( + self, + _idx + ): + + isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") + self.conditionWidget.setVisible(isCond) + self.actionLabel.setText( + "执行步骤:" if isCond else "ELSE 执行步骤:" + ) + + + def getBlockType( + self + ) -> str: + + return self.typeCombo.currentData() + + + def getConditionRows( + self + ): + + return list(self._conditionRows) + + + def getActionSteps( + self + ): + + return list(self._actionWidgets) + + + def countActionSteps( + self + ) -> int: + + return len(self._actionWidgets) + + + def toScriptLines( + self + ) -> list: + + blockType = self.getBlockType() + lines = [] + + if blockType in ("IF", "ELSE IF"): + condTexts = [ + r.toConditionText() for r in self._conditionRows if r.toConditionText() + ] + if not condTexts: + condTexts = [".TRUE."] + + if len(condTexts) == 1: + combined = condTexts[0] + else: + parts = [] + for i, ct in enumerate(condTexts): + if i > 0: + logic = self._conditionRows[i].getLogic() or ".AND." + parts.append(f" {logic} ") + parts.append(f"({ct})") + combined = "".join(parts) + if blockType == "IF": + lines.append(f"IF({combined}) THEN") + else: + lines.append(f"ELSE IF({combined}) THEN") + else: + lines.append("ELSE") + for step in self._actionWidgets: + scriptLine = step.toScriptLine() + if scriptLine: + lines.append(scriptLine) + return lines + + + def refreshVarCombos( + self + ): + + for row in self._conditionRows: + row.refreshVarCombos() + for step in self._actionWidgets: + step.refreshVarCombos() + + + def setPrevBlockType( + self, + prevType: str | None + ): + + model = self.typeCombo.model() + if model is None: + return + for data in ("ELSE IF", "ELSE"): + idx = self.typeCombo.findData(data) + if idx < 0: + continue + item = model.item(idx) + shouldEnable = prevType != "ELSE" + item.setEnabled(shouldEnable) + if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"): + self.typeCombo.setCurrentIndex(0) diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py new file mode 100644 index 0000000..21f838a --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -0,0 +1,296 @@ +""" +Orchestration dialog for visually composing AutoScript scripts. +""" +from PySide6.QtCore import Slot +from PySide6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QFrame, + QMessageBox, + QPushButton, + QScrollArea, + QVBoxLayout, + QWidget, +) + +from gui.ALAutoScriptOrchDialog._precheck import precheck +from gui.ALAutoScriptOrchDialog._orchestrate import parseBlocks + +from gui.ALAutoScriptOrchDialog._helpers import ( + COMPARE_OPERATORS, + PRESET_NAMES, + VariableManager, + findOperatorIn, + splitTopLevel, + stripOuterParens, +) +from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock +from gui.ALAutoScriptOrchDialog._widgets import ConditionRowFrame + + +class ALAutoScriptOrchDialog(QDialog): + + def __init__( + self, + parent = None, + existingScript: str = "" + ): + + super().__init__(parent) + self._blocks = [] + self._varMgr = VariableManager(self) + + self.setupUi() + self.connectSignals() + if existingScript and existingScript.strip(): + self.loadFromScript(existingScript) + else: + self.addBlock() + self._scrollLayout.addStretch() + + + def setupUi( + self + ): + + self.setWindowTitle("AutoScript 指令编排 - AutoLibrary") + self.setMinimumSize(640, 600) + self.setModal(True) + mainLayout = QVBoxLayout(self) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.NoFrame) + scrollContent = QWidget() + self._scrollLayout = QVBoxLayout(scrollContent) + self._scrollLayout.setSpacing(5) + scroll.setWidget(scrollContent) + mainLayout.addWidget(scroll) + self.addBlockBtn = QPushButton("+ 添加判断块") + self.addBlockBtn.setFixedHeight(25) + mainLayout.addWidget(self.addBlockBtn) + self.btnBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | + QDialogButtonBox.StandardButton.Cancel + ) + self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") + self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") + mainLayout.addWidget(self.btnBox) + + + def connectSignals( + self + ): + + self.btnBox.accepted.connect(self.onAccept) + self.btnBox.rejected.connect(self.reject) + self.addBlockBtn.clicked.connect(self.addBlock) + + + def _updateBlockTypeRestrictions( + self + ): + + prevType = None + for block in self._blocks: + block.setPrevBlockType(prevType) + prevType = block.getBlockType() + + + def addBlock( + self + ): + + block = ConditionalBlock( + len(self._blocks), self._varMgr, parent=self + ) + block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block)) + block.typeCombo.currentIndexChanged.connect(self._updateBlockTypeRestrictions) + block.addActionStep() + self._blocks.append(block) + self._updateBlockTypeRestrictions() + if self._scrollLayout.count() > 0: + lastItem = self._scrollLayout.itemAt( + self._scrollLayout.count() - 1 + ) + if lastItem and lastItem.spacerItem(): + self._scrollLayout.insertWidget( + self._scrollLayout.count() - 1, block + ) + return + self._scrollLayout.addWidget(block) + + + def removeBlock( + self, + block: ConditionalBlock + ): + + if len(self._blocks) <= 1: + QMessageBox.information(self, "提示", "至少保留一个判断块。") + return + if block in self._blocks: + self._blocks.remove(block) + self._scrollLayout.removeWidget(block) + block.hide() + block.deleteLater() + for i, blk in enumerate(self._blocks): + blk.blockIndex = i + if i == 0: + blk.typeCombo.setEnabled(False) + blk.typeCombo.setCurrentIndex(0) + else: + blk.typeCombo.setEnabled(True) + blk.refreshVarCombos() + self._updateBlockTypeRestrictions() + + + def getScript( + self + ) -> str: + + parts = [] + prevType = None + for block in self._blocks: + blockType = block.getBlockType() + if blockType == "IF" and prevType is not None: + parts.append("ENDIF") + lines = block.toScriptLines() + parts.extend(lines) + prevType = blockType + if self._blocks and self._blocks[0].getBlockType() == "IF": + parts.append("ENDIF") + return "\n".join(parts) + + @Slot() + def onAccept( + self + ): + + script = self.getScript().strip() + if not script: + QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。") + return + self.accept() + + @staticmethod + def precheckScriptForOrchestration( + script: str + ) -> tuple[bool, str]: + + return precheck(script, allowed_vars=PRESET_NAMES) + + def loadFromScript( + self, + script: str + ): + + if not script.strip(): + self.addBlock() + return + ok, err = self.precheckScriptForOrchestration(script) + if not ok: + QMessageBox.warning( + self, "无法编排", + f"脚本检查失败:\n{err}\n\n" + "请通过\"编辑\"按钮打开脚本编辑窗口进行修改。" + ) + self.addBlock() + return + # Structured block data via observer-based parsing — no duplicate logic + typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2} + parsedBlocks = parseBlocks(script) + self._blocks.clear() + while self._scrollLayout.count(): + item = self._scrollLayout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + try: + for blockType, condition, actions in parsedBlocks: + self.addBlock() + block = self._blocks[-1] + idx = typeIdxMap.get(blockType, 0) + block.typeCombo.setCurrentIndex(idx) + block.onTypeChanged(idx) + for oldStep in list(block._actionWidgets): + block.removeActionStep(oldStep) + for target, valueExpr, opType in actions: + block.addActionStep() + step = block.getActionSteps()[-1] + step.setOpType(opType) + step.loadFromScript(target, valueExpr) + if blockType in ("IF", "ELSE IF") and condition: + self._parseConditions(block, condition) + except Exception: + self._blocks.clear() + while self._scrollLayout.count(): + item = self._scrollLayout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + self._updateBlockTypeRestrictions() + if not self._blocks: + self.addBlock() + + + def _parseConditions( + self, + block: ConditionalBlock, + condStr: str + ): + + s = condStr.strip() + if not s: + return + s = stripOuterParens(s) + orParts = splitTopLevel(s, ".OR.") + allSubConds = [] + allLogics = [] + for pi, part in enumerate(orParts): + part = part.strip() + if pi > 0: + allLogics.append(".OR.") + andParts = splitTopLevel(part, ".AND.") + for ai, ap in enumerate(andParts): + ap = ap.strip() + if ai > 0: + allLogics.append(".AND.") + allSubConds.append(ap) + for row in list(block._conditionRows): + block._condRowsLayout.removeWidget(row) + row.hide() + row.deleteLater() + block._conditionRows.clear() + for i, subCond in enumerate(allSubConds): + subCond = subCond.strip() + subCond = stripOuterParens(subCond) + isFirst = (i == 0) + row = ConditionRowFrame( + self._varMgr, block.blockIndex, + isFirst=isFirst, parent=block + ) + if not isFirst: + row.deleteBtn.clicked.connect( + lambda _checked=False, r=row: block.removeConditionRow(r) + ) + if i - 1 < len(allLogics): + logic = allLogics[i - 1] + for li in range(row.logicCombo.count()): + if row.logicCombo.itemData(li) == logic: + row.logicCombo.setCurrentIndex(li) + break + block._conditionRows.append(row) + block._condRowsLayout.addWidget(row) + subUp = subCond.upper() + if subUp in (".TRUE.", ".FALSE."): + row.loadFromParts(subUp, "", "") + else: + opSyms = [op for _, op in COMPARE_OPERATORS] + result = findOperatorIn(subCond, opSyms) + if result: + idx, op = result + leftPart = subCond[:idx].strip() + rightPart = subCond[idx + len(op):].strip() + row.loadFromParts(leftPart, op, rightPart) + else: + row.loadFromParts(subCond, "", "") + if not block._conditionRows: + block.addInitialConditionRow() diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py new file mode 100644 index 0000000..cd8261f --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -0,0 +1,688 @@ +""" +Helper utilities and constants for the AutoScript orchestration dialog. +""" +import re + +from PySide6.QtCore import QObject, QDate, QTime +from PySide6.QtWidgets import ( + QComboBox, + QDateEdit, + QDoubleSpinBox, + QHBoxLayout, + QLabel, + QLineEdit, + QSizePolicy, + QSpinBox, + QStackedWidget, + QTimeEdit, + QWidget, +) + +from autoscript import ALL_VARIABLES + + +VAR_TYPE_ORDER = [ + "String", + "Int", + "Float", + "Boolean", + "Date", + "Time" +] +PRESET_VARIABLES = [ + { + "name": name.upper(), + "type": vtype, + "display": display + } + for display, (name, vtype) in ALL_VARIABLES.items() +] +PRESET_NAMES = { + p["name"] for p in PRESET_VARIABLES +} +COMPARE_OPERATORS = sorted([ + ("等于", ".EQ."), + ("不等于", ".NEQ."), + ("大于", ".BGT."), + ("小于", ".BLT."), + ("大于等于", ".BGE."), + ("小于等于", ".BLE."), +], key=lambda x: len(x[1]), reverse=True) +LOGIC_OPERATORS = [ + ("并且 (.AND.)", ".AND."), + ("或者 (.OR.)", ".OR."), +] +ACTION_TYPES = [ + ("设置为", "set"), + ("增加", "add"), + ("减少", "sub"), +] +ARITH_TYPES = { + "Date", + "Time", + "Int", + "Float" +} +DATE_RELATIVE_OPTIONS = [ + ("前天", "day_before_yesterday"), + ("昨天", "yesterday"), + ("今天", "today"), + ("明天", "tomorrow"), + ("后天", "day_after_tomorrow") +] +DATE_OFFSET_UNITS = [ + ("天", "days"), + ("周", "weeks"), + ("月", "months"), + ("年", "years"), +] + + +class VariableManager(QObject): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self._vars = [] + self._nameMap = {} + + self._initPresetVars() + + + def _initPresetVars( + self + ): + + for p in PRESET_VARIABLES: + entry = {"name": p["name"], "type": p["type"], "display": p["display"]} + self._vars.append(entry) + self._nameMap[p["name"]] = entry + + + def getInfoByName( + self, + name: str + ): + + return self._nameMap.get(name.upper().strip()) + + + def populateCombo( + self, + combo: QComboBox + ): + + currentData = combo.currentData() + combo.blockSignals(True) + combo.clear() + for entry in self._vars: + combo.addItem( + entry["display"], + (entry["name"], entry["type"]) + ) + if currentData: + for i in range(combo.count()): + d = combo.itemData(i) + if d and d[0] == currentData[0]: + combo.setCurrentIndex(i) + break + combo.blockSignals(False) + + + def findExactNameEntry( + self, + combo: QComboBox, + name: str + ) -> int: + + name = name.upper().strip() + for i in range(combo.count()): + d = combo.itemData(i) + if d and len(d) >= 1 and d[0].upper().strip() == name: + return i + return -1 + + +def makeValueWidget( + var_type: str, + parent: QWidget = None +) -> QWidget: + + if var_type == "Int": + w = QSpinBox(parent) + w.setRange(-999999, 999999) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Float": + w = QDoubleSpinBox(parent) + w.setRange(-999999.0, 999999.0) + w.setDecimals(2) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "String": + w = QLineEdit(parent) + w.setPlaceholderText("输入值") + w.setFixedHeight(25) + w.setMinimumWidth(120) + return w + if var_type == "Boolean": + w = QComboBox(parent) + w.addItem("是 (.TRUE.)", ".TRUE.") + w.addItem("否 (.FALSE.)", ".FALSE.") + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Date": + return _DateInputContainer(parent) + if var_type == "Time": + return _TimeInputContainer(parent) + w = QLineEdit(parent) + w.setPlaceholderText("输入值") + w.setFixedHeight(25) + w.setMinimumWidth(120) + return w + + +def makeOffsetWidget( + var_type: str, + parent: QWidget = None +) -> QWidget: + + if var_type == "Int": + w = QSpinBox(parent) + w.setRange(-999999, 999999) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Float": + w = QDoubleSpinBox(parent) + w.setRange(-999999.0, 999999.0) + w.setDecimals(2) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Date": + return _DateOffsetContainer(parent) + if var_type == "Time": + return _TimeOffsetContainer(parent) + w = QLabel("(不支持该操作)", parent) + w.setFixedHeight(25) + return w + + +def makeVarRefCombo( + parent: QWidget = None +) -> QComboBox: + + cb = QComboBox(parent) + cb.setFixedHeight(25) + cb.setMinimumWidth(120) + cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + return cb + + +def makeComboWidget( + items, + min_width: int = 80, + parent: QWidget = None +) -> QComboBox: + + cb = QComboBox(parent) + for display, data in items: + cb.addItem(display, data) + cb.setFixedHeight(25) + cb.setMinimumWidth(min_width) + return cb + + +def makeLabel( + text: str, + parent: QWidget = None, + width: int = None +) -> QLabel: + + lbl = QLabel(text, parent) + lbl.setFixedHeight(25) + if width: + lbl.setFixedWidth(width) + return lbl + + +class _DateInputContainer(QWidget): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self.setupUi() + + + def setupUi( + self + ): + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + self._modeCombo = QComboBox(self) + self._modeCombo.addItem("相对日期", "relative") + self._modeCombo.addItem("绝对日期", "absolute") + self._modeCombo.setFixedHeight(25) + self._stack = QStackedWidget(self) + self._relCombo = QComboBox(self) + for display, data in DATE_RELATIVE_OPTIONS: + self._relCombo.addItem(display, data) + self._relCombo.setFixedHeight(25) + self._stack.addWidget(self._relCombo) + self._dateEdit = QDateEdit(self) + self._dateEdit.setDisplayFormat("yyyy-MM-dd") + self._dateEdit.setCalendarPopup(True) + self._dateEdit.setFixedHeight(25) + self._stack.addWidget(self._dateEdit) + self._modeCombo.currentIndexChanged.connect( + lambda i: self._stack.setCurrentIndex(i) + ) + layout.addWidget(self._modeCombo) + layout.addWidget(self._stack) + layout.addStretch() + + + def getValue( + self + ) -> str: + + mode = self._modeCombo.currentData() + if mode == "relative": + return self._relCombo.currentText() + return self._dateEdit.date().toString("yyyy-MM-dd") + + + def setValue( + self, + expr: str + ): + + s = expr.strip().upper() + if s == "CURRENT_DATE - 2": + self._modeCombo.setCurrentIndex(0) + self._relCombo.setCurrentIndex(4) + elif s == "CURRENT_DATE - 1": + self._modeCombo.setCurrentIndex(0) + self._relCombo.setCurrentIndex(3) + elif s in ("CURRENT_DATE", "TODAY"): + self._modeCombo.setCurrentIndex(0) + self._relCombo.setCurrentIndex(0) + elif s == "CURRENT_DATE + 1" or s == "TOMORROW": + self._modeCombo.setCurrentIndex(0) + self._relCombo.setCurrentIndex(1) + elif s == "CURRENT_DATE + 2": + self._modeCombo.setCurrentIndex(0) + self._relCombo.setCurrentIndex(2) + elif s.startswith("DATE("): + self._modeCombo.setCurrentIndex(1) + m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) + if m: + parts = m.group(1).split("-") + self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) + + +class _TimeInputContainer(QWidget): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self._timeEdit = QTimeEdit(self) + self._timeEdit.setDisplayFormat("HH:mm") + self._timeEdit.setFixedHeight(25) + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self._timeEdit) + + + def getValue( + self + ) -> str: + + return self._timeEdit.time().toString("HH:mm") + + + def setValue( + self, + expr: str + ): + + s = expr.strip().upper() + m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s) + if m: + parts = m.group(1).split(":") + self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1]))) + + +class _DateOffsetContainer(QWidget): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self._spinBox = QSpinBox(self) + self._spinBox.setRange(0, 99999) + self._spinBox.setFixedHeight(25) + self._unitCombo = QComboBox(self) + for display, data in DATE_OFFSET_UNITS: + self._unitCombo.addItem(display, data) + self._unitCombo.setFixedHeight(25) + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(4) + layout.addWidget(self._spinBox) + layout.addWidget(self._unitCombo) + layout.addStretch() + + + def getValue( + self + ) -> str: + + return str(self.getOffsetDays()) + + + def setValue( + self, + expr: str + ): + + s = expr.strip().lstrip("+") + try: + self._spinBox.setValue(int(s)) + except ValueError: + pass + + + def getOffsetDays( + self + ) -> int: + + val = self._spinBox.value() + unit = self._unitCombo.currentData() + if unit == "weeks": + return val * 7 + if unit == "months": + return val * 30 + if unit == "years": + return val * 365 + return val + + + def getRawValue( + self + ) -> str: + + return str(self._spinBox.value()) + + +class _TimeOffsetContainer(QWidget): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self._spinBox = QSpinBox(self) + self._spinBox.setRange(0, 99999) + self._spinBox.setSuffix(" 小时") + self._spinBox.setFixedHeight(25) + + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self._spinBox) + + + def getValue( + self + ) -> str: + + return str(self.getOffsetHours()) + + + def setValue( + self, + expr: str + ): + + s = expr.strip().lstrip("+") + try: + self._spinBox.setValue(int(s)) + except ValueError: + pass + + + def getOffsetHours( + self + ) -> int: + + return self._spinBox.value() + + + def getRawValue( + self + ) -> str: + + return str(self._spinBox.value()) + + +def getValueFromWidget( + w: QWidget +) -> str: + + if hasattr(w, "getValue"): + return w.getValue() + if isinstance(w, QTimeEdit): + return w.time().toString("HH:mm") + if isinstance(w, QDateEdit): + return w.date().toString("yyyy-MM-dd") + if isinstance(w, QComboBox): + return w.currentData() or w.currentText() + if isinstance(w, QSpinBox): + return str(w.value()) + if isinstance(w, QDoubleSpinBox): + return str(w.value()) + if isinstance(w, QLineEdit): + return w.text() + return "" + + +def setWidgetValue( + w: QWidget, + var_type: str, + expr: str +): + + if hasattr(w, "setValue"): + w.setValue(expr) + return + s = expr.strip().upper() + if isinstance(w, QTimeEdit): + m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s) + if m: + parts = m.group(1).split(":") + w.setTime(QTime(int(parts[0]), int(parts[1]))) + elif isinstance(w, QDateEdit): + m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) + if m: + parts = m.group(1).split("-") + w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) + elif isinstance(w, QComboBox): + for i in range(w.count()): + if w.itemData(i) == s or w.itemText(i).upper() == s: + w.setCurrentIndex(i) + return + elif isinstance(w, QSpinBox): + try: + w.setValue(int(expr)) + except ValueError: + pass + elif isinstance(w, QDoubleSpinBox): + try: + w.setValue(float(expr)) + except ValueError: + pass + elif isinstance(w, QLineEdit): + inner = expr.strip() + if (inner.startswith("'") and inner.endswith("'")) or \ + (inner.startswith('"') and inner.endswith('"')): + inner = inner[1:-1].replace("''", "'") + w.setText(inner) + + +def encodeValueStr( + raw_value: str, + var_type: str +) -> str: + + if var_type == "Time": + if raw_value.startswith("+") or raw_value.startswith("-"): + return raw_value + if raw_value.startswith("TIME_OFFSET"): + m = re.match(r"TIME_OFFSET\(([+-]\d+),(\w+)\)", raw_value) + if m: + return m.group(1) + return raw_value + return f"TIME({raw_value})" + if var_type == "Date": + relMap = { + "前天": "CURRENT_DATE - 2", + "昨天": "CURRENT_DATE - 1", + "今天": "CURRENT_DATE", + "明天": "CURRENT_DATE + 1", + "后天": "CURRENT_DATE + 2" + } + if raw_value in relMap: + return relMap[raw_value] + return f"DATE({raw_value})" + if var_type == "Boolean": + up = raw_value.upper().strip() + if up in (".TRUE.", ".FALSE."): + return up + return ".TRUE." if raw_value else ".FALSE." + if var_type == "String": + escaped = raw_value.replace("'", "''") + return f"'{escaped}'" + return raw_value + + +def splitTopLevel( + text: str, + delimiter: str +) -> list: + + parts = [] + depth = 0 + buf = "" + i = 0 + textUpper = text.upper() + delimUpper = delimiter.upper() + dlen = len(delimUpper) + while i < len(text): + if text[i] == "(": + depth += 1 + buf += text[i] + elif text[i] == ")": + depth -= 1 + buf += text[i] + elif depth == 0 and textUpper[i:i + dlen] == delimUpper: + parts.append(buf) + buf = "" + i += dlen + continue + else: + buf += text[i] + i += 1 + if buf.strip(): + parts.append(buf) + return parts + + +def stripOuterParens( + s: str +) -> str: + + s = s.strip() + if s.startswith("(") and s.endswith(")"): + depth = 0 + for i, ch in enumerate(s): + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0 and i < len(s) - 1: + return s + return s[1:-1].strip() + return s + + +def isVarReference( + expr: str +) -> bool: + + s = expr.strip() + up = s.upper() + if up in (".TRUE.", ".FALSE."): + return False + if re.match(r"^TIME\(|^DATE\(|^CURRENT_", up): + return False + if up.startswith("'") or up.startswith('"'): + return False + if re.match(r"^[+-]?\d", s): + return False + return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up)) + + +def findOperatorIn( + text: str, + operators: list +) -> tuple[int, str] | None: + + for op in operators: + op_upper = op.upper() + start = 0 + while True: + idx = text.upper().find(op_upper, start) + if idx < 0: + break + if _isInsideLiteral(text, idx): + start = idx + 1 + continue + return (idx, op) + return None + + +def _isInsideLiteral( + text: str, + pos: int +) -> bool: + + in_single = False + in_double = False + for i, ch in enumerate(text): + if i >= pos: + break + if ch == "'" and not in_double: + in_single = not in_single + elif ch == '"' and not in_single: + in_double = not in_double + return in_single or in_double diff --git a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py new file mode 100644 index 0000000..b518d38 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py @@ -0,0 +1,107 @@ +""" +Orchestration observer for AutoScript scripts. +Subscribes to ASTokenizer parsing events to produce a structured +block representation for the orchestration dialog UI. +""" +from autoscript.ASObserver import ParsingObserver +from autoscript.ASTokenizer import ( + ASTokenizer, + K_IF, + K_ELSE_IF, + K_ELSE, + K_ENDIF, + K_SET, + K_ADD, + K_SUB, +) + + +__all__ = ["ScriptOrchObserver", "parseBlocks"] + + +class ScriptOrchObserver(ParsingObserver): + """ + Builds an ordered list of (block_type, condition, actions) tuples + from tokenization events. + + Each block: + (type: str, condition: str | None, actions: list[(target, value_expr, op_type)]) + """ + + def __init__( + self + ): + + super().__init__() + self._blocks = [] + self._current_type = None + self._current_condition = None + self._current_actions = [] + + + def onTokenParsed( + self, + kind: str | None, + data, + line_num: int, + raw_line: str + ): + + if kind in (K_IF, K_ELSE_IF, K_ELSE): + self._flushCurrentBlock() + self._current_type = kind + self._current_condition = data if kind != K_ELSE else None + self._current_actions = [] + elif kind in (K_SET, K_ADD, K_SUB): + target, value = data + if kind == K_SET: + self._current_actions.append((target, value, "set")) + elif kind == K_ADD: + self._current_actions.append((target, f"+{value}", "add")) + else: + self._current_actions.append((target, f"-{value}", "sub")) + elif kind == K_ENDIF: + self._flushCurrentBlock() + self._current_type = None + self._current_condition = None + self._current_actions = [] + + + def onParseComplete( + self, + statements: list + ): + + self._flushCurrentBlock() + + + def _flushCurrentBlock( + self + ): + + if self._current_type is not None: + self._blocks.append(( + self._current_type, + self._current_condition, + list(self._current_actions), + )) + + @property + def blocks( + self + ) -> list: + + return list(self._blocks) + + +def parseBlocks( + script: str +) -> list: + """ + Tokenize a script via observer pipeline and return its + structured block representation. + """ + + observer = ScriptOrchObserver() + ASTokenizer.tokenizeWithObservers(script, [observer]) + return observer.blocks diff --git a/src/gui/ALAutoScriptOrchDialog/_precheck.py b/src/gui/ALAutoScriptOrchDialog/_precheck.py new file mode 100644 index 0000000..25d424b --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_precheck.py @@ -0,0 +1,163 @@ +""" +Pre-check observer for AutoScript scripts. +Subscribes to ASTokenizer parsing events to validate script syntax +before it reaches the orchestration dialog, eliminating duplicate parsing. +""" +from autoscript.ASObserver import ParsingObserver +from autoscript.ASTokenizer import ( + K_IF, + K_ELSE_IF, + K_ELSE, + K_ENDIF, + K_SET, + K_ADD, + K_SUB, + ASTokenizer, +) + + +__all__ = ["ScriptPrecheckObserver", "precheck"] + + +class ScriptPrecheckObserver(ParsingObserver): + """ + Validates script syntax and structure during tokenization. + + Checks performed: + - IF/ENDIF depth matching + - No nested IF blocks (orchestration limitation) + - ELSE IF / ELSE appear only inside an IF block + - Only allowed variables appear in SET/ADD/SUB targets + - No completely unrecognized syntax lines + """ + + def __init__( + self, + allowed_vars: set = None + ): + + super().__init__() + self._allowed = allowed_vars or set() + self._if_depth = 0 + self.errors = [] + self._stmts = [] + + + def onTokenParsed( + self, + kind: str | None, + data, + line_num: int, + raw_line: str + ): + + if kind == K_IF: + self._if_depth += 1 + if self._if_depth > 1: + self.errors.append( + f"静态检查:错误(第{line_num}行): 检测到嵌套 IF,编排窗口不支持嵌套条件块。" + ) + elif kind == K_ELSE_IF: + if self._if_depth < 1: + self.errors.append( + f"静态检查:错误(第{line_num}行): ELSE IF 前缺少 IF。" + ) + elif kind == K_ELSE: + if self._if_depth < 1: + self.errors.append( + f"静态检查:错误(第{line_num}行): ELSE 前缺少 IF。" + ) + elif kind == K_ENDIF: + self._if_depth -= 1 + if self._if_depth < 0: + self.errors.append( + f"静态检查:错误(第{line_num}行): 多余的 ENDIF。" + ) + elif kind is None: + self.errors.append( + f"静态检查:错误(第{line_num}行): 无法识别的语法 '{raw_line}'。" + ) + elif kind in (K_SET, K_ADD, K_SUB): + target = data[0] if isinstance(data, tuple) else "" + if self._allowed and target.upper() not in self._allowed: + self.errors.append( + f"静态检查:错误(第{line_num}行): 目标变量 '{target}' 不是预设变量," + f"编排窗口不支持。" + ) + + + def onParseComplete( + self, + statements: list + ): + + if self._if_depth != 0: + self.errors.append( + f"静态检查:错误(不适用): IF 与 ENDIF 不匹配。") + self._stmts = statements + + @property + def valid( + self + ) -> bool: + + return len(self.errors) == 0 + + + def getErrorMessage( + self + ) -> str: + + return self.errors[0] if self.errors else "" + + + def buildSimplifiedScript( + self + ) -> str: + """Replace all non-control-flow statements with PASS for engine validation.""" + + lines = [] + for stmt in self._stmts: + if stmt.kind in (K_IF, K_ELSE_IF, K_ELSE, K_ENDIF): + lines.append(stmt.raw_line) + else: + lines.append("PASS") + return "\n".join(lines) + + +def precheck( + script: str, + allowed_vars: set = None +) -> tuple[bool, str]: + """ + Run the full precheck pipeline on a script. + + Steps: + 1. Create a ScriptPrecheckObserver and subscribe it to an ASTokenizer. + 2. Tokenize — the observer validates syntax during token events. + 3. Replace action lines with PASS and run engine validation + with mock target data. + """ + + if not script or not script.strip(): + return True, "" + observer = ScriptPrecheckObserver(allowed_vars=allowed_vars) + ASTokenizer.tokenizeWithObservers(script, [observer]) + if not observer.valid: + return False, observer.getErrorMessage() + simplified = observer.buildSimplifiedScript() + if not simplified.strip(): + return True, "" + try: + from autoscript import ( + registerDefaultTargetVars, + buildMockTargetData, + execute + ) + registerDefaultTargetVars() + execute(simplified, buildMockTargetData()) + except ValueError as e: + return False, f"运行时检查: {e}" + except Exception: + return False, "执行环境异常,请检查 AutoScript 配置。" + return True, "" diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py new file mode 100644 index 0000000..d0fced6 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -0,0 +1,534 @@ +""" +Widget components for the AutoScript orchestration dialog. +""" +from PySide6.QtCore import Slot +from PySide6.QtWidgets import ( + QComboBox, + QFrame, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QStackedWidget +) + +from gui.ALAutoScriptOrchDialog._helpers import ( + ACTION_TYPES, + ARITH_TYPES, + COMPARE_OPERATORS, + LOGIC_OPERATORS, + PRESET_VARIABLES, + VAR_TYPE_ORDER, + encodeValueStr, + getValueFromWidget, + isVarReference, + makeComboWidget, + makeLabel, + makeOffsetWidget, + makeValueWidget, + makeVarRefCombo, + setWidgetValue, +) + + +class ConditionRowFrame(QFrame): + + def __init__( + self, + varMgr, + parentBlockIndex: int = 0, + isFirst: bool = False, + parent = None + ): + + super().__init__(parent) + self._varMgr = varMgr + self._blockIndex = parentBlockIndex + self._isFirst = isFirst + self._isBoolMode = False + + self.setupUi() + self.connectSignals() + + + def setupUi( + self + ): + + self.setUpdatesEnabled(False) + self.setFrameShape(QFrame.StyledPanel) + self.setFrameShadow(QFrame.Raised) + self.setFixedHeight(32) + layout = QHBoxLayout(self) + layout.setContentsMargins(2, 2, 2, 2) + layout.setSpacing(4) + if self._isFirst: + self.logicCombo = None + else: + self.logicCombo = makeComboWidget(LOGIC_OPERATORS, min_width=110, parent=self) + layout.addWidget(self.logicCombo) + self.leftVarCombo = QComboBox(self) + self.leftVarCombo.setFixedHeight(25) + self.leftVarCombo.setMinimumWidth(120) + self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.populateLeftVarCombo() + layout.addWidget(self.leftVarCombo) + self.opCombo = makeComboWidget(COMPARE_OPERATORS, min_width=80, parent=self) + layout.addWidget(self.opCombo) + self._compTypeCombo = makeComboWidget([ + ("特定值", "literal"), + ("变量", "variable"), + ], min_width=70, parent=self) + layout.addWidget(self._compTypeCombo) + self._rhsStack = QStackedWidget(self) + self._rhsStack.setFixedHeight(25) + self._literalStack = QStackedWidget(self) + self._literalStack.setFixedHeight(25) + self._literalWidgets = {} + for vt in VAR_TYPE_ORDER: + w = makeValueWidget(vt, self._literalStack) + self._literalWidgets[vt] = w + self._literalStack.addWidget(w) + self._literalStack.setCurrentWidget(self._literalWidgets.get("String")) + self._rhsStack.addWidget(self._literalStack) + self.rhsVarCombo = makeVarRefCombo(self) + self._rhsStack.addWidget(self.rhsVarCombo) + self._rhsStack.setCurrentIndex(0) + layout.addWidget(self._rhsStack) + if not self._isFirst: + self.deleteBtn = QPushButton("×", self) + self.deleteBtn.setFixedSize(25, 25) + self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") + layout.addWidget(self.deleteBtn) + else: + self.deleteBtn = None + layout.addStretch() + self.setUpdatesEnabled(True) + + + def populateLeftVarCombo( + self + ): + + self._varMgr.populateCombo(self.leftVarCombo) + + + def populateRhsVarCombo( + self + ): + + self._varMgr.populateCombo(self.rhsVarCombo) + + + def connectSignals( + self + ): + + self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) + self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) + + + @Slot(int) + def onLeftVarChanged( + self, + idx + ): + + if idx < 0: + return + data = self.leftVarCombo.itemData(idx) + if not data: + return + _, vartype = data + self.updateRhsLiteralWidget(vartype) + + + def updateRhsLiteralWidget( + self, + vartype: str + ): + + if vartype not in self._literalWidgets: + vartype = "String" + self._literalStack.setCurrentWidget(self._literalWidgets[vartype]) + + + @Slot(int) + def onCompTypeChanged( + self, + idx + ): + + isVar = (self._compTypeCombo.currentData() == "variable") + self._rhsStack.setCurrentIndex(1 if isVar else 0) + if isVar: + self.populateRhsVarCombo() + + + def getLogic( + self + ) -> str: + + return self.logicCombo.currentData() if self.logicCombo else "" + + + def toConditionText( + self + ) -> str: + + data = self.leftVarCombo.currentData() + if not data: + return "" + name, vartype = data + opSym = self.opCombo.currentData() + isVarRef = (self._compTypeCombo.currentData() == "variable") + if isVarRef: + rd = self.rhsVarCombo.currentData() + if rd: + rhsName = rd[0] + return f"{name} {opSym} {rhsName}" + rhsText = self.rhsVarCombo.currentText().strip() + if rhsText: + return f"{name} {opSym} {rhsText}" + return "" + w = self._literalWidgets.get(vartype) + if w: + rawVal = getValueFromWidget(w) + encoded = encodeValueStr(rawVal, vartype) + return f"{name} {opSym} {encoded}" + return "" + + + def loadFromParts( + self, + operandName: str, + opSym: str, + valueExpr: str + ): + + for ci in range(self.leftVarCombo.count()): + d = self.leftVarCombo.itemData(ci) + if d and d[0] == operandName: + self.leftVarCombo.setCurrentIndex(ci) + break + if opSym: + for oi in range(self.opCombo.count()): + if self.opCombo.itemData(oi) == opSym: + self.opCombo.setCurrentIndex(oi) + break + if not valueExpr: + return + up = valueExpr.strip().upper() + data = self.leftVarCombo.currentData() + vartype = data[1] if data else "String" + if isVarReference(valueExpr) or self._isKnownVar(up): + self._compTypeCombo.setCurrentIndex(1) + self.populateRhsVarCombo() + found = self._varMgr.findExactNameEntry(self.rhsVarCombo, up) + if found >= 0: + self.rhsVarCombo.setCurrentIndex(found) + else: + self.rhsVarCombo.addItem(up, (up, "String")) + self.rhsVarCombo.setCurrentIndex(self.rhsVarCombo.count() - 1) + else: + self._compTypeCombo.setCurrentIndex(0) + w = self._literalWidgets.get(vartype) + if w: + setWidgetValue(w, vartype, valueExpr) + + + def _isKnownVar( + self, + name: str + ) -> bool: + + return self._varMgr.getInfoByName(name) is not None + + + def refreshVarCombos( + self + ): + + self.populateLeftVarCombo() + self.populateRhsVarCombo() + + +class ActionStepFrame(QFrame): + + def __init__( + self, + varMgr, + parentBlockIndex: int = 0, + parent = None + ): + + super().__init__(parent) + self._varMgr = varMgr + self._blockIndex = parentBlockIndex + self._currentTargetType = "String" + + self.setupUi() + self.connectSignals() + + + def setupUi( + self + ): + + self.setUpdatesEnabled(False) + self.setFrameShape(QFrame.StyledPanel) + self.setFrameShadow(QFrame.Raised) + self.setFixedHeight(35) + layout = QHBoxLayout(self) + layout.setContentsMargins(2, 2, 2, 2) + layout.setSpacing(4) + self.opTypeCombo = makeComboWidget(ACTION_TYPES, min_width=70, parent=self) + layout.addWidget(self.opTypeCombo) + layout.addWidget(makeLabel("设置", self)) + self.targetCombo = QComboBox(self) + self.targetCombo.setFixedHeight(25) + self.targetCombo.setMinimumWidth(120) + self.buildTargetCombo() + layout.addWidget(self.targetCombo) + layout.addWidget(makeLabel("为", self)) + self._valueSrcCombo = makeComboWidget([ + ("特定值", "literal"), + ("变量", "variable"), + ], min_width=70, parent=self) + layout.addWidget(self._valueSrcCombo) + self._valueStack = QStackedWidget(self) + self._valueStack.setFixedHeight(25) + self.initValueStacks() + layout.addWidget(self._valueStack) + self.existingVarCombo = makeVarRefCombo(self) + self.existingVarCombo.setVisible(False) + layout.addWidget(self.existingVarCombo) + self.deleteBtn = QPushButton("×", self) + self.deleteBtn.setFixedSize(25, 25) + self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") + layout.addWidget(self.deleteBtn) + self.setUpdatesEnabled(True) + + + def buildTargetCombo( + self + ): + + self.targetCombo.blockSignals(True) + self.targetCombo.clear() + for p in PRESET_VARIABLES: + if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): + continue + info = self._varMgr.getInfoByName(p["name"]) + if info: + self.targetCombo.addItem( + info["display"], + (info["name"], info["type"]) + ) + self.targetCombo.blockSignals(False) + + + def initValueStacks( + self + ): + + self._literalWidgets = {} + self._offsetWidgets = {} + for vt in VAR_TYPE_ORDER: + self._literalWidgets[vt] = makeValueWidget(vt, self._valueStack) + self._valueStack.addWidget(self._literalWidgets[vt]) + if vt in ARITH_TYPES: + self._offsetWidgets[vt] = makeOffsetWidget(vt, self._valueStack) + self._valueStack.addWidget(self._offsetWidgets[vt]) + else: + lbl = QLabel("(不支持该操作)", self._valueStack) + lbl.setFixedHeight(25) + self._offsetWidgets[vt] = lbl + self._valueStack.addWidget(lbl) + + + def connectSignals( + self + ): + + self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged) + self.targetCombo.currentIndexChanged.connect(self.onTargetChanged) + self._valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) + + @Slot(int) + def onTargetChanged( + self, + idx + ): + + if idx < 0: + return + data = self.targetCombo.itemData(idx) + if not data: + return + _, vartype = data + self._currentTargetType = vartype + self.updateRHSWidget() + self.onValueSrcChanged(self._valueSrcCombo.currentIndex()) + + @Slot(int) + def onOpTypeChanged( + self, + idx + ): + + self.updateRHSWidget() + + + def updateRHSWidget( + self + ): + + op = self.opTypeCombo.currentData() + isArith = (op in ("add", "sub")) + actualType = self._currentTargetType + if isArith and actualType in self._offsetWidgets: + self._valueStack.setCurrentWidget(self._offsetWidgets[actualType]) + elif actualType in self._literalWidgets: + self._valueStack.setCurrentWidget(self._literalWidgets[actualType]) + else: + self._valueStack.setCurrentWidget(self._literalWidgets.get("String")) + + @Slot(int) + def onValueSrcChanged( + self, + idx + ): + + isVar = (self._valueSrcCombo.currentData() == "variable") + self._valueStack.setVisible(not isVar) + self.existingVarCombo.setVisible(isVar) + if isVar: + self._varMgr.populateCombo(self.existingVarCombo) + else: + self.updateRHSWidget() + + + def getTargetName( + self + ) -> str: + + data = self.targetCombo.currentData() + return data[0] if data else "" + + + def toScriptLine( + self + ) -> str: + + target = self.getTargetName() + if not target: + return "" + op = self.opTypeCombo.currentData() + rawVal = self._getValueRaw() + if op == "set": + vartype = self._currentTargetType + encoded = encodeValueStr(rawVal, vartype) + if vartype == "Time": + if rawVal.startswith("+"): + return f" {target} .ADD. {rawVal[1:]}" + if rawVal.startswith("-"): + return f" {target} .SUB. {rawVal[1:]}" + return f" SET {target} = {encoded}" + elif op == "add": + vartype = self._currentTargetType + if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"): + days = self._valueStack.currentWidget().getOffsetDays() + return f" {target} .ADD. {days}" + if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"): + hours = self._valueStack.currentWidget().getOffsetHours() + return f" {target} .ADD. {hours}" + return f" {target} .ADD. {rawVal}" + elif op == "sub": + vartype = self._currentTargetType + if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"): + days = self._valueStack.currentWidget().getOffsetDays() + return f" {target} .SUB. {days}" + if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"): + hours = self._valueStack.currentWidget().getOffsetHours() + return f" {target} .SUB. {hours}" + return f" {target} .SUB. {rawVal}" + return "" + + + def _getValueRaw( + self + ) -> str: + + if self._valueSrcCombo.currentData() == "variable": + return self.existingVarCombo.currentText().strip() + w = self._valueStack.currentWidget() + if w: + return getValueFromWidget(w) + return "" + + + def setOpType( + self, + opType: str + ): + + for i in range(self.opTypeCombo.count()): + if self.opTypeCombo.itemData(i) == opType: + self.opTypeCombo.setCurrentIndex(i) + break + + + def loadFromScript( + self, + targetVar: str, + valueExpr: str + ): + + targetUp = targetVar.upper().strip() + for ci in range(self.targetCombo.count()): + d = self.targetCombo.itemData(ci) + if d and d[0] == targetUp: + self.targetCombo.setCurrentIndex(ci) + break + self._setValueFromExpr(valueExpr) + + + def _setValueFromExpr( + self, + expr: str + ): + + s = expr.strip() + if not s: + return + up = s.upper() + if isVarReference(s): + self._valueSrcCombo.setCurrentIndex(1) + self._varMgr.populateCombo(self.existingVarCombo) + idx = self._varMgr.findExactNameEntry(self.existingVarCombo, up) + if idx >= 0: + self.existingVarCombo.setCurrentIndex(idx) + else: + self.existingVarCombo.addItem(up, (up, "String")) + self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) + else: + self._valueSrcCombo.setCurrentIndex(0) + w = self._valueStack.currentWidget() + if w: + setWidgetValue(w, self._currentTargetType, expr) + + + def refreshVarCombos( + self + ): + + currentData = self.targetCombo.currentData() + self.buildTargetCombo() + if currentData: + for i in range(self.targetCombo.count()): + d = self.targetCombo.itemData(i) + if d and d[0] == currentData[0]: + self.targetCombo.setCurrentIndex(i) + break + self._varMgr.populateCombo(self.existingVarCombo) diff --git a/src/gui/ALAutoScriptPrevDialog.py b/src/gui/ALAutoScriptPrevDialog.py deleted file mode 100644 index 4becb08..0000000 --- a/src/gui/ALAutoScriptPrevDialog.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- 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. -""" - -from PySide6.QtCore import Slot - -from PySide6.QtGui import ( - QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QIcon -) -from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QPlainTextEdit, - QDialogButtonBox, QPushButton, QLabel, QApplication, QStyle -) - - -class ALScriptHighlighter(QSyntaxHighlighter): - - def __init__( - self, - parent=None - ): - - super().__init__(parent) - self._rules = [] - - keywordFmt = QTextCharFormat() - keywordFmt.setForeground(QColor("#316BFF")) - keywordFmt.setFontWeight(QFont.Weight.Bold) - for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF", - "SET", "PASS", "THEN"]: - pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b" - self._rules.append((pattern, keywordFmt)) - literalFmt = QTextCharFormat() - literalFmt.setForeground(QColor("#C2185B")) - literalFmt.setFontWeight(QFont.Weight.Bold) - for lit in [".TRUE.", ".FALSE."]: - self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt)) - opFmt = QTextCharFormat() - opFmt.setForeground(QColor("#9C27B0")) - for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.", - r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]: - self._rules.append((op, opFmt)) - varFmt = QTextCharFormat() - varFmt.setForeground(QColor("#E65100")) - for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME", - "RESERVE_DATE", "USERNAME", "USER_ENABLE", - "PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]: - self._rules.append((r"\b" + var + r"\b", varFmt)) - funcFmt = QTextCharFormat() - funcFmt.setForeground(QColor("#2E7D32")) - self._rules.append((r"\bTIME\([^)]+\)", funcFmt)) - self._rules.append((r"\bDATE\([^)]+\)", funcFmt)) - strFmt = QTextCharFormat() - strFmt.setForeground(QColor("#388E3C")) - self._rules.append((r"'[^']*'", strFmt)) - numFmt = QTextCharFormat() - numFmt.setForeground(QColor("#D32F2F")) - self._rules.append((r"\b\d+\b", numFmt)) - commentFmt = QTextCharFormat() - commentFmt.setForeground(QColor("#999999")) - commentFmt.setFontItalic(True) - self._rules.append((r"//[^\n]*", commentFmt)) - - - def highlightBlock( - self, - text - ): - - import re - for pattern, fmt in self._rules: - for match in re.finditer(pattern, text, re.IGNORECASE): - start = match.start() - length = match.end() - match.start() - self.setFormat(start, length, fmt) - - -class ALAutoScriptPreviewDialog(QDialog): - - def __init__( - self, - parent=None, - script: str = "" - ): - - super().__init__(parent) - self.__fontSize = 13 - - self.modifyUi() - self.connectSignals() - - self._textEdit.setPlainText(script) - self._highlighter = ALScriptHighlighter( - self._textEdit.document() - ) - - - def modifyUi( - self - ): - - self.setWindowTitle("AutoScript 预览 - AutoLibrary") - self.setMinimumSize(520, 360) - - layout = QVBoxLayout(self) - toolbarLayout = QHBoxLayout() - self._zoomInBtn = QPushButton("+") - self._zoomInBtn.setFixedSize(30, 25) - self._zoomOutBtn = QPushButton("-") - self._zoomOutBtn.setFixedSize(30, 25) - self._zoomResetBtn = QPushButton( - QApplication.style().standardIcon( - QStyle.StandardPixmap.SP_BrowserReload - ), "" - ) - self._zoomResetBtn.setFixedSize(30, 25) - self._zoomResetBtn.setToolTip("重置缩放") - self._zoomLabel = QLabel(f"{self.__fontSize}px") - self._zoomLabel.setFixedHeight(25) - toolbarLayout.addWidget(self._zoomInBtn) - toolbarLayout.addWidget(self._zoomOutBtn) - toolbarLayout.addWidget(self._zoomResetBtn) - toolbarLayout.addWidget(self._zoomLabel) - toolbarLayout.addStretch() - self._copyBtn = QPushButton( - QApplication.style().standardIcon( - QStyle.StandardPixmap.SP_FileDialogDetailedView - ), "" - ) - self._copyBtn.setFixedSize(30, 25) - self._copyBtn.setToolTip("复制脚本") - toolbarLayout.addWidget(self._copyBtn) - layout.addLayout(toolbarLayout) - self._textEdit = QPlainTextEdit(self) - self._textEdit.setReadOnly(True) - self._textEdit.setLineWrapMode( - QPlainTextEdit.LineWrapMode.NoWrap - ) - self._textEdit.setStyleSheet( - "QPlainTextEdit {" - " font-family: 'Courier New', 'Consolas', monospace;" - " font-size: 13px;" - "}" - ) - layout.addWidget(self._textEdit) - - self._btnBox = QDialogButtonBox( - QDialogButtonBox.StandardButton.Close - ) - self._btnBox.button( - QDialogButtonBox.StandardButton.Close - ).setText("关闭") - layout.addWidget(self._btnBox) - - - def connectSignals( - self - ): - - self._btnBox.rejected.connect(self.reject) - self._zoomInBtn.clicked.connect(self.onZoomIn) - self._zoomOutBtn.clicked.connect(self.onZoomOut) - self._zoomResetBtn.clicked.connect(self.onZoomReset) - self._copyBtn.clicked.connect(self.onCopy) - - - def updateFontSize( - self - ): - - font = self._textEdit.font() - font.setPointSize(self.__fontSize) - self._textEdit.setFont(font) - self._textEdit.setStyleSheet( - "QPlainTextEdit {" - " font-family: 'Courier New', 'Consolas', monospace;" - f" font-size: {self.__fontSize}px;" - "}" - ) - self._zoomLabel.setText(f"{self.__fontSize}px") - - @Slot() - def onZoomIn( - self - ): - - self.__fontSize = min(self.__fontSize + 2, 40) - self.updateFontSize() - - @Slot() - def onZoomOut( - self - ): - - self.__fontSize = max(self.__fontSize - 2, 8) - self.updateFontSize() - - @Slot() - def onZoomReset( - self - ): - - self.__fontSize = 13 - self.updateFontSize() - - @Slot() - def onCopy( - self - ): - - clipboard = QApplication.clipboard() - clipboard.setText(self._textEdit.toPlainText()) - original = self._copyBtn.text() - self._copyBtn.setText("已复制") - self._copyBtn.setEnabled(False) - from PySide6.QtCore import QTimer - QTimer.singleShot(2000, lambda: ( - self._copyBtn.setText(original), - self._copyBtn.setEnabled(True) - )) diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index f6003d3..f1680b0 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -18,7 +18,7 @@ from PySide6.QtCore import ( from base.MsgBase import MsgBase from operators.AutoLib import AutoLib from utils.JSONReader import JSONReader -from dsl.AutoScriptEngine import AutoScriptEngine +from autoscript import execute, registerDefaultTargetVars class AutoLibWorker(MsgBase, QThread): @@ -225,7 +225,8 @@ class TimerTaskWorker(AutoLibWorker): continue for user in group.get("users", []): try: - AutoScriptEngine.execute(auto_script, user) + registerDefaultTargetVars() + execute(auto_script, user) affected_count += 1 except ValueError as e: self._showTrace( diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index dab9139..21d13fa 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -102,10 +102,9 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.AutoScriptSetButton.setMinimumHeight(25) self.AutoScriptSetButton.setFixedWidth(130) autoScriptBtnLayout.addWidget(self.AutoScriptSetButton) - self.AutoScriptPreviewButton = QPushButton("预览") + self.AutoScriptPreviewButton = QPushButton("编辑") self.AutoScriptPreviewButton.setMinimumHeight(25) self.AutoScriptPreviewButton.setFixedWidth(60) - self.AutoScriptPreviewButton.setEnabled(False) autoScriptBtnLayout.addWidget(self.AutoScriptPreviewButton) autoScriptBtnLayout.addStretch() self.AutoScriptHelpButton = QPushButton("?") @@ -170,7 +169,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.__auto_script = auto_script self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") - self.AutoScriptPreviewButton.setEnabled(True) self.ConfirmButton.setText("保存") @@ -299,20 +297,24 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): if script: self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") - self.AutoScriptPreviewButton.setEnabled(True) else: self.AutoScriptStatusLabel.setText("未设置") self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") - self.AutoScriptPreviewButton.setEnabled(False) dlg.deleteLater() @Slot() def onPreviewAutoScript(self): - if not self.__auto_script: - return - from gui.ALAutoScriptPrevDialog import ALAutoScriptPreviewDialog - dlg = ALAutoScriptPreviewDialog(self, self.__auto_script) - dlg.exec() + from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog + dlg = ALAutoScriptEditDialog(self, self.__auto_script) + if dlg.exec() == QDialog.DialogCode.Accepted: + script = dlg.getScript() + self.__auto_script = script + if script: + self.AutoScriptStatusLabel.setText("已设置") + self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") + else: + self.AutoScriptStatusLabel.setText("未设置") + self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") dlg.deleteLater() @Slot() From c038c8005dd18baf1b5f3d3a51f3a2b3e981788c Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 16:01:10 +0800 Subject: [PATCH 08/49] =?UTF-8?q?refactor(autoscript):=20=E5=85=AC?= =?UTF-8?q?=E5=BC=80=20splitTopLevel=20=E5=B9=B6=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=EF=BC=8C=E6=B6=88=E9=99=A4=E5=86=97=E4=BD=99?= =?UTF-8?q?=E5=A7=94=E6=89=98=E4=B8=8E=E9=87=8D=E5=A4=8D=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASEngine.py | 15 ++++++++--- src/autoscript/ASOperator.py | 49 ++++++++++++------------------------ src/autoscript/__init__.py | 24 +++++++++--------- 3 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index f73657b..352f4a8 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -33,7 +33,7 @@ from .ASTokenizer import ( ) -__all__ = ["execute", "addTargetVar"] +__all__ = ["execute", "addTargetVar", "splitTopLevel"] # Engine state @@ -69,7 +69,7 @@ _RE_CUR_DATE_OFFSET = re.compile(r"^CURRENT_DATE\s*\+\s*(\d+)$", re.IGNORECASE) _RE_CUR_TIME_OFFSET = re.compile(r"^CURRENT_TIME\s*\+\s*(\d+)$", re.IGNORECASE) -def _splitTopLevel( +def splitTopLevel( text: str, delimiter: str ) -> list: @@ -281,13 +281,13 @@ def _evaluateCondition( s = condition_str.strip() if not s: return False - or_parts = _splitTopLevel(s, ".OR.") + or_parts = splitTopLevel(s, ".OR.") if len(or_parts) > 1: return any( _evaluateCondition(p.strip(), target_data, line) for p in or_parts ) - and_parts = _splitTopLevel(s, ".AND.") + and_parts = splitTopLevel(s, ".AND.") if len(and_parts) > 1: return all( _evaluateCondition(p.strip(), target_data, line) @@ -466,12 +466,14 @@ class _EngineExecutor(NodeVisitor): return self._cur_line + def _incLine( self ): self._cur_line += 1 + def visitScript( self, _node: Script @@ -480,6 +482,7 @@ class _EngineExecutor(NodeVisitor): for child in _node.body: child.accept(self) + def visitIf( self, _node: IfNode @@ -506,6 +509,7 @@ class _EngineExecutor(NodeVisitor): for child in _node.else_body: child.accept(self) + def visitSet( self, _node: SetNode @@ -515,6 +519,7 @@ class _EngineExecutor(NodeVisitor): full_line = f"SET {_node.target} = {_node.value}" _executeSet(full_line, self._target_data, self._line) + def visitOp( self, _node: OpNode @@ -525,6 +530,7 @@ class _EngineExecutor(NodeVisitor): full_line = f"{_node.target} .{op_upper}. {_node.value}" _executeOperation(full_line, self._target_data, self._line) + def visitPass( self, _node: PassNode @@ -532,6 +538,7 @@ class _EngineExecutor(NodeVisitor): self._incLine() + def visitUnrecog( self, _node: UnrecogNode diff --git a/src/autoscript/ASOperator.py b/src/autoscript/ASOperator.py index 49facd7..60d6195 100644 --- a/src/autoscript/ASOperator.py +++ b/src/autoscript/ASOperator.py @@ -17,7 +17,7 @@ from datetime import ( from .ASObject import ASObject -__all__ = ["ASOperator"] +__all__ = ["ASOperator", "ARITH_TYPES", "COMPARISON_OPERATORS"] class ASOperator: @@ -96,12 +96,11 @@ class ASOperator: target_val = target.getValue(target_data) if target_val is None: raise ValueError(f"'{target.name}' 的值为空,无法进行运算") - if op == ".ADD.": - cls._arithAdd(target, target_val, operand, target_data) - elif op == ".SUB.": - cls._arithSub(target, target_val, operand, target_data) - else: + op_name = "ADD" if op == ".ADD." else "SUB" if op == ".SUB." else None + if op_name is None: raise ValueError(f"不支持的操作 '{op}'") + sign = 1 if op == ".ADD." else -1 + cls._arithBinary(target, target_val, operand, target_data, sign, op_name) @classmethod def _arithBinary( @@ -110,13 +109,13 @@ class ASOperator: target_val, operand: ASObject, target_data: dict, - sign: int + sign: int, + op_name: str = "" ): - """Apply ADD (sign=1) or SUB (sign=-1) per type.""" + """Apply arithmetic per type.""" tp = target.var_type raw_op = operand._value - op_name = "ADD" if sign == 1 else "SUB" if tp == "Date": if not isinstance(target_val, date): @@ -129,35 +128,13 @@ class ASOperator: dt = datetime.combine(datetime.today(), target_val) + delta new_val = dt.time() elif tp == "Int": - new_val = int(target_val) + int(raw_op) * sign + new_val = int(target_val) + int(raw_op)*sign elif tp == "Float": - new_val = float(target_val) + float(raw_op) * sign + new_val = float(target_val) + float(raw_op)*sign else: raise ValueError(f"'{tp}' 类型不支持 {op_name} 操作") target.setValue(new_val, target_data) - @classmethod - def _arithAdd( - cls, - target: ASObject, - target_val, - operand: ASObject, - target_data: dict - ): - """Dispatch ADD per type.""" - cls._arithBinary(target, target_val, operand, target_data, 1) - - @classmethod - def _arithSub( - cls, - target: ASObject, - target_val, - operand: ASObject, - target_data: dict - ): - """Dispatch SUB per type.""" - cls._arithBinary(target, target_val, operand, target_data, -1) - @classmethod def compare( cls, @@ -206,3 +183,9 @@ class ASOperator: f"无法比较 '{left.name}' ({left.var_type}) " f"与 '{right.name}' ({right.var_type})" ) + + +# Public constants +# may be used by the GUI orchestration dialog. +ARITH_TYPES = ASOperator._ARITH_TYPES +COMPARISON_OPERATORS = set(ASOperator._COMPARE.keys()) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index c761e6d..c570fe5 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -14,13 +14,16 @@ from autoscript.ASTokenizer import ( from autoscript.ASEngine import ( execute, addTargetVar, + splitTopLevel, ) from autoscript.ASObject import _META_VARS as META_VARS from autoscript.ASObserver import ParsingObserver + __all__ = [ "execute", "addTargetVar", + "splitTopLevel", "registerDefaultTargetVars", "buildMockTargetData", "META_VARS", @@ -35,18 +38,6 @@ __all__ = [ "ParsingObserver", ] -# All variables available to scripts (display_name -> (name, type)). -# This mirrors the old AutoScriptEngine.VARIABLE_META for backward -# compatibility in the UI orchestration dialog. -ALL_VARIABLES: dict = { - "用户名": ("USERNAME", "String"), - "用户启用": ("USER_ENABLE", "Boolean"), - "预约日期": ("RESERVE_DATE", "Date"), - "预约开始时间": ("RESERVE_BEGIN_TIME", "Time"), - "预约结束时间": ("RESERVE_END_TIME", "Time"), - "当前时间": ("CURRENT_TIME", "Time"), - "当前日期": ("CURRENT_DATE", "Date"), -} # Key paths into target_data dict for each target variable. # (name, type, key_path, display_name) @@ -57,6 +48,15 @@ _TARGET_VAR_DEFS = [ ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), ] + +# All variables (display_name -> (name, type)), derived from target vars + meta vars. +ALL_VARIABLES = { + display_name: (name, var_type) + for name, var_type, _, display_name in _TARGET_VAR_DEFS +} | { + obj.display_name: (obj.name, obj.var_type) + for obj in META_VARS.values() +} _MOCK_TYPE_VALUES = { "String": "__mock__", "Boolean": True, From 600a304ab812c8fc1a1f7f4041b5959acc4c2b7b Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 16:01:16 +0800 Subject: [PATCH 09/49] =?UTF-8?q?style(gui):=20=E8=A7=84=E8=8C=83=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E5=AF=B9=E8=AF=9D=E6=A1=86=E5=B1=9E=E6=80=A7=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E5=B9=B6=E6=B6=88=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAutoScriptOrchDialog/_blocks.py | 22 ++--- src/gui/ALAutoScriptOrchDialog/_dialog.py | 32 +++---- src/gui/ALAutoScriptOrchDialog/_helpers.py | 98 ++++++++------------ src/gui/ALAutoScriptOrchDialog/_widgets.py | 100 ++++++++++----------- 4 files changed, 112 insertions(+), 140 deletions(-) diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py index 4422e80..e0e3625 100644 --- a/src/gui/ALAutoScriptOrchDialog/_blocks.py +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -79,9 +79,9 @@ class ConditionalBlock(QGroupBox): condLayout.setContentsMargins(4, 4, 4, 4) condLayout.setSpacing(6) - self._condRowsLayout = QVBoxLayout() - self._condRowsLayout.setSpacing(4) - condLayout.addLayout(self._condRowsLayout) + self.condRowsLayout = QVBoxLayout() + self.condRowsLayout.setSpacing(4) + condLayout.addLayout(self.condRowsLayout) self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget) self.addCondBtn.setFixedHeight(25) condLayout.addWidget(self.addCondBtn) @@ -89,9 +89,9 @@ class ConditionalBlock(QGroupBox): self.actionLabel = QLabel("执行步骤:", self) self.actionLabel.setFixedHeight(25) mainLayout.addWidget(self.actionLabel) - self._actionsLayout = QVBoxLayout() - self._actionsLayout.setSpacing(4) - mainLayout.addLayout(self._actionsLayout) + self.actionsLayout = QVBoxLayout() + self.actionsLayout.setSpacing(4) + mainLayout.addLayout(self.actionsLayout) self.addActionBtn = QPushButton("+ 添加执行步骤", self) self.addActionBtn.setFixedHeight(25) mainLayout.addWidget(self.addActionBtn) @@ -116,7 +116,7 @@ class ConditionalBlock(QGroupBox): isFirst=True, parent=self ) self._conditionRows.append(row) - self._condRowsLayout.addWidget(row) + self.condRowsLayout.addWidget(row) def addConditionRow( @@ -129,7 +129,7 @@ class ConditionalBlock(QGroupBox): ) row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row)) self._conditionRows.append(row) - self._condRowsLayout.addWidget(row) + self.condRowsLayout.addWidget(row) def removeConditionRow( @@ -139,7 +139,7 @@ class ConditionalBlock(QGroupBox): if row in self._conditionRows and len(self._conditionRows) > 1: self._conditionRows.remove(row) - self._condRowsLayout.removeWidget(row) + self.condRowsLayout.removeWidget(row) row.hide() row.deleteLater() @@ -151,7 +151,7 @@ class ConditionalBlock(QGroupBox): step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self) step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step)) self._actionWidgets.append(step) - self._actionsLayout.addWidget(step) + self.actionsLayout.addWidget(step) def removeActionStep( @@ -161,7 +161,7 @@ class ConditionalBlock(QGroupBox): if step in self._actionWidgets: self._actionWidgets.remove(step) - self._actionsLayout.removeWidget(step) + self.actionsLayout.removeWidget(step) step.hide() step.deleteLater() diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 21f838a..c5fedad 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -46,7 +46,7 @@ class ALAutoScriptOrchDialog(QDialog): self.loadFromScript(existingScript) else: self.addBlock() - self._scrollLayout.addStretch() + self.scrollLayout.addStretch() def setupUi( @@ -61,8 +61,8 @@ class ALAutoScriptOrchDialog(QDialog): scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.NoFrame) scrollContent = QWidget() - self._scrollLayout = QVBoxLayout(scrollContent) - self._scrollLayout.setSpacing(5) + self.scrollLayout = QVBoxLayout(scrollContent) + self.scrollLayout.setSpacing(5) scroll.setWidget(scrollContent) mainLayout.addWidget(scroll) self.addBlockBtn = QPushButton("+ 添加判断块") @@ -108,16 +108,16 @@ class ALAutoScriptOrchDialog(QDialog): block.addActionStep() self._blocks.append(block) self._updateBlockTypeRestrictions() - if self._scrollLayout.count() > 0: - lastItem = self._scrollLayout.itemAt( - self._scrollLayout.count() - 1 + if self.scrollLayout.count() > 0: + lastItem = self.scrollLayout.itemAt( + self.scrollLayout.count() - 1 ) if lastItem and lastItem.spacerItem(): - self._scrollLayout.insertWidget( - self._scrollLayout.count() - 1, block + self.scrollLayout.insertWidget( + self.scrollLayout.count() - 1, block ) return - self._scrollLayout.addWidget(block) + self.scrollLayout.addWidget(block) def removeBlock( @@ -130,7 +130,7 @@ class ALAutoScriptOrchDialog(QDialog): return if block in self._blocks: self._blocks.remove(block) - self._scrollLayout.removeWidget(block) + self.scrollLayout.removeWidget(block) block.hide() block.deleteLater() for i, blk in enumerate(self._blocks): @@ -200,8 +200,8 @@ class ALAutoScriptOrchDialog(QDialog): typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2} parsedBlocks = parseBlocks(script) self._blocks.clear() - while self._scrollLayout.count(): - item = self._scrollLayout.takeAt(0) + while self.scrollLayout.count(): + item = self.scrollLayout.takeAt(0) if item.widget(): item.widget().deleteLater() try: @@ -222,8 +222,8 @@ class ALAutoScriptOrchDialog(QDialog): self._parseConditions(block, condition) except Exception: self._blocks.clear() - while self._scrollLayout.count(): - item = self._scrollLayout.takeAt(0) + while self.scrollLayout.count(): + item = self.scrollLayout.takeAt(0) if item.widget(): item.widget().deleteLater() self._updateBlockTypeRestrictions() @@ -255,7 +255,7 @@ class ALAutoScriptOrchDialog(QDialog): allLogics.append(".AND.") allSubConds.append(ap) for row in list(block._conditionRows): - block._condRowsLayout.removeWidget(row) + block.condRowsLayout.removeWidget(row) row.hide() row.deleteLater() block._conditionRows.clear() @@ -278,7 +278,7 @@ class ALAutoScriptOrchDialog(QDialog): row.logicCombo.setCurrentIndex(li) break block._conditionRows.append(row) - block._condRowsLayout.addWidget(row) + block.condRowsLayout.addWidget(row) subUp = subCond.upper() if subUp in (".TRUE.", ".FALSE."): row.loadFromParts(subUp, "", "") diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index cd8261f..220c7e8 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -3,7 +3,11 @@ Helper utilities and constants for the AutoScript orchestration dialog. """ import re -from PySide6.QtCore import QObject, QDate, QTime +from PySide6.QtCore import ( + QObject, + QDate, + QTime +) from PySide6.QtWidgets import ( QComboBox, QDateEdit, @@ -18,7 +22,14 @@ from PySide6.QtWidgets import ( QWidget, ) -from autoscript import ALL_VARIABLES +from autoscript import ( + ALL_VARIABLES, + splitTopLevel +) +from autoscript.ASOperator import ( + ARITH_TYPES, + COMPARISON_OPERATORS +) VAR_TYPE_ORDER = [ @@ -40,14 +51,19 @@ PRESET_VARIABLES = [ PRESET_NAMES = { p["name"] for p in PRESET_VARIABLES } -COMPARE_OPERATORS = sorted([ - ("等于", ".EQ."), - ("不等于", ".NEQ."), - ("大于", ".BGT."), - ("小于", ".BLT."), - ("大于等于", ".BGE."), - ("小于等于", ".BLE."), -], key=lambda x: len(x[1]), reverse=True) +# Operator display names (UI-specific), symbols derived from engine +_COMPARE_DISPLAY_MAP = { + ".EQ.": "等于", + ".NEQ.": "不等于", + ".BGT.": "大于", + ".BLT.": "小于", + ".BGE.": "大于等于", + ".BLE.": "小于等于", +} +COMPARE_OPERATORS = sorted( + [(name, op) for op, name in _COMPARE_DISPLAY_MAP.items() if op in COMPARISON_OPERATORS], + key=lambda x: len(x[1]), reverse=True +) LOGIC_OPERATORS = [ ("并且 (.AND.)", ".AND."), ("或者 (.OR.)", ".OR."), @@ -57,12 +73,6 @@ ACTION_TYPES = [ ("增加", "add"), ("减少", "sub"), ] -ARITH_TYPES = { - "Date", - "Time", - "Int", - "Float" -} DATE_RELATIVE_OPTIONS = [ ("前天", "day_before_yesterday"), ("昨天", "yesterday"), @@ -310,21 +320,17 @@ class _DateInputContainer(QWidget): ): s = expr.strip().upper() - if s == "CURRENT_DATE - 2": + _RELATIVE_MAP = { + "CURRENT_DATE": 0, "TODAY": 0, + "CURRENT_DATE + 1": 1, "TOMORROW": 1, + "CURRENT_DATE + 2": 2, + "CURRENT_DATE - 1": 3, + "CURRENT_DATE - 2": 4, + } + idx = _RELATIVE_MAP.get(s) + if idx is not None: self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(4) - elif s == "CURRENT_DATE - 1": - self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(3) - elif s in ("CURRENT_DATE", "TODAY"): - self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(0) - elif s == "CURRENT_DATE + 1" or s == "TOMORROW": - self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(1) - elif s == "CURRENT_DATE + 2": - self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(2) + self._relCombo.setCurrentIndex(idx) elif s.startswith("DATE("): self._modeCombo.setCurrentIndex(1) m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) @@ -585,38 +591,6 @@ def encodeValueStr( return raw_value -def splitTopLevel( - text: str, - delimiter: str -) -> list: - - parts = [] - depth = 0 - buf = "" - i = 0 - textUpper = text.upper() - delimUpper = delimiter.upper() - dlen = len(delimUpper) - while i < len(text): - if text[i] == "(": - depth += 1 - buf += text[i] - elif text[i] == ")": - depth -= 1 - buf += text[i] - elif depth == 0 and textUpper[i:i + dlen] == delimUpper: - parts.append(buf) - buf = "" - i += dlen - continue - else: - buf += text[i] - i += 1 - if buf.strip(): - parts.append(buf) - return parts - - def stripOuterParens( s: str ) -> str: diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index d0fced6..800e07a 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -80,21 +80,21 @@ class ConditionRowFrame(QFrame): ("变量", "variable"), ], min_width=70, parent=self) layout.addWidget(self._compTypeCombo) - self._rhsStack = QStackedWidget(self) - self._rhsStack.setFixedHeight(25) - self._literalStack = QStackedWidget(self) - self._literalStack.setFixedHeight(25) - self._literalWidgets = {} + self.rhsStack = QStackedWidget(self) + self.rhsStack.setFixedHeight(25) + self.literalStack = QStackedWidget(self) + self.literalStack.setFixedHeight(25) + self.literalWidgets = {} for vt in VAR_TYPE_ORDER: - w = makeValueWidget(vt, self._literalStack) - self._literalWidgets[vt] = w - self._literalStack.addWidget(w) - self._literalStack.setCurrentWidget(self._literalWidgets.get("String")) - self._rhsStack.addWidget(self._literalStack) + w = makeValueWidget(vt, self.literalStack) + self.literalWidgets[vt] = w + self.literalStack.addWidget(w) + self.literalStack.setCurrentWidget(self.literalWidgets.get("String")) + self.rhsStack.addWidget(self.literalStack) self.rhsVarCombo = makeVarRefCombo(self) - self._rhsStack.addWidget(self.rhsVarCombo) - self._rhsStack.setCurrentIndex(0) - layout.addWidget(self._rhsStack) + self.rhsStack.addWidget(self.rhsVarCombo) + self.rhsStack.setCurrentIndex(0) + layout.addWidget(self.rhsStack) if not self._isFirst: self.deleteBtn = QPushButton("×", self) self.deleteBtn.setFixedSize(25, 25) @@ -127,7 +127,6 @@ class ConditionRowFrame(QFrame): self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) - @Slot(int) def onLeftVarChanged( self, @@ -148,10 +147,9 @@ class ConditionRowFrame(QFrame): vartype: str ): - if vartype not in self._literalWidgets: + if vartype not in self.literalWidgets: vartype = "String" - self._literalStack.setCurrentWidget(self._literalWidgets[vartype]) - + self.literalStack.setCurrentWidget(self.literalWidgets[vartype]) @Slot(int) def onCompTypeChanged( @@ -160,7 +158,7 @@ class ConditionRowFrame(QFrame): ): isVar = (self._compTypeCombo.currentData() == "variable") - self._rhsStack.setCurrentIndex(1 if isVar else 0) + self.rhsStack.setCurrentIndex(1 if isVar else 0) if isVar: self.populateRhsVarCombo() @@ -191,7 +189,7 @@ class ConditionRowFrame(QFrame): if rhsText: return f"{name} {opSym} {rhsText}" return "" - w = self._literalWidgets.get(vartype) + w = self.literalWidgets.get(vartype) if w: rawVal = getValueFromWidget(w) encoded = encodeValueStr(rawVal, vartype) @@ -232,7 +230,7 @@ class ConditionRowFrame(QFrame): self.rhsVarCombo.setCurrentIndex(self.rhsVarCombo.count() - 1) else: self._compTypeCombo.setCurrentIndex(0) - w = self._literalWidgets.get(vartype) + w = self.literalWidgets.get(vartype) if w: setWidgetValue(w, vartype, valueExpr) @@ -291,15 +289,15 @@ class ActionStepFrame(QFrame): self.buildTargetCombo() layout.addWidget(self.targetCombo) layout.addWidget(makeLabel("为", self)) - self._valueSrcCombo = makeComboWidget([ + self.valueSrcCombo = makeComboWidget([ ("特定值", "literal"), ("变量", "variable"), ], min_width=70, parent=self) - layout.addWidget(self._valueSrcCombo) - self._valueStack = QStackedWidget(self) - self._valueStack.setFixedHeight(25) + layout.addWidget(self.valueSrcCombo) + self.valueStack = QStackedWidget(self) + self.valueStack.setFixedHeight(25) self.initValueStacks() - layout.addWidget(self._valueStack) + layout.addWidget(self.valueStack) self.existingVarCombo = makeVarRefCombo(self) self.existingVarCombo.setVisible(False) layout.addWidget(self.existingVarCombo) @@ -335,16 +333,16 @@ class ActionStepFrame(QFrame): self._literalWidgets = {} self._offsetWidgets = {} for vt in VAR_TYPE_ORDER: - self._literalWidgets[vt] = makeValueWidget(vt, self._valueStack) - self._valueStack.addWidget(self._literalWidgets[vt]) + self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack) + self.valueStack.addWidget(self._literalWidgets[vt]) if vt in ARITH_TYPES: - self._offsetWidgets[vt] = makeOffsetWidget(vt, self._valueStack) - self._valueStack.addWidget(self._offsetWidgets[vt]) + self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack) + self.valueStack.addWidget(self._offsetWidgets[vt]) else: - lbl = QLabel("(不支持该操作)", self._valueStack) + lbl = QLabel("(不支持该操作)", self.valueStack) lbl.setFixedHeight(25) self._offsetWidgets[vt] = lbl - self._valueStack.addWidget(lbl) + self.valueStack.addWidget(lbl) def connectSignals( @@ -353,7 +351,7 @@ class ActionStepFrame(QFrame): self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged) self.targetCombo.currentIndexChanged.connect(self.onTargetChanged) - self._valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) + self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) @Slot(int) def onTargetChanged( @@ -369,7 +367,7 @@ class ActionStepFrame(QFrame): _, vartype = data self._currentTargetType = vartype self.updateRHSWidget() - self.onValueSrcChanged(self._valueSrcCombo.currentIndex()) + self.onValueSrcChanged(self.valueSrcCombo.currentIndex()) @Slot(int) def onOpTypeChanged( @@ -388,11 +386,11 @@ class ActionStepFrame(QFrame): isArith = (op in ("add", "sub")) actualType = self._currentTargetType if isArith and actualType in self._offsetWidgets: - self._valueStack.setCurrentWidget(self._offsetWidgets[actualType]) + self.valueStack.setCurrentWidget(self._offsetWidgets[actualType]) elif actualType in self._literalWidgets: - self._valueStack.setCurrentWidget(self._literalWidgets[actualType]) + self.valueStack.setCurrentWidget(self._literalWidgets[actualType]) else: - self._valueStack.setCurrentWidget(self._literalWidgets.get("String")) + self.valueStack.setCurrentWidget(self._literalWidgets.get("String")) @Slot(int) def onValueSrcChanged( @@ -400,8 +398,8 @@ class ActionStepFrame(QFrame): idx ): - isVar = (self._valueSrcCombo.currentData() == "variable") - self._valueStack.setVisible(not isVar) + isVar = (self.valueSrcCombo.currentData() == "variable") + self.valueStack.setVisible(not isVar) self.existingVarCombo.setVisible(isVar) if isVar: self._varMgr.populateCombo(self.existingVarCombo) @@ -437,20 +435,20 @@ class ActionStepFrame(QFrame): return f" SET {target} = {encoded}" elif op == "add": vartype = self._currentTargetType - if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"): - days = self._valueStack.currentWidget().getOffsetDays() + if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): + days = self.valueStack.currentWidget().getOffsetDays() return f" {target} .ADD. {days}" - if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"): - hours = self._valueStack.currentWidget().getOffsetHours() + if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): + hours = self.valueStack.currentWidget().getOffsetHours() return f" {target} .ADD. {hours}" return f" {target} .ADD. {rawVal}" elif op == "sub": vartype = self._currentTargetType - if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"): - days = self._valueStack.currentWidget().getOffsetDays() + if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): + days = self.valueStack.currentWidget().getOffsetDays() return f" {target} .SUB. {days}" - if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"): - hours = self._valueStack.currentWidget().getOffsetHours() + if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): + hours = self.valueStack.currentWidget().getOffsetHours() return f" {target} .SUB. {hours}" return f" {target} .SUB. {rawVal}" return "" @@ -460,9 +458,9 @@ class ActionStepFrame(QFrame): self ) -> str: - if self._valueSrcCombo.currentData() == "variable": + if self.valueSrcCombo.currentData() == "variable": return self.existingVarCombo.currentText().strip() - w = self._valueStack.currentWidget() + w = self.valueStack.currentWidget() if w: return getValueFromWidget(w) return "" @@ -504,7 +502,7 @@ class ActionStepFrame(QFrame): return up = s.upper() if isVarReference(s): - self._valueSrcCombo.setCurrentIndex(1) + self.valueSrcCombo.setCurrentIndex(1) self._varMgr.populateCombo(self.existingVarCombo) idx = self._varMgr.findExactNameEntry(self.existingVarCombo, up) if idx >= 0: @@ -513,8 +511,8 @@ class ActionStepFrame(QFrame): self.existingVarCombo.addItem(up, (up, "String")) self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) else: - self._valueSrcCombo.setCurrentIndex(0) - w = self._valueStack.currentWidget() + self.valueSrcCombo.setCurrentIndex(0) + w = self.valueStack.currentWidget() if w: setWidgetValue(w, self._currentTargetType, expr) From e800f6ece128c00a219db4bd93ad84f5e22228d4 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 16:01:22 +0800 Subject: [PATCH 10/49] =?UTF-8?q?refactor(gui):=20=E7=BB=9F=E4=B8=80=20set?= =?UTF-8?q?upUi=20=E5=91=BD=E5=90=8D=E5=B9=B6=E8=B0=83=E6=95=B4=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAutoScriptEditDialog.py | 108 +++++++++++++--------------- src/gui/ALSeatFrame.py | 2 + src/gui/ALTimerTaskHistoryDialog.py | 5 +- 3 files changed, 54 insertions(+), 61 deletions(-) diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 2cbfdef..35b1462 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -110,16 +110,15 @@ class ALAutoScriptEditDialog(QDialog): super().__init__(parent) self._fontSize = 19 - self.modifyUi() + self.setupUi() self.connectSignals() - - self._textEdit.setPlainText(script) + self.textEdit.setPlainText(script) self._highlighter = ALScriptHighlighter( - self._textEdit.document() + self.textEdit.document() ) - def modifyUi( + def setupUi( self ): @@ -129,58 +128,53 @@ class ALAutoScriptEditDialog(QDialog): layout.setSpacing(4) layout.setContentsMargins(4, 4, 4, 4) toolbarLayout = QHBoxLayout() - self._zoomInBtn = QPushButton("+") - self._zoomInBtn.setFixedSize(25, 25) - self._zoomOutBtn = QPushButton("-") - self._zoomOutBtn.setFixedSize(25, 25) - self._zoomResetBtn = QPushButton( + self.zoomInBtn = QPushButton("+") + self.zoomInBtn.setFixedSize(25, 25) + self.zoomOutBtn = QPushButton("-") + self.zoomOutBtn.setFixedSize(25, 25) + self.zoomResetBtn = QPushButton( QApplication.style().standardIcon( QStyle.StandardPixmap.SP_BrowserReload ), "" ) - self._zoomResetBtn.setFixedSize(25, 25) - self._zoomResetBtn.setToolTip("重置缩放") - self._zoomLabel = QLabel(f"{self._fontSize}px") - self._zoomLabel.setFixedHeight(25) - toolbarLayout.addWidget(self._zoomInBtn) - toolbarLayout.addWidget(self._zoomOutBtn) - toolbarLayout.addWidget(self._zoomResetBtn) - toolbarLayout.addWidget(self._zoomLabel) + self.zoomResetBtn.setFixedSize(25, 25) + self.zoomResetBtn.setToolTip("重置缩放") + self.zoomLabel = QLabel(f"{self._fontSize}px") + self.zoomLabel.setFixedHeight(25) + toolbarLayout.addWidget(self.zoomInBtn) + toolbarLayout.addWidget(self.zoomOutBtn) + toolbarLayout.addWidget(self.zoomResetBtn) + toolbarLayout.addWidget(self.zoomLabel) toolbarLayout.addStretch() - self._copyBtn = QPushButton( + self.copyBtn = QPushButton( QApplication.style().standardIcon( QStyle.StandardPixmap.SP_FileDialogDetailedView ), "" ) - self._copyBtn.setFixedSize(25, 25) - self._copyBtn.setToolTip("复制脚本") - toolbarLayout.addWidget(self._copyBtn) + self.copyBtn.setFixedSize(25, 25) + self.copyBtn.setToolTip("复制脚本") + toolbarLayout.addWidget(self.copyBtn) layout.addLayout(toolbarLayout) - self._textEdit = QPlainTextEdit(self) - self._textEdit.setLineWrapMode( + self.textEdit = QPlainTextEdit(self) + self.textEdit.setLineWrapMode( QPlainTextEdit.LineWrapMode.NoWrap ) - self._textEdit.setStyleSheet( + self.textEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" f" font-size: {self._fontSize}px;" "}" ) - layout.addWidget(self._textEdit) - + layout.addWidget(self.textEdit) self._createButtonPanel(layout) - - self._btnBox = QDialogButtonBox( + self.btnBox = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) - self._btnBox.button( - QDialogButtonBox.StandardButton.Ok - ).setText("保存") - self._btnBox.button( - QDialogButtonBox.StandardButton.Cancel - ).setText("取消") - layout.addWidget(self._btnBox) + self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") + self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") + layout.addWidget(self.btnBox) + def _createButtonPanel( self, @@ -271,6 +265,7 @@ class ALAutoScriptEditDialog(QDialog): tab_widget.addTab(var_widget, "变量") parent_layout.addWidget(tab_widget) + def _addButtonsToGrid( self, grid_layout, @@ -308,43 +303,43 @@ class ALAutoScriptEditDialog(QDialog): template = btn.property("template") if not template: return - cursor = self._textEdit.textCursor() + cursor = self.textEdit.textCursor() cursor.insertText(template) + def connectSignals( self ): - self._btnBox.accepted.connect(self.accept) - self._btnBox.rejected.connect(self.reject) - self._zoomInBtn.clicked.connect(self.onZoomIn) - self._zoomOutBtn.clicked.connect(self.onZoomOut) - self._zoomResetBtn.clicked.connect(self.onZoomReset) - self._copyBtn.clicked.connect(self.onCopy) + self.btnBox.accepted.connect(self.accept) + self.btnBox.rejected.connect(self.reject) + self.zoomInBtn.clicked.connect(self.onZoomIn) + self.zoomOutBtn.clicked.connect(self.onZoomOut) + self.zoomResetBtn.clicked.connect(self.onZoomReset) + self.copyBtn.clicked.connect(self.onCopy) def getScript( self ) -> str: - return self._textEdit.toPlainText() + return self.textEdit.toPlainText() def updateFontSize( self ): - font = self._textEdit.font() + font = self.textEdit.font() font.setPointSize(self._fontSize) - self._textEdit.setFont(font) - self._textEdit.setStyleSheet( + self.textEdit.setFont(font) + self.textEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" f" font-size: {self._fontSize}px;" "}" ) - self._zoomLabel.setText(f"{self._fontSize}px") - + self.zoomLabel.setText(f"{self._fontSize}px") @Slot() def onZoomIn( @@ -354,7 +349,6 @@ class ALAutoScriptEditDialog(QDialog): self._fontSize = min(self._fontSize + 2, 40) self.updateFontSize() - @Slot() def onZoomOut( self @@ -363,7 +357,6 @@ class ALAutoScriptEditDialog(QDialog): self._fontSize = max(self._fontSize - 2, 8) self.updateFontSize() - @Slot() def onZoomReset( self @@ -372,19 +365,18 @@ class ALAutoScriptEditDialog(QDialog): self._fontSize = 13 self.updateFontSize() - @Slot() def onCopy( self ): clipboard = QApplication.clipboard() - clipboard.setText(self._textEdit.toPlainText()) - original = self._copyBtn.text() - self._copyBtn.setText("已复制") - self._copyBtn.setEnabled(False) + clipboard.setText(self.textEdit.toPlainText()) + original = self.copyBtn.text() + self.copyBtn.setText("已复制") + self.copyBtn.setEnabled(False) from PySide6.QtCore import QTimer QTimer.singleShot(2000, lambda: ( - self._copyBtn.setText(original), - self._copyBtn.setEnabled(True) + self.copyBtn.setText(original), + self.copyBtn.setEnabled(True) )) diff --git a/src/gui/ALSeatFrame.py b/src/gui/ALSeatFrame.py index 8524e00..331862c 100644 --- a/src/gui/ALSeatFrame.py +++ b/src/gui/ALSeatFrame.py @@ -31,6 +31,7 @@ class ALSeatFrame(QFrame): self.setupUi() + def setupUi( self ): @@ -54,6 +55,7 @@ class ALSeatFrame(QFrame): self.Label.setAlignment(Qt.AlignCenter) self.Label.setGeometry(0, 0, 60, 40) + def mousePressEvent( self, event diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py index 8500c96..e656c47 100644 --- a/src/gui/ALTimerTaskHistoryDialog.py +++ b/src/gui/ALTimerTaskHistoryDialog.py @@ -28,15 +28,14 @@ class ALTimerTaskHistoryDialog(QDialog): ): super().__init__(parent) - self.__task_data = task_data self.__history = task_data.get("repeat_history", []) - self.modifyUi() + self.setupUi() self.connectSignals() - def modifyUi( + def setupUi( self ): From 87787ad3dc4c046ea1c854abb3177e38abd9fe71 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 17:59:00 +0800 Subject: [PATCH 11/49] =?UTF-8?q?style(gui):=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E9=AB=98=E4=BA=AE=E9=85=8D=E8=89=B2=E6=9B=B4=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20VSCode=20C=20=E9=A3=8E=E6=A0=BC=E5=B9=B6=E4=B8=BA=E5=B8=83?= =?UTF-8?q?=E5=B0=94=E5=AD=97=E9=9D=A2=E9=87=8F=E7=8B=AC=E7=AB=8B=E9=85=8D?= =?UTF-8?q?=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAutoScriptEditDialog.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 35b1462..7d63b40 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -44,44 +44,44 @@ class ALScriptHighlighter(QSyntaxHighlighter): self._rules = [] keywordFmt = QTextCharFormat() - keywordFmt.setForeground(QColor("#007ACC")) + keywordFmt.setForeground(QColor("#569CD6")) keywordFmt.setFontWeight(QFont.Weight.Bold) for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF", "SET", "PASS", "THEN"]: pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b" self._rules.append((pattern, keywordFmt)) opFmt = QTextCharFormat() - opFmt.setForeground(QColor("#AF00DB")) + opFmt.setForeground(QColor("#C586C0")) opFmt.setFontWeight(QFont.Weight.Normal) for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.", r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\.", r"\.AND\.", r"\.OR\."]: self._rules.append((op, opFmt)) - literalFmt = QTextCharFormat() - literalFmt.setForeground(QColor("#AF00DB")) - literalFmt.setFontWeight(QFont.Weight.Bold) - for lit in [".TRUE.", ".FALSE."]: - self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt)) + boolFmt = QTextCharFormat() + boolFmt.setForeground(QColor("#4FC1FF")) + boolFmt.setFontWeight(QFont.Weight.Bold) + self._rules.append((r"\.TRUE\.", boolFmt)) + self._rules.append((r"\.FALSE\.", boolFmt)) funcFmt = QTextCharFormat() - funcFmt.setForeground(QColor("#795E26")) + funcFmt.setForeground(QColor("#DCDCAA")) funcFmt.setFontWeight(QFont.Weight.Normal) self._rules.append((r"\b(?:DATE|TIME)\b", funcFmt)) varFmt = QTextCharFormat() - varFmt.setForeground(QColor("#267F99")) + varFmt.setForeground(QColor("#9CDCFE")) varFmt.setFontWeight(QFont.Weight.Normal) var_names = [name for _, (name, _) in ALL_VARIABLES.items()] for var in var_names: self._rules.append((r"\b" + var + r"\b", varFmt)) strFmt = QTextCharFormat() - strFmt.setForeground(QColor("#A31515")) + strFmt.setForeground(QColor("#CE9178")) strFmt.setFontWeight(QFont.Weight.Normal) self._rules.append((r"'[^']*'", strFmt)) numFmt = QTextCharFormat() - numFmt.setForeground(QColor("#098658")) + numFmt.setForeground(QColor("#B5CEA8")) numFmt.setFontWeight(QFont.Weight.Normal) self._rules.append((r"\b\d+\b", numFmt)) commentFmt = QTextCharFormat() - commentFmt.setForeground(QColor("#008000")) + commentFmt.setForeground(QColor("#6A9955")) commentFmt.setFontItalic(True) self._rules.append((r"//[^\n]*", commentFmt)) From b8c0a29c596c31683d69e700c137290fb1c74e11 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 17:59:04 +0800 Subject: [PATCH 12/49] =?UTF-8?q?fix(gui):=20=E8=B0=83=E6=95=B4=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=AF=B9=E8=AF=9D=E6=A1=86=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E8=BE=B9=E8=B7=9D=E4=B8=8E=E9=97=B4=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALTimerTaskAddDialog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 21d13fa..160453e 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -58,6 +58,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.TimerTypeComboBox.setCurrentIndex(0) self.SpecificTimerWidget = QWidget() self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget) + self.SpecificTimerLayout.setContentsMargins(0, 0, 0, 0) + self.SpecificTimerLayout.setSpacing(5) self.SpecificTimerLayout.addWidget(QLabel("定时时间:")) self.SpecificDateTimeEdit = QDateTimeEdit() self.SpecificDateTimeEdit.setCalendarPopup(True) @@ -69,6 +71,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.RelativeTimerWidget = QWidget() self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget) + self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0) + self.RelativeTimerLayout.setSpacing(5) self.RelativeTimerLayout.addWidget(QLabel("相对时间:")) self.RelativeDaySpinBox = QSpinBox() self.RelativeDaySpinBox.setMinimum(0) @@ -325,5 +329,3 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): QDesktopServices.openUrl( QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript") ) - - From 23467c1d3daa6f1aa637bdc4aa96d3033cceebd0 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 20:13:46 +0800 Subject: [PATCH 13/49] =?UTF-8?q?feat(autoscript):=20=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?//=20=E8=A1=8C=E5=86=85=E6=B3=A8=E9=87=8A=E4=B8=8E=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=B3=A8=E9=87=8A=E8=A1=8C=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASTokenizer.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/autoscript/ASTokenizer.py b/src/autoscript/ASTokenizer.py index bdd2809..427afad 100644 --- a/src/autoscript/ASTokenizer.py +++ b/src/autoscript/ASTokenizer.py @@ -381,6 +381,20 @@ class ASTokenizer: stmt.op_type = OP_SUB return stmt + @classmethod + def _stripComment( + cls, + line: str + ) -> str: + + in_single = False + for i, ch in enumerate(line): + if ch == "'": + in_single = not in_single + elif ch == "/" and i + 1 < len(line) and line[i + 1] == "/" and not in_single: + return line[:i].rstrip() + return line + @classmethod def _tokenizeImpl( cls, @@ -389,11 +403,11 @@ class ASTokenizer: statements = [] for raw_line in script.split("\n"): - stripped = raw_line.strip() - if not stripped: + code = cls._stripComment(raw_line.strip()) + if not code: continue - kind, data = cls._matchLine(stripped) - statements.append(cls._buildStmt(stripped, kind, data)) + kind, data = cls._matchLine(code) + statements.append(cls._buildStmt(code, kind, data)) return statements @classmethod @@ -476,12 +490,12 @@ class ASTokenizer: cls._notifyObservers(observers, "onParseStart", script) statements = [] for i, raw_line in enumerate(script.split("\n"), 1): - stripped = raw_line.strip() - if not stripped: + code = cls._stripComment(raw_line.strip()) + if not code: continue - kind, data = cls._matchLine(stripped) - cls._notifyObservers(observers, "onTokenParsed", kind, data, i, stripped) - statements.append(cls._buildStmt(stripped, kind, data)) + kind, data = cls._matchLine(code) + cls._notifyObservers(observers, "onTokenParsed", kind, data, i, code) + statements.append(cls._buildStmt(code, kind, data)) cls._notifyObservers(observers, "onParseComplete", statements) return statements From 5800437ba217bd2323527b7d6692c5ca04153da2 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 20:43:48 +0800 Subject: [PATCH 14/49] =?UTF-8?q?fix(gui):=20=E7=BC=96=E6=8E=92=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20END=20IF=20=E7=BB=93=E6=9D=9F=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAutoScriptOrchDialog/_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index c5fedad..3d3d751 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -153,12 +153,12 @@ class ALAutoScriptOrchDialog(QDialog): for block in self._blocks: blockType = block.getBlockType() if blockType == "IF" and prevType is not None: - parts.append("ENDIF") + parts.append("END IF") lines = block.toScriptLines() parts.extend(lines) prevType = blockType if self._blocks and self._blocks[0].getBlockType() == "IF": - parts.append("ENDIF") + parts.append("END IF") return "\n".join(parts) @Slot() From 4642916fd5f94483f5c366968dd542a698d701e2 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 18 May 2026 20:47:35 +0800 Subject: [PATCH 15/49] =?UTF-8?q?fix(gui):=20=E4=BF=AE=E6=AD=A3=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E7=AA=97=E5=8F=A3=E6=97=A5=E6=9C=9F=E6=98=A0=E5=B0=84?= =?UTF-8?q?=20CURRENT=5FDATE=20=E8=AF=AF=E8=AF=86=E5=88=AB=E4=B8=BA?= =?UTF-8?q?=E5=89=8D=E5=A4=A9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAutoScriptOrchDialog/_helpers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index 220c7e8..b4ae601 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -321,11 +321,11 @@ class _DateInputContainer(QWidget): s = expr.strip().upper() _RELATIVE_MAP = { - "CURRENT_DATE": 0, "TODAY": 0, - "CURRENT_DATE + 1": 1, "TOMORROW": 1, - "CURRENT_DATE + 2": 2, - "CURRENT_DATE - 1": 3, - "CURRENT_DATE - 2": 4, + "CURRENT_DATE": 2, "TODAY": 2, + "CURRENT_DATE + 1": 3, "TOMORROW": 3, + "CURRENT_DATE + 2": 4, + "CURRENT_DATE - 1": 1, + "CURRENT_DATE - 2": 0, } idx = _RELATIVE_MAP.get(s) if idx is not None: From 1d4b03d162fe9d2cc1f3443813f45c8ab256761b Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 00:27:43 +0800 Subject: [PATCH 16/49] =?UTF-8?q?feat(autoscript):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=AE=97=E6=9C=AF=E8=A1=A8=E8=BE=BE=E5=BC=8F=E4=B8=8E=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=8F=82=E4=B8=8E=E5=8A=A0=E5=87=8F=E8=BF=90=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/autoscript/ASEngine.py | 73 ++++++++++++++++++++++++++++------- src/autoscript/ASOperator.py | 2 +- src/autoscript/ASTokenizer.py | 19 ++++++--- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index 352f4a8..1781f13 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -65,8 +65,6 @@ def _errPos( # Pre-compiled regex patterns for value resolution _RE_TIME = re.compile(r"^TIME\((\d{1,2}):(\d{2})\)$", re.IGNORECASE) _RE_DATE = re.compile(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", re.IGNORECASE) -_RE_CUR_DATE_OFFSET = re.compile(r"^CURRENT_DATE\s*\+\s*(\d+)$", re.IGNORECASE) -_RE_CUR_TIME_OFFSET = re.compile(r"^CURRENT_TIME\s*\+\s*(\d+)$", re.IGNORECASE) def splitTopLevel( @@ -169,7 +167,7 @@ def _resolveValue( - DATE(yyyy-mm-dd) - .TRUE. / .FALSE. - Single/double quoted strings (with escaped single quotes) - - CURRENT_DATE + N / CURRENT_TIME + N (relative offsets) + - Arithmetic expressions: operand (+|-) operand (Date ± Int, Int ± Int, etc.) - Numeric literals (int / float) - Field references (resolved via _resolveFieldObj) @@ -197,14 +195,6 @@ def _resolveValue( return s[1:-1].replace("''", "'") if s.startswith('"') and s.endswith('"'): return s[1:-1] - m = _RE_CUR_DATE_OFFSET.match(s) - if m: - days = int(m.group(1)) - return datetime.now().date() + timedelta(days=days) - m = _RE_CUR_TIME_OFFSET.match(s) - if m: - hours = int(m.group(1)) - return (datetime.now() + timedelta(hours=hours)).time() try: return int(s) except ValueError: @@ -213,6 +203,9 @@ def _resolveValue( return float(s) except ValueError: pass + arith_result = _resolveArithExpr(s, target_data) + if arith_result is not None: + return arith_result obj = _resolveFieldObj(s) if obj: return obj.getValue(target_data) @@ -250,6 +243,55 @@ def _resolveAsObject( return ASObject._makeTemp(value, inferred) +def _resolveArithExpr( + expr: str, + target_data: dict, + line: int = 0 +): + """ + Try to evaluate expr as a two-operand arithmetic expression: left (+|-) right. + + Each operand is resolved via _resolveAsObject, reusing the full literal / + field / script-variable resolution stack. The left operand's value is + copied into a temporary ASObject and ASOperator.apply() performs the + type-safe calculation on the copy, so the original variable is never + mutated. + + Returns the computed Python value, or None if expr is not a recognised + arithmetic pattern. + """ + + s = expr.strip() + m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) + if not m: + # Fallback for no-space expressions like RESERVE_DATE+1 + # (e.g. when extracted from IF(RESERVE_DATE.EQ.CURRENT_DATE+1)). + # Left operand must be an identifier (letter/underscore start) to + # avoid false-matching date strings like 2026-05-20. + m = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s) + if not m: + return None + left_expr = m.group(1).strip() + op_symbol = m.group(2).strip() + right_expr = m.group(3).strip() + if " + " in left_expr or " - " in left_expr: + return None + if " + " in right_expr or " - " in right_expr: + return None + left_obj = _resolveAsObject(left_expr, target_data) + right_obj = _resolveAsObject(right_expr, target_data) + op = ".ADD." if op_symbol == "+" else ".SUB." + left_val = left_obj.getValue(target_data) + result_type = left_obj.var_type + if left_obj.var_type == "Int" and right_obj.var_type == "Float": + result_type = "Float" + elif left_obj.var_type == "Float" and right_obj.var_type == "Int": + result_type = "Float" + temp = ASObject._makeTemp(left_val, result_type) + ASOperator.apply(temp, right_obj, op, target_data) + return temp.getValue(target_data) + + def _evaluateCondition( condition_str: str, target_data: dict, @@ -349,7 +391,12 @@ def _executeSet( resolved = _resolveValue(value_str, target_data) stripped = value_str.strip() if resolved == "" and stripped not in ("''", '""') and len(stripped.split()) > 1: - raise ValueError(_errPos(line, f"SET 值中存在多余内容 '{stripped}'")) + try: + resolved = _resolveArithExpr(stripped, target_data, line) + except ValueError as e: + raise ValueError(_errPos(line, str(e))) + if resolved is None: + raise ValueError(_errPos(line, f"SET 值中存在多余内容 '{stripped}'")) upper_name = field_name.upper().strip() obj = _FIELD_MAP.get(upper_name) if not obj: @@ -567,7 +614,7 @@ class _EngineExecutor(NodeVisitor): paren_open = upper.find("(") if paren_open < 0: raise ValueError(_errPos(self._line, "ELSE IF 缺少左括号")) - _executeOperation(_node.raw_line, self._target_data, self._line) + raise ValueError(_errPos(self._line, f"无法识别的语法 '{_node.raw_line}'")) def execute( diff --git a/src/autoscript/ASOperator.py b/src/autoscript/ASOperator.py index 60d6195..9d233ba 100644 --- a/src/autoscript/ASOperator.py +++ b/src/autoscript/ASOperator.py @@ -115,7 +115,7 @@ class ASOperator: """Apply arithmetic per type.""" tp = target.var_type - raw_op = operand._value + raw_op = operand.getValue(target_data) if tp == "Date": if not isinstance(target_val, date): diff --git a/src/autoscript/ASTokenizer.py b/src/autoscript/ASTokenizer.py index 427afad..d82ddc7 100644 --- a/src/autoscript/ASTokenizer.py +++ b/src/autoscript/ASTokenizer.py @@ -46,8 +46,8 @@ _RE_ELSE_IF = re.compile(r"^ELSE\s+IF\((.+)\)(?:\s+THEN\s*)?$", re.IGNORECASE) _RE_ELSE = re.compile(r"^ELSE\s*$", re.IGNORECASE) _RE_ENDIF = re.compile(r"^(ENDIF|END IF)$", re.IGNORECASE) _RE_SET = re.compile(r"^SET\s+(\w+)\s*=\s*(.+)$", re.IGNORECASE) -_RE_ADD = re.compile(r"^(\w+)\s+\.ADD\.\s+(\d+)$", re.IGNORECASE) -_RE_SUB = re.compile(r"^(\w+)\s+\.SUB\.\s+(\d+)$", re.IGNORECASE) +_RE_ADD = re.compile(r"^(\w+)\s+\.ADD\.\s+(-?\d+(?:\.\d+)?|\w+)$", re.IGNORECASE) +_RE_SUB = re.compile(r"^(\w+)\s+\.SUB\.\s+(-?\d+(?:\.\d+)?|\w+)$", re.IGNORECASE) _RE_PASS = re.compile(r"^\s*PASS\s*$", re.IGNORECASE) @@ -388,11 +388,20 @@ class ASTokenizer: ) -> str: in_single = False - for i, ch in enumerate(line): - if ch == "'": + in_double = False + i = 0 + while i < len(line): + ch = line[i] + if ch == "'" and not in_double: + if i + 1 < len(line) and line[i + 1] == "'": + i += 2 + continue in_single = not in_single - elif ch == "/" and i + 1 < len(line) and line[i + 1] == "/" and not in_single: + elif ch == '"' and not in_single: + in_double = not in_double + elif ch == "/" and i + 1 < len(line) and line[i + 1] == "/" and not in_single and not in_double: return line[:i].rstrip() + i += 1 return line @classmethod From fe7453fe024efc8cd3cc574de9b9937b7a275f06 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 00:27:59 +0800 Subject: [PATCH 17/49] =?UTF-8?q?feat(gui):=20=E7=BC=96=E6=8E=92=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E6=94=AF=E6=8C=81=E7=AE=97=E6=9C=AF=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E5=BC=8F=E8=A7=A3=E6=9E=90=E4=B8=8E=E5=9B=9E=E6=98=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAutoScriptOrchDialog/_helpers.py | 18 ++ .../ALAutoScriptOrchDialog/_orchestrate.py | 6 +- src/gui/ALAutoScriptOrchDialog/_widgets.py | 155 ++++++++++++++++-- 3 files changed, 161 insertions(+), 18 deletions(-) diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index b4ae601..dc42004 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -560,6 +560,8 @@ def encodeValueStr( var_type: str ) -> str: + if isArithExpr(raw_value): + return raw_value if var_type == "Time": if raw_value.startswith("+") or raw_value.startswith("-"): return raw_value @@ -609,6 +611,20 @@ def stripOuterParens( return s +# Pre-compiled pattern for detecting arithmetic expressions like "A + B" / "C - D" +_RE_ARITH_EXPR = re.compile(r'^.+?\s+[+-]\s+.+$') + + +def isArithExpr( + expr: str +) -> bool: + """ + Return True if expr looks like a two-operand arithmetic expression (A ± B). + """ + + return bool(_RE_ARITH_EXPR.match(expr.strip())) + + def isVarReference( expr: str ) -> bool: @@ -623,6 +639,8 @@ def isVarReference( return False if re.match(r"^[+-]?\d", s): return False + if isArithExpr(s): + return False return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up)) diff --git a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py index b518d38..5bbe941 100644 --- a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py +++ b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py @@ -57,9 +57,11 @@ class ScriptOrchObserver(ParsingObserver): if kind == K_SET: self._current_actions.append((target, value, "set")) elif kind == K_ADD: - self._current_actions.append((target, f"+{value}", "add")) + prefixed = value if value.startswith("-") else f"+{value}" + self._current_actions.append((target, prefixed, "add")) else: - self._current_actions.append((target, f"-{value}", "sub")) + prefixed = value if value.startswith("-") else f"-{value}" + self._current_actions.append((target, prefixed, "sub")) elif kind == K_ENDIF: self._flushCurrentBlock() self._current_type = None diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index 800e07a..1d2fa3d 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -1,6 +1,8 @@ """ Widget components for the AutoScript orchestration dialog. """ +import re + from PySide6.QtCore import Slot from PySide6.QtWidgets import ( QComboBox, @@ -21,6 +23,7 @@ from gui.ALAutoScriptOrchDialog._helpers import ( VAR_TYPE_ORDER, encodeValueStr, getValueFromWidget, + isArithExpr, isVarReference, makeComboWidget, makeLabel, @@ -46,6 +49,7 @@ class ConditionRowFrame(QFrame): self._blockIndex = parentBlockIndex self._isFirst = isFirst self._isBoolMode = False + self._rawRhsExpr = "" self.setupUi() self.connectSignals() @@ -133,6 +137,7 @@ class ConditionRowFrame(QFrame): idx ): + self._rawRhsExpr = "" if idx < 0: return data = self.leftVarCombo.itemData(idx) @@ -157,6 +162,7 @@ class ConditionRowFrame(QFrame): idx ): + self._rawRhsExpr = "" isVar = (self._compTypeCombo.currentData() == "variable") self.rhsStack.setCurrentIndex(1 if isVar else 0) if isVar: @@ -179,6 +185,8 @@ class ConditionRowFrame(QFrame): return "" name, vartype = data opSym = self.opCombo.currentData() + if self._rawRhsExpr: + return f"{name} {opSym} {self._rawRhsExpr}" isVarRef = (self._compTypeCombo.currentData() == "variable") if isVarRef: rd = self.rhsVarCombo.currentData() @@ -204,21 +212,31 @@ class ConditionRowFrame(QFrame): valueExpr: str ): - for ci in range(self.leftVarCombo.count()): - d = self.leftVarCombo.itemData(ci) - if d and d[0] == operandName: - self.leftVarCombo.setCurrentIndex(ci) - break - if opSym: - for oi in range(self.opCombo.count()): - if self.opCombo.itemData(oi) == opSym: - self.opCombo.setCurrentIndex(oi) + self._rawRhsExpr = "" + self.leftVarCombo.blockSignals(True) + self.opCombo.blockSignals(True) + self._compTypeCombo.blockSignals(True) + try: + for ci in range(self.leftVarCombo.count()): + d = self.leftVarCombo.itemData(ci) + if d and d[0] == operandName: + self.leftVarCombo.setCurrentIndex(ci) break + if opSym: + for oi in range(self.opCombo.count()): + if self.opCombo.itemData(oi) == opSym: + self.opCombo.setCurrentIndex(oi) + break + finally: + self.leftVarCombo.blockSignals(False) + self.opCombo.blockSignals(False) + self._compTypeCombo.blockSignals(False) + data = self.leftVarCombo.currentData() + vartype = data[1] if data else "String" + self.updateRhsLiteralWidget(vartype) if not valueExpr: return up = valueExpr.strip().upper() - data = self.leftVarCombo.currentData() - vartype = data[1] if data else "String" if isVarReference(valueExpr) or self._isKnownVar(up): self._compTypeCombo.setCurrentIndex(1) self.populateRhsVarCombo() @@ -228,12 +246,45 @@ class ConditionRowFrame(QFrame): else: self.rhsVarCombo.addItem(up, (up, "String")) self.rhsVarCombo.setCurrentIndex(self.rhsVarCombo.count() - 1) + elif isArithExpr(valueExpr): + self._tryLoadCondArithExpr(valueExpr, vartype) else: self._compTypeCombo.setCurrentIndex(0) w = self.literalWidgets.get(vartype) if w: setWidgetValue(w, vartype, valueExpr) + def _tryLoadCondArithExpr( + self, + expr: str, + vartype: str + ): + """Try to decompose a condition RHS arithmetic expression into UI state.""" + + s = expr.strip() + m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) + if not m: + self._rawRhsExpr = s + return + left = m.group(1).strip() + op = m.group(2).strip() + right = m.group(3).strip() + left_up = left.upper() + + if vartype == "Date" and left_up == "CURRENT_DATE": + try: + n = int(right) + offset = n if op == "+" else -n + if offset in (-2, -1, 0, 1, 2): + self._compTypeCombo.setCurrentIndex(0) + w = self.literalWidgets.get("Date") + if w and hasattr(w, "setValue"): + w.setValue(s) + return + except ValueError: + pass + self._rawRhsExpr = s + def _isKnownVar( self, @@ -426,12 +477,9 @@ class ActionStepFrame(QFrame): rawVal = self._getValueRaw() if op == "set": vartype = self._currentTargetType + if isArithExpr(rawVal): + return f" SET {target} = {rawVal}" encoded = encodeValueStr(rawVal, vartype) - if vartype == "Time": - if rawVal.startswith("+"): - return f" {target} .ADD. {rawVal[1:]}" - if rawVal.startswith("-"): - return f" {target} .SUB. {rawVal[1:]}" return f" SET {target} = {encoded}" elif op == "add": vartype = self._currentTargetType @@ -500,6 +548,12 @@ class ActionStepFrame(QFrame): s = expr.strip() if not s: return + op = self.opTypeCombo.currentData() + if op in ("add", "sub") and s.startswith("-"): + s = s[1:] + self.opTypeCombo.setCurrentIndex( + 2 if op == "add" else 1 + ) up = s.upper() if isVarReference(s): self.valueSrcCombo.setCurrentIndex(1) @@ -510,12 +564,81 @@ class ActionStepFrame(QFrame): else: self.existingVarCombo.addItem(up, (up, "String")) self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) + elif isArithExpr(s): + self._tryLoadArithExpr(s) else: self.valueSrcCombo.setCurrentIndex(0) w = self.valueStack.currentWidget() if w: setWidgetValue(w, self._currentTargetType, expr) + def _tryLoadArithExpr( + self, + expr: str + ): + """Try to decompose an arithmetic expression into UI state.""" + + s = expr.strip() + m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) + if not m: + self._storeAsCustomExpr(s) + return + left = m.group(1).strip() + op = m.group(2).strip() + right = m.group(3).strip() + left_up = left.upper() + + # CURRENT_DATE ± N for Date targets — try relative combo for ±0/1/2, + # otherwise store as custom expression to preserve relative semantics + if self._currentTargetType == "Date" and left_up == "CURRENT_DATE": + try: + n = int(right) + offset = n if op == "+" else -n + if offset in (-2, -1, 0, 1, 2): + w = self._literalWidgets.get("Date") + if w and hasattr(w, "setValue"): + w.setValue(s) + self.valueSrcCombo.setCurrentIndex(0) + return + except ValueError: + pass + self._storeAsCustomExpr(s) + return + + # CURRENT_TIME ± N for Time targets — map to add/sub with offset + if self._currentTargetType == "Time" and left_up == "CURRENT_TIME": + try: + hours = int(right) + if op == "-": + hours = -hours + self.opTypeCombo.setCurrentIndex( + 1 if hours >= 0 else 2 + ) + self.valueSrcCombo.setCurrentIndex(0) + w = self._offsetWidgets.get("Time") + if w and hasattr(w, "setValue"): + w.setValue(str(abs(hours))) + return + except ValueError: + pass + + self._storeAsCustomExpr(s) + + def _storeAsCustomExpr( + self, + expr: str + ): + """Store a raw expression in the variable combo when it can't be decomposed.""" + + self.valueSrcCombo.setCurrentIndex(1) + self._varMgr.populateCombo(self.existingVarCombo) + found = self._varMgr.findExactNameEntry(self.existingVarCombo, expr) + if found < 0: + self.existingVarCombo.addItem(expr, (expr, self._currentTargetType)) + self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) + else: + self.existingVarCombo.setCurrentIndex(found) + def refreshVarCombos( self From e097b5afc9246a3de583f72c595e1959289e96f9 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 04:15:21 +0800 Subject: [PATCH 18/49] =?UTF-8?q?refactor(gui):=20=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=AE=80=E5=8C=96=E4=B8=BA=E7=BA=AF=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E8=A7=A3=E6=9E=90=E4=B8=8E=E9=A2=84=E6=A3=80?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAutoScriptOrchDialog/_dialog.py | 144 +--------- src/gui/ALAutoScriptOrchDialog/_helpers.py | 91 ++++++- .../ALAutoScriptOrchDialog/_orchestrate.py | 109 -------- src/gui/ALAutoScriptOrchDialog/_precheck.py | 163 ----------- src/gui/ALAutoScriptOrchDialog/_widgets.py | 252 ++---------------- 5 files changed, 116 insertions(+), 643 deletions(-) delete mode 100644 src/gui/ALAutoScriptOrchDialog/_orchestrate.py delete mode 100644 src/gui/ALAutoScriptOrchDialog/_precheck.py diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 3d3d751..8cecfed 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -13,27 +13,15 @@ from PySide6.QtWidgets import ( QWidget, ) -from gui.ALAutoScriptOrchDialog._precheck import precheck -from gui.ALAutoScriptOrchDialog._orchestrate import parseBlocks - -from gui.ALAutoScriptOrchDialog._helpers import ( - COMPARE_OPERATORS, - PRESET_NAMES, - VariableManager, - findOperatorIn, - splitTopLevel, - stripOuterParens, -) +from gui.ALAutoScriptOrchDialog._helpers import VariableManager from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock -from gui.ALAutoScriptOrchDialog._widgets import ConditionRowFrame class ALAutoScriptOrchDialog(QDialog): def __init__( self, - parent = None, - existingScript: str = "" + parent = None ): super().__init__(parent) @@ -42,10 +30,7 @@ class ALAutoScriptOrchDialog(QDialog): self.setupUi() self.connectSignals() - if existingScript and existingScript.strip(): - self.loadFromScript(existingScript) - else: - self.addBlock() + self.addBlock() self.scrollLayout.addStretch() @@ -171,126 +156,3 @@ class ALAutoScriptOrchDialog(QDialog): QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。") return self.accept() - - @staticmethod - def precheckScriptForOrchestration( - script: str - ) -> tuple[bool, str]: - - return precheck(script, allowed_vars=PRESET_NAMES) - - def loadFromScript( - self, - script: str - ): - - if not script.strip(): - self.addBlock() - return - ok, err = self.precheckScriptForOrchestration(script) - if not ok: - QMessageBox.warning( - self, "无法编排", - f"脚本检查失败:\n{err}\n\n" - "请通过\"编辑\"按钮打开脚本编辑窗口进行修改。" - ) - self.addBlock() - return - # Structured block data via observer-based parsing — no duplicate logic - typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2} - parsedBlocks = parseBlocks(script) - self._blocks.clear() - while self.scrollLayout.count(): - item = self.scrollLayout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - try: - for blockType, condition, actions in parsedBlocks: - self.addBlock() - block = self._blocks[-1] - idx = typeIdxMap.get(blockType, 0) - block.typeCombo.setCurrentIndex(idx) - block.onTypeChanged(idx) - for oldStep in list(block._actionWidgets): - block.removeActionStep(oldStep) - for target, valueExpr, opType in actions: - block.addActionStep() - step = block.getActionSteps()[-1] - step.setOpType(opType) - step.loadFromScript(target, valueExpr) - if blockType in ("IF", "ELSE IF") and condition: - self._parseConditions(block, condition) - except Exception: - self._blocks.clear() - while self.scrollLayout.count(): - item = self.scrollLayout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - self._updateBlockTypeRestrictions() - if not self._blocks: - self.addBlock() - - - def _parseConditions( - self, - block: ConditionalBlock, - condStr: str - ): - - s = condStr.strip() - if not s: - return - s = stripOuterParens(s) - orParts = splitTopLevel(s, ".OR.") - allSubConds = [] - allLogics = [] - for pi, part in enumerate(orParts): - part = part.strip() - if pi > 0: - allLogics.append(".OR.") - andParts = splitTopLevel(part, ".AND.") - for ai, ap in enumerate(andParts): - ap = ap.strip() - if ai > 0: - allLogics.append(".AND.") - allSubConds.append(ap) - for row in list(block._conditionRows): - block.condRowsLayout.removeWidget(row) - row.hide() - row.deleteLater() - block._conditionRows.clear() - for i, subCond in enumerate(allSubConds): - subCond = subCond.strip() - subCond = stripOuterParens(subCond) - isFirst = (i == 0) - row = ConditionRowFrame( - self._varMgr, block.blockIndex, - isFirst=isFirst, parent=block - ) - if not isFirst: - row.deleteBtn.clicked.connect( - lambda _checked=False, r=row: block.removeConditionRow(r) - ) - if i - 1 < len(allLogics): - logic = allLogics[i - 1] - for li in range(row.logicCombo.count()): - if row.logicCombo.itemData(li) == logic: - row.logicCombo.setCurrentIndex(li) - break - block._conditionRows.append(row) - block.condRowsLayout.addWidget(row) - subUp = subCond.upper() - if subUp in (".TRUE.", ".FALSE."): - row.loadFromParts(subUp, "", "") - else: - opSyms = [op for _, op in COMPARE_OPERATORS] - result = findOperatorIn(subCond, opSyms) - if result: - idx, op = result - leftPart = subCond[:idx].strip() - rightPart = subCond[idx + len(op):].strip() - row.loadFromParts(leftPart, op, rightPart) - else: - row.loadFromParts(subCond, "", "") - if not block._conditionRows: - block.addInitialConditionRow() diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index dc42004..4cdef03 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -271,6 +271,7 @@ class _DateInputContainer(QWidget): ): super().__init__(parent) + self._dynamicItems = {} # index -> raw expression, for one-way parsed items self.setupUi() @@ -303,6 +304,9 @@ class _DateInputContainer(QWidget): layout.addWidget(self._stack) layout.addStretch() + _RE_CURRENT_DATE_OFFSET = re.compile( + r'^CURRENT_DATE\s*([+-])\s*(\d+)$', re.IGNORECASE + ) def getValue( self @@ -310,6 +314,9 @@ class _DateInputContainer(QWidget): mode = self._modeCombo.currentData() if mode == "relative": + idx = self._relCombo.currentIndex() + if idx in self._dynamicItems: + return self._dynamicItems[idx] return self._relCombo.currentText() return self._dateEdit.date().toString("yyyy-MM-dd") @@ -331,6 +338,23 @@ class _DateInputContainer(QWidget): if idx is not None: self._modeCombo.setCurrentIndex(0) self._relCombo.setCurrentIndex(idx) + elif self._RE_CURRENT_DATE_OFFSET.match(s): + m = self._RE_CURRENT_DATE_OFFSET.match(s) + sign = m.group(1) + n = int(m.group(2)) + offset = n if sign == "+" else -n + label = f"{n}天后" if offset >= 0 else f"{n}天前" + raw = f"CURRENT_DATE {'+' if sign == '+' else '-'} {n}" + self._modeCombo.setCurrentIndex(0) + # Add dynamic item if not already present + for ci in range(self._relCombo.count()): + if ci in self._dynamicItems and self._dynamicItems[ci] == raw: + self._relCombo.setCurrentIndex(ci) + return + idx = self._relCombo.count() + self._relCombo.addItem(label) + self._dynamicItems[idx] = raw + self._relCombo.setCurrentIndex(idx) elif s.startswith("DATE("): self._modeCombo.setCurrentIndex(1) m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) @@ -565,13 +589,14 @@ def encodeValueStr( if var_type == "Time": if raw_value.startswith("+") or raw_value.startswith("-"): return raw_value - if raw_value.startswith("TIME_OFFSET"): - m = re.match(r"TIME_OFFSET\(([+-]\d+),(\w+)\)", raw_value) - if m: - return m.group(1) + if raw_value.upper().startswith("TIME("): return raw_value return f"TIME({raw_value})" if var_type == "Date": + if raw_value.upper().startswith("DATE("): + return raw_value + if raw_value.upper().startswith("CURRENT_DATE"): + return raw_value relMap = { "前天": "CURRENT_DATE - 2", "昨天": "CURRENT_DATE - 1", @@ -611,8 +636,11 @@ def stripOuterParens( return s -# Pre-compiled pattern for detecting arithmetic expressions like "A + B" / "C - D" -_RE_ARITH_EXPR = re.compile(r'^.+?\s+[+-]\s+.+$') +# Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B) +# Must match both spaced form (CURRENT_DATE + 1) and no-space form (RESERVE_DATE+1), +# consistent with ASEngine._resolveArithExpr. +_RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$') +_RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$') def isArithExpr( @@ -622,7 +650,56 @@ def isArithExpr( Return True if expr looks like a two-operand arithmetic expression (A ± B). """ - return bool(_RE_ARITH_EXPR.match(expr.strip())) + s = expr.strip() + return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s)) + + +# Pre-compiled patterns for SET value whitelist validation, +# matching the priority order of ASEngine._resolveValue. +_RE_VAL_TIME = re.compile(r'^TIME\(\d{1,2}:\d{2}\)$', re.IGNORECASE) +_RE_VAL_DATE = re.compile(r'^DATE\(\d{4}-\d{2}-\d{2}\)$', re.IGNORECASE) +_RE_VAL_ARITH_SPACED = _RE_ARITH_SPACED +_RE_VAL_ARITH_NOSPACE = _RE_ARITH_NOSPACE +_RE_VAL_VAR_REF = re.compile(r'^[A-Z_][A-Z0-9_]*$', re.IGNORECASE) + + +def _isValidSetValue( + value: str +) -> bool: + """ + Whitelist validation: return True if value matches one of the + legal SET-value patterns recognised by ASEngine._resolveValue. + + Order matches _resolveValue priority: TIME → DATE → bool → + quoted string → int → float → arith expr → variable reference. + """ + + s = value.strip() + if not s: + return False + if _RE_VAL_TIME.match(s): + return True + if _RE_VAL_DATE.match(s): + return True + if s.upper() in (".TRUE.", ".FALSE."): + return True + if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')): + return True + try: + int(s) + return True + except ValueError: + pass + try: + float(s) + return True + except ValueError: + pass + if _RE_VAL_ARITH_SPACED.match(s) or _RE_VAL_ARITH_NOSPACE.match(s): + return True + if _RE_VAL_VAR_REF.match(s): + return True + return False def isVarReference( diff --git a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py deleted file mode 100644 index 5bbe941..0000000 --- a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Orchestration observer for AutoScript scripts. -Subscribes to ASTokenizer parsing events to produce a structured -block representation for the orchestration dialog UI. -""" -from autoscript.ASObserver import ParsingObserver -from autoscript.ASTokenizer import ( - ASTokenizer, - K_IF, - K_ELSE_IF, - K_ELSE, - K_ENDIF, - K_SET, - K_ADD, - K_SUB, -) - - -__all__ = ["ScriptOrchObserver", "parseBlocks"] - - -class ScriptOrchObserver(ParsingObserver): - """ - Builds an ordered list of (block_type, condition, actions) tuples - from tokenization events. - - Each block: - (type: str, condition: str | None, actions: list[(target, value_expr, op_type)]) - """ - - def __init__( - self - ): - - super().__init__() - self._blocks = [] - self._current_type = None - self._current_condition = None - self._current_actions = [] - - - def onTokenParsed( - self, - kind: str | None, - data, - line_num: int, - raw_line: str - ): - - if kind in (K_IF, K_ELSE_IF, K_ELSE): - self._flushCurrentBlock() - self._current_type = kind - self._current_condition = data if kind != K_ELSE else None - self._current_actions = [] - elif kind in (K_SET, K_ADD, K_SUB): - target, value = data - if kind == K_SET: - self._current_actions.append((target, value, "set")) - elif kind == K_ADD: - prefixed = value if value.startswith("-") else f"+{value}" - self._current_actions.append((target, prefixed, "add")) - else: - prefixed = value if value.startswith("-") else f"-{value}" - self._current_actions.append((target, prefixed, "sub")) - elif kind == K_ENDIF: - self._flushCurrentBlock() - self._current_type = None - self._current_condition = None - self._current_actions = [] - - - def onParseComplete( - self, - statements: list - ): - - self._flushCurrentBlock() - - - def _flushCurrentBlock( - self - ): - - if self._current_type is not None: - self._blocks.append(( - self._current_type, - self._current_condition, - list(self._current_actions), - )) - - @property - def blocks( - self - ) -> list: - - return list(self._blocks) - - -def parseBlocks( - script: str -) -> list: - """ - Tokenize a script via observer pipeline and return its - structured block representation. - """ - - observer = ScriptOrchObserver() - ASTokenizer.tokenizeWithObservers(script, [observer]) - return observer.blocks diff --git a/src/gui/ALAutoScriptOrchDialog/_precheck.py b/src/gui/ALAutoScriptOrchDialog/_precheck.py deleted file mode 100644 index 25d424b..0000000 --- a/src/gui/ALAutoScriptOrchDialog/_precheck.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -Pre-check observer for AutoScript scripts. -Subscribes to ASTokenizer parsing events to validate script syntax -before it reaches the orchestration dialog, eliminating duplicate parsing. -""" -from autoscript.ASObserver import ParsingObserver -from autoscript.ASTokenizer import ( - K_IF, - K_ELSE_IF, - K_ELSE, - K_ENDIF, - K_SET, - K_ADD, - K_SUB, - ASTokenizer, -) - - -__all__ = ["ScriptPrecheckObserver", "precheck"] - - -class ScriptPrecheckObserver(ParsingObserver): - """ - Validates script syntax and structure during tokenization. - - Checks performed: - - IF/ENDIF depth matching - - No nested IF blocks (orchestration limitation) - - ELSE IF / ELSE appear only inside an IF block - - Only allowed variables appear in SET/ADD/SUB targets - - No completely unrecognized syntax lines - """ - - def __init__( - self, - allowed_vars: set = None - ): - - super().__init__() - self._allowed = allowed_vars or set() - self._if_depth = 0 - self.errors = [] - self._stmts = [] - - - def onTokenParsed( - self, - kind: str | None, - data, - line_num: int, - raw_line: str - ): - - if kind == K_IF: - self._if_depth += 1 - if self._if_depth > 1: - self.errors.append( - f"静态检查:错误(第{line_num}行): 检测到嵌套 IF,编排窗口不支持嵌套条件块。" - ) - elif kind == K_ELSE_IF: - if self._if_depth < 1: - self.errors.append( - f"静态检查:错误(第{line_num}行): ELSE IF 前缺少 IF。" - ) - elif kind == K_ELSE: - if self._if_depth < 1: - self.errors.append( - f"静态检查:错误(第{line_num}行): ELSE 前缺少 IF。" - ) - elif kind == K_ENDIF: - self._if_depth -= 1 - if self._if_depth < 0: - self.errors.append( - f"静态检查:错误(第{line_num}行): 多余的 ENDIF。" - ) - elif kind is None: - self.errors.append( - f"静态检查:错误(第{line_num}行): 无法识别的语法 '{raw_line}'。" - ) - elif kind in (K_SET, K_ADD, K_SUB): - target = data[0] if isinstance(data, tuple) else "" - if self._allowed and target.upper() not in self._allowed: - self.errors.append( - f"静态检查:错误(第{line_num}行): 目标变量 '{target}' 不是预设变量," - f"编排窗口不支持。" - ) - - - def onParseComplete( - self, - statements: list - ): - - if self._if_depth != 0: - self.errors.append( - f"静态检查:错误(不适用): IF 与 ENDIF 不匹配。") - self._stmts = statements - - @property - def valid( - self - ) -> bool: - - return len(self.errors) == 0 - - - def getErrorMessage( - self - ) -> str: - - return self.errors[0] if self.errors else "" - - - def buildSimplifiedScript( - self - ) -> str: - """Replace all non-control-flow statements with PASS for engine validation.""" - - lines = [] - for stmt in self._stmts: - if stmt.kind in (K_IF, K_ELSE_IF, K_ELSE, K_ENDIF): - lines.append(stmt.raw_line) - else: - lines.append("PASS") - return "\n".join(lines) - - -def precheck( - script: str, - allowed_vars: set = None -) -> tuple[bool, str]: - """ - Run the full precheck pipeline on a script. - - Steps: - 1. Create a ScriptPrecheckObserver and subscribe it to an ASTokenizer. - 2. Tokenize — the observer validates syntax during token events. - 3. Replace action lines with PASS and run engine validation - with mock target data. - """ - - if not script or not script.strip(): - return True, "" - observer = ScriptPrecheckObserver(allowed_vars=allowed_vars) - ASTokenizer.tokenizeWithObservers(script, [observer]) - if not observer.valid: - return False, observer.getErrorMessage() - simplified = observer.buildSimplifiedScript() - if not simplified.strip(): - return True, "" - try: - from autoscript import ( - registerDefaultTargetVars, - buildMockTargetData, - execute - ) - registerDefaultTargetVars() - execute(simplified, buildMockTargetData()) - except ValueError as e: - return False, f"运行时检查: {e}" - except Exception: - return False, "执行环境异常,请检查 AutoScript 配置。" - return True, "" diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index 1d2fa3d..ecc9f92 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -1,8 +1,6 @@ """ Widget components for the AutoScript orchestration dialog. """ -import re - from PySide6.QtCore import Slot from PySide6.QtWidgets import ( QComboBox, @@ -24,13 +22,11 @@ from gui.ALAutoScriptOrchDialog._helpers import ( encodeValueStr, getValueFromWidget, isArithExpr, - isVarReference, makeComboWidget, makeLabel, makeOffsetWidget, makeValueWidget, makeVarRefCombo, - setWidgetValue, ) @@ -114,7 +110,23 @@ class ConditionRowFrame(QFrame): self ): + wasBool = self._isBoolMode + boolName = None + if wasBool: + data = self.leftVarCombo.currentData() + if data: + boolName = data[0] self._varMgr.populateCombo(self.leftVarCombo) + # Append boolean literal sentinels at the end + self.leftVarCombo.insertSeparator(self.leftVarCombo.count()) + self.leftVarCombo.addItem(".TRUE.", (".TRUE.", "Boolean")) + self.leftVarCombo.addItem(".FALSE.", (".FALSE.", "Boolean")) + if wasBool and boolName: + for ci in range(self.leftVarCombo.count()): + d = self.leftVarCombo.itemData(ci) + if d and d[0] == boolName: + self.leftVarCombo.setCurrentIndex(ci) + break def populateRhsVarCombo( @@ -143,8 +155,14 @@ class ConditionRowFrame(QFrame): data = self.leftVarCombo.itemData(idx) if not data: return - _, vartype = data - self.updateRhsLiteralWidget(vartype) + name, vartype = data + isBool = name in (".TRUE.", ".FALSE.") + self._isBoolMode = isBool + self.opCombo.setVisible(not isBool) + self._compTypeCombo.setVisible(not isBool) + self.rhsStack.setVisible(not isBool) + if not isBool: + self.updateRhsLiteralWidget(vartype) def updateRhsLiteralWidget( @@ -181,6 +199,8 @@ class ConditionRowFrame(QFrame): ) -> str: data = self.leftVarCombo.currentData() + if self._isBoolMode and data: + return data[0] if not data: return "" name, vartype = data @@ -205,95 +225,6 @@ class ConditionRowFrame(QFrame): return "" - def loadFromParts( - self, - operandName: str, - opSym: str, - valueExpr: str - ): - - self._rawRhsExpr = "" - self.leftVarCombo.blockSignals(True) - self.opCombo.blockSignals(True) - self._compTypeCombo.blockSignals(True) - try: - for ci in range(self.leftVarCombo.count()): - d = self.leftVarCombo.itemData(ci) - if d and d[0] == operandName: - self.leftVarCombo.setCurrentIndex(ci) - break - if opSym: - for oi in range(self.opCombo.count()): - if self.opCombo.itemData(oi) == opSym: - self.opCombo.setCurrentIndex(oi) - break - finally: - self.leftVarCombo.blockSignals(False) - self.opCombo.blockSignals(False) - self._compTypeCombo.blockSignals(False) - data = self.leftVarCombo.currentData() - vartype = data[1] if data else "String" - self.updateRhsLiteralWidget(vartype) - if not valueExpr: - return - up = valueExpr.strip().upper() - if isVarReference(valueExpr) or self._isKnownVar(up): - self._compTypeCombo.setCurrentIndex(1) - self.populateRhsVarCombo() - found = self._varMgr.findExactNameEntry(self.rhsVarCombo, up) - if found >= 0: - self.rhsVarCombo.setCurrentIndex(found) - else: - self.rhsVarCombo.addItem(up, (up, "String")) - self.rhsVarCombo.setCurrentIndex(self.rhsVarCombo.count() - 1) - elif isArithExpr(valueExpr): - self._tryLoadCondArithExpr(valueExpr, vartype) - else: - self._compTypeCombo.setCurrentIndex(0) - w = self.literalWidgets.get(vartype) - if w: - setWidgetValue(w, vartype, valueExpr) - - def _tryLoadCondArithExpr( - self, - expr: str, - vartype: str - ): - """Try to decompose a condition RHS arithmetic expression into UI state.""" - - s = expr.strip() - m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) - if not m: - self._rawRhsExpr = s - return - left = m.group(1).strip() - op = m.group(2).strip() - right = m.group(3).strip() - left_up = left.upper() - - if vartype == "Date" and left_up == "CURRENT_DATE": - try: - n = int(right) - offset = n if op == "+" else -n - if offset in (-2, -1, 0, 1, 2): - self._compTypeCombo.setCurrentIndex(0) - w = self.literalWidgets.get("Date") - if w and hasattr(w, "setValue"): - w.setValue(s) - return - except ValueError: - pass - self._rawRhsExpr = s - - - def _isKnownVar( - self, - name: str - ) -> bool: - - return self._varMgr.getInfoByName(name) is not None - - def refreshVarCombos( self ): @@ -301,7 +232,6 @@ class ConditionRowFrame(QFrame): self.populateLeftVarCombo() self.populateRhsVarCombo() - class ActionStepFrame(QFrame): def __init__( @@ -471,9 +401,11 @@ class ActionStepFrame(QFrame): ) -> str: target = self.getTargetName() + op = self.opTypeCombo.currentData() + if op == "pass": + return " PASS" if not target: return "" - op = self.opTypeCombo.currentData() rawVal = self._getValueRaw() if op == "set": vartype = self._currentTargetType @@ -514,132 +446,6 @@ class ActionStepFrame(QFrame): return "" - def setOpType( - self, - opType: str - ): - - for i in range(self.opTypeCombo.count()): - if self.opTypeCombo.itemData(i) == opType: - self.opTypeCombo.setCurrentIndex(i) - break - - - def loadFromScript( - self, - targetVar: str, - valueExpr: str - ): - - targetUp = targetVar.upper().strip() - for ci in range(self.targetCombo.count()): - d = self.targetCombo.itemData(ci) - if d and d[0] == targetUp: - self.targetCombo.setCurrentIndex(ci) - break - self._setValueFromExpr(valueExpr) - - - def _setValueFromExpr( - self, - expr: str - ): - - s = expr.strip() - if not s: - return - op = self.opTypeCombo.currentData() - if op in ("add", "sub") and s.startswith("-"): - s = s[1:] - self.opTypeCombo.setCurrentIndex( - 2 if op == "add" else 1 - ) - up = s.upper() - if isVarReference(s): - self.valueSrcCombo.setCurrentIndex(1) - self._varMgr.populateCombo(self.existingVarCombo) - idx = self._varMgr.findExactNameEntry(self.existingVarCombo, up) - if idx >= 0: - self.existingVarCombo.setCurrentIndex(idx) - else: - self.existingVarCombo.addItem(up, (up, "String")) - self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) - elif isArithExpr(s): - self._tryLoadArithExpr(s) - else: - self.valueSrcCombo.setCurrentIndex(0) - w = self.valueStack.currentWidget() - if w: - setWidgetValue(w, self._currentTargetType, expr) - - def _tryLoadArithExpr( - self, - expr: str - ): - """Try to decompose an arithmetic expression into UI state.""" - - s = expr.strip() - m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) - if not m: - self._storeAsCustomExpr(s) - return - left = m.group(1).strip() - op = m.group(2).strip() - right = m.group(3).strip() - left_up = left.upper() - - # CURRENT_DATE ± N for Date targets — try relative combo for ±0/1/2, - # otherwise store as custom expression to preserve relative semantics - if self._currentTargetType == "Date" and left_up == "CURRENT_DATE": - try: - n = int(right) - offset = n if op == "+" else -n - if offset in (-2, -1, 0, 1, 2): - w = self._literalWidgets.get("Date") - if w and hasattr(w, "setValue"): - w.setValue(s) - self.valueSrcCombo.setCurrentIndex(0) - return - except ValueError: - pass - self._storeAsCustomExpr(s) - return - - # CURRENT_TIME ± N for Time targets — map to add/sub with offset - if self._currentTargetType == "Time" and left_up == "CURRENT_TIME": - try: - hours = int(right) - if op == "-": - hours = -hours - self.opTypeCombo.setCurrentIndex( - 1 if hours >= 0 else 2 - ) - self.valueSrcCombo.setCurrentIndex(0) - w = self._offsetWidgets.get("Time") - if w and hasattr(w, "setValue"): - w.setValue(str(abs(hours))) - return - except ValueError: - pass - - self._storeAsCustomExpr(s) - - def _storeAsCustomExpr( - self, - expr: str - ): - """Store a raw expression in the variable combo when it can't be decomposed.""" - - self.valueSrcCombo.setCurrentIndex(1) - self._varMgr.populateCombo(self.existingVarCombo) - found = self._varMgr.findExactNameEntry(self.existingVarCombo, expr) - if found < 0: - self.existingVarCombo.addItem(expr, (expr, self._currentTargetType)) - self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) - else: - self.existingVarCombo.setCurrentIndex(found) - - def refreshVarCombos( self ): From 82738be99a8e0897360c3c5596a96f54d36d607c Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 04:15:40 +0800 Subject: [PATCH 19/49] =?UTF-8?q?feat(gui):=20=E7=BC=96=E8=BE=91=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E6=94=AF=E6=8C=81=E8=B0=83=E8=AF=95=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E4=B8=8E=E5=8A=A8=E6=80=81=E6=A8=A1=E6=8B=9F=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/autoscript/__init__.py | 2 + src/gui/ALAutoScriptEditDialog.py | 382 +++++++++++++++++++++++++----- src/gui/ALTimerTaskAddDialog.py | 29 +-- 3 files changed, 330 insertions(+), 83 deletions(-) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index c570fe5..d1a474d 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -28,6 +28,8 @@ __all__ = [ "buildMockTargetData", "META_VARS", "ALL_VARIABLES", + "_TARGET_VAR_DEFS", + "_MOCK_TYPE_VALUES", "ASTokenizer", "Stmt", "Script", diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 7d63b40..945695f 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -7,7 +7,9 @@ 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. """ -from PySide6.QtCore import Qt, Slot +from copy import deepcopy + +from PySide6.QtCore import QDate, Qt, QTime, QTimer, Slot from PySide6.QtGui import ( QColor, QFont, @@ -16,21 +18,40 @@ from PySide6.QtGui import ( ) from PySide6.QtWidgets import ( QApplication, + QComboBox, + QDateEdit, QDialog, QDialogButtonBox, + QDoubleSpinBox, + QFormLayout, + QFrame, QGridLayout, + QGroupBox, QHBoxLayout, + QHeaderView, QLabel, + QLineEdit, + QMessageBox, QPlainTextEdit, QPushButton, - + QSpinBox, + QSplitter, QStyle, QTabWidget, + QTableWidget, + QTableWidgetItem, + QTimeEdit, QVBoxLayout, QWidget, ) -from autoscript import ALL_VARIABLES +from autoscript import ( + ALL_VARIABLES, + _MOCK_TYPE_VALUES, + _TARGET_VAR_DEFS, + execute, + registerDefaultTargetVars, +) class ALScriptHighlighter(QSyntaxHighlighter): @@ -99,16 +120,47 @@ class ALScriptHighlighter(QSyntaxHighlighter): self.setFormat(start, length, fmt) +class _DebugResultDialog(QDialog): + + def __init__( + self, + changes: list, + parent = None + ): + + super().__init__(parent) + self.setWindowTitle("调试运行结果 - AutoLibrary") + self.setMinimumSize(600, 200) + layout = QVBoxLayout(self) + table = QTableWidget(len(changes), 3) + table.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"]) + table.horizontalHeader().setStretchLastSection(True) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes): + label = f"{display_name}: {name}({var_type})" + table.setItem(row, 0, QTableWidgetItem(label)) + table.setItem(row, 1, QTableWidgetItem(str(before_val))) + table.setItem(row, 2, QTableWidgetItem(str(after_val))) + layout.addWidget(table) + btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") + btnBox.accepted.connect(self.accept) + layout.addWidget(btnBox) + + class ALAutoScriptEditDialog(QDialog): def __init__( self, parent = None, - script: str = "" + script: str = "", + mockData: dict = None ): super().__init__(parent) self._fontSize = 19 + self._mockWidgets = {} self.setupUi() self.connectSignals() @@ -116,6 +168,8 @@ class ALAutoScriptEditDialog(QDialog): self._highlighter = ALScriptHighlighter( self.textEdit.document() ) + if mockData: + self.setMockData(mockData) def setupUi( @@ -123,10 +177,10 @@ class ALAutoScriptEditDialog(QDialog): ): self.setWindowTitle("AutoScript 编辑 - AutoLibrary") - self.setMinimumSize(640, 600) + self.setMinimumSize(660, 600) layout = QVBoxLayout(self) - layout.setSpacing(4) - layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(3) + layout.setContentsMargins(3, 3, 3, 3) toolbarLayout = QHBoxLayout() self.zoomInBtn = QPushButton("+") self.zoomInBtn.setFixedSize(25, 25) @@ -141,6 +195,19 @@ class ALAutoScriptEditDialog(QDialog): self.zoomResetBtn.setToolTip("重置缩放") self.zoomLabel = QLabel(f"{self._fontSize}px") self.zoomLabel.setFixedHeight(25) + self.orchBtn = QPushButton("编排") + self.orchBtn.setFixedHeight(25) + self.orchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置") + toolbarLayout.addWidget(self.orchBtn) + self.debugBtn = QPushButton("▶ 调试运行") + self.debugBtn.setFixedHeight(25) + self.debugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化") + toolbarLayout.addWidget(self.debugBtn) + sep = QFrame() + sep.setFrameShape(QFrame.Shape.VLine) + sep.setFrameShadow(QFrame.Shadow.Sunken) + sep.setFixedWidth(1) + toolbarLayout.addWidget(sep) toolbarLayout.addWidget(self.zoomInBtn) toolbarLayout.addWidget(self.zoomOutBtn) toolbarLayout.addWidget(self.zoomResetBtn) @@ -181,46 +248,38 @@ class ALAutoScriptEditDialog(QDialog): parent_layout ): - - tab_widget = QTabWidget() - tab_widget.setMaximumHeight(200) - basic_widget = QWidget() - basic_layout = QGridLayout(basic_widget) - basic_layout.setSpacing(4) - basic_layout.setContentsMargins(4, 4, 4, 4) - basic_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - control_buttons = [ + splitter = QSplitter(Qt.Orientation.Horizontal) + tabWidget = QTabWidget() + tabWidget.setMaximumHeight(150) + basicWidget = QWidget() + basicLayout = QGridLayout(basicWidget) + basicLayout.setSpacing(4) + basicLayout.setContentsMargins(4, 4, 4, 4) + basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + controlButtons = [ ("IF", "IF()\n \nEND IF"), ("ELSE IF", "ELSE IF()\n "), ("ELSE", "ELSE"), ("END IF", "END IF"), ("PASS", "PASS"), ] - self._addButtonsToGrid(basic_layout, control_buttons, 0, 0, 5) - - assign_buttons = [ + self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5) + assignButtons = [ ("SET", "SET = "), ] - self._addButtonsToGrid(basic_layout, assign_buttons, 0, 5, 1) - - func_buttons = [ - ("DATE()", "DATE()"), - ("TIME()", "TIME()"), - ] - self._addButtonsToGrid(basic_layout, func_buttons, 1, 0, 2) - - tab_widget.addTab(basic_widget, "基本语法") - operator_widget = QWidget() - operator_layout = QGridLayout(operator_widget) - operator_layout.setSpacing(4) - operator_layout.setContentsMargins(4, 4, 4, 4) - operator_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - arithmetic_buttons = [ + self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1) + tabWidget.addTab(basicWidget, "基本语法") + operatorWidget = QWidget() + operatorLayout = QGridLayout(operatorWidget) + operatorLayout.setSpacing(4) + operatorLayout.setContentsMargins(4, 4, 4, 4) + operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + arithmeticButtons = [ (".ADD.", ".ADD."), (".SUB.", ".SUB."), ] - self._addButtonsToGrid(operator_layout, arithmetic_buttons, 0, 0, 2) - compare_buttons = [ + self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2) + compareButtons = [ (".EQ.", ".EQ."), (".NEQ.", ".NEQ."), (".BGT.", ".BGT."), @@ -228,42 +287,54 @@ class ALAutoScriptEditDialog(QDialog): (".BGE.", ".BGE."), (".BLE.", ".BLE."), ] - self._addButtonsToGrid(operator_layout, compare_buttons, 1, 0, 6) + self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6) logic_buttons = [ (".AND.", ".AND."), (".OR.", ".OR."), ] - self._addButtonsToGrid(operator_layout, logic_buttons, 2, 0, 2) - tab_widget.addTab(operator_widget, "运算符") - literal_widget = QWidget() - literal_layout = QGridLayout(literal_widget) - literal_layout.setSpacing(4) - literal_layout.setContentsMargins(4, 4, 4, 4) - literal_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2) + tabWidget.addTab(operatorWidget, "运算符") + literalWidget = QWidget() + literalLayout = QGridLayout(literalWidget) + literalLayout.setSpacing(4) + literalLayout.setContentsMargins(4, 4, 4, 4) + literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) bool_buttons = [ (".TRUE.", ".TRUE."), (".FALSE.", ".FALSE."), ] - self._addButtonsToGrid(literal_layout, bool_buttons, 0, 0, 2) - hint_buttons = [ - ("字符串", "'文本'"), - ("数字", "123"), - ("注释", "// 注释"), + self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2) + dateTimeButtons = [ + ("DATE()", "DATE(2025-01-01)"), + ("TIME()", "TIME(00:00)"), ] - self._addButtonsToGrid(literal_layout, hint_buttons, 1, 0, 3) - tab_widget.addTab(literal_widget, "字面量") - var_widget = QWidget() - var_layout = QGridLayout(var_widget) - var_layout.setSpacing(4) - var_layout.setContentsMargins(4, 4, 4, 4) - var_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) - var_buttons = [ + self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2) + hintButtons = [ + ("字符串", "'请输入文本'"), + ("数字", "123"), + ("注释", "// 请输入注释"), + ] + self._addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) + tabWidget.addTab(literalWidget, "字面量") + varWidget = QWidget() + varLayout = QGridLayout(varWidget) + varLayout.setSpacing(4) + varLayout.setContentsMargins(4, 4, 4, 4) + varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + varButtons = [ (display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() ] - self._addButtonsToGrid(var_layout, var_buttons, 0, 0, 5) - tab_widget.addTab(var_widget, "变量") - parent_layout.addWidget(tab_widget) + self._addButtonsToGrid(varLayout, varButtons, 0, 0, 5) + tabWidget.addTab(varWidget, "变量") + mockPanel = self._createMockPanel() + mockPanel.setMinimumWidth(260) + splitter.addWidget(tabWidget) + splitter.addWidget(mockPanel) + splitter.setStretchFactor(0, 1) + splitter.setStretchFactor(1, 0) + splitter.setSizes([660, 400]) + parent_layout.addWidget(splitter) def _addButtonsToGrid( @@ -283,7 +354,7 @@ class ALAutoScriptEditDialog(QDialog): btn.setProperty("template", template) btn.clicked.connect(self._insertTemplate) btn.setFixedWidth(100) - btn.setFixedHeight(30) + btn.setFixedHeight(25) btn.setToolTip(f"插入: {template}") grid_layout.addWidget(btn, row, col) @@ -307,12 +378,186 @@ class ALAutoScriptEditDialog(QDialog): cursor.insertText(template) + def _createMockPanel( + self + ) -> QGroupBox: + + group = QGroupBox("模拟目标数据") + form = QFormLayout(group) + form.setSpacing(4) + form.setContentsMargins(5, 10, 5, 5) + self._mockWidgets = {} + for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + default = _MOCK_TYPE_VALUES.get(var_type, "") + widget = self._makeMockInput(var_type, default) + label = QLabel(f"{display_name}: {name}({var_type})") + form.addRow(label, widget) + self._mockWidgets[name] = (widget, var_type, key_path) + return group + + + def _makeMockInput( + self, + var_type: str, + default + ) -> QWidget: + + if var_type == "String": + w = QLineEdit() + w.setText(str(default)) + return w + if var_type == "Boolean": + w = QComboBox() + w.addItems(["是", "否"]) + w.setCurrentIndex(0 if default else 1) + return w + if var_type == "Date": + w = QDateEdit() + w.setCalendarPopup(True) + w.setDisplayFormat("yyyy-MM-dd") + w.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) + return w + if var_type == "Time": + w = QTimeEdit() + w.setDisplayFormat("HH:mm") + w.setTime(QTime.fromString(str(default), "HH:mm")) + return w + if var_type == "Int": + w = QSpinBox() + w.setMinimum(-999999) + w.setMaximum(999999) + w.setValue(int(default) if default else 0) + return w + if var_type == "Float": + w = QDoubleSpinBox() + w.setMinimum(-999999.0) + w.setMaximum(999999.0) + w.setDecimals(2) + w.setValue(float(default) if default else 0.0) + return w + w = QLineEdit() + w.setText(str(default)) + return w + + + def getMockData( + self + ) -> dict: + + data = {} + for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + widget, _, _ = self._mockWidgets[name] + value = self._getMockValue(widget, var_type) + d = data + for key in key_path[:-1]: + d = d.setdefault(key, {}) + d[key_path[-1]] = value + return data + + + def setMockData( + self, + data: dict + ): + + if not data: + return + for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + d = data + try: + for key in key_path: + d = d[key] + except (KeyError, TypeError): + continue + widget, _, _ = self._mockWidgets[name] + self._setMockValue(widget, var_type, d) + + + def _getMockValue( + self, + widget: QWidget, + var_type: str + ): + + if var_type == "Boolean": + return widget.currentIndex() == 0 + if var_type == "Date": + return widget.date().toString("yyyy-MM-dd") + if var_type == "Time": + return widget.time().toString("HH:mm") + if var_type == "Int": + return widget.value() + if var_type == "Float": + return widget.value() + return widget.text() + + + def _setMockValue( + self, + widget: QWidget, + var_type: str, + value + ): + + if var_type == "Boolean": + widget.setCurrentIndex(0 if value else 1) + elif var_type == "Date": + widget.setDate(QDate.fromString(str(value), "yyyy-MM-dd")) + elif var_type == "Time": + widget.setTime(QTime.fromString(str(value), "HH:mm")) + elif var_type == "Int": + widget.setValue(int(value)) + elif var_type == "Float": + widget.setValue(float(value)) + else: + widget.setText(str(value)) + + + @Slot() + def onDebugRun( + self + ): + + script = self.textEdit.toPlainText().strip() + if not script: + QMessageBox.warning(self, "提示", "脚本内容为空。") + return + target_data = self.getMockData() + before = deepcopy(target_data) + try: + registerDefaultTargetVars() + execute(script, target_data) + except ValueError as e: + QMessageBox.warning(self, "运行错误", str(e)) + return + changes = [] + for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + before_val = before + after_val = target_data + try: + for key in key_path: + before_val = before_val[key] + after_val = after_val[key] + except (KeyError, TypeError): + continue + if before_val != after_val: + changes.append((display_name, name, var_type, before_val, after_val)) + if not changes: + QMessageBox.information(self, "调试运行", "目标变量未发生变化。") + return + dlg = _DebugResultDialog(changes, self) + dlg.exec() + dlg.deleteLater() + + def connectSignals( self ): self.btnBox.accepted.connect(self.accept) self.btnBox.rejected.connect(self.reject) + self.orchBtn.clicked.connect(self.onOpenOrchDialog) + self.debugBtn.clicked.connect(self.onDebugRun) self.zoomInBtn.clicked.connect(self.onZoomIn) self.zoomOutBtn.clicked.connect(self.onZoomOut) self.zoomResetBtn.clicked.connect(self.onZoomReset) @@ -375,8 +620,21 @@ class ALAutoScriptEditDialog(QDialog): original = self.copyBtn.text() self.copyBtn.setText("已复制") self.copyBtn.setEnabled(False) - from PySide6.QtCore import QTimer QTimer.singleShot(2000, lambda: ( self.copyBtn.setText(original), self.copyBtn.setEnabled(True) )) + + @Slot() + def onOpenOrchDialog( + self + ): + + from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog + dlg = ALAutoScriptOrchDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + script = dlg.getScript() + if script: + cursor = self.textEdit.textCursor() + cursor.insertText(script) + dlg.deleteLater() diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 160453e..019de61 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -17,7 +17,6 @@ from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog -from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog from utils.TimerUtils import TimerUtils @@ -102,10 +101,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3) self.AutoScriptLayout.setSpacing(3) autoScriptBtnLayout = QHBoxLayout() - self.AutoScriptSetButton = QPushButton("设置指令") - self.AutoScriptSetButton.setMinimumHeight(25) - self.AutoScriptSetButton.setFixedWidth(130) - autoScriptBtnLayout.addWidget(self.AutoScriptSetButton) self.AutoScriptPreviewButton = QPushButton("编辑") self.AutoScriptPreviewButton.setMinimumHeight(25) self.AutoScriptPreviewButton.setFixedWidth(60) @@ -136,6 +131,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): ) self.AutoScriptGroupBox.setVisible(False) self.__auto_script = "" + self.__mock_target_data = None def loadTask( @@ -173,6 +169,9 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.__auto_script = auto_script self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") + mock_data = task.get("mock_target_data") + if mock_data: + self.__mock_target_data = mock_data self.ConfirmButton.setText("保存") @@ -184,7 +183,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.ConfirmButton.clicked.connect(self.accept) self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged) self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled) - self.AutoScriptSetButton.clicked.connect(self.onSetAutoScript) self.AutoScriptPreviewButton.clicked.connect(self.onPreviewAutoScript) self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp) @@ -220,6 +218,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): task_data["status"] = ALTimerTaskStatus.PENDING task_data["executed"] = False task_data["repeat_auto_script"] = self.__auto_script + task_data["mock_target_data"] = self.__mock_target_data else: task_data = { "name": name, @@ -232,6 +231,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): "executed": False, "repeat": self.RepeatCheckBox.isChecked(), "repeat_auto_script": self.__auto_script, + "mock_target_data": self.__mock_target_data, } repeat = self.RepeatCheckBox.isChecked() @@ -292,27 +292,14 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.SunCheckBox.setEnabled(checked) self.AutoScriptGroupBox.setVisible(checked) - @Slot() - def onSetAutoScript(self): - dlg = ALAutoScriptOrchDialog(self, existingScript=self.__auto_script) - if dlg.exec() == QDialog.DialogCode.Accepted: - script = dlg.getScript() - self.__auto_script = script - if script: - self.AutoScriptStatusLabel.setText("已设置") - self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") - else: - self.AutoScriptStatusLabel.setText("未设置") - self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") - dlg.deleteLater() - @Slot() def onPreviewAutoScript(self): from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog - dlg = ALAutoScriptEditDialog(self, self.__auto_script) + dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data) if dlg.exec() == QDialog.DialogCode.Accepted: script = dlg.getScript() self.__auto_script = script + self.__mock_target_data = dlg.getMockData() if script: self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") From 9b47886e5b996f7a33ed46f749b3cc9b7ba4de1f Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 04:16:03 +0800 Subject: [PATCH 20/49] =?UTF-8?q?fix(autoscript):=20SET=20=E8=B5=8B?= =?UTF-8?q?=E5=80=BC=E5=BC=BA=E5=88=B6=E5=BC=BA=E7=B1=BB=E5=9E=8B=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=EF=BC=8C=E7=A6=81=E6=AD=A2=E8=B7=A8=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E9=9A=90=E5=BC=8F=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/autoscript/ASObject.py | 44 +++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/autoscript/ASObject.py b/src/autoscript/ASObject.py index af16bca..32302bb 100644 --- a/src/autoscript/ASObject.py +++ b/src/autoscript/ASObject.py @@ -32,6 +32,21 @@ _TYPE_DEFAULTS = { "String": "" } +# Mapping from Python type to AutoScript type name for error messages +_PYTHON_TO_AS_TYPE = { + bool: "Boolean", + int: "Int", + float: "Float", + str: "String", + date: "Date", + time: "Time", +} + +def _asTypeName( + value +) -> str: + return _PYTHON_TO_AS_TYPE.get(type(value), "UnknowType") + class ASObject: """ @@ -174,34 +189,29 @@ class ASObject: target_data: dict = None ): """ - Assign a new value to this variable, with type coercion. + Assign a new value to this variable, with strict type checking. - Performs coercion for Boolean (string -> bool), Int, and Float types. - For config variables, dates/times are converted back to strings before - writing into target_data. + AutoScript is strongly typed: only values whose Python type matches the + declared variable type are accepted. Int->Float widening is allowed; + all other cross-type assignments raise ValueError. Args: value: The value to assign. target_data (dict): The application data dict (required for config vars). Raises: - ValueError: If the variable is read-only or value cannot be coerced. + ValueError: If the variable is read-only or value type mismatches the variable type. """ if self.read_only: raise ValueError(f"不能修改只读变量 '{self.name}'") - if self.var_type == "Boolean" and not isinstance(value, bool): - value = (str(value).upper() == "TRUE") - if self.var_type == "Int" and not isinstance(value, int): - try: - value = int(value) - except (ValueError, TypeError): - raise ValueError(f"无法将值 '{value}' 转换为 Int 类型") - if self.var_type == "Float" and not isinstance(value, float): - try: - value = float(value) - except (ValueError, TypeError): - raise ValueError(f"无法将值 '{value}' 转换为 Float 类型") + vt = self.var_type + value_type = _asTypeName(value) + if vt != value_type and not (vt == "Float" and value_type == "Int"): + raise ValueError( + f"{vt} 类型变量 '{self.name}' 不能接受 {value_type} 类型的值" + ) + if self.is_config: if self.var_type == "Date" and isinstance(value, date): value = value.strftime("%Y-%m-%d") From a0fd03f12f09d91e716f99766b10c2095945e3c9 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 18:22:36 +0800 Subject: [PATCH 21/49] =?UTF-8?q?refactor(autoscript):=20ASEngine=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=87=B3=20Lua=20=E6=B2=99=E7=AE=B1=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=8C=E5=BC=BA=E5=8C=96=E7=B1=BB=E5=9E=8B=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E4=B8=8E=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 Co-Authored-By: Claude Opus 4.7 --- requirement.txt | Bin 1504 -> 1490 bytes src/autoscript/ASEngine.py | 852 ++++++++++++---------------------- src/autoscript/ASObject.py | 269 ----------- src/autoscript/ASObserver.py | 78 ---- src/autoscript/ASOperator.py | 191 -------- src/autoscript/ASTokenizer.py | 584 ----------------------- src/autoscript/__init__.py | 37 +- 7 files changed, 302 insertions(+), 1709 deletions(-) delete mode 100644 src/autoscript/ASObject.py delete mode 100644 src/autoscript/ASObserver.py delete mode 100644 src/autoscript/ASOperator.py delete mode 100644 src/autoscript/ASTokenizer.py diff --git a/requirement.txt b/requirement.txt index 199a2464f9e5bf43e875a9ca02ef773d290e4a06..b85c98d87b0d8fdbee7985539adb91ceeaf2801b 100644 GIT binary patch delta 34 ocmaFBeTkdt|G$lDVvK@045bVO42cZ3Kxo9E$6&G9oN*-!0KW?fBLDyZ delta 52 zcmcb_{eWBT|Gz|r9EK8xbcP~^M1}%}3oG22 F0RV2g3rzq3 diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index 1781f13..129e803 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -7,640 +7,368 @@ 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 re from datetime import ( - datetime, - timedelta, date, - time + datetime, ) -from .ASObject import ( - ASObject, - _META_VARS, - _inferType -) -from .ASOperator import ASOperator -from .ASTokenizer import ( - ASTokenizer, - NodeVisitor, - Script, - IfNode, - SetNode, - OpNode, - PassNode, - UnrecogNode -) +from lupa import LuaRuntime as _LuaRuntime -__all__ = ["execute", "addTargetVar", "splitTopLevel"] +__all__ = ["execute", "addTargetVar", "resetEngine"] # Engine state -# User-registered target variables (bound to target_data paths) -_TARGET_VARS = {} -# Free-form script variables (not bound to target_data) -_SCRIPT_VARS = {} -# Name -> ASObject lookup map built from _META_VARS, _TARGET_VARS, and display names -_FIELD_MAP = {} +_TARGET_VARS: dict[str, dict] = {} +_lua = None + +# Built-in meta variable definitions (name / type / display-name) +META_VARS = { + "CURRENT_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"}, + "CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "display": "当前时间"}, +} -def _errPos( - line: int, - message: str -) -> str: - """ - Format an error message with a script line number. - - Args: - line (int): The script line number where the error occurred. - message (str): The error description. - - Returns: - str: A formatted error string like "AutoScript syntax error(line X): message". - """ - return f"AutoScript 语法错误(第{line}行): {message}" - - -# Pre-compiled regex patterns for value resolution -_RE_TIME = re.compile(r"^TIME\((\d{1,2}):(\d{2})\)$", re.IGNORECASE) -_RE_DATE = re.compile(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", re.IGNORECASE) - - -def splitTopLevel( - text: str, - delimiter: str -) -> list: - """ - Split a condition expression by a delimiter (.AND. / .OR.), respecting parentheses. - - Only splits at the top nesting level; delimiters inside parentheses are ignored. - - Args: - text (str): The condition expression to split. - delimiter (str): The delimiter string, e.g. ".OR." or ".AND.". - - Returns: - list: A list of sub-expression strings (stripped of leading/trailing whitespace). - """ - - parts = [] - depth = 0 - buf = "" - i = 0 - text_upper = text.upper() - delim_upper = delimiter.upper() - dlen = len(delim_upper) - while i < len(text): - if text[i] == "(": - depth += 1 - buf += text[i] - elif text[i] == ")": - depth -= 1 - buf += text[i] - elif depth == 0 and text_upper[i:i + dlen] == delim_upper: - parts.append(buf) - buf = "" - i += dlen - continue - else: - buf += text[i] - i += 1 - if buf.strip(): - parts.append(buf) - return parts - - -def _buildFieldMap(): - """ - Rebuild the _FIELD_MAP lookup from _META_VARS and _TARGET_VARS. - - Each variable is registered under both its canonical name (uppercased) - and its display_name (if present), so that scripts can refer to either. - """ - - _FIELD_MAP.clear() - for ch_name, obj in _META_VARS.items(): - _FIELD_MAP[obj.name.upper()] = obj - _FIELD_MAP[ch_name.upper().strip()] = obj - for obj in _TARGET_VARS.values(): - _FIELD_MAP[obj.name.upper()] = obj - if obj.display_name: - _FIELD_MAP[obj.display_name.upper().strip()] = obj - - -def _resolveFieldObj( - field_name: str +def _getLua( ): """ - Resolve a field name to its ASObject by looking up _FIELD_MAP then _SCRIPT_VARS. - - Unlike getting a raw value, this returns the ASObject instance itself, - preserving type information for operations and comparisons. - - Args: - field_name (str): The field name (case-insensitive). - - Returns: - ASObject or None: The resolved ASObject, or None if not found. + Return the sandboxed Lua runtime singleton. """ - upper_name = field_name.upper().strip() - obj = _FIELD_MAP.get(upper_name) - if obj: - return obj - obj = _SCRIPT_VARS.get(upper_name) - if obj: - return obj - return None + global _lua + if _lua is None: + _lua = _LuaRuntime(unpack_returned_tuples = True) + _sandbox(_lua) + _registerHelpers(_lua) + return _lua -def _resolveValue( - value_str: str, - target_data: dict +def _sandbox( + lua, +) -> None: + """ + Remove dangerous Lua globals while keeping os.date / os.time for date-time helpers. + """ + + lua.execute(""" + io = nil + require = nil + dofile = nil + loadfile = nil + load = nil + package = nil + rawget = nil + rawset = nil + rawequal = nil + getfenv = nil + setfenv = nil + debug = nil + -- selectively disable dangerous os functions, keep date / time + if os then + os.execute = nil + os.exit = nil + os.getenv = nil + os.remove = nil + os.rename = nil + os.tmpname = nil + os.setlocale = nil + end + """) + + +def _registerHelpers( + lua, +) -> None: + """ + Inject Date / Time helpers as pure Lua functions. + + Date values are os.time timestamps (seconds since epoch). + Time values are minutes since midnight (0-1439). + + This keeps Date / Time as native Lua numbers during script execution, + enabling type-safe arithmetic (+, -) and comparisons (<, <=, ==, ~=). + """ + + lua.execute(""" + function date(y, m, d) + return os.time({year = y, month = m, day = d}) + end + + function time(h, m) + return h * 60 + m + end + + function CURRENT_DATE() + local now = os.date("*t") + return os.time({year = now.year, month = now.month, day = now.day}) + end + + function CURRENT_TIME() + local now = os.date("*t") + return now.hour * 60 + now.min + end + + function date_add(date_val, n) + return date_val + n * 86400 + end + + function time_add(time_val, n) + return (time_val + n * 60) % 1440 + end + + -- push helpers: string -> native type + function _to_date(iso_str) + local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)") + return os.time({year = y, month = m, day = d}) + end + + function _to_time(hm_str) + local h, m = hm_str:match("(%d+):(%d+)") + return h * 60 + m + end + + -- pull helpers: native type -> string + function _from_date(ts) + return os.date("%Y-%m-%d", ts) + end + + function _from_time(m) + return string.format("%02d:%02d", math.floor(m / 60), m % 60) + end + """) + + +def _navigatePath( + data: dict, + key_path: list, + default = None, ): """ - Parse and resolve a value string from a script into a Python object. - - Supports the following literal forms: - - TIME(hh:mm) - - DATE(yyyy-mm-dd) - - .TRUE. / .FALSE. - - Single/double quoted strings (with escaped single quotes) - - Arithmetic expressions: operand (+|-) operand (Date ± Int, Int ± Int, etc.) - - Numeric literals (int / float) - - Field references (resolved via _resolveFieldObj) - - Args: - value_str (str): The raw value string from the script. - target_data (dict): The application data dict. - - Returns: - The resolved Python value. + Walk *key_path* into *data* and return the value at the leaf. """ - s = value_str.strip() - m = _RE_TIME.match(s) - if m: - return time(int(m.group(1)), int(m.group(2))) - m = _RE_DATE.match(s) - if m: - return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) - up = s.upper() - if up == ".TRUE.": - return True - if up == ".FALSE.": - return False - if s.startswith("'") and s.endswith("'"): - return s[1:-1].replace("''", "'") - if s.startswith('"') and s.endswith('"'): - return s[1:-1] - try: - return int(s) - except ValueError: - pass - try: - return float(s) - except ValueError: - pass - arith_result = _resolveArithExpr(s, target_data) - if arith_result is not None: - return arith_result - obj = _resolveFieldObj(s) - if obj: - return obj.getValue(target_data) - return "" + d = data + for key in key_path[:-1]: + d = d.get(key, {}) + if not isinstance(d, dict): + return default + return d.get(key_path[-1], default) -def _resolveAsObject( - expr: str, - target_data: dict -) -> ASObject: +def _assignPath( + data: dict, + key_path: list, + value, +) -> None: """ - Resolve a value expression to an ASObject. - - - If the expression is a registered field name, returns its ASObject directly. - - If the expression is a literal (number, string, DATE(), TIME(), bool), - creates a temporary ASObject with the inferred type. - - This is the key function that ensures all internal operations work - with typed ASObject instances rather than raw Python values. - - Args: - expr (str): The raw expression string from the script. - target_data (dict): The application data dict. - - Returns: - ASObject: A registered or temporary ASObject representing the expression value. + Walk *key_path* into *data* and set *value* at the leaf. """ - s = expr.strip() - obj = _resolveFieldObj(s) - if obj is not None: - return obj - value = _resolveValue(s, target_data) - inferred = _inferType(value, s) - return ASObject._makeTemp(value, inferred) + d = data + for key in key_path[:-1]: + d = d.setdefault(key, {}) + d[key_path[-1]] = value -def _resolveArithExpr( - expr: str, - target_data: dict, - line: int = 0 -): +def _checkType( + var_name: str, + var_type: str, + value, +) -> None: """ - Try to evaluate expr as a two-operand arithmetic expression: left (+|-) right. + Validate that *value* matches the declared variable type. - Each operand is resolved via _resolveAsObject, reusing the full literal / - field / script-variable resolution stack. The left operand's value is - copied into a temporary ASObject and ASOperator.apply() performs the - type-safe calculation on the copy, so the original variable is never - mutated. - - Returns the computed Python value, or None if expr is not a recognised - arithmetic pattern. + Date / Time values arrive as ISO / HH:MM strings (already converted + from Lua native types during the pull phase). + Int / Float / Boolean / String check Python type identity. + Int -> Float widening is allowed. """ - s = expr.strip() - m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) - if not m: - # Fallback for no-space expressions like RESERVE_DATE+1 - # (e.g. when extracted from IF(RESERVE_DATE.EQ.CURRENT_DATE+1)). - # Left operand must be an identifier (letter/underscore start) to - # avoid false-matching date strings like 2026-05-20. - m = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s) - if not m: - return None - left_expr = m.group(1).strip() - op_symbol = m.group(2).strip() - right_expr = m.group(3).strip() - if " + " in left_expr or " - " in left_expr: - return None - if " + " in right_expr or " - " in right_expr: - return None - left_obj = _resolveAsObject(left_expr, target_data) - right_obj = _resolveAsObject(right_expr, target_data) - op = ".ADD." if op_symbol == "+" else ".SUB." - left_val = left_obj.getValue(target_data) - result_type = left_obj.var_type - if left_obj.var_type == "Int" and right_obj.var_type == "Float": - result_type = "Float" - elif left_obj.var_type == "Float" and right_obj.var_type == "Int": - result_type = "Float" - temp = ASObject._makeTemp(left_val, result_type) - ASOperator.apply(temp, right_obj, op, target_data) - return temp.getValue(target_data) - - -def _evaluateCondition( - condition_str: str, - target_data: dict, - line: int = 0 -) -> bool: - """ - Evaluate a condition expression and return a boolean result. - - Supports: - - Boolean literals: .TRUE., .FALSE. - - .AND. / .OR. operators (lowest precedence) - - Parenthesised sub-expressions - - Comparison operators: .EQ., .NEQ., .BGT., .BLT., .BGE., .BLE. - - All operands are resolved as ASObject instances (via _resolveAsObject) - and comparisons are delegated to ASOperator.compare(). - - Args: - condition_str (str): The raw condition expression from the script. - target_data (dict): The application data dict. - - Returns: - bool: The evaluation result. - - Raises: - ValueError: If the expression contains unrecognised tokens or type-mismatched comparisons. - """ - - s = condition_str.strip() - if not s: - return False - or_parts = splitTopLevel(s, ".OR.") - if len(or_parts) > 1: - return any( - _evaluateCondition(p.strip(), target_data, line) - for p in or_parts - ) - and_parts = splitTopLevel(s, ".AND.") - if len(and_parts) > 1: - return all( - _evaluateCondition(p.strip(), target_data, line) - for p in and_parts - ) - s = s.strip() - if s.startswith("(") and s.endswith(")"): - return _evaluateCondition(s[1:-1], target_data, line) - up = s.upper() - if up == ".TRUE.": - return True - if up == ".FALSE.": - return False - for op in ASOperator._COMPARE: - idx = up.find(op.upper()) - if idx < 0: - continue - left_raw = s[:idx].strip() - right_raw = s[idx + len(op):].strip() - try: - left_obj = _resolveAsObject(left_raw, target_data) - right_obj = _resolveAsObject(right_raw, target_data) - return ASOperator.compare(left_obj, right_obj, op, target_data) - except ValueError as e: - raise ValueError(_errPos(line, str(e))) - raise ValueError( - _errPos(line, f"无法识别的条件表达式 '{condition_str}'") - ) - - -def _executeSet( - line_text: str, - target_data: dict, - line: int = 0 -): - """ - Execute a SET statement to assign a value to a field or script variable. - - Parses the line as "SET field_name = value_expr", resolves the value, - and assigns it. If the target field does not exist, a new script variable - is created with an inferred type. - - Args: - line (str): The raw SET line from the script. - target_data (dict): The application data dict. - - Raises: - ValueError: If the value string contains unexpected extra tokens. - """ - - rest = line_text[3:].strip() - eq_idx = rest.find("=") - if eq_idx < 0: + if var_type == "Date": + if not isinstance(value, str): + raise ValueError( + f"Date 类型变量 '{var_name}' 只能接受日期字符串," + f"不能接受 {type(value).__name__} 类型" + ) + date.fromisoformat(value) return - field_name = rest[:eq_idx].strip() - value_str = rest[eq_idx + 1:].strip() - if not field_name: + if var_type == "Time": + if not isinstance(value, str): + raise ValueError( + f"Time 类型变量 '{var_name}' 只能接受时间字符串," + f"不能接受 {type(value).__name__} 类型" + ) + datetime.strptime(value, "%H:%M") return - resolved = _resolveValue(value_str, target_data) - stripped = value_str.strip() - if resolved == "" and stripped not in ("''", '""') and len(stripped.split()) > 1: - try: - resolved = _resolveArithExpr(stripped, target_data, line) - except ValueError as e: - raise ValueError(_errPos(line, str(e))) - if resolved is None: - raise ValueError(_errPos(line, f"SET 值中存在多余内容 '{stripped}'")) - upper_name = field_name.upper().strip() - obj = _FIELD_MAP.get(upper_name) - if not obj: - obj = _SCRIPT_VARS.get(upper_name) - if obj: - try: - obj.setValue(resolved, target_data) - except ValueError as e: - raise ValueError(_errPos(line, str(e))) + if var_type == "Int": + if isinstance(value, bool): + raise ValueError( + f"Int 类型变量 '{var_name}' 不能接受 Boolean 类型的值" + ) + if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)): + raise ValueError( + f"Int 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + ) return - inferred_type = _inferType(resolved, stripped) - new_var = ASObject( - upper_name, - inferred_type, - read_only=False, - is_config=False, - default_value=resolved - ) - _SCRIPT_VARS[upper_name] = new_var - - -def _executeOperation( - line_text: str, - target_data: dict, - line: int = 0 -): - """ - Execute a field operation statement: "FIELD .ADD. N" or "FIELD .SUB. N". - - Resolves the left side as a registered ASObject and the right side - as a temporary numeric ASObject, then delegates to ASOperator.apply(). - - Args: - line (str): The raw operation line from the script (e.g. "RESERVE_DATE .ADD. 1"). - target_data (dict): The application data dict. - - Raises: - ValueError: If the field is unknown, the operand is invalid, - or the type does not support the operation. - """ - - parts = line_text.split() - if len(parts) < 3: + if var_type == "Float": + if isinstance(value, bool): + raise ValueError( + f"Float 类型变量 '{var_name}' 不能接受 Boolean 类型的值" + ) + if not isinstance(value, (int, float)): + raise ValueError( + f"Float 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + ) + return + if var_type == "Boolean": + if not isinstance(value, bool): + raise ValueError( + f"Boolean 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + ) + return + if var_type == "String": + if not isinstance(value, str): + raise ValueError( + f"String 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + ) return - if len(parts) > 3: - raise ValueError( - _errPos(line, f"操作语句中存在多余内容 '{' '.join(parts[3:])}'") - ) - field_name = parts[0].upper().strip() - op = parts[1].upper().strip() - raw_value = parts[2].strip() - target = _resolveFieldObj(field_name) - if target is None: - raise ValueError(_errPos(line, f"未知字段 '{field_name}'")) - try: - operand = _resolveAsObject(raw_value, target_data) - ASOperator.apply(target, operand, op, target_data) - except ValueError as e: - raise ValueError(_errPos(line, str(e))) def addTargetVar( name: str, var_type: str, key_path: list, - display_name: str = None -): + display_name: str = None, +) -> None: """ Register a new target variable bound to a path in the application data dict. - Once registered, the variable can be read, written, and operated on in scripts - using its canonical name or display_name. - Args: name (str): The canonical variable name (e.g. "RESERVE_DATE"). - var_type (str): The type ("Int", "Float", "Boolean", "Date", "Time", "String"). - key_path (list): The nested path into target_data, e.g. ["reserve_info", "date"]. - display_name (str): An optional Chinese alias for use in script conditions. - - Example: - >>> addTargetVar("MY_FIELD", "String", ["custom", "field"], display_name="自定义字段") + var_type (str): "Int" | "Float" | "Boolean" | "Date" | "Time" | "String". + key_path (list): Nested path into target_data, e.g. ["reserve_info", "date"]. + display_name (str): Optional Chinese alias (unused by the engine). """ upper_name = name.upper().strip() - obj = ASObject( - upper_name, - var_type, - is_config=True, - key_path=key_path, - display_name=display_name - ) - _TARGET_VARS[upper_name] = obj + _TARGET_VARS[upper_name] = { + "type": var_type, + "key_path": key_path, + } -class _EngineExecutor(NodeVisitor): +def resetEngine( +) -> None: """ - AST visitor that executes AutoScript against target_data. - Walks the AST and dispatches SET / ADD / SUB operations - via visitScript / visitIf / visitSet / visitOp / visitPass / visitUnrecog. + Reset the engine to its initial state: clear all target variables + and release the Lua runtime. + """ + global _TARGET_VARS, _lua + _TARGET_VARS = {} + _lua = None + + +def _push( + target_data: dict, +) -> None: + """ + Push target_data values into Lua globals. + Date / Time strings are converted to native Lua types (timestamp / minutes). """ - def __init__( - self, - target_data: dict - ): + lua = _getLua() + g = lua.globals() + _toDate = g["_to_date"] + _toTime = g["_to_time"] - super().__init__() - self._target_data = target_data - self._cur_line = 0 + for var_name, info in _TARGET_VARS.items(): + key_path = info["key_path"] + vt = info["type"] + raw = _navigatePath(target_data, key_path) - @property - def _line(self) -> int: - """Return current line number for _errPos calls.""" - - return self._cur_line - - - def _incLine( - self - ): - - self._cur_line += 1 - - - def visitScript( - self, - _node: Script - ): - - for child in _node.body: - child.accept(self) - - - def visitIf( - self, - _node: IfNode - ): - - self._incLine() - if not _node.closed: - raise ValueError(_errPos(self._line, "IF 与 ENDIF / END IF 不匹配")) - matched = _evaluateCondition(_node.condition, self._target_data, self._line) - if matched: - for child in _node.body: - child.accept(self) + if vt == "Date": + if raw and isinstance(raw, str): + try: + date.fromisoformat(raw.strip()) + except (ValueError, AttributeError): + raw = "2099-01-01" + else: + raw = "2099-01-01" + g[var_name] = _toDate(raw) + elif vt == "Time": + if raw and isinstance(raw, str): + try: + datetime.strptime(raw.strip(), "%H:%M") + except (ValueError, AttributeError): + raw = "00:00" + else: + raw = "00:00" + g[var_name] = _toTime(raw) else: - executed = False - for elif_node in _node.elif_branches: - self._incLine() - if _evaluateCondition(elif_node.condition, self._target_data, self._line): - for child in elif_node.body: - child.accept(self) - executed = True - break - if not executed and _node.else_body: - self._incLine() - for child in _node.else_body: - child.accept(self) + if raw is None: + raw = "" if vt == "String" else 0 if vt == "Int" else 0.0 if vt == "Float" else False + g[var_name] = raw - def visitSet( - self, - _node: SetNode - ): +def _pull( + target_data: dict, +) -> None: + """ + Pull Lua global values back into target_data. + Date / Time native types are converted back to ISO / HH:MM strings. + """ - self._incLine() - full_line = f"SET {_node.target} = {_node.value}" - _executeSet(full_line, self._target_data, self._line) + lua = _getLua() + g = lua.globals() + _fromDate = g["_from_date"] + _fromTime = g["_from_time"] - - def visitOp( - self, - _node: OpNode - ): - - self._incLine() - op_upper = _node.op_type.upper() - full_line = f"{_node.target} .{op_upper}. {_node.value}" - _executeOperation(full_line, self._target_data, self._line) - - - def visitPass( - self, - _node: PassNode - ): - - self._incLine() - - - def visitUnrecog( - self, - _node: UnrecogNode - ): - - self._incLine() - upper = _node.raw_line.upper().strip() - if upper.startswith("IF"): - paren_open = upper.find("(") - if paren_open < 0: - raise ValueError(_errPos(self._line, "IF 缺少左括号")) - depth = 1 - for ci in range(paren_open + 1, len(upper)): - if upper[ci] == "(": - depth += 1 - elif upper[ci] == ")": - depth -= 1 - if depth == 0: - remaining = upper[ci + 1:].strip() - if remaining and remaining != "THEN": - raise ValueError(_errPos(self._line, f"IF 条件后存在多余内容 '{remaining}'")) - break - if depth > 0: - raise ValueError(_errPos(self._line, "IF 缺少右括号")) - elif upper.startswith("ELSE IF"): - paren_open = upper.find("(") - if paren_open < 0: - raise ValueError(_errPos(self._line, "ELSE IF 缺少左括号")) - raise ValueError(_errPos(self._line, f"无法识别的语法 '{_node.raw_line}'")) + for var_name, info in _TARGET_VARS.items(): + try: + lua_val = g[var_name] + except (KeyError, AttributeError): + continue + vt = info["type"] + if vt == "Date": + lua_val = _fromDate(lua_val) + elif vt == "Time": + lua_val = _fromTime(lua_val) + elif vt == "Float" and isinstance(lua_val, int) and not isinstance(lua_val, bool): + lua_val = float(lua_val) + _checkType(var_name, vt, lua_val) + _assignPath(target_data, info["key_path"], lua_val) def execute( script_text: str, - target_data: dict -): + target_data: dict, +) -> None: """ - Execute an AutoScript on the given target data. + Execute an AutoScript (Lua) on the given target data. - Parses the script into an AST via ASTokenizer.parse(), - then walks the tree with a visitor to evaluate conditions - and dispatch SET / ADD / SUB operations. + The script runs in a sandboxed Lua environment with target variables + exposed as globals. The following helpers are available as Lua functions: - Args: - script_text (str): The AutoScript source code. - target_data (dict): The application data dict to read from / write to. + date(y, m, d) -> timestamp (os.time seconds) + time(h, m) -> minutes since midnight (0-1439) + CURRENT_DATE() -> today's timestamp + CURRENT_TIME() -> current minutes since midnight + date_add(ts, n) -> ts + n * 86400 + time_add(m, n) -> (m + n * 60) % 1440 + + Date and Time values are native Lua numbers during execution. + Arithmetic (+, -) and comparisons (<, <=, ==, ~=, >, >=) work + with strong type safety — no implicit string coercion. Raises: - ValueError: On syntax errors, unbalanced IF/END IF, unknown fields, etc. + ValueError: On Lua compilation/runtime errors or type mismatches. """ - _buildFieldMap() if not script_text or not script_text.strip(): return - ast = ASTokenizer.parse(script_text) - if not ast.body: - return - executor = _EngineExecutor(target_data) - ast.accept(executor) + _push(target_data) + try: + _getLua().execute(script_text) + _pull(target_data) + except Exception as e: + raise ValueError(f"AutoScript 执行错误: {e}") diff --git a/src/autoscript/ASObject.py b/src/autoscript/ASObject.py deleted file mode 100644 index 32302bb..0000000 --- a/src/autoscript/ASObject.py +++ /dev/null @@ -1,269 +0,0 @@ -# -*- 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 re -from datetime import ( - datetime, - date, - time -) - - -__all__ = [ - "ASObject", - "_META_VARS", - "_inferType" -] - - -# Default values for each supported type when no value is present -_TYPE_DEFAULTS = { - "Int": 0, - "Float": 0.0, - "Boolean": False, - "Date": None, - "Time": None, - "String": "" -} - -# Mapping from Python type to AutoScript type name for error messages -_PYTHON_TO_AS_TYPE = { - bool: "Boolean", - int: "Int", - float: "Float", - str: "String", - date: "Date", - time: "Time", -} - -def _asTypeName( - value -) -> str: - return _PYTHON_TO_AS_TYPE.get(type(value), "UnknowType") - - -class ASObject: - """ - Represents a variable object used throughout the AutoScript engine. - - An ASObject can be a meta variable (read-only, e.g. CURRENT_DATE), - a target variable (bound to a config data dict via key_path), - or a script variable (free-form, stored internally). - - Args: - name (str): The canonical name of the variable (case-insensitive). - var_type (str): One of "Int", "Float", "Boolean", "Date", "Time", "String". - read_only (bool): Whether the variable is read-only (default: False). - is_config (bool): Whether the variable maps to a target_data path (default: False). - key_path (list): The nested key path into target_data, e.g. ["reserve_info", "date"]. - default_value: The fallback value when no target_data value is found. - display_name (str): An alias for use in script conditions and assignments. - - Example: - >>> obj = ASObject("MY_DATE", "Date", is_config=True, - ... key_path=["reserve_info", "date"], - ... display_name="预约日期") - >>> obj.getValue({"reserve_info": {"date": "2026-05-01"}}) - datetime.date(2026, 5, 1) - """ - - _TEMP_COUNTER = 0 - - def __init__( - self, - name: str, - var_type: str, *, - read_only: bool = False, - is_config: bool = False, - key_path: list = None, - default_value=None, - display_name: str = None - ): - - self.name = name - self.var_type = var_type - self.read_only = read_only - self.is_config = is_config - self.key_path = key_path or [] - self._value = default_value - self.display_name = display_name - - @classmethod - def _makeTemp( - cls, - value, - inferred_type: str - ): - """ - Create a temporary unnamed ASObject from a literal value. - - Temporary objects are used for inline script literals (e.g. 42, - 'hello', DATE(2026-01-01)) so they can participate in typed - operations alongside registered variables. - - Args: - value: The resolved Python value. - inferred_type (str): The AutoScript type name. - - Returns: - ASObject: A temporary, non-config, read-write ASObject. - """ - - cls._TEMP_COUNTER += 1 - return cls( - f"__TMP_{cls._TEMP_COUNTER}", - inferred_type, - read_only=False, - is_config=False, - default_value=value - ) - - - def _typeEmpty( - self - ): - """ - Return the type-appropriate empty / default value. - """ - - return _TYPE_DEFAULTS.get(self.var_type, "") - - - def getValue( - self, - target_data: dict = None - ): - """ - Retrieve the current value of this variable. - - For read-only variables (CURRENT_DATE, CURRENT_TIME), returns the - live datetime. For config variables, traverses the key_path into - target_data and parses Date/Time strings. Otherwise returns the - internal _value. - - Args: - target_data (dict): The application data dict (required for config vars). - - Returns: - The resolved value, or a type-appropriate default if missing. - """ - - if self.read_only: - if self.name == "CURRENT_DATE": - return datetime.now().date() - if self.name == "CURRENT_TIME": - return datetime.now().time() - return self._value - if self.is_config and target_data is not None and self.key_path: - d = target_data - for key in self.key_path[:-1]: - d = d.get(key, {}) - if not isinstance(d, dict): - return self._typeEmpty() - raw = d.get(self.key_path[-1]) - if raw is None: - return self._typeEmpty() - if self.var_type == "Date" and isinstance(raw, str): - try: - return datetime.strptime(raw, "%Y-%m-%d").date() - except ValueError: - return self._typeEmpty() - if self.var_type == "Time" and isinstance(raw, str): - try: - return datetime.strptime(raw, "%H:%M").time() - except ValueError: - return self._typeEmpty() - return raw - return self._value - - - def setValue( - self, - value, - target_data: dict = None - ): - """ - Assign a new value to this variable, with strict type checking. - - AutoScript is strongly typed: only values whose Python type matches the - declared variable type are accepted. Int->Float widening is allowed; - all other cross-type assignments raise ValueError. - - Args: - value: The value to assign. - target_data (dict): The application data dict (required for config vars). - - Raises: - ValueError: If the variable is read-only or value type mismatches the variable type. - """ - - if self.read_only: - raise ValueError(f"不能修改只读变量 '{self.name}'") - vt = self.var_type - value_type = _asTypeName(value) - if vt != value_type and not (vt == "Float" and value_type == "Int"): - raise ValueError( - f"{vt} 类型变量 '{self.name}' 不能接受 {value_type} 类型的值" - ) - - if self.is_config: - if self.var_type == "Date" and isinstance(value, date): - value = value.strftime("%Y-%m-%d") - if self.var_type == "Time" and isinstance(value, time): - value = value.strftime("%H:%M") - if self.is_config and target_data is not None and self.key_path: - d = target_data - for key in self.key_path[:-1]: - d = d.setdefault(key, {}) - d[self.key_path[-1]] = value - else: - self._value = value - - -# Built-in read-only meta variables available to all scripts -_META_VARS = { - "CURRENT_DATE": ASObject("CURRENT_DATE", "Date", read_only=True, display_name="当前日期"), - "CURRENT_TIME": ASObject("CURRENT_TIME", "Time", read_only=True, display_name="当前时间"), -} - - -def _inferType( - value, - raw_expr: str = None -) -> str: - """ - Infer the ASObject type string from a Python value or raw expression. - - When the Python type is ambiguous (e.g. int can be Int or a component - of Date), the raw_expr is used as a hint. - - Args: - value: The resolved Python value. - raw_expr (str): The original expression string from the script (optional). - - Returns: - str: One of "Boolean", "Int", "Float", "Date", "Time", "String". - """ - - if isinstance(value, bool): - return "Boolean" - if isinstance(value, int): - return "Int" - if isinstance(value, float): - return "Float" - if isinstance(value, date): - return "Date" - if isinstance(value, time): - return "Time" - if raw_expr: - if re.match(r"^DATE\(\d{4}-\d{2}-\d{2}\)$", raw_expr, re.IGNORECASE): - return "Date" - if re.match(r"^TIME\(\d{1,2}:\d{2}\)$", raw_expr, re.IGNORECASE): - return "Time" - return "String" diff --git a/src/autoscript/ASObserver.py b/src/autoscript/ASObserver.py deleted file mode 100644 index 7fa3138..0000000 --- a/src/autoscript/ASObserver.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- 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. -""" - - -class ParsingObserver: - """ - Base observer for AutoScript parsing events. - - Subclass and override the relevant methods to react to - tokenization / parsing events produced by ASTokenizer. - This is the core abstraction that lets pre-check and - orchestration modules subscribe to the same parsing pipeline. - """ - - def onParseStart( - self, - script_text: str - ): - """ - Called when tokenization of a new script begins. - - Args: - script_text (str): The full script text being parsed. - """ - pass - - def onTokenParsed( - self, - kind: str | None, - data, - line_num: int, - raw_line: str - ): - """ - Called after each script line has been classified as a token. - - Args: - kind (str | None): Token kind (K_IF, K_ELSE_IF, K_ELSE, - K_ENDIF, K_SET, K_ADD, K_SUB) or - None if unrecognised. - data: Token payload — condition string for IF/ELSE IF, - (target, value) tuple for SET/ADD/SUB, - None for ELSE/ENDIF/unrecognised. - line_num (int): 1-based line number. - raw_line (str): The stripped raw line text. - """ - pass - - def onParseComplete( - self, - statements: list - ): - """ - Called when flat tokenization is complete. - - Args: - statements (list[Stmt]): The list of parsed Stmt objects. - """ - pass - - def onASTReady( - self, - ast - ): - """ - Called when full AST construction is complete (after parse()). - - Args: - ast (Script): The root Script AST node. - """ - pass diff --git a/src/autoscript/ASOperator.py b/src/autoscript/ASOperator.py deleted file mode 100644 index 9d233ba..0000000 --- a/src/autoscript/ASOperator.py +++ /dev/null @@ -1,191 +0,0 @@ -# -*- 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. -""" -from datetime import ( - datetime, - timedelta, - date, - time -) - -from .ASObject import ASObject - - -__all__ = ["ASOperator", "ARITH_TYPES", "COMPARISON_OPERATORS"] - - -class ASOperator: - """ - Centralised type-safe operations for AutoScript engine types. - - All arithmetic (ADD / SUB) and comparison operators are routed through - this class, which dispatches to the correct Python-level logic based on - the ASObject's var_type. This keeps type-specific branching in one - place instead of scattering it across the engine. - - Args: - op (str): One of ".ADD.", ".SUB.", ".EQ.", ".NEQ.", ".BGT.", - ".BLT.", ".BGE.", ".BLE.". - - Example: - >>> obj = ASObject("X", "Int", default_value=10) - >>> ASOperator.apply(obj, ASObject._makeTemp(5, "Int"), ".ADD.", None) - >>> obj.getValue() - 15 - >>> ASOperator.compare( - ... obj, - ... ASObject._makeTemp(15, "Int"), - ... ".EQ.", None - ... ) - True - """ - - _COMPARE = { - ".EQ." : lambda a, b: a == b, - ".NEQ.": lambda a, b: a != b, - ".BGT.": lambda a, b: a > b, - ".BLT.": lambda a, b: a < b, - ".BGE.": lambda a, b: a >= b, - ".BLE.": lambda a, b: a <= b, - } - _ARITH_TYPES = {"Date", "Time", "Int", "Float"} - # Comparison-compatible type groups - _COMPATIBLE_GROUPS = [ - {"String"}, - {"Boolean"}, - {"Int", "Float"}, - {"Date"}, - {"Time"}, - ] - - @classmethod - def apply( - cls, - target: ASObject, - operand: ASObject, - op: str, - target_data: dict - ): - """ - Apply ADD or SUB to a target ASObject, modifying it in place. - - Args: - target (ASObject): The variable to modify. - operand (ASObject): The operand (numeric value for Date/Time/Int/Float). - op (str): ".ADD." or ".SUB.". - target_data (dict): Application data dict (passed through to getValue/setValue). - - Raises: - ValueError: If the type does not support the operation or values are invalid. - """ - - tp = target.var_type - op_tp = operand.var_type - if tp not in cls._ARITH_TYPES: - raise ValueError(f"'{tp}' 类型字段不支持操作运算") - if op_tp not in ("Int", "Float"): - raise ValueError(f"操作数类型 '{op_tp}' 不能用于运算,需要数值类型 (Int / Float)") - if tp in ("Date", "Time") and op_tp != "Int": - raise ValueError(f"'{tp}' 类型的加减法操作数必须为 Int 类型,不允许 Float") - target_val = target.getValue(target_data) - if target_val is None: - raise ValueError(f"'{target.name}' 的值为空,无法进行运算") - op_name = "ADD" if op == ".ADD." else "SUB" if op == ".SUB." else None - if op_name is None: - raise ValueError(f"不支持的操作 '{op}'") - sign = 1 if op == ".ADD." else -1 - cls._arithBinary(target, target_val, operand, target_data, sign, op_name) - - @classmethod - def _arithBinary( - cls, - target: ASObject, - target_val, - operand: ASObject, - target_data: dict, - sign: int, - op_name: str = "" - ): - """Apply arithmetic per type.""" - - tp = target.var_type - raw_op = operand.getValue(target_data) - - if tp == "Date": - if not isinstance(target_val, date): - raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期") - new_val = target_val + timedelta(days=int(raw_op)) * sign - elif tp == "Time": - if not isinstance(target_val, time): - raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间") - delta = timedelta(hours=int(raw_op)) * sign - dt = datetime.combine(datetime.today(), target_val) + delta - new_val = dt.time() - elif tp == "Int": - new_val = int(target_val) + int(raw_op)*sign - elif tp == "Float": - new_val = float(target_val) + float(raw_op)*sign - else: - raise ValueError(f"'{tp}' 类型不支持 {op_name} 操作") - target.setValue(new_val, target_data) - - @classmethod - def compare( - cls, - left: ASObject, - right: ASObject, - op: str, - target_data: dict - ) -> bool: - """ - Compare two ASObjects using the given comparison operator. - - Args: - left (ASObject): Left-hand side. - right (ASObject): Right-hand side. - op (str): One of ".EQ.", ".NEQ.", ".BGT.", ".BLT.", ".BGE.", ".BLE.". - target_data (dict): Application data dict. - - Returns: - bool: The comparison result. - - Raises: - ValueError: If the types are incompatible for comparison. - """ - - cmp_func = cls._COMPARE.get(op) - if cmp_func is None: - raise ValueError(f"未知的比较操作 '{op}'") - left_tp = left.var_type - right_tp = right.var_type - if left_tp != right_tp: - same_group = any( - left_tp in g and right_tp in g - for g in cls._COMPATIBLE_GROUPS - ) - if not same_group: - raise ValueError( - f"类型不兼容: 无法将 '{left.name}' ({left_tp}) " - f"与 '{right.name}' ({right_tp}) 进行比较" - ) - left_val = left.getValue(target_data) - right_val = right.getValue(target_data) - try: - return cmp_func(left_val, right_val) - except TypeError: - raise ValueError( - f"无法比较 '{left.name}' ({left.var_type}) " - f"与 '{right.name}' ({right.var_type})" - ) - - -# Public constants -# may be used by the GUI orchestration dialog. -ARITH_TYPES = ASOperator._ARITH_TYPES -COMPARISON_OPERATORS = set(ASOperator._COMPARE.keys()) diff --git a/src/autoscript/ASTokenizer.py b/src/autoscript/ASTokenizer.py deleted file mode 100644 index d82ddc7..0000000 --- a/src/autoscript/ASTokenizer.py +++ /dev/null @@ -1,584 +0,0 @@ -# -*- 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 re - - -__all__ = [ - "ASTokenizer", - "Stmt", - "Script", - "IfNode", - "ElifNode", - "SetNode", - "OpNode", - "PassNode", - "UnrecogNode", - "NodeVisitor", - "LineStrategy" -] - - -# Token kind constants -K_IF = "IF" -K_ELSE_IF = "ELSE IF" -K_ELSE = "ELSE" -K_ENDIF = "ENDIF" -K_SET = "SET" -K_ADD = "ADD" -K_SUB = "SUB" -K_PASS = "PASS" - -# Op-type constants -OP_SET = "set" -OP_ADD = "add" -OP_SUB = "sub" - -# Compiled line patterns -_RE_IF = re.compile(r"^IF\((.+)\)(?:\s+THEN\s*)?$", re.IGNORECASE) -_RE_ELSE_IF = re.compile(r"^ELSE\s+IF\((.+)\)(?:\s+THEN\s*)?$", re.IGNORECASE) -_RE_ELSE = re.compile(r"^ELSE\s*$", re.IGNORECASE) -_RE_ENDIF = re.compile(r"^(ENDIF|END IF)$", re.IGNORECASE) -_RE_SET = re.compile(r"^SET\s+(\w+)\s*=\s*(.+)$", re.IGNORECASE) -_RE_ADD = re.compile(r"^(\w+)\s+\.ADD\.\s+(-?\d+(?:\.\d+)?|\w+)$", re.IGNORECASE) -_RE_SUB = re.compile(r"^(\w+)\s+\.SUB\.\s+(-?\d+(?:\.\d+)?|\w+)$", re.IGNORECASE) -_RE_PASS = re.compile(r"^\s*PASS\s*$", re.IGNORECASE) - - -class Script: - """ - Root AST node for an entire AutoScript. - Contains an ordered list of top-level statement nodes. - """ - - def __init__( - self, - body: list = None - ): - - self.body = body or [] - - def accept( - self, - visitor - ): - - return visitor.visitScript(self) - - -class IfNode: - """ - IF conditional block with optional ELSE IF / ELSE branches. - - Attributes: - condition (str): Raw condition expression. - body (list): Statements executed when condition is true. - elif_branches (list[ElifNode]): ELSE IF branches in order. - else_body (list): Statements executed for the ELSE branch. - closed (bool): Whether this IF has a matching ENDIF token. - """ - - def __init__( - self, - condition: str = "", - body: list = None, - elif_branches: list = None, - else_body: list = None, - closed: bool = True - ): - - self.condition = condition - self.body = body or [] - self.elif_branches = elif_branches or [] - self.else_body = else_body or [] - self.closed = closed - - def accept( - self, - visitor - ): - - return visitor.visitIf(self) - - -class ElifNode: - """ - ELSE IF branch within an IfNode. - """ - - def __init__( - self, - condition: str = "", - body: list = None - ): - - self.condition = condition - self.body = body or [] - - -class SetNode: - """ - SET assignment statement. - """ - - def __init__( - self, - target: str = "", - value: str = "" - ): - - self.target = target - self.value = value - - def accept( - self, - visitor - ): - - return visitor.visitSet(self) - - -class OpNode: - """ - .ADD. / .SUB. operation statement. - """ - - def __init__( - self, - op_type: str = "", - target: str = "", - value: str = "" - ): - - self.op_type = op_type - self.target = target - self.value = value - - def accept( - self, - visitor - ): - - return visitor.visitOp(self) - - -class PassNode: - """ - PASS no-op statement. - """ - - def accept( - self, - visitor - ): - - return visitor.visitPass(self) - - -class UnrecogNode: - """ - Unrecognised line preserved for downstream error reporting. - """ - - def __init__( - self, - raw_line: str = "" - ): - - self.raw_line = raw_line - - def accept( - self, - visitor - ): - - return visitor.visitUnrecog(self) - - -class NodeVisitor: - """ - Base visitor for the AutoScript AST. - - Subclass and override visit* methods to implement - custom traversal logic. Default walks tree depth-first. - """ - - def visitScript( - self, - _node: Script - ): - - for child in _node.body: - child.accept(self) - - def visitIf( - self, - _node: IfNode - ): - - for child in _node.body: - child.accept(self) - for elif_node in _node.elif_branches: - for child in elif_node.body: - child.accept(self) - for child in _node.else_body: - child.accept(self) - - def visitSet( - self, - _node: SetNode - ): - - pass - - def visitOp( - self, - _node: OpNode - ): - - pass - - def visitPass( - self, - _node: PassNode - ): - - pass - - def visitUnrecog( - self, - _node: UnrecogNode - ): - - pass - - -class LineStrategy: - """ - Encapsulates a regex pattern and its data-extraction handler. - Used by the tokenizer to classify a single line. - """ - - def __init__( - self, - pattern, - handler - ): - - self.pattern = pattern - self.handler = handler - - def match( - self, - line: str - ): - - m = self.pattern.match(line) - if m: - return self.handler(m) - return None - - -# Strategy instances — one per recognised AutoScript syntax form -_LINE_STRATEGIES = [ - LineStrategy(_RE_IF, lambda m: (K_IF, m.group(1))), - LineStrategy(_RE_ELSE_IF, lambda m: (K_ELSE_IF, m.group(1))), - LineStrategy(_RE_ELSE, lambda m: (K_ELSE, None)), - LineStrategy(_RE_ENDIF, lambda m: (K_ENDIF, None)), - LineStrategy(_RE_SET, lambda m: (K_SET, (m.group(1).strip(), m.group(2).strip()))), - LineStrategy(_RE_ADD, lambda m: (K_ADD, (m.group(1).strip(), m.group(2).strip()))), - LineStrategy(_RE_SUB, lambda m: (K_SUB, (m.group(1).strip(), m.group(2).strip()))), - LineStrategy(_RE_PASS, lambda m: (K_PASS, None)), -] - - -class Stmt: - """ - Flat statement container, backward-compatible with the original - tokenize() output and the orchestration dialog's _classifyLine. - """ - - def __init__( - self, - kind: str | None = None, - condition: str | None = None, - target: str | None = None, - value: str | None = None, - op_type: str | None = None, - raw_line: str = "" - ): - - self.kind = kind - self.condition = condition - self.target = target - self.value = value - self.op_type = op_type - self.raw_line = raw_line - - -class ASTokenizer: - """ - Tokenizer / parser for the AutoScript DSL. - - Main class-level entry points (engine-facing): - - classifyLine(line) — single-line classifier. - - tokenize(script) — flat Stmt list. - - parse(script) — structured AST (Script root). - - Observer-enabled API (used by pre-check & orchestration): - >>> obs = ScriptPrecheckObserver() - >>> stmts = ASTokenizer.tokenizeWithObservers(script, [obs]) - """ - - @classmethod - def _notifyObservers( - cls, - observers: list, - method: str, - *args - ): - - for obs in observers: - getattr(obs, method)(*args) - - @classmethod - def _matchLine( - cls, - stripped: str - ): - - for strategy in _LINE_STRATEGIES: - result = strategy.match(stripped) - if result: - return result - return (None, None) - - @classmethod - def _buildStmt( - cls, - stripped: str, - kind: str | None, - data - ) -> Stmt: - - stmt = Stmt(kind=kind, raw_line=stripped) - if kind == K_IF or kind == K_ELSE_IF: - stmt.condition = data - elif kind == K_SET: - stmt.target, stmt.value = data - stmt.op_type = OP_SET - elif kind == K_ADD: - stmt.target, stmt.value = data - stmt.op_type = OP_ADD - elif kind == K_SUB: - stmt.target, stmt.value = data - stmt.op_type = OP_SUB - return stmt - - @classmethod - def _stripComment( - cls, - line: str - ) -> str: - - in_single = False - in_double = False - i = 0 - while i < len(line): - ch = line[i] - if ch == "'" and not in_double: - if i + 1 < len(line) and line[i + 1] == "'": - i += 2 - continue - in_single = not in_single - elif ch == '"' and not in_single: - in_double = not in_double - elif ch == "/" and i + 1 < len(line) and line[i + 1] == "/" and not in_single and not in_double: - return line[:i].rstrip() - i += 1 - return line - - @classmethod - def _tokenizeImpl( - cls, - script: str - ) -> list: - - statements = [] - for raw_line in script.split("\n"): - code = cls._stripComment(raw_line.strip()) - if not code: - continue - kind, data = cls._matchLine(code) - statements.append(cls._buildStmt(code, kind, data)) - return statements - - @classmethod - def _parseTokens( - cls, - tokens: list - ) -> Script: - - body = [] - i = 0 - while i < len(tokens): - tok = tokens[i] - kind = tok.kind - - if kind == K_IF: - node, consumed = cls._parseIfBlock(tokens, i) - body.append(node) - i += consumed - elif kind in (K_ELSE_IF, K_ELSE, K_ENDIF): - i += 1 - elif kind == K_SET: - body.append(SetNode(target=tok.target, value=tok.value)) - i += 1 - elif kind in (K_ADD, K_SUB): - body.append(OpNode( - op_type=tok.op_type, - target=tok.target, - value=tok.value - )) - i += 1 - elif kind == K_PASS: - body.append(PassNode()) - i += 1 - else: - body.append(UnrecogNode(raw_line=tok.raw_line)) - i += 1 - return Script(body=body) - - @classmethod - def classifyLine( - cls, - stripped: str - ): - - kind, data = cls._matchLine(stripped) - if kind is None or kind == K_PASS: - return None - return (kind, data) - - @classmethod - def tokenize( - cls, - script: str - ) -> list: - - return cls._tokenizeImpl(script) - - @classmethod - def parse( - cls, - script: str - ) -> Script: - - return cls._parseTokens(cls._tokenizeImpl(script)) - - @classmethod - def tokenizeWithObservers( - cls, - script: str, - observers: list - ) -> list: - """ - Tokenize and notify observers for each classified line. - - Fires onParseStart, onTokenParsed, and onParseComplete - events to each observer. This is the single tokenization - pipeline shared by pre-check and orchestration modules. - """ - - cls._notifyObservers(observers, "onParseStart", script) - statements = [] - for i, raw_line in enumerate(script.split("\n"), 1): - code = cls._stripComment(raw_line.strip()) - if not code: - continue - kind, data = cls._matchLine(code) - cls._notifyObservers(observers, "onTokenParsed", kind, data, i, code) - statements.append(cls._buildStmt(code, kind, data)) - cls._notifyObservers(observers, "onParseComplete", statements) - return statements - - @classmethod - def parseWithObservers( - cls, - script: str, - observers: list - ) -> Script: - """ - Parse and notify observers throughout the pipeline. - - Calls tokenizeWithObservers (which fires per-token events), - then builds the AST and fires onASTReady. - """ - - tokens = cls.tokenizeWithObservers(script, observers) - ast = cls._parseTokens(tokens) - cls._notifyObservers(observers, "onASTReady", ast) - return ast - - @classmethod - def _parseIfBlock( - cls, - tokens: list, - start: int - ): - - first = tokens[start] - node = IfNode(condition=first.condition or "") - body = [] - elif_branches = [] - else_body = [] - current_target = body - i = start + 1 - - while i < len(tokens): - tok = tokens[i] - kind = tok.kind - if kind == K_IF: - sub_node, consumed = cls._parseIfBlock(tokens, i) - current_target.append(sub_node) - i += consumed - elif kind == K_ELSE_IF: - elif_branches.append(ElifNode(condition=tok.condition or "")) - current_target = elif_branches[-1].body - i += 1 - elif kind == K_ELSE: - else_body = [] - current_target = else_body - i += 1 - elif kind == K_ENDIF: - node.body = body - node.elif_branches = elif_branches - node.else_body = else_body - return (node, i - start + 1) - elif kind == K_SET: - current_target.append(SetNode(target=tok.target, value=tok.value)) - i += 1 - elif kind in (K_ADD, K_SUB): - current_target.append(OpNode( - op_type=tok.op_type, - target=tok.target, - value=tok.value - )) - i += 1 - elif kind == K_PASS: - current_target.append(PassNode()) - i += 1 - else: - current_target.append(UnrecogNode(raw_line=tok.raw_line)) - i += 1 - node.body = body - node.elif_branches = elif_branches - node.else_body = else_body - node.closed = False - return (node, i - start) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index d1a474d..480d09e 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -1,43 +1,30 @@ +# -*- coding: utf-8 -*- """ - AutoScript module for the AutoLibrary project. - A lightweight scripting DSL for preprocessing user reservation data. +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. """ -from autoscript.ASTokenizer import ( - ASTokenizer, - Stmt, - ElifNode, - Script, - IfNode, - SetNode, - OpNode, -) from autoscript.ASEngine import ( execute, addTargetVar, - splitTopLevel, + resetEngine, + META_VARS, ) -from autoscript.ASObject import _META_VARS as META_VARS -from autoscript.ASObserver import ParsingObserver __all__ = [ "execute", "addTargetVar", - "splitTopLevel", + "resetEngine", "registerDefaultTargetVars", "buildMockTargetData", "META_VARS", "ALL_VARIABLES", "_TARGET_VAR_DEFS", "_MOCK_TYPE_VALUES", - "ASTokenizer", - "Stmt", - "Script", - "IfNode", - "SetNode", - "OpNode", - "ElifNode", - "ParsingObserver", ] @@ -56,8 +43,8 @@ ALL_VARIABLES = { display_name: (name, var_type) for name, var_type, _, display_name in _TARGET_VAR_DEFS } | { - obj.display_name: (obj.name, obj.var_type) - for obj in META_VARS.values() + v["display"]: (v["name"], v["type"]) + for v in META_VARS.values() } _MOCK_TYPE_VALUES = { "String": "__mock__", From 3cea7df736668a4613ed0604e097a758de388ae2 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 21 May 2026 18:22:49 +0800 Subject: [PATCH 22/49] =?UTF-8?q?refactor(gui):=20=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E7=AA=97=E5=8F=A3=E9=80=82=E9=85=8D=20Lua=20?= =?UTF-8?q?=E5=BC=95=E6=93=8E=E6=96=B0=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAutoScriptEditDialog.py | 86 +++--- src/gui/ALAutoScriptOrchDialog/_blocks.py | 28 +- src/gui/ALAutoScriptOrchDialog/_dialog.py | 18 +- src/gui/ALAutoScriptOrchDialog/_helpers.py | 326 ++++++++++++--------- src/gui/ALAutoScriptOrchDialog/_widgets.py | 51 ++-- 5 files changed, 296 insertions(+), 213 deletions(-) diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 945695f..0884f82 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -55,6 +55,9 @@ from autoscript import ( class ALScriptHighlighter(QSyntaxHighlighter): + """ + Syntax highlighter for Lua-based AutoScript. + """ def __init__( self, @@ -67,26 +70,32 @@ class ALScriptHighlighter(QSyntaxHighlighter): keywordFmt = QTextCharFormat() keywordFmt.setForeground(QColor("#569CD6")) keywordFmt.setFontWeight(QFont.Weight.Bold) - for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF", - "SET", "PASS", "THEN"]: - pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b" - self._rules.append((pattern, keywordFmt)) - opFmt = QTextCharFormat() - opFmt.setForeground(QColor("#C586C0")) - opFmt.setFontWeight(QFont.Weight.Normal) - for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.", - r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\.", - r"\.AND\.", r"\.OR\."]: - self._rules.append((op, opFmt)) + for kw in [ + "if", "elseif", "else", "end", "then", + "and", "or", "not", + "local", "function", "return", "nil", + ]: + self._rules.append((r"\b" + kw + r"\b", keywordFmt)) boolFmt = QTextCharFormat() boolFmt.setForeground(QColor("#4FC1FF")) boolFmt.setFontWeight(QFont.Weight.Bold) - self._rules.append((r"\.TRUE\.", boolFmt)) - self._rules.append((r"\.FALSE\.", boolFmt)) + self._rules.append((r"\btrue\b", boolFmt)) + self._rules.append((r"\bfalse\b", boolFmt)) + cmpFmt = QTextCharFormat() + cmpFmt.setForeground(QColor("#C586C0")) + cmpFmt.setFontWeight(QFont.Weight.Normal) + for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]: + self._rules.append((op, cmpFmt)) + arithFmt = QTextCharFormat() + arithFmt.setForeground(QColor("#C586C0")) + arithFmt.setFontWeight(QFont.Weight.Normal) + for op in [r"\+", r"-", r"\*", r"/", r"\.\."]: + self._rules.append((op, arithFmt)) funcFmt = QTextCharFormat() funcFmt.setForeground(QColor("#DCDCAA")) funcFmt.setFontWeight(QFont.Weight.Normal) - self._rules.append((r"\b(?:DATE|TIME)\b", funcFmt)) + for fn in ["CURRENT_DATE", "CURRENT_TIME", "date_add", "time_add"]: + self._rules.append((r"\b" + fn + r"\b", funcFmt)) varFmt = QTextCharFormat() varFmt.setForeground(QColor("#9CDCFE")) varFmt.setFontWeight(QFont.Weight.Normal) @@ -96,15 +105,16 @@ class ALScriptHighlighter(QSyntaxHighlighter): strFmt = QTextCharFormat() strFmt.setForeground(QColor("#CE9178")) strFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r'"[^"]*"', strFmt)) self._rules.append((r"'[^']*'", strFmt)) numFmt = QTextCharFormat() numFmt.setForeground(QColor("#B5CEA8")) numFmt.setFontWeight(QFont.Weight.Normal) - self._rules.append((r"\b\d+\b", numFmt)) + self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt)) commentFmt = QTextCharFormat() commentFmt.setForeground(QColor("#6A9955")) commentFmt.setFontItalic(True) - self._rules.append((r"//[^\n]*", commentFmt)) + self._rules.append((r"--[^\n]*", commentFmt)) def highlightBlock( @@ -257,15 +267,15 @@ class ALAutoScriptEditDialog(QDialog): basicLayout.setContentsMargins(4, 4, 4, 4) basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) controlButtons = [ - ("IF", "IF()\n \nEND IF"), - ("ELSE IF", "ELSE IF()\n "), - ("ELSE", "ELSE"), - ("END IF", "END IF"), - ("PASS", "PASS"), + ("if", "if then\n \nend"), + ("elseif", "elseif then\n "), + ("else", "else"), + ("end", "end"), + ("-- pass", "-- pass"), ] self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5) assignButtons = [ - ("SET", "SET = "), + ("=", " = "), ] self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1) tabWidget.addTab(basicWidget, "基本语法") @@ -275,22 +285,22 @@ class ALAutoScriptEditDialog(QDialog): operatorLayout.setContentsMargins(4, 4, 4, 4) operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) arithmeticButtons = [ - (".ADD.", ".ADD."), - (".SUB.", ".SUB."), + ("+", " + "), + ("-", " - "), ] self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2) compareButtons = [ - (".EQ.", ".EQ."), - (".NEQ.", ".NEQ."), - (".BGT.", ".BGT."), - (".BLT.", ".BLT."), - (".BGE.", ".BGE."), - (".BLE.", ".BLE."), + ("==", " == "), + ("~=", " ~= "), + (">", " > "), + ("<", " < "), + (">=", " >= "), + ("<=", " <= "), ] self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6) logic_buttons = [ - (".AND.", ".AND."), - (".OR.", ".OR."), + ("and", " and "), + ("or", " or "), ] self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2) tabWidget.addTab(operatorWidget, "运算符") @@ -300,19 +310,19 @@ class ALAutoScriptEditDialog(QDialog): literalLayout.setContentsMargins(4, 4, 4, 4) literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) bool_buttons = [ - (".TRUE.", ".TRUE."), - (".FALSE.", ".FALSE."), + ("true", "true"), + ("false", "false"), ] self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2) dateTimeButtons = [ - ("DATE()", "DATE(2025-01-01)"), - ("TIME()", "TIME(00:00)"), + ("日期", '"2099-01-01"'), + ("时间", '"00:00"'), ] self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2) hintButtons = [ - ("字符串", "'请输入文本'"), + ("字符串", '"请输入文本"'), ("数字", "123"), - ("注释", "// 请输入注释"), + ("注释", "-- 请输入注释"), ] self._addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) tabWidget.addTab(literalWidget, "字面量") diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py index e0e3625..5ad7a94 100644 --- a/src/gui/ALAutoScriptOrchDialog/_blocks.py +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -1,5 +1,14 @@ +# -*- coding: utf-8 -*- """ -Conditional block widget for the AutoScript orchestration dialog. +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. +""" +""" + Conditional block widget for the AutoScript orchestration dialog. """ from PySide6.QtCore import Slot from PySide6.QtWidgets import ( @@ -52,7 +61,6 @@ class ConditionalBlock(QGroupBox): mainLayout = QVBoxLayout(self) mainLayout.setSpacing(6) mainLayout.setContentsMargins(8, 8, 8, 8) - headerLayout = QHBoxLayout() headerLayout.setSpacing(8) self.typeCombo = QComboBox(self) @@ -70,7 +78,6 @@ class ConditionalBlock(QGroupBox): self.deleteBlockBtn.setFixedHeight(25) headerLayout.addWidget(self.deleteBlockBtn) mainLayout.addLayout(headerLayout) - self.conditionWidget = QWidget(self) self.conditionWidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred @@ -78,7 +85,6 @@ class ConditionalBlock(QGroupBox): condLayout = QVBoxLayout(self.conditionWidget) condLayout.setContentsMargins(4, 4, 4, 4) condLayout.setSpacing(6) - self.condRowsLayout = QVBoxLayout() self.condRowsLayout.setSpacing(4) condLayout.addLayout(self.condRowsLayout) @@ -209,16 +215,18 @@ class ConditionalBlock(QGroupBox): def toScriptLines( self ) -> list: + """ + Generate Lua script lines for this conditional block. + """ blockType = self.getBlockType() lines = [] - if blockType in ("IF", "ELSE IF"): condTexts = [ r.toConditionText() for r in self._conditionRows if r.toConditionText() ] if not condTexts: - condTexts = [".TRUE."] + condTexts = ["true"] if len(condTexts) == 1: combined = condTexts[0] @@ -226,16 +234,16 @@ class ConditionalBlock(QGroupBox): parts = [] for i, ct in enumerate(condTexts): if i > 0: - logic = self._conditionRows[i].getLogic() or ".AND." + logic = self._conditionRows[i].getLogic() or "and" parts.append(f" {logic} ") parts.append(f"({ct})") combined = "".join(parts) if blockType == "IF": - lines.append(f"IF({combined}) THEN") + lines.append(f"if {combined} then") else: - lines.append(f"ELSE IF({combined}) THEN") + lines.append(f"elseif {combined} then") else: - lines.append("ELSE") + lines.append("else") for step in self._actionWidgets: scriptLine = step.toScriptLine() if scriptLine: diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 8cecfed..35c3ab4 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -1,5 +1,14 @@ +# -*- coding: utf-8 -*- """ -Orchestration dialog for visually composing AutoScript scripts. +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. +""" +""" + Orchestration dialog for visually composing AutoScript scripts. """ from PySide6.QtCore import Slot from PySide6.QtWidgets import ( @@ -132,18 +141,21 @@ class ALAutoScriptOrchDialog(QDialog): def getScript( self ) -> str: + """ + Generate the complete Lua script from all blocks. + """ parts = [] prevType = None for block in self._blocks: blockType = block.getBlockType() if blockType == "IF" and prevType is not None: - parts.append("END IF") + parts.append("end") lines = block.toScriptLines() parts.extend(lines) prevType = blockType if self._blocks and self._blocks[0].getBlockType() == "IF": - parts.append("END IF") + parts.append("end") return "\n".join(parts) @Slot() diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index 4cdef03..10ca230 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -1,5 +1,14 @@ +# -*- coding: utf-8 -*- """ -Helper utilities and constants for the AutoScript orchestration dialog. +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. +""" +""" + Helper utilities and constants for the AutoScript orchestration dialog. """ import re @@ -24,14 +33,10 @@ from PySide6.QtWidgets import ( from autoscript import ( ALL_VARIABLES, - splitTopLevel -) -from autoscript.ASOperator import ( - ARITH_TYPES, - COMPARISON_OPERATORS ) - +# Types that support arithmetic operations (add/sub) +ARITH_TYPES = {"Date", "Time", "Int", "Float"} VAR_TYPE_ORDER = [ "String", "Int", @@ -51,22 +56,22 @@ PRESET_VARIABLES = [ PRESET_NAMES = { p["name"] for p in PRESET_VARIABLES } -# Operator display names (UI-specific), symbols derived from engine +# Operator display names (UI-specific), using Lua operator symbols _COMPARE_DISPLAY_MAP = { - ".EQ.": "等于", - ".NEQ.": "不等于", - ".BGT.": "大于", - ".BLT.": "小于", - ".BGE.": "大于等于", - ".BLE.": "小于等于", + "==": "等于", + "~=": "不等于", + ">": "大于", + "<": "小于", + ">=": "大于等于", + "<=": "小于等于", } COMPARE_OPERATORS = sorted( - [(name, op) for op, name in _COMPARE_DISPLAY_MAP.items() if op in COMPARISON_OPERATORS], + [(name, op) for op, name in _COMPARE_DISPLAY_MAP.items()], key=lambda x: len(x[1]), reverse=True ) LOGIC_OPERATORS = [ - ("并且 (.AND.)", ".AND."), - ("或者 (.OR.)", ".OR."), + ("并且 (and)", "and"), + ("或者 (or)", "or"), ] ACTION_TYPES = [ ("设置为", "set"), @@ -182,8 +187,8 @@ def makeValueWidget( return w if var_type == "Boolean": w = QComboBox(parent) - w.addItem("是 (.TRUE.)", ".TRUE.") - w.addItem("否 (.FALSE.)", ".FALSE.") + w.addItem("是 (true)", "true") + w.addItem("否 (false)", "false") w.setFixedHeight(25) w.setMinimumWidth(100) return w @@ -304,8 +309,8 @@ class _DateInputContainer(QWidget): layout.addWidget(self._stack) layout.addStretch() - _RE_CURRENT_DATE_OFFSET = re.compile( - r'^CURRENT_DATE\s*([+-])\s*(\d+)$', re.IGNORECASE + _RE_DATE_ADD_CURRENT = re.compile( + r'^date_add\(CURRENT_DATE\(\),\s*(-?\d+)\)$', re.IGNORECASE ) def getValue( @@ -326,27 +331,24 @@ class _DateInputContainer(QWidget): expr: str ): - s = expr.strip().upper() - _RELATIVE_MAP = { - "CURRENT_DATE": 2, "TODAY": 2, - "CURRENT_DATE + 1": 3, "TOMORROW": 3, - "CURRENT_DATE + 2": 4, - "CURRENT_DATE - 1": 1, - "CURRENT_DATE - 2": 0, - } - idx = _RELATIVE_MAP.get(s) - if idx is not None: + s = expr.strip() + up = s.upper() + if up == "CURRENT_DATE()": self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(idx) - elif self._RE_CURRENT_DATE_OFFSET.match(s): - m = self._RE_CURRENT_DATE_OFFSET.match(s) - sign = m.group(1) - n = int(m.group(2)) - offset = n if sign == "+" else -n - label = f"{n}天后" if offset >= 0 else f"{n}天前" - raw = f"CURRENT_DATE {'+' if sign == '+' else '-'} {n}" + self._relCombo.setCurrentIndex(2) + return + m_add = self._RE_DATE_ADD_CURRENT.match(up) + if m_add: + n = int(m_add.group(1)) + _OFFSET_IDX = {-2: 0, -1: 1, 0: 2, 1: 3, 2: 4} + idx = _OFFSET_IDX.get(n) + if idx is not None: + self._modeCombo.setCurrentIndex(0) + self._relCombo.setCurrentIndex(idx) + return + label = f"{n}天后" if n >= 0 else f"{-n}天前" + raw = f"CURRENT_DATE {'+' if n >= 0 else '-'} {abs(n)}" self._modeCombo.setCurrentIndex(0) - # Add dynamic item if not already present for ci in range(self._relCombo.count()): if ci in self._dynamicItems and self._dynamicItems[ci] == raw: self._relCombo.setCurrentIndex(ci) @@ -355,12 +357,21 @@ class _DateInputContainer(QWidget): self._relCombo.addItem(label) self._dynamicItems[idx] = raw self._relCombo.setCurrentIndex(idx) - elif s.startswith("DATE("): + return + m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up) + if m_date_ctor: self._modeCombo.setCurrentIndex(1) - m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) - if m: - parts = m.group(1).split("-") - self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) + self._dateEdit.setDate(QDate( + int(m_date_ctor.group(1)), + int(m_date_ctor.group(2)), + int(m_date_ctor.group(3)), + )) + return + m_date = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s) + if m_date: + self._modeCombo.setCurrentIndex(1) + parts = m_date.group(1).split("-") + self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) class _TimeInputContainer(QWidget): @@ -392,8 +403,16 @@ class _TimeInputContainer(QWidget): expr: str ): - s = expr.strip().upper() - m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s) + s = expr.strip() + up = s.upper() + m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up) + if m_time_ctor: + self._timeEdit.setTime(QTime( + int(m_time_ctor.group(1)), + int(m_time_ctor.group(2)), + )) + return + m = re.match(r'^"(\d{1,2}:\d{2})"$', s) if m: parts = m.group(1).split(":") self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1]))) @@ -541,24 +560,45 @@ def setWidgetValue( var_type: str, expr: str ): + """ + Set a widget's value from a Lua script expression. + """ if hasattr(w, "setValue"): w.setValue(expr) return - s = expr.strip().upper() + s = expr.strip() + up = s.upper() if isinstance(w, QTimeEdit): - m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s) - if m: - parts = m.group(1).split(":") - w.setTime(QTime(int(parts[0]), int(parts[1]))) + m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up) + if m_time_ctor: + w.setTime(QTime(int(m_time_ctor.group(1)), int(m_time_ctor.group(2)))) + else: + m = re.match(r'^"(\d{1,2}:\d{2})"$', s) + if m: + parts = m.group(1).split(":") + w.setTime(QTime(int(parts[0]), int(parts[1]))) elif isinstance(w, QDateEdit): - m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) - if m: - parts = m.group(1).split("-") - w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) + m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up) + if m_date_ctor: + w.setDate(QDate( + int(m_date_ctor.group(1)), + int(m_date_ctor.group(2)), + int(m_date_ctor.group(3)), + )) + else: + m = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s) + if m: + parts = m.group(1).split("-") + w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) elif isinstance(w, QComboBox): for i in range(w.count()): - if w.itemData(i) == s or w.itemText(i).upper() == s: + d = w.itemData(i) + if d is not None: + if str(d).upper() == up: + w.setCurrentIndex(i) + return + if w.itemText(i).upper() == up: w.setCurrentIndex(i) return elif isinstance(w, QSpinBox): @@ -573,9 +613,8 @@ def setWidgetValue( pass elif isinstance(w, QLineEdit): inner = expr.strip() - if (inner.startswith("'") and inner.endswith("'")) or \ - (inner.startswith('"') and inner.endswith('"')): - inner = inner[1:-1].replace("''", "'") + if inner.startswith('"') and inner.endswith('"'): + inner = inner[1:-1].replace('\\"', '"') w.setText(inner) @@ -583,39 +622,86 @@ def encodeValueStr( raw_value: str, var_type: str ) -> str: + """ + Encode a raw widget value as a Lua expression. - if isArithExpr(raw_value): - return raw_value - if var_type == "Time": - if raw_value.startswith("+") or raw_value.startswith("-"): - return raw_value - if raw_value.upper().startswith("TIME("): - return raw_value - return f"TIME({raw_value})" - if var_type == "Date": - if raw_value.upper().startswith("DATE("): - return raw_value - if raw_value.upper().startswith("CURRENT_DATE"): - return raw_value - relMap = { - "前天": "CURRENT_DATE - 2", - "昨天": "CURRENT_DATE - 1", - "今天": "CURRENT_DATE", - "明天": "CURRENT_DATE + 1", - "后天": "CURRENT_DATE + 2" - } - if raw_value in relMap: - return relMap[raw_value] - return f"DATE({raw_value})" + Arithmetic expressions (A + B) are passed through for numeric types; + Date/Time arithmetic is translated to ``date_add()`` / ``time_add()`` calls. + """ + + if var_type in ("Date", "Time"): + return _encodeDateOrTime(str(raw_value), var_type) + if isinstance(raw_value, bool): + return "true" if raw_value else "false" + s = str(raw_value) + if isArithExpr(s): + return s if var_type == "Boolean": - up = raw_value.upper().strip() - if up in (".TRUE.", ".FALSE."): - return up - return ".TRUE." if raw_value else ".FALSE." + up = s.upper().strip() + if up in ("TRUE", "FALSE"): + return up.lower() + return "true" if raw_value else "false" if var_type == "String": - escaped = raw_value.replace("'", "''") - return f"'{escaped}'" - return raw_value + escaped = s.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + return s + + +def _encodeDateOrTime( + raw_value: str, + var_type: str +) -> str: + """ + Translate a date/time widget value into a Lua expression. + """ + + s = raw_value.strip() + up = s.upper() + m_arith_spaced = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) + m_arith_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s) + m_arith = m_arith_spaced or m_arith_nospace + if m_arith: + left = m_arith.group(1).strip().upper() + sign = m_arith.group(2) + right = m_arith.group(3).strip() + operand = right if sign == "+" else f"-{right}" + if left == "CURRENT_DATE": + return f"date_add(CURRENT_DATE(), {operand})" + if left == "CURRENT_TIME": + return f"time_add(CURRENT_TIME(), {operand})" + if var_type == "Date": + return f"date_add({left}, {operand})" + if var_type == "Time": + return f"time_add({left}, {operand})" + return f"{left} {sign} {right}" + if up == "CURRENT_DATE": + return "CURRENT_DATE()" + if up == "CURRENT_TIME": + return "CURRENT_TIME()" + _REL_MAP = { + "前天": "date_add(CURRENT_DATE(), -2)", + "昨天": "date_add(CURRENT_DATE(), -1)", + "今天": "CURRENT_DATE()", + "明天": "date_add(CURRENT_DATE(), 1)", + "后天": "date_add(CURRENT_DATE(), 2)", + } + if s in _REL_MAP: + return _REL_MAP[s] + if var_type == "Date": + m_date = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", s) + if m_date: + y, m, d = int(m_date.group(1)), int(m_date.group(2)), int(m_date.group(3)) + return f"date({y}, {m}, {d})" + if var_type == "Time": + m_time = re.match(r"^(\d{1,2}):(\d{2})$", s) + if m_time: + h, m = int(m_time.group(1)), int(m_time.group(2)) + return f"time({h}, {m})" + if re.match(r"^[+-]?\d+$", s): + return s + if re.match(r"^[A-Za-z_]\w*$", s): + return s + return f'"{s}"' def stripOuterParens( @@ -637,8 +723,6 @@ def stripOuterParens( # Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B) -# Must match both spaced form (CURRENT_DATE + 1) and no-space form (RESERVE_DATE+1), -# consistent with ASEngine._resolveArithExpr. _RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$') _RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$') @@ -654,65 +738,21 @@ def isArithExpr( return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s)) -# Pre-compiled patterns for SET value whitelist validation, -# matching the priority order of ASEngine._resolveValue. -_RE_VAL_TIME = re.compile(r'^TIME\(\d{1,2}:\d{2}\)$', re.IGNORECASE) -_RE_VAL_DATE = re.compile(r'^DATE\(\d{4}-\d{2}-\d{2}\)$', re.IGNORECASE) -_RE_VAL_ARITH_SPACED = _RE_ARITH_SPACED -_RE_VAL_ARITH_NOSPACE = _RE_ARITH_NOSPACE -_RE_VAL_VAR_REF = re.compile(r'^[A-Z_][A-Z0-9_]*$', re.IGNORECASE) - - -def _isValidSetValue( - value: str -) -> bool: - """ - Whitelist validation: return True if value matches one of the - legal SET-value patterns recognised by ASEngine._resolveValue. - - Order matches _resolveValue priority: TIME → DATE → bool → - quoted string → int → float → arith expr → variable reference. - """ - - s = value.strip() - if not s: - return False - if _RE_VAL_TIME.match(s): - return True - if _RE_VAL_DATE.match(s): - return True - if s.upper() in (".TRUE.", ".FALSE."): - return True - if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')): - return True - try: - int(s) - return True - except ValueError: - pass - try: - float(s) - return True - except ValueError: - pass - if _RE_VAL_ARITH_SPACED.match(s) or _RE_VAL_ARITH_NOSPACE.match(s): - return True - if _RE_VAL_VAR_REF.match(s): - return True - return False - - def isVarReference( expr: str ) -> bool: + """ + Return True if *expr* looks like a variable name reference + (as opposed to a literal value or function call). + """ s = expr.strip() up = s.upper() - if up in (".TRUE.", ".FALSE."): + if up in ("TRUE", "FALSE"): return False - if re.match(r"^TIME\(|^DATE\(|^CURRENT_", up): + if re.match(r"^DATE\(|^TIME\(|^DATE_ADD\(|^TIME_ADD\(|^CURRENT_DATE\(|^CURRENT_TIME\(|^CURRENT_", up): return False - if up.startswith("'") or up.startswith('"'): + if up.startswith('"') or up.startswith("'"): return False if re.match(r"^[+-]?\d", s): return False diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index ecc9f92..c62e370 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -1,5 +1,14 @@ +# -*- coding: utf-8 -*- """ -Widget components for the AutoScript orchestration dialog. +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. +""" +""" + Widget components for the AutoScript orchestration dialog. """ from PySide6.QtCore import Slot from PySide6.QtWidgets import ( @@ -21,7 +30,6 @@ from gui.ALAutoScriptOrchDialog._helpers import ( VAR_TYPE_ORDER, encodeValueStr, getValueFromWidget, - isArithExpr, makeComboWidget, makeLabel, makeOffsetWidget, @@ -119,8 +127,8 @@ class ConditionRowFrame(QFrame): self._varMgr.populateCombo(self.leftVarCombo) # Append boolean literal sentinels at the end self.leftVarCombo.insertSeparator(self.leftVarCombo.count()) - self.leftVarCombo.addItem(".TRUE.", (".TRUE.", "Boolean")) - self.leftVarCombo.addItem(".FALSE.", (".FALSE.", "Boolean")) + self.leftVarCombo.addItem("true", ("true", "Boolean")) + self.leftVarCombo.addItem("false", ("false", "Boolean")) if wasBool and boolName: for ci in range(self.leftVarCombo.count()): d = self.leftVarCombo.itemData(ci) @@ -156,7 +164,7 @@ class ConditionRowFrame(QFrame): if not data: return name, vartype = data - isBool = name in (".TRUE.", ".FALSE.") + isBool = name in ("true", "false") self._isBoolMode = isBool self.opCombo.setVisible(not isBool) self._compTypeCombo.setVisible(not isBool) @@ -204,6 +212,9 @@ class ConditionRowFrame(QFrame): if not data: return "" name, vartype = data + # CURRENT_DATE / CURRENT_TIME are Lua functions — call them, not reference them + if name in ("CURRENT_DATE", "CURRENT_TIME"): + name = f"{name}()" opSym = self.opCombo.currentData() if self._rawRhsExpr: return f"{name} {opSym} {self._rawRhsExpr}" @@ -212,6 +223,8 @@ class ConditionRowFrame(QFrame): rd = self.rhsVarCombo.currentData() if rd: rhsName = rd[0] + if rhsName in ("CURRENT_DATE", "CURRENT_TIME"): + rhsName = f"{rhsName}()" return f"{name} {opSym} {rhsName}" rhsText = self.rhsVarCombo.currentText().strip() if rhsText: @@ -399,38 +412,37 @@ class ActionStepFrame(QFrame): def toScriptLine( self ) -> str: + """ + Generate a single line of Lua script from the current widget state. + """ target = self.getTargetName() op = self.opTypeCombo.currentData() if op == "pass": - return " PASS" + return " -- pass" if not target: return "" rawVal = self._getValueRaw() + vartype = self._currentTargetType if op == "set": - vartype = self._currentTargetType - if isArithExpr(rawVal): - return f" SET {target} = {rawVal}" encoded = encodeValueStr(rawVal, vartype) - return f" SET {target} = {encoded}" + return f" {target} = {encoded}" elif op == "add": - vartype = self._currentTargetType if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): days = self.valueStack.currentWidget().getOffsetDays() - return f" {target} .ADD. {days}" + return f" {target} = date_add({target}, {days})" if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): hours = self.valueStack.currentWidget().getOffsetHours() - return f" {target} .ADD. {hours}" - return f" {target} .ADD. {rawVal}" + return f" {target} = time_add({target}, {hours})" + return f" {target} = {target} + {rawVal}" elif op == "sub": - vartype = self._currentTargetType if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): days = self.valueStack.currentWidget().getOffsetDays() - return f" {target} .SUB. {days}" + return f" {target} = date_add({target}, -{days})" if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): hours = self.valueStack.currentWidget().getOffsetHours() - return f" {target} .SUB. {hours}" - return f" {target} .SUB. {rawVal}" + return f" {target} = time_add({target}, -{hours})" + return f" {target} = {target} - {rawVal}" return "" @@ -439,7 +451,8 @@ class ActionStepFrame(QFrame): ) -> str: if self.valueSrcCombo.currentData() == "variable": - return self.existingVarCombo.currentText().strip() + data = self.existingVarCombo.currentData() + return data[0] if data else "" w = self.valueStack.currentWidget() if w: return getValueFromWidget(w) From 531b05651e0a7cd719426a2dba53a1980393c7dc Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 23 May 2026 19:26:00 +0800 Subject: [PATCH 23/49] =?UTF-8?q?refactor(gui):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20AutoLibrary=20Logo=20=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=B8=BA=E5=85=A8=E6=96=B0=E8=AE=BE=E8=AE=A1=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/ALAboutDialog.py | 2 +- src/gui/ALMainWindow.py | 2 +- src/gui/resources/ALResource.qrc | 2 +- .../resources/icons/AutoLibrary_1024x1024.ico | Bin 803706 -> 0 bytes .../resources/icons/AutoLibrary_128x128.ico | Bin 15237 -> 0 bytes src/gui/resources/icons/AutoLibrary_32x32.ico | Bin 21238 -> 0 bytes src/gui/resources/icons/AutoLibrary_Logo.svg | 15 +++++++++++++++ 7 files changed, 18 insertions(+), 3 deletions(-) delete mode 100644 src/gui/resources/icons/AutoLibrary_1024x1024.ico delete mode 100644 src/gui/resources/icons/AutoLibrary_128x128.ico delete mode 100644 src/gui/resources/icons/AutoLibrary_32x32.ico create mode 100644 src/gui/resources/icons/AutoLibrary_Logo.svg diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 119c06f..6769cdb 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -43,7 +43,7 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): self ): - self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48)) + self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_Logo.svg").pixmap(48, 48)) info_text = self.generateAboutText() self.AboutInfoBrowser.setHtml(info_text) browser_font = self.AboutInfoBrowser.font() diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 3974b34..2c00b1f 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -64,7 +64,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - self.icon = QIcon(":/res/icon/icons/AutoLibrary_32x32.ico") + self.icon = QIcon(":/res/icon/icons/AutoLibrary_Logo.svg") self.setWindowIcon(self.icon) self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) self.ManualAction.triggered.connect(self.onManualActionTriggered) diff --git a/src/gui/resources/ALResource.qrc b/src/gui/resources/ALResource.qrc index 3a1910b..3590e4b 100644 --- a/src/gui/resources/ALResource.qrc +++ b/src/gui/resources/ALResource.qrc @@ -1,6 +1,6 @@ - icons/AutoLibrary_32x32.ico + icons/AutoLibrary_Logo.svg translators/qtbase_zh_CN.qm diff --git a/src/gui/resources/icons/AutoLibrary_1024x1024.ico b/src/gui/resources/icons/AutoLibrary_1024x1024.ico deleted file mode 100644 index e48e78f5886d9f39b2dbdf71b92e637c785117d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 803706 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

H@;O* zep1glv=IX;jyNB)~?ul12HGnxAP>)Dd_6G5_Uz~{QM?wN?3NB#XmonsO_IkSn ziEIhN_{yB=XPob78qgpVb$|f6S>% z2fGc24{Bi|sk_>%i>t_mQJ1Uyd4Vg2&5wpIRu>h1V36JUFn5^NcU~)V@iJr&@+Lub z7t@;*iW1w-*@}K_)JL@owa=sy=)29-gVg*u#EZm(2@m1bYFcoA2XlybWr8LgK(laz zA?CE#^G2^c!d;l%4LgA!1?|Y)fGf>i^ z^G$_jHYt95-^dU9%Oh@8VO>DTsr$g;DLjVZP&49IbZg?hA1qL2`CCad_t@PBSidQ& z13nW|5~YxOxPyN5-db4elbU*(VJhJ#ogLHRbQCWI=JeF-3acQdg*s&36%kh^Nv(Oe zODg`YA1y}%OWkTt&BIv9=+tDyq0QPDb^vb4iRnqvH@kgsll~3j)h*fWJX~p#V)e zKd-?HH8&(s{w>np1t#(AMS!Z)dKbQ$de%*Kzyss2c_6c?A%1^nDj}ewtW7cQbBh!e zdu$EmP6k_u2U>~kdXQ8u*Jb}sobu%sFgra81A1-4={kpMwo8I#8rzdu5`-cZKX|>= ze6*q<(E8%mRPjYbP_0HM=-$D%-ld$j%xV+(A9^v#m_2$n+27bX!4o{lSfWyQ>H;49 zqg2q2W{u%yW=d9BZ`{~U2<`R||EM%rz4huT2D6sBWm@1jR)+4CA3Nq4u=T=Og^e_| zVMA;OJrDqd1=8&g& zR-Wa{Ww%Wkx7BW|1b(Bvi=1XU7Q0iwyhN2N{y2P#SK$dn)9H)y(f)gJYP0uYF=W59qz#rY1hhNYv@Y+9fs2`@F$Tjw zif84nOXU>e7#3yk8FCeH(@#1b=vn1K2>wlf6^HSZN$%dSr^{ji0Krqiaf zSkJA=wIkFDC1c;`78RkQrm_1p;i_%Vy%M+qErdY3&pM#Laflp zvh+;8_Kwoh((d?IBqTM)r;aKOdX>2a2XptjGtegd31)1*66fYC5Vml%mn74~0#${sY-TB~KXT zK%(y=joj8;x^w|%ke!EO&d5n(hZ=X=r3qxc_pV!=(+cA;vKP)@Xh8qVw6bV_F=RT< zgp85@Zru-ooqFzPc8|)ACcfSPFC`RN@8BO#KVKw66oYs{KF_B%e>OxuX+x1{4)WsRN*sse5mPx>^)AD zkvReHzsL2MWJhNcO$^g{$sq&ik#A2`E2@BdUIpO8{(9kuXqC<1Ob)WsSmxSP6bY~u zdU;k+RVBa%{u5#sru?^;vCi=n*1lYAlTQ1gSS~&91|Q(&zucy2`~dVuDvRN;cyKTM?yYl*X@OPB-@rx1;Csmf& zrG;yW=?Yp2Y?UdyKxD0A*y^4U)R=n5z>)D^O$MIgD!KmOH(;+jG3``1JSHbUJ9Vsl zJV!)-ng-2P$v-FiRfMrR|KIq=X*k8YXgkIIxJbI3I6L)H8D&&sPE0` z2e69g%QWNQ*wt_(yQz>s%~x*=8W&^c7*V&pTc~AE**fm{F>g@)t1`%sx`eF(3Nzxc z^cVc*n`GO@+vl-aWy2%l9q=*g6z?(mV*S%iYEnhH&=H zI=JwJh~ZymX2UAuq3^}j(IMe~dHBNnrxzTCyuu#~P@h#dO`*s_J2LO1gKP`GKeUvhx&av0Q{OyQcqgniX_ zZ+c#633puN3e!t}zbH3zBmCfQLPnp|0ph(+R#C$6@ry4F%* ztxntd(;+vaB=RrHt&iYM<0&R-2G<{&h&3El6%iY+;-4+NTt8ppMApk>IH)9TdJrEA zT}z9!FnW0Dzoh6ExJ^Z=r2QQn;c^%*jwPNs8(zL>3R+-daT&0xabKFa_)9mVqJbz~ z|0ju;aXzGDSh4+^@9Ph3{z4f*UiHj(PL4|_PN4>5`$L zG7IMsW$!@S`Z{IYYcZ<#c(V0 zHsET+h2((CbY1*4%v&pXSxn?aIS!_93GvLvdjj{agbWgEe0D7y(>K)`By2}vc_hKR zZf3sVo%F6;6&rplqOiw1pXP^dboX73GAzb;dc0*AI+}e$fyF%zKNL0vkB<@`4!zIX zAk+jN7ao&O-w5#YjDCCui>MuhW+8=XJo@x0e^O33Cy*Kk(tyt&OM_lSs6WrCl)w}9 zC0OD+AHl&R@3C7dgK{N0TtPt~0Srr(bf&s>!_2C0@a9^GUmg==sn2C$9qiD6KZj&_ zDNMjy>QR?ofpYuQv^w6jG|ng``vC{#=+B?Y`P8u0CX&*(r%VjrZkY(IbuMuMYxRG> zEc!Ms!>S_rSkU_~ui_71LBhym2*PN^@Pkw^NCWp_&l=en>o}IY!vE1c7OW>ECR8{7 z%%^5)u@g}=^$Z`{6Vx({`dZQ2_H*ha?UJEInuL3;_VhS6zL?7?u6|uPlx&_J?`=Oi z*ti(f@Q?Y(j4cbwsostMsh?SjcBDo-B4c-VQaw_KS2bp3@U4d@*AYIQJrR({&?l3s z9HY*lZ@uJAT;cCa72KgyHh;P$c_FJgCCE$KiT4p7rhINmr}F)lnJO?6z?suXvEX|j z<1&k&vis5N2- zt-$H>&WGW9usuZy>Dv_1?J{U`PH3gb-)w&gWf*LkE&!j`O1eZhZ6UGNEE=(8CiUjf zKZ0ZFqnOuw~KLyw#!)|9An-Rcr83PkiJTa@>FD3h?Mt zb6e^B`k41G;A@R@633w!>cr(74}MTxdsJY01LhF?HuP*(bV2!8Ks? z)rsI`*$s3(gm*cA=rGr)RwnrJ5%Do*m%e0}$FgYk-*^{%Bm4@p$~E$MeEJlZXXqcb zxD7(7qSnCoE#+;R%2aPbgJCYgz~6OQT`fuvn)LnUv}%|iNn)^-`QYt|G^FL@+nY&a z=r|v!pjC8b7@rK`F7T=fW7nI?SE2-B4nz-<4}8u7rhoMo@c&*xd9d9Ezuc^6ID%#g zeD*EOW=gG||CjaDn>+CjgD1H-M0}kE7>=?HydNDOYaUb!?Wd%p2l6{3@SRKAaCW}c3$v>tWRIB|>tx#BrcEk`DYE~{4f2-{gSdDz$F1PS+A zg_XiRIgR`RXCX*8hlEty={mf?l}MX_jNt8DB=^i8l(;0Vdyi2AKfP;#WHQ0mPX`)_ zWHXvKZ?HJ#H4K4W3`|)H;T)?kN~cA1YdEQ25xC);Fvkc^K`}^oy!Z0jWb@*PZcEep zpZ;3ItA|S0!y`g%O=%rPKq;PeY8qnX%CGisnGU=5``Nmd53s~XeKJ?I^|u?BtSSbB z;svL^I(xw&ys@Mfm8Cz#EoIR!630AxnyoxiW-#_h&5-*G*vU%=TgjC5BBdjE?qG!e zCOi=EXl+8fnIbkLmDjZTA}FYwqbP`O$Zo@jUR8nxPR3;6()WLH&(VHiORel{VMtya z{nd~ayR+)LvyaVk!O1T)1i5-6q&1AC-S2q9!(|x1_~ONwtLRo%ql4Bb3SboX!*?&( zyp<5&u8~cTq6Ai)UoCVSIbf6U4B$c8u2r8YL%}LT8rSRREBG}x7((qd#z~BN9Nm$N zt+g?rRBPpav#n-kn#vlyk73NcKjB|lA~DH(_Tg#E=$enV_){YU(w};Sat-2kXA-%c z(4l%ocQ{-v=A-64GAr%-;OT9@p|PlLrAjpKo$*Q1xj@!XJn5wKr#kf{d)NiKnFpAB zc*;L%H7n5Z_aW>V-NRI+p`~NQZ%FXva=>Y#&}ONWcu2&-&s&}uLvc?d z@!wgEc+nbErmm9G^5tY&f7>sc^keUp<4VW`Js>gJ?O;kA61(P$)3u-(`0Tz!a~Xv$ z`}X~WY7wmQ`S2?iL(Y?1o&asn}Cct)$u8;X*am0$1==WRePYbwEjVF7& zWXHnuFzA&Goz(f9oFXIASt#7XRyY}f4E7GHjhWAZd?S^yDT}1E3yx9?{n4SfKt-Pv2`5hRxDI=qKj7pwmUC9fNm9$L22cQ@eoFQPqqnLZcWq>D-aYD9 zg^=O3$jIZnp5U$iIb;FxI~T;3W7_g4QmBejW355*cVQiRxMT48VqSfQ)({{UNZ~_KPh(pl4i=cmc@@dzx+hJe0f0$#r zy7j*8nm&QT>UUhgd4!_z66(D#+j}H(h70n0NOW)j( zi2OZMZ9B++H6iE*R~GgWQS?0_I6@|+it>MkL${AJN)ygmH4U7`KA3L}dyBQE=c@y%)qY02GDsfqk4zYX&b_Jn zxo^;1e*INZe38Zvv)a}crY*RTHb?`S_;EiaZ9^r#=fg2kG>~EgAt+NWZcU`oT&C!P z1izU3``nwcb7}DZ#&<&kG_tMe%px!L|e)}6%Tp|8m&(xcksWSbm8-yPq>Y=3o-u(lFLn&$^rHrTK^ zCAI%w%(@Z3@f*Eg!FL))A0DP`DW{E7eup|~ZDne3u70i4@$De%^;w1zb9k8)w*jbN z(xL5%=4y?T>iVur+k*iGHlR($ors~@B;sj zyUV@s`0gZdbe+Dov{;SYYrC8_9?0v=I1|iT&1_K@kzJLso70#Fcv85-VC*_X(Fcuk$sWLrh3`GGPa2SWNbn>0%nG z0IM$oSa+_*(9!;wCcNZ<;JbHIfL}}YHbT=DKl)xTFV|cX!$Z&IAi|rYRIYnl&n80V zsCR8Y(qoRH$;TJ+9(STn-naJOzhJLF@M)vh{%JnZ(MDmg4lt)GR`kimu^fXrW`m|$9M<()KBwg|U@hRVJvCs+nN91vHY5zBflizoZ$+w%T zyob{#BGu3&=bz5~ga%LFZccb1yossut|&c+b*GjeqY#w^ir4A`msv&uot*x@$i%y&gPU%SVhx7E~6YDvl ziNvMw4R&ahtx?2FcKsmNfnWJ6sa%+DOFp`Lyo)zc2%v-~f+?XN*iR6N*t-S-kZIIZZ-z$o_C5?;!2+iwM83~*>L zUlN`Cd3_f4N-J)?1bE}w51S36DoI+zlPZq{QGvJNR>uiAT#FD+eGl@3XYplEfy`uC zn=9TOG8$KWI&Ie~G@mxtDyUcZj_RAO0qvc;x-j~0LN2hp%4@#x_;hjd0?ZSghPrbe zsiYHajJapmd~^>fSbo4+8}`Wiav|5pUMrp+WDPLhn5vxY&q;1BO%p9jtR<*s5sOFt z-T~g0hJ_~x___yW;BJXH%DkoIhnCsDS7~^gDPe#a6I3XT%HP{k?#2BZ`|Ww-y4fu< zaptFn4OTvMWrEjvdoElxV9)az8ojAD`yMQ_dM7>(?=v7(6E+Ce-+wXf$Ab#Yc9JRd zc2@bwDX3E(n>R2b@|j(0Yjs7hC-jD3ECx*EA7f^ddeJx=Y9jRtXbcMjNf_BS2p+l3 zr7ex7sd?=nyoVzaUYfmBjiN?F>?q%wCYH$i(5hyU&ZKuqkgX@$HpS_EeJLetUp?5Y4&^ zxf>g8i%77^_Gcm3KkNFHqwu?}A>G0A<2~*DE7$V#Cq2d3F`BQE|DdJZ3@)u0YA|^~t2 z7gByfJ9d4OfqwUs`Ld%xal4*#O%8JIn%}=Sh@s@+-H_HlJ{N+&*QpQLtoYkhw7XJf z*}sg<9J};l%IP+GktlON^{f+H__yH{K>dP;3%JGeCi*Ci=83;;49|Tl(EY}W{D7tL zx_Y+NDd|QjgK#wMXd#72Hd-CN!deXv0o(OM;?jwCV~<#_Bp8vU z3EHY*38-E;HJU!81b7`hbt3l^ThJ_P9<*`H2PTiVl2ngI@9>>G=MKuHe{*bHlbbf& zf-e6j2^Yx`#0?jVZ?@OJ0@TiRIA&9Z5|#=_zyp)b@^{Z<&OT?v#Hw|qy~eQFfQo=p zY(B1B)(f| z4m&w~Gv+^tS+{3-^XRD%F24`z{d$W0+96?c@q6H%Sl00DV<+w>dEDw1-v&kxVOz){ zpUwnFwQG;jopR|pd&|(qZ4ts~Cwy0GKM%^Cko53T$MZRf%OlFsi_TpEM;l_ll_rmz zO^uI+@Jurh-omirdxb1Nn{0x9PvOt)G3ouH^C{ie`K&lNwX^<78<49p0UWeqsi|-` zPPV!*(#^xJ=V3IHD|ckeO-{R{1^?UUt?Cdvrn;ZgG%DBs8-^`18o>4TV|5U`pWW{> zpTD~LpLji_7m{{Ij);66J9dPxs1s?!UOjuCTT6js`yx-dmBV+RLjYHIr%glT4Ygge zJWKX%NEgVgu&?dl6mIhE_jD6~V!ygZW$cQ;xptevd5a;rryKI z6AN1rK~$}A-gtBHG_}vpi{w4N=JR{W zykemL;}kV5Kf6U_nY^oUO-+_RXbuQkPV%JahRb_dtbtu9djU01jaVt)5A(_E(R$JZ z$VoRnxciLIks^P2(~_W1&0WCOY1i$Hw$slIp(ZP=PSb=M&BDClu-L7DROf+0d&T!q ze~#?A*?1DSho3-WGW_+D@QCIu*|E0tS<@=*a8QDOI{wxC)B0`1+k}BAn1W>a(TUN4 zSIyKXqFn*Ru5QlYs3$*5aUbSCAK0q+%*WC1`VwMp@Qjm_$34zt-b1Pt@XD&80-nEvKqY#Pv^k0vaXj`GG56ec(3V zMAnG@5XATD`Y!)D1JyUSHroHGX3Jt*a@zv^kn^^Ihiz3Wywke3or_R%*tt6;m1Mn^ z8$#Y|B|+!AynB#ugbK%?>uYZHzOX%48I=L=fL5#0G~hA{)f&rEut|f)|01%*6)@Kpw6b50FmXvlhJr%*r<9|92eUVLB~a@}#OLcWjhi!IX^EqZE6ZxBoAgA^ zuZrO?M|tHZbCP{6r+`I9HrQYQnw$i&2{l^#*K$18r2pDUy<)Dz!e<(#0XkZjIBB`U z7&d`W_&27s@^0qk*#Q=0|M5on!_S%jfzpdYUt<3y(n2yPiJuXV(XBt}z|G94N9?m! zn{NrNOd_gF1;MUHSqJx*j|sls1^2L{InHO<@Fg(}#+BB(r*|}TE#R!u;@m+=H@_06 zeGpdz?WG##al-RjMG6QaVA-`BCu(zHgjNysZF?;cb$vG+If&B4xWlXXWhPo2v+vPC zdxI(_j!T3pq@~5c(J_8zByakFu+;JOG>ZXfbadp)yo-p49V&Y<;Yc)LAF0bTosx9pXn!gejgh3Ze95&9&PFwy%D68#k{UUCsn{9Bo1dlh;o*S zCKux9O@%M^&keQD&#Bh%yvOLp*;=7q56octA#3r%z?=Ja7fz)W8Q}BZlh zOQ(UWCeB1no$d|ZIYX7${Fd~#NK0yiuQIB{3gq$hd|BE&Kc3d>uZsF23(~f_OjT~J zWgD}i|85;2x62efH+34to%KU2Q%qs0lo&OE4FY%YcrbUTg$MYMF0*dEbwx$4pEdYB zVgelu7GoFMrZ)!_Qz+|AG}hTae?)AV9g7`=8CLRg=$wJ5_P#~`@_YxYrD8uOWJBqgK0HT=4i)&d@hLZIQqn~^dE zcxbaZnJFyx4kq&mUsJzksTyPnj9OCO{#~MWm8Q6OjmoC8yYg~Q8gPV-S?mpF1m@dgI-8Mw%m^_{8KUOA2+CT<@}G5wd4JV1jAv?oZi?0YPA2~iUQ z3iplICL8)I_UfK5r~IVa3a23ZoW*&`KWNKKx7FHOfy;F5t251u8rsdCke41}5om`>y&~b|-K=#smzv zF1=NBB|eN7Bu{Z~H@`KJt>T$orDC}8h~1X$;i`o=V!{cAFA_QT%s&TN^$_^){Lx1C zO49I3Dm-cyDKSQ2o>$|&uYHjr`+P4Pg>Kc~L_CC>tAxB9 zVxSG|$IZpOCv-!t^q77E`lyS?n(#2j=yL+eIWa%QWpB99R#3K)CG z1lto9yzjxNS2+rug@3NEEZ=z0L6|2%C_d|+SE&clp?eWp9AQKmpMTDhvO9c_uAS`Q zkAp`F(H~fw!ZZC+qin-&>vR=|@9aKu{%iJITenQr;aHQifYo3kWp&SLr)!u0RPLny zl)&=n`9(VWaY4rkY2-kR_UXczvFd%e_wb)_un8U>)ETw^alZFjn(ubx^|$z27D9%r zA*Yf4bYXk|A7*;uWf~Do)tx-|r*--1v(v>oX|R+QJh?x57NDUQ6fiBaA(CJpBJdjm zo0K6GB~*DQ$lb{*wy6on2ZgQ>la&J;f8+AF8MaxQMlPD`rC3~Fg%1VmLIEg~C$1%_O zG$ebIaE?*P-s|8H*&JKeIrg!SGjR6#`UBp-yszuNuKRwyp3lc4JtZZTV<^RF-OBkN%}vJIj{TLt z3{yQc$_5M3alv27R~3))p&tE3I41rNVB-1iQ53f|+4^|EZclWuh*u|(?tRGpPjbEB zZy5(S$Nlf6BY_j~K;LV+Q>WjY%-IK-)M}&x;sfyCP9BcUYgu0<{0%;0bHX1orWu*{ zAy_PocV45|&fM^UkKxmmv(;b2uARv+7H#}(WQ2UIMERA)rJuVxE(j%_3fnuU4$UDy z-ObSO_2_NH{6$O=M255Tp~j&$#M2|sA|Hevw7fAi>W8mom~^-=rE#+2WDLFom;$Tp z1d~&frxAkdHBFSs-nhu2V+gn}axn>?KqbG{Pi!}qwj!1HS4q3yyW7133AApBf!YOl z9JJq_;M=)p6k>7U{l3vsQBNRLxH>;8RX9`d@h$2N_CHeYAJ-?y8%h299GYVvPTxx8 z)+2!kARXRp+qwD)8r$XYS={9-U&B0X?6^kC%4VW*Ydi=&5G|y zFuKYV);AMaQ@9ar)$&2hkA{I*W$4j=12r5M^fw9u;;zM)7}i*DfL?UL+!JOUpdsYT zicK!31gEX^bPIxyXUr7kYz6kEiD8dD3!Wu`_q4?yg6l@o#$3-IzBTx2mt+(AzE^F# z*BIM5{xW(yLv#&z9V?{p^|)N++j<~;{Sl(px%*CiJ98AeyaP6a(~+M;XM&3_AAV?m zAW2#H(u@5dH>|3?8ubY3H_I=fv0_Bv;n4EANFZeI{`R(6v&!)pJ$o$vPr@SDU51km zY;4~bdZ(gtJrd4uC!|(DyrfcN5Xq;Lb#QzgURWeE#J{qbwYa52Lr8I(_l;2RX?wer z_b~6<)wk5#*a+K|vWfedJ0D$<$2FeXbx@$aNGdy0TV;c&nEgG%2=t6?@jg}TBO^Con3Zm-5gQKoLl*w) zU=`KV8!8T&ps^MA6zZvHa=N6;oqqaZ;gzFFu?058D5_ zfR}Q>1amCG;d7_^wNbX`n%J(}C9aMWgw|1UtMVF7YuJvwkLaE&pb^jP)hDd4CJC*Q zw<0|q!}NHaxpM-G3K$Ahm6{s6jSvCW-n0LF@>gG&`7x3+iGr>eqX*p>UL((p&F zE%C!e3GU?p|IjtWjnCiDZ~nDqrG!z1B8>oyh(BSFiq=m)J8FR%%9#XKg|Qe1Wj1in zOy?@}8mK$EEtd5h)VEj{C;gZsnYGB|b5J&pCp+n5M31TqK9sUPc!4P2ynO!f^|jL! zVQuRiAw>*OFZWob4Rc^?Oc0yN<-*7z29L$4w@)UDU1*}zTX>*Wy)^8!Ex?-ir5LR{ z#_nfY3OU?-)B87~O zY8NwM?q=;?aAUBZca5fI?`T}{u!<9ua~K}X zC~kcJ)GKWQ-K3}tJ$wd7q<;x+xi{(I;zGYa^5_msFlg9d8@C@qRN%Z9PI9DnQ)4;z z{!k=ets%BEnqYwLk6br>qL%7@l>M|SD(SC$_sVE$eergIyoi*|p}lJB#WB#pL_ysqMfQSQUdoq>tui_W1IB^#L$&ns`man`WX7u~pC1 z)=Yj`w;F!Rj-7|TOc9^sSL9r%t;Df4z0qDxPDz_)8n}c+cJD~Jw7Oe{%n<2XXF+Gz z_0XKfPe4;mlL2F4#qB9&gNf2EzrfZQ?SrK5ISy@i5q{j($+~KUPfKkSz8-d9x$b>1 z*rRzWFxT%+_!q7SfkL(G2px*22>OxQVAh5D3X?O0T>fa_D*T*Tv#8pxahP{z5g+s6 z+(EEPtnHoE zGjnaf%I97}hCUIt{ODK)N4e4-P4qt*Bwc+bq$go$*r!{_nDK#3`gBH|8kS0(Ze8X| zTV-$-9nCG>8P3$n^QVO!Z_qElpJC=mOutm0ba)}WG*}DE)xe0e`K0}g!v3oMR9^-% z5BR2jY=}AiP|H4Gh4B!j(6}hjv8m4z-ubcm?-5|Xz#)~1R^;f+pLxp@+1}XiMYF_l z(;ti1hPT=9eDY!wKeZeGz2eq#h+uV6a-HG`9$Q7fo?a^b{ zo%KD2cDia<%KcoISE{CocNh0RqDK5}!|D!U9jW3u+=KdMxTnkq_z1L;<-)^$xEijp z4{EUby*@nKfB*Py>rx5Yk%#h8q~@0?g?ouj)FHaPRU31=d5L7X;-a(iLWNwQpXbhcoiG{t$$@KwsTS+}32>ym@6$kuwQ%+vRI69j zE`KA(+nCP@q?4G6oi4$uze%@oc8iHUIHAdzQU$&hzn#n{oUj50{~6vOih!{Fkvp!v z8^zSf`O_J%DjVVNNX+x|7biHc>>yfAR?QL6qnN(IP(9pqVEO_5HjqPed~a_u6eRNA zgw=Y@9%p5+SNgw3O|A8bS@{z>>65~3Y8A65_*1~5|LwXbHOEO`+=EwYw;7r|FR8*v z7r`Yj!k=ax`8oNO0a6KG33c8@w=pdZz&#AE-*zy!_PKz_P2X!{r?0e|8LH)~-)<0~7Kz$3S@#v< zbR#}Cy)^&6!cO9lZq2_Gj@5>N%NQ!U9Wm3BzAI9W_CTik%cl2;VfU($XJH$eI60E6 z6a$nK`G6_eWKO(#Mf&{Wm_>Mh$k9h1EtmmheQFbg`-nJft7uplx_BP|P^B>!C9P6nW^c}JSvKBfvB+dNiW?#wRXHJ4Ga86$c@5^V*@a5P? z)IZeF5Y>+41P!?K9X9%ht}&k{87<7woiN;unP|0ZNW@bZ0H1R(v1!k5rTr!xUm<)Z zJI^+#26KPoDSmzbUs@X>23Puxj$%5|72tKrhO9$-D-nIfNxDsP;+u^FhI6Slo|t!_ zyf$FkLnBLBx1UAk7T&)ajF0#iPK|hn}H+_i&Zng~g!0l=!?S1{d z)hbJCq}u*DvxwQof^0$UsLQyVgu^b15hNI3FFZ&yrtMnV z_X6BFha2|P9m7tJ^z#lxu1`Gi=OK*z*Y?0rJet@0lh}<*oh5tqRvqV!UzV*JLyWOz z+-WQ@C2Xp~I${MkxZsBU#&&B1X_|7=8PmfVp3FhfI)Q9U@+RwR zDR6a71jf_HJNRyo(u{}3Hn-Zvn>u2KgQ}MSg=19hry)K)8;N zSEJE0eLyWJGY+uB$jMF(OE(&PnLR|j&b@vkIVV48rCS_0?9zZ7bYX> z$8v8@wPh8nW?%u(UW=JSMFR~&5;)nD134onnd_4=2h^Fq%5N?t9&;HV%-O+8aZ{-` z*^>EWV|}gck=3V1E4qCH<6jZu@FVo*K&xB1z$EwNf`hr=na^6QlkXp#+p};GYIM3J zEWHT8n9&JDvhYohyK~26!-7^#@o?09?3<%P|137d zXfYgHZYZIQ>K{2M%U=CFtqkGV3Jv4#%WJMZ7I(IC>VE4Fb5tSoacq(7i_}xO7Tdl# zRNtjS1tYoVy;9ymZ9&!#Mlt7S0Upi2zU8gOpXrwEq30F3giBm1ZZ+r79@dL(8D6j! zCSiZIm_Ysb*0)ol)tQrkQIC4@!!g6@SiJEl^5#b>cbfn@Z{_@Q)=qL-O#4Ci&&3{a zCiAKn7W%G@ebA52qtQ0jTWB{-xf8_>L;R<{j%@Ze+2SF)q9(q!i+G3DbBDpm?=a-h zDr4hz=olj1rF%;Pn|Xd|zCt0qtaA7$s2WhgkcM0~Z|IMITRD77Cwp4h%%vx8qkF~X zj%UC((7g*;k3$;LKNp5Ko-@>MtOZqnSoKVpRRoR^jjO*T9#MpcV26udV~Ii;8#|S& zC~Fui{Ka71XGnU=K2KdrL16rA{L*dKciW|to#l^&`=TadV|v*P!XMP*k(g!569^W z?yHp+c=-4$-#R&$es0_;wVV*c%*~DAZ-0}|c5vRfl{M>Tv-Otq(~rmQ_V-BJrzXh| z3RaDc5l7=MIWelu6EpWn_6^_9zPYtgZ`=T@S_dcpVMLgR6tKWF)SYU~f{}8f% z-c!|~a(MCK)bF=>- z$S1G9MbTn`r_dTBq29iJ`7}KIq#f7 z*atkfbV^in5({XLI~@DEbWJ<1l2j}*@X*l|`+9*>v(9~S9l!YfDPl{7^y=W}eAEw8 z0pihnIHf7p#OT?EPpgMau(%S-0Pj4b%RTe-;dkVu-VNUmS0u&X|os1 zuY1`X&49YzJg;Bd>`3u@>)D&)RyuKNJauSg7C#e4&*sBe>~*wNxZ>iRb=4lRd1=;* z^i|h<+AIed@-qE0e?_tFy>$zHVmduPLQ}R`r;2^! zL^RyDanYLJqpub?p)jUA`E83c6cJ?A!Dwuc&+B&s9XsT9mmHR$??cVBWO+fK8`clZ z?RE_*CZUEozf9b>n77%YmE?fyQ>w4dIHM1TqNyC#o^OmNt)JXxT*ahAs_mULq!txv zD$s&Y2QE^$IvA0;Ec}6{ zr}Vb==8BQn^TZ{W&?ls%d!asaNu?cVEAYM~paD=~mHLY)1Pa*xiOX1$_9`P}0^mHl7=dZ_n4HEi> z>9(3sjx!3quYwxN*B+L|`Y~6EINNRw(eJ@M!14ZSa|OP(K3_l59kpX#2S0heh1a?! zdQcFm5WUF0SaZ)-?aAFE&y1{Of9u3RKH~X}82IkH*uZrCE^^2EP-Xl}p)Y%LqCF8%c>Y4c?{~#HE zBZH$s%t%%~j`yS9ceC%yS7eqLDXos#yvDG-aVXV_3M$Nk7P*YFKVC1O&0Ja)#Y$r$ zFMV0L+<(-d$re+{OoqHogWIW1iQMg%9V}+1bda9)G!5-rh~QIxFkHUJ*m1DzvWwcB z^dh<)FG-WG1T4Aoj}{$=1(_RxpX1fr{^zM-8o+|y*`hqw{)9fK7766dMmE0 z+G7lgTg+&fkPR8L-|*VVOZ<#WUV9K|g{Dnkyv27ZV(m*@;6y+ieAZk~{62s5Q}r?9 zH|%q0`VG}l`|bV+{_wR+5?+)tyqlBneuC2J6A7B{%7ZFEMAO3vQ>CMIM*RJX964=q z9M!kc&}H@1hhgced=z=c?^Z+s&qjtgS?`twO|nA&xXbpU%-?MpR4Wjg;g_{~r;Iar zWZ0ls)I+@Cf9KhrzjR$(q8(6lR_R*DGn7#sp^{|0@ov z-Yl?!l#t`BAa!j{KW|ZFd#-IZGA+gmbf=zGp5EBK=`DDskg=f&wNd{Q_|bM`Z#4J_ zDrd`D4tg>S=$5O{s_ygl{JPV{Vx4qOBM)uP<}m5Zw==|#^2MK?C!^!cRFn#cQ}jCT zBOe&hzbd*RQ0pf>&*M79;WT3?r5i;kUlkIi{TO>ZH+JyVI#45i)A-%hx0;K-U8GbV zccwP8Nm(07dmT+t*+qJ*1!(u0Hj7;~2gmJwSS`-f&pQNUq4bZEccBFL?IR9Kx7u}y{{D0eNock8_`t5Gv_|#`9vJ+5x~z5gU0L{py1l)eLnx`;t#&l9q}Zgy-%Jh zpc66CUbX}Ev=HnHc`N>cxSX48+Pw8&i)}0__{ib#4@490<(rQm73nY7&N>CZ2;?Sv zFIC27K)E_mDO##K|R@a?v-;U|X4cMcXdV?Q3)joOigizn%?hF5NS zWzFmuw{U)HU782Xojo?)LivB22y>X9aXuNZ(TN!pT0bV&;;Y0+@7Noqc-DETBxu+l zXxEnA@0_u6kK5S?^d$?qMHO+gv?C4;|B=SFrVRqIroymw-vTlNn}fOl;#2&8M^g_P zw<%g7&txW`s1TjE7noN6NV*?skBxCoHI=}3)CVY1M1Aa1gkcEmq>=Unk zp<;#4d141d3)JG)ppv}YwJD8C{cXQWYG?iY&k*`ZzIw1WpNWI=F5=PdH(bLv@%Mdu_F~0qhk?ho;6TXv{NTpT_T@tSv0@J zbe_V*rE~a(dYKliPXqcSZdoae1pJWJrhFJ`LbKKet$jCaEBj^O09s@Y86??hB*&zD?8H&PS}Q2~F0P>w_5hl9hB4x=rb+L1#u`N8|teWSY% zekCk*ECCJ^BpGIS4r;|JoP3WQ0d3*w#~A^Spar&dERxR#iE$x~qpbh9|HF2&=-RNu zCQ&hRp`G+_9aPIYXlKBnbL;+V;e8%$+)C3~)|+~V69}LjV^2ytwC!fe@sM=jFMaee ze|B$jPOFi~mel3KMJc)F9ZmMmCQIDASlWsG>ZW}p_MqDRX6P(G_>eUiPwa7=DJlXB z_%eSXRY@_7zu%9>#`UDRV&Io33jriI`yT(LBW2cz-XHpTjI+HPlyb&pCF~$&sREVG#Us5GUqDUk6IW(-+l)#Yff^=y@}bCFVnfK95w=HOOojZyX&qR0Cpw{=F>v z5ls@aSurIcE|{zN-b^)^K9fK|thR{3vM@7yU^j#YG^V_q3+ayMA zip1T^8P%WAST~#4uv_qThLpeupe2Fp4|YXu2PGf~nQh=?3YQ?J4hRKL?RlCd;~fQ4 zb$)dQ0WV|C#%%XDyPhyFN7;X`Qwn(7^Qrc}c=BIJBhCsdMqW2yRb+T&hJWB03yJBJ zMxMHmAq(YuA%?1UPr&|u{jhDDr^OAL)O8mHgO;m*>x!>B5o3vee_hv*zvNL@vCjsd z;K<=s;bk^t?;oA=e=)@8y3g`&brzu?O-fn-Zzkyw4MG|C0*3{S67ik)!bkkzpO+Tv z*aUaXKHsTpT~o?UIP$RPR{N?DzTm$zyl2(>3jGlRQEZleKvW;9)wA9~I1)vp#@+34 z3OjGhoYiL!nQ=!xea8XEkFcs5sg^~@H+vALiHmH7UB4NvXfdyq@fH5HVX{GGQfa`k zV!tTv*}+U7WU5WvIJ~0266FPwZprf+ggOtMkmsLir-46|jk+I!drmu4Dk>3@WN#jq|>){BZ=)AJ1_NPN3|YF#>x&3pGXo*vp1a%1SBvVXR=64bS&I z4wb^_N9XdSk%$^^FN$>JG0!+!UF-3(mvPNChR2Q#?eT5awpIJw&J?t+F`@V;-E^}F=IODvI-jikCJ@h z)sflWCF0*`=3lT?sM_vYnDI@f`mV?JqCRIf_*5C%#>iP+Oo1vebdWUbeoW-*Scm>$ zso|33;;V8dwIpN>ObSF@6z|=B#wg@F+jS$feVK$$r}L;K@LwETdZuc0Kt+}_%F_yh zYi`f`emD?9lCXivi;#!Nd7fpnLjtnQbYiYMcad2l6tR`vIU`0PJ~eKIe`uGZU^HLoT6CPZ7h>>OifWoK`$}ofSf1*p@*Qg zLE&h?s)gC4vmrHLXA?jh)E}2)itYvD6S96+;7o&QkKuPw{7rO9 zFiaIe(tre|bB-XjJPy88hK;>`y&`#-4y-tiByUF^w)gipQdp;3H^LA30cr2NlBOzG zE8x3X|8;}H4nbX>Qp!oqIplhx#ypWgW~90;9AicQqoK@~Yl0+(VoPj1+nmPNs`B-j zuPm)bY+`9L7{#4>1)Z#LL=ND3FYK@bd)$}hO`}&4qEbVYxe~3X(u@`<7e!xXkJ~okpKQ)_0BO$ zTINF2!mltl`Y>|+@;6xDfaF$|7$kW*OFFyz_PB(l;l@?f(vL1iC3UY=oQKxts@AnE z#4Y9!anhw1#tQhG`W(M4+=`Tg-{3a27ze<5XWjj{A}#I#E6*G0^vhF*$1YWdb{Q|_ z24C^~w?#LC|H)YniWmfU1UE?eViZQfd}(9}ID)KUfs~5NeSDPiM)PY#LJXrH-(PeW zmI@sZe(WisQ`j@V>0cKs-876-Bs_6YK)kn(|Bxa+zMfJNm4(RBF%|7eS>Ma#u}*~s z)@OxXiz&q*t;7s5HWD}PYp6ZCxMf(^=jQuR&!*liNtrWEp~>SQ+ZhjjYw_R+NSBeZg7>Ca6_g|DAl}X@>Ih?9vEi!od6*wb#q*pb`H> zOR6FWf6<);S9t;Xn#-@QVjOa_Y+SL^D5Rl z*V>;~3k|MiHO!vi*GHe8bpU4i@XJ6txIk!2%6l6MeLQxhYvQb^<6eG- zv@4~yD|G*qR^&^#{Ga3M_YdE_0~BkOJ3GA{717!|Ewd`|W+3P7srSQWn�{_Jn2^ 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

`wOOJjPV}&{uVKarTlWPLgHkh?+0ktxv)px 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

?T`4-OTixH7=E{(B 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�T_-LhDo`4th9tN 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�AL#Y2t1~5g@AWvZ`~19X{f9sL_TT-bum8#S_Ph6Q-+ua`9&fJo zh{vPiDX12ttZ0Z!Ppm1%`egbCYYsU!@oP=dE8rwG)2w_FVY4)v&g6UE@sqDh+@7S9A+&bl3F zj-21$Iy1*opm96mtcg^J%k`KV3+6w0Fzt{?mR@-zD-02rEe*0nA?Oae{)z}#6q7A~ z$9dZ#rKep2tslBc_+EJh+JTkqA>L!W*MT6)_A+<7!W-`+8*DI`>0x_lDPj+|b51XQ z`1&`GGvv0nevu`ACAng#8N~cbvYvr(rIpsb9lZ%Pl?Ex9$2s9CYZXLKY82X!=L2Cb(W9Z z1*7B-dGg@5Ojtk1sQ}UF+q}S`~nfExt_lNvT*GasjGOp-A1Iw>|)HY)TPprm!bdl7%o)d2C^Y5$}oQ8Gf1; zHVvYiaITD}N3+ozVCR-SO-5I^*&<5l8spal>YzK$nsn}^=oRy{Suctr2Zism` zCm;Fd1*Sww)`-{X)~rrU0Qd(LQu2pfF>6!L#=TuLh8};Z;H{w?EV*T!{(k

02eY z`}7SUlqwy4JwMg=fH^DnHwZc!NTtpG0LkyosiP0xvVj@7Szt_^{|0?};OMxP7zb>R z4814x#rsdJlFB<^vGnrqFc0tFZ@EfndG}{v9|q2Q&!Z@i3q6xZ9YQrPw$Y26^GjXx z8tcS?V}j!y59uIboq&+?C;+EiCqSpyw((vd2=Tj6Y+B1~#Zhj%N_`2~kK&eS=yC|@ zyK6kpfUXCjZ`3~|5d5`mRh?aGiflb%efs|0-}zfVv)0v5p?#K7mj|7hGYQN!1aSa^ zP&IryWiC>=k8GK&l_~-pc-Tl1A0xB|NhnDL^?G7$Q#S{IgRhRSR1Qj>LEV#L=aEf$ z^ithdJb(7t&h-c1y8iCJ`1!j)%mc< zJQA5YiHC&d%PHBZEAPGyz2g}kgO84clj36Q(&(s4a^~1PsgL?n4-2ZFwQ6@$+m*&j zKF7E)>?T*M!8^c6{tLx<~2R9db7;J(2102XfPF;%^A)nLD` z6xkmx#B#%ESCE|i!)E4F{WNG-+1R}eHre%Zk<~mGOPcU7`X_|GQO-}g%g%Y~(uiVJ z2g)CNfL;t$_Xh7eFGyT1xxRFf3eDYD+bevoWjMMSj;!$I5I*JrKg`cdo@e+v*F~q;YtrNacibX30X_okMr!Dk#1Y}4L>^f} zLnSjJ|FA6TK~xHx-P^D7HENy;9C{JZv9f=AceFJxAIDsE8|N5wQUX&Ij>|FZ1_Tt0 z$Y|ewccf6#VS|H<*++|BNY~Hoh{w`NPcTNY2p9^k-6%2xsvMng#L5*wB@_a|NzD=9 z(JpjB_{Lbpax=sE3o0DujF|*~5@pI7e1(DckLwQQ2P@6H$vjr)Si-Rf{)|4|6bgRT z=Uh)2XzgdjP2uO2J9H4&eE<#`a~*lT zQ**bkoqL0Ytpt|@_nEj)h;+q~G+VBh>NkTPGm?X%7T$E?qw(xj7ELCPT>65|A7{!b zCEHO>D5Yc}2mw(l2=@!YWcV?3@oP}OP^+iszywANpv^?{nWz<%zM1Rbsk|M4=GYJ> zL?Xgk^Afk2vEezMVQ~M1ZFLV{lW6{nmfqd;rKjHVHZ$^s$I_`IJv7pm06wdDn zXrMgK{nD{--M}3{!O%uf}-`&?^Eso5?I)C(P<|p!){k_3ihv-heeGHQ+0~ z-IkfnLQZL{hm4pJG4~MY2j_d0@hPy+q47G_^GH6*f^Hz6QZNU0wwd6}N5pzO)(?I8 z%Rm0tf8>#C9acfN22Ir)U$W3UZvs{oO6~~$Hr#e%H&*r}`X>~$f#cz%nP}3+>ag%b zb*BtVGsZHBvJX)cb=&hS@R@>Qm6M>^7Z-Bv=UprR?0Whi|JA?yH^2P{>&=I^@87?B z_x>gFd$B@~9>)!GdOv!n0Ap*!0~vW zZVo+smTAYhe()LL(zW71WB_H^Sw1feswrJPApPT2ZoE~vwj%1*Tz06KCDVdhtjYGi z`rU4J&+wq?RNQDiN2w%`l^0A`^b@xFT_8<@h65}B#QJG`yAcii&^dQD!R)d41%upR z?BSz|Rq^V2+%V3(|99CR{8cl2kKuR44LD9SvUPHB-%d8Qk7I|KF+!Cra~$LZ!t<=x zZ{Yj+vK3Qyi9Ql{$fSF4Ap=%VO@v$pc~@E^Z`)g5(2(~{4K?Qy#O}suRIoMPNo>r zog-&0(`@4w!%NMISA2J~{1t*l8u`9s@MSMN?Szf{^PXGyocm+m4|kO7Q+Mhx&WKyQ zMc2?vvBpY$bzKeuw@L2X+|1|KSc#wWJ~Kxl98T_NPS(=MlW@LL922@{AwDvVV^U{# zby@g}5xtsj`SRRHv+}8)!tDwIhhFeytdXWndsgK1Dmou_+$bwjXN{K|x5- z5Rp>%Euh}~NN<%KRbn&?`>Tu-DE2-OLyP_#%>kxQSeXll4rgu*J$o0i;Fq#|)W;qP zg=+8}`RgEFk-{Ci`vL}L=l%J;>8SUg0GBIoCj;Si!lqo_TE8Xkd#xJQndH2s_fHv~ zBoS$jGYn_m?`MsxDp;Gjdz%+%NyTu()vut)tlN92hrnvD9EjJ+!ze2+??dcs!m@n- zMU5RSd_1=0aR^14?W{s3IP(M1>8aw|jqmarSc4u*=nC;Is|C2Ks_uOf7A6#F5^mF6 z{m?f2$MIk5>E-T6#^3napZL-z@9Mg;9xs35X+2hYRU8Me1$^3RVsMB(Q4o*u^@gdN z`lD{)K}n?X5U>Li_a9w$6^d%elVVFe+q?4&nKY>|a6uI@2&@VUZ^W-b+11bAbOojbCzR0V83wjtgd)vN-%8q>J9+t`lcg_Eu^D6tL9M;2s^L&yWxZpw)&QDz~ZDN`PuY+1HCk8-)He{f%mfT_?KxfYQCd~4Cplq!+IxW~N5$xfwEGF2I5 zTyz0Deeif0=Y;<=I_dBF{HvnVtRrRgQ+n7K5wayUDW#MI?myBsEjG> zFoU4Gb$Wfj3@p5hZr%?3WTh&7V)hj=Av%?*2GEi6@r=iJ7r;&mWqK0|2Zw_rkSJBt z$NJ+Kew|%-h7!IXeiS1ba7E>Rd_2VW1pOHCAphc{>vF0i6{&<}uzY^k;4OsToIpBV zE#xTXzmQVT;>S>UwO#c@ZEf?pSs#PrVKQN8c5WR(*Yb>BrB> z+jG*ZAneb3#60Hp8nC` zXoy$x&eVC>)V)@-@S!)4g=D>E?gWdbY!)9x4c+=6?;L*~W)S{{4sdPFqeD-=zB3KT zUX-6xroJ!gx9l2wp#MQP(I{_L%hWWh&A-M+k%)-@?&_M_StF27z&)|4L5FyXyQ{2b zQ3~QkktKMNN}JYjQql4QDA?S+Ve&E!TBQp3`TG?n%s z68DJ$bQ;hKDlZHlF}WTbbhUiWZrrvs2cnAfAa12Uq6&3e_N=)MMlrZwoDoo&5m06mhK03n&vC9aD$Ewm zF6iOzmmOQsD(oPTj+~228Uo%IwlbsfxUT*DT>13%-~E$+@GIZ=&U3!`@c!NVxAErP z*(^u*b178a%vGM>Uq~)!oVmbJ;{pp?6;mZkVIeyh+g6v?Pe^|up-UrNl1tZlaXOg6 z!_sGGl*`@J$d3I4P+t|JwTb>H&eBZMoF=t~cR{p6{W;aUXrCy9YMa@%2n z3*e!yy^m2@|a#VCO(nHtjjnIa}ALw%jMA(7nMb)SS4%r!J0mqIgXYwYG+G~d=_AbY@Whx9E zY9f~@^FCMoxM#Rr{@}W~pEK;Jn_D{IvhgwHddTs>d}T00N8O|W-Tm0q{o}fG2}YbK z0GAG^YnXxitkU$<$Sn-aSTkgL0Ap6{g=AyICP8kqOP0=Aa!fME1UdcyKNOU}dyrJ4 zj~yfPalNp7a`Inve3cYmZ+!+5q`RH*Q9prj;ggPII%BY2=53DA^7T;Q$~?sFge0yp z`vBxvb57;X`{VmO=Of-uesn(_4F1!0B(yLN!Bb~azY|DW&ndk*^Z~$rOYK>*Hrt#n z=-M2?4R{0D3j_kv365ib`x5Y%>P}%kcfSzy-4E>?(zYu52e`AA*cgrdFC0s&yv^_& zsbfPV0{rO~tjO`Zo!aV{XIww9JoKK|8Ug$Qhg^E_1kYmXJ;wx~$lGOcQ_ve+Y@4Fz zG#6HV0Ib?~bf6dMur=c7^Znq3SEywr_3T&3K=2gK*4Bm!5TKPMJ^M_n2pSM0!X-Ld zE4{8c4!z+w#)0G3W4frjf4_g|@dx@pXah7y_>frohXiQ~EW&z7jAbjowXA*WUSL48 z4?w%@OIl3)#-7I>XXuE_dTf*o&{-8=n2VH#!)59}v9um?^a1emkJr9KGrtP;E91M` zC8=7Q$Yb613w6hr#GSi^d;ImUeC5Y}_=h5|WGjn>eixj39uO(3o0~E`6M2AsKLN8& z2QAni%3~DP!x@ytFpdgv@S2zU2{46}&=F^!Js1plu4|J*?31oab!drl!IwYsI8nk; z^W4`}`}unOhd=p?zyHhM_`^TF9`C;R_T9TH9?z9moQ{Sf@xMI$A@cPG>Uxfi&Dt8S zA?9$F65PcWLd|y6VS_L0$j!f!91T zz{E%lNFkP)$VGR^t)#tJ0dv6~qQ6H{!s=XK6a%6@;pwvgC0nNAygtb+pSu2)!#?YP z&#~WSHxhMJ8~v2_0iRaaQNVUE%>=?3qx^1{h)DVL$7l}59RWSKtl$NERuYvPK1B^j zbkMeLZeNV{J36x9brN;LvV<_LRI+$v@8Pd-M!a0;lOQxja#J*~)E0U9t`=NKHOE1kaMtl)c^}4YNaNSJX%nf_CXy4KuVkZ{yvIH z;yy9De_U@xB+iSd3-s%9%nRp{5thR;xD}f$exqH8Ok(nI^ansL zpvL^D4?ynya)9P~^wIfp)T*5*!vOhjo5JA0PQ-7<@kQjhRsDMpLqVd^Q?YUY9{^-Y zEL<|Ke6wai+R&xJ=`j%zwRP@;sLkF0cf>b6N70tPM_OGbQiTf@)_bOH z0?<%-x@r*b6m;gk2VvCuyx`_-XunJ7%NiX_tFZsT7QC{{_OWH3S5O>?;px_rQVZX@ z1Ik`)XN6W^wi<3nY{f@7ahC;3kd< z`TF?W*bXCuKR9ez9v~mQno2HXHQN#JQv?eucs(|)aPau8l@e%aqqrN#=$1~g4^v`Q zT>Co01S_r{gQx%ez5EBi^jp9A`~Ugr-Me>h-$p)md+?9-5@k|?tfH>CPJbIox+pom z#h9ysKvrETwJVi~z>W#H-=ltH3kj#(8GcEB*wQ-K*If3sP4SN{^T+}e>~=*M)Ym^& z<0!8gW)*%{LO8@!2b4uP&>%kv9j&B%nYkVn>VsqE-4Zw8?imdd zBz+~E`{R1043NB@LQ1ZNcNe4XXLz`&v($M92E^(HG^U+;_S*0EgVM=#5eQJ%V#q6u zz^2M{rvF%ag}A+^`vAmjt0?pp{(#m)g*$!W7Z?4;&Ew5(FzjdEJUNGt`ZV0fI&3#9#G@#B5H=JPY+cE6-{g?EkT)Pxqk|NGNb zP_pL#EHg8LcurV=zHnIQvRPQm>63=s zc)vI1O*xsuJCt|ESPC~r3)X(lG7eNBc>q$prI36!^wLCAiap2kWAKfZB*fFX6gQM& zx~zzw^?N=P^8{Jtj8P+Rv}Jb0=;&Cb6@tHdGHk4tl%qi6bk2tzVw#r^ub_GNew? z%pCsL2=@dCo?5WfiI0Fhlte=~{$#lmn^+}m2J4~Me2*uS1sh;lLT9w#>S_<81#=(Lh36Ieq8q%-s-4$}xkvqTct*?2YXoeqeovWGDO z%37<;0&Y{%AsiH-Xr>o>J-y7%lO!*X!?~YpKR?&Ep4UJA#_#;=-~Ro|Pv5@%P?49| z2Hi2BLX6+_GMbCdY;WK~GZJE4JsCj_H7kQM$vBK!Qu;QTyI^t2d4t8~z&SZm5$cn` z1+f&Mv%oZ!r5$HUE@!GM+&3d6Z$3_fVC0J4x}Zg9H{Q6UIgbOPqi~>>^I*2`vKrLJ zDKsO1s1^S6(rbsUPuR5Nu#@IKCq4J{I13mk0s314r^zzl^c1L7Om=sYWZ)z`Frdn$ z(&d#}57h22ZT+A=66!fpB2UN>YqRw~HTmz1@AID7u}F$Bh>V*ZHYO_pEEm4$1k-!JZr5wCfE^}7Z*=k@a6 zZ*(q2)OUJQ)~i_z$9R9`_3!tb&%J(I81XtX8RNzr8p)&yNtQeNHe=*BsDY85t8(<~ zI^;@P@mA#7rwwq_MVL};pMg#wsc-lYI~k40sODl}mF%;>T+R$94o&%ARx`!Pc!qGXfsQaD@H_!Oaz6TRA z*lhGp$}uWnQI+SOl|?spqeL{i5)<*gH9#X&)^lIvhj+B zW|6txY5?asx9X=ikPyB-JLBRl^EfaVOG4flWRkg9At9VsKXjCJ>`K7lDu$ zhv9J=ZLhWJt3UXqAN%3Ih9kI%c6;Nk;;J&xXX`kq(>%31!@Qk>jkIlWb6lp%Q}!eV zHq5iQw0TSUUS%Qd6%cDfp1-7A00?=JWR0-oI?mJS8r6dF-^-`3|L!0E<6rxYKd*fE z;oW{bPH6Es$;!B{z10t7vmtG`x(jB2hfNN#xEjj)I%0jla@1AM_>FzqB$L)EV^Xvj z6X8}W!5lDrTUd&aJMF3LzD3Li!ILQ#V1C@vOmbjDbP7aykDjMhw9nyLBnU7(mlCp6 z_#~m`s&7zKLjN!u^QP{;kJOagPOpy15k&QC@Hd26R;U7=I5F%My0euBspq8;6OLNk zemBTFr!q?HTIsZ75y@l?*HpT{&Q}L1CdGjm8RUR(k{EG-!$zf724|^LJ0VC;}lnm2}YX&3s zjq+v|&UZCE(oM=!237nDX0_n|n{7kapR|NfB(DigwB3*of%TQF)MGrwSpDMlF=?d; zhX}$Pvl@IChf)=<{^I-3UOMF+=gfp2J1V)x#`HLhE1bC7cL{0mBamQ27qF8OTuDez z&)MWZmnMbBteAaD(S!=^Qqd5x~=v=LS21w-XAgX`#BTNdAz!A zMFO2i;>pDw!LKo*uO~9@I;=5>3;8;3nRZVr^c>_$)oYG1_QJ=H-BB zyXtTJ%#VNi;ayzycsyPl$sGW&gU4Cg?%jF-M8l{c!VD1xI#r@mr2p#cM(ZL&vGcl{ zy|k&(S(IA1YPyUUT1VCfe$F}-O{~pY-AbUkW5v<@eE#0`yz}?I^%wu}U;Vp3{`1G< z?T30iT^mB5tM%90aiJhfD!0^}Y}jw={bT=1AYrj>`Afus8jN58`DqeWtaSlve1+aQ zA0ivCVp=a#eIz>NlB}#w)*aksdwgUxJ3m7A=f3IvW4LG6734G!eq+BU?11~=ZXduI zu7^uGI6NFs$0F~3$(}Rlr{j_-{eRct)Y9{#*Adr$8gED47ZS9|Zno@~vYIBHgU7wL zszMk}tJdw$pr<2p8;Ev7LEHvN{gS4Rg={ErZrQ@pOP*Bd)rR>pvVTv0b>_@Y*h8l|2Y2w-b@ zO2aS2>M{S2N+LocG6|am4uE(DP@Mr?8nbsi~XT{}5pp zkB1D4a_HvW%$>^jsVM|m{*!t`f+Ye0ckKOG!sjaMI;r|A=r8OVKBAiZg{2|-tdidU z-XNluOM4i5vks|GJ3M1EeP1LvkzbB5t5z4QAmz7j0~(UDtuAVz2sU;70l)}R+r*E{ z{FC-5U@nd60jW*W=7o(!Kc7*Je6EAZCE6OO03edhF`LgmmjS+aQ%9Urx!KCwO%KmF z9DVxsOp1~sOr{q(+BK3&ar-UURF58X{WWe9@Bplk7=I}8IDJE;FGy=Y-@Ye372|*< z($M@a>GY!jvbavVwnVaPL+^iub=(C9Ykqak7ZA`QGF2{?c#!_8)xv?YmFjym@o&y&jML!y835ZD|PR zVLhmJNye*bj+++AK#bRO<xi3A)|Qk8-i z0sAv1WRG=HG0IrvisBGsoe7;X;&i)){Gkg(^}XDW1QRx!_+xGuD!j z$FNs*#-TQdBD0)|e_v-nhFF|_6Lf2pW2&oV4FA{>Y+)ty&ZQJfCrIQ9_ zOZ(d6R&}4>Y-P9P7b9m1G^zsK*YUUAr2*XJ$r__Iz{LugsF1jreT4YfGts}N9oyS~ zlCLUHp~jPPq+lX)#P9sYfnN^p2Jq9tDscJ7$i3BYp=+>w?!HvS4brx6MY@=urlV`z zpQ-_{iZq@`6N*>m&p8KDlceXG|6#u*ni9wA=H;8*BSv0h3xPwmyLW`q!+e4X|Mf7HLH_7 z)%tEaB{^3#O^mU8I!+pK-yik*ImX3xJ-*L%-yb7yeY^k8+Mn<9Y6Z+}*DV(Fx_Nbf zjgxTPUw3xU=Z=H^3*R}FB^WWc?=MGNZD(PYEVir=MtdK?EY)pJe!AsH4s^FblK?{0 zgxw4>mk%yk;>vUJHXhu-RF06jgMED<54R2vA{br3h{~c+L(x z^8Lmyi*q9(n7SVr;gaZxBu`qmdz&@JY2qT6NQbeH&KrPP4qcWLXeaY$E7qb(gU&Rf z-!*{PUO3RvA3E`|n0paDzNOh4C)@E_Cp-D#tXs&C?tXLAUC)rCk3nmpc>EcJW;*9X z39jb;f)boF)l@aCIC3CTmvDSUK#m727ksMhz%nyN>3PqR1DnK0D<(HKUiUp9Z|asm z6C%yN7G7648{^gZs2pbj`={lQQ=EITQ5jj#oJXAif@Fi7d5oJ!>Eu;d!29xm$7BDs zuYUQ*e)#`a&!4TycC0kNE7f=kHzDpVymz_8Z^)_5bS+ zg~w9-Oz&y>W#`M#JbKMO2_}Cu z;8~>esWvkfdcZ5cRWxd>1Mxg&%d&j4+gbCf&63!z+SsO zEDg1fY#Zg*f6=dqQMIZ?INbB^bH9K5?k5PLMMN@r5JFRF(ih7~7MpCBymk7)BuwTI z$0V1e*e`52rkD z+t0Z9nXjTVImtQ)Ts0rYj4{r_KjA)s8D`^kn@q9sWzEDE+yIDNSQzM4bDD9Y;Bt>u zlXF2TR4nehqkuk!gc*R4Bg%Jt3? zpE6PufY@D#4!xkM;3wwb>bVa2zeBE=;}`l~j1Nd-_xU0%9V5Nbt{1`x`8fXYsvrH4 zAO6Y@e5nJ%F4#~0dV2=o^2vL%9SElS+IhMm?*anW|Sw$@_RS)F9k1ZJr92E$N|L zKn$ykV1fl|=$)(UqBAI{FTg4XAuX+H?w59XK$!-w1A}P=+ra*uVik%iwUlg=Ls-r_ zjKr?4yNt(;xGivoHoWC?1W3vzhyT?g+AWsL2Zj5d@c-RKdUxX0&@Ym9G0_3M=l1XV zQw0DkjS{JGBZkB%*Di$FE5#g_xZf~z2llykd&x&^`$7^s&+-+BKp3oUph6A6+t=l- z&%e^%i2aOw<;RShBW`08;y1ZY*WPT}ZhG%~ImiL#alpuNu$dhF+Yz3~$n7~Jb}-BW zpGMfau>a6@)aR*YdaRfncyI9Nl@^tLcWl`Z@!heQUT+NHy|mTG;CLC?3@p7EJeQcU zD-VNL8EDPs2pp{OGGYilWW)u@pQ%Op+k=k=WZt=sk5DpBkct6^jo<@KUybW~#$q@a zH4j6j)159QXA3+aUmxvZN^MoJ5N^+LkfeOnP_Sb`!7cf($l{~-;iNWBT_qhTofPB# zu^N4S@NnELw6A}3KQb((DB>@>j(mn!aqGx}%rf#vXTG+c1IQdoKwH1Rim`o9`&kS{ z{pFVp@o2XiT-~JJ%R#q06wSL%cD8AwPP`go7In=-nu(b7U1HlUyd>`L-UNxH6-@@U z?u~SxA2=G&)SLH`*E^D*N^NcG=zCdDz#PvX?&a$5Iz2_vOk1gp1s-tL)Caezr=ngY+HWMbpM1Q_Rs(1qZ_$0ztn!~8T5loKHOh}C(B}aHQiXQ>3*?=cnaBw&Kqy_ z8C)R8Q$M@dthwo{p-(B_Q3z>PH>z(q0r(m*q@9UxyL{~h$-xn(`lIt9&fk1bS&yE=T2kRe#s7PAK zmFefxsi!B~Ya06&y!8RFTQ^cF>lMljL$7{8@E&Ir;&@{S&1 z$m!1U{;`MxRp#V3DGAWBVpCVLhJ{ey0HvPK87=?QN#44>KeVUL>n)HOsxBjmkJ?>tR}pqjtUO0|0p~L60SGK1xWlqMRG6JaNK09t0kr zBSOsEb=6o@)QDh&-Wl%}7EUDUqwDedI?0G5=KFs+d;6!`vZFk#d!6@=BqW3pLI_DV zwo`Wg|6h|-Dv9l?IL4_mmDnZoVPhm)0u%#6h?#l!>eN~NeDpr|;#6gTVP?<1`|Q0w zdUZej^wauXQ6=Yd7~zWdYKGe?>6$+<%1&W=8)3wjbF+B6)k9b@;g5 zo@=Y^fnA#l5~UmU&+SYIkpmR0>9D^7u`Vq$(51Mk;{kt2y`tVZc_j3CU z-(8lTGYR+0dg5731NQTQb5_o?e9DuazDogwo;=-i|JK4!0;SdMoJ1liaQij>7!$5X z=!HA8z$^{FtBB7TBZIC#NX0<xhyyW1JNMT+7bZ(Dj(rB)clC?3jKw@5aPTfufOWV|hA&pldcawB=YMhj)GUQy=n(D?IHvAY~goLnT(8Z51q5cg0w&w*iQ=p;ryzUrdnryF>ehw$Y!WL{4 z&>5LjDMhh;_Va%(;jGl;!a5|vd4K*$np>VocXS);Uo@Fb;)$}oAvcAlxO)&hUw2b3kA+!U ztuEeDe}J}=I>%xUn;AR(+WM_^>?CT|T3Rnpd$JQB^arIgNZexhccjv-D_=r`gKEt7 z(HycUs+B)bwEruVkR1L=gs6#PB&MJZUWY%Wz*kw=t<>Vk8cAn`{*pRp&gUaq6)AM` z--sw@ZNDyV{1-t-gJE~BtIY`esxt0ih;r)#5HHu62PP29StGI(V?JM>GkN@qdS31?_5sk} z`MZLA?m%Y5N5AXjhT>#j|Mcd#r!-4}R-6zxBwC^Tyt*Z@J>ScI|ydT$x|& z{N3OG>F@rBpYF#u9_z6xzI?x~iuFjyh$W0nFz3Kch+|GDN~-7wo8CF%H5QJsar4AD zTCxx3flzOrfS&K>^`KO5DQP$B{B@pdoZ(Pf(m|Cv@H)F16 zz9%$}qU;+Zk&vijrFs*)@?z1ziXeba?Q5?X{K%pVnEI0xg?&={RJ9i=ge*Oe)>TvQ zVIa3FDWnoms)vWA;eGztqphDR)yM>0T+^9@sG!25>Mr-ge8#!#?@cv85yyQA`$TdCqk_&!g(T;6h z_Zd-Np&8!|?o>D@q)=x(C-`6aoR9AR_G9Ld@Hzv%k#k6nXDCfei=AYka=FqOq`C>m+2@cU8hmUI82c)u@5d9BNpD4_=(5f7 ze3{Y=BcgfSnwS1A%#rFt3hRe0e0LJ}P<|-}K~S8_p4>wJ2bH!Y2h}xw1##FItGdU3 z&-JM|4DXLo@>V^h9f@r25VV*z_{{t$oK6d>s0T)RtVjLRfAE8^eRv#|%F`LQ%gdQE z0BY4=Usw~gN9+g-J7e9S7>w34VEtZ{tr5D91#N8uIapcVI09h#U-!U;kZ0)ZWvi@a zDH1*zfs;Kttfv)q)vh1^)#v}?@BQJ=>%)hyd|02oeSTg0YECxGHQgLCv{UCe14E^TYPjw>=u=_$LK`n`UQ|y1$N$CP z+`J#xE8_KfIxim|{fXx^U1NiKRN*h#a}W0o&|b&r5SY@h^QZDQEHUzdvdhD&-jL(E zziL=H)Zs!7pHSxzEB(-YPG&g#3k`Xld%{vZn9uiw8{3(!mG#-J0vAh|kR zC(;?t<>)I&>Dr$>;w?6_Vk>xABRFKwO@soK*)EF1dq zDx5(i?WM)E;Ys+P<7Aif9NfUty~%=b4^Fz5_pCq^MSXo%L2wlnw%)QVQ=<|=9sK}{ zT5>go8!A0qQ~&g?_K=O#^FYx6V#oG>&I(I^IP6V3HvNvdyL#FCF;?4BK2JX zUN0~{tmh!+%J)Q&9qB$80>f(I)zW<%8WU~jA=9}pB>6E{nXC0%divhhdW)~VJ^s7j z`TH>3>$JT%`{nB8#M+zye28Rm%4^_%-OM=zdH+nHi8wDeAn7x@e47cSRldXjbljEjlzmy13j5KI0)EWFdRg{8AjbLA);;6(-Ed(s>9M4b zp47!U|Hvwl{#UMZik1KqSxX7{WgI`zbuPP+3lo3$yczZT@n6b)IkH<$uk!n|jV}qu z7zci@d^6~$wJZ=ic8&TR{_cP?H|v$zS3&zojzT&+>3m>3Bo%b(;5C;^xFXi0?9xIm zmFu*P^mrfHnS@{#&XDfUEigNgvrFpQ&2($8lU%l(kwa4MXkp`oV+G$yzIE>uG_XOF zHSa>^Na$5E>bv$yo(Kd@ywC9IVq*Lpmihjx@;1>tBh?2WI&oTr zJup)+Ba7f1>$Zl#Uj_Lp?2c1WQ8SwMvK0?qWbPWSp!xTNCT`ae3vpkJF;V<<)Ag^v z`?|9Sf5-h{kA5OHT3Yk|5siJ~IT3fCKZL5xFVWH*ZyeJ5uMsie0I%D7nl}K4mnU*x z-yu*$9XHDemC!J_rp<{weE?8YN7g&>es~oMNViKA46g(++HQo1g;grO;UCZ-p5;g# zbHbLsKXw1WGj6*6aiazG^S3s|0eaZ0wXpoH2amP2ogU{>DRoh%e*8_Z-Di30@rQM< zP2xN6xk=OAp?U_qCrR)?o7mMWK$S>>4Mn@QZyQV!z3pv?Tp36dLNlX)C+`t81r6n5 zVN#M-98*U>hLkarWaE1W9kgcRc+4-~UtCKjaXFsgJ(ilet4{*@Fwmgy!RSyh&*y_V zT;l*RzFYWR(K53{rA>Barm$eK{{xSxJ>~@ovyyq~->}}l*HsZx1Fn14A$4vBiB-)b zEG4rmAF+2)t$#&)=i9&jgYW+4{_?$>Ln9J>N7fnKH+fUrxgVXDoCD(@N||Y+rqLcH z=<0snF3qnxx1f9__DJ47U39um+WtqPw`fTOlNUT*3PXW2YH(}PSw1&pC;#&^Vkttg~)Jz*xA zH}(NQIM7RsnMrcKifx*cgavDeh7k3XC<~Axxp~S-3=_$bQLgWW_doys3H^RPN6){x zA2~-p{#(!g==%Nsu*X00G`D(#`*(f({&8o~#fV61tkxL3s4+eq@Q=Qit6;u=3w3ocT`M7SmRHF>F3uM!WSK8D^)pdfi5do!O4Q1wG(FN%sqr@!!+B>P2JKEzs_D}k zAT8=T*+h6-^J>GIX{SpdmCRtuf{9+*cEzLDRdhlbGxJqmV z0E`)2ikRq=?GbD74Dnr!CF9&rqWW+I zn5u1+dA(=)_&)RAHBLO+k$Npj$CK4!$RHcXZ+1 zH=uJ10X~1nt&g*PG0KYxloge)?k%cV2#3!_M#C{UszIu@I#cMigCNd`PQ&a0vO_8t`4j3!7r8Mh_8I zjbUX%JouXIB#{*Dkx1IaJ!)X@JW1kE+KTy6>9GutGz8(n-a3z}n1WkO{fQNGCq1DD@UuzD=SdZleZ2tZ5 z9MWqt>z2)6#H;=Q`D&?$uD|zw+AaJjZqxH?;^nvB)gFI&{@6XY_a9a_M!Y_*k6ogb zg8Ns*WN>AS=Y+Elqi!J-XX|@o-k;t%<2?x$yNa3&`n&`0w7RD|;f~DlX3u5u2ucZ@ zL@>ff$bYjq#-i)S?4=&&KAv&o>R_cHWp3?i>I~&!4cgP2*~-nD}=hBIlf^-cu6iy zV!b4RAj3`(mR{00jjqi(vOX@Rs8+b|Gn)$aT}< zbr~5PE>3dOB-wD)Bf#||5>mnMjgc`(f7=>_%rJVUrTq+UX;({n;sr%59y}IKLjzbX z^z^DsQoKs>MJeIN?W+Z22#8+~ke$Hg;`)TVVFqw$>F2xrl!`_C2jxL3d0r-a^Gi!z&5-&NFS#F6hoptr zulK9Zq31GuJpTNb`+xeq|L|wO_%c6ySa0z~J;R}=^M5@DCYuGt$Oa7W099Tey@ie< ziOf@s!YfTRzmS)P%YFZVnOp%_f&_5Z1C>H4~o55k1JE9%w;^VD5=m)34*{I!mkQ5V=1< zctvw##GT&Dh`vTnLVK<=WD&1wt2r8&kKd&=kT+H%_opH8HkL`U60N}48t*v~b?$^uFP|fE!A~5Ax&!1_}xIG^d_8&c8dvPXf$2lF;`=@7P zIzXj4HZk@zy3FM8bobkGfNC`+% zpZ@6Fhk|J&;ajUG#o$kdQ)!tj#5$?(ttq<2_sQ9NtJL?PLi+l8Pq_K1I*R_*1s@LI z-SBR`@RIQ-*8O{n!teu#fIc@Xkt(cT9tIr+nV~1K)==wzpHOL25GDAWCH;OGh_kL% z#(aK!{GRxR#dUA#5i%?AyHK%V7>*5??w>$i_3U>IwM}6tg&G&7mNLgUuOolomA^Q+ z`r=wyzxHci|A&A7ySr+!@AgQ&;!!&m>`bPzwb?D+%O{&mkbPLkN2$=gpHo=_cIs3i z=un&M4FrkR=)MyKEpWQ=9D-V{6TgEcHERcChoAf5iZ9>yFTSkb{iC1!@F#!yzP{E< zjfqa$8}dlcKYI0<&BPIb*M%0u3%Myse4xIft?4SK{W4y3_c622xq8%XzrciUy6-FP zegx_nqSKwQNr{S$Qt5<&(A;+&eID`#kw=XSr9^hH9HYZIWoR=@K~Cis4is{vVnr~A z9awRzmEkyqR8CeIWa>dc9*Bvm0r{>Q?L_tvCEhyRa*dSPVlFO`s>al6oY{k+s}hi# zSm%)F`Wf}>DA%RH&h!1VYvhsgxZ4@@Qt3K~s9P#w*dK@laoltmFNMge4}tLTsosGd z-lUozr}0uU=X##Z>v0YwbP*_}$TP=GlF8eBJd|PqB3Wp%`Ntor{LxxHj;z6# z&zbfXrhiHO`;N+z{(4YfzkAyczb@^q)er$(>ICOv*cP8c33Nz>Pzr5iG&yZXuElT! zwCiH{OMzZFO}GOvDHWge`*#!t^`(=%Sc*OD{-Z_;{-W$>XV+T1c;pddyeGcb??_~7 zV1eWQh;cyaWi>A`{r(t!;0O!G_*CA^TR7<)j;D|rCL!?&kzw0E(wJbs4dhwuolaEp;{ts9mTXRSrM&3kOz z7y`+ZG316U{gnu7IsU>*q+ebG*y#I#jN*PlM4PBIuR|;c)x~E>Hj_8t>fqhTNALOz zA7j$pFZT_Ih{4Omya2o=mfGVSxAhrCp8r^HUd?|07R?eOgqmaaCFnAdn!M_jT_*qh zzX!I|%3P2AepSWeln;OZ+u#1?uY3(VZSQAa|g{$dp_6E+mSiBj*8wA`qtz&NPfYp0!(P0u>eSOx}kmz8kbH{hkeL0QDe`uY;2 zl6wIVOK3Cdh020JIZZP7bJ zEVDkqr=XuLxGM~LlOA8E{ezjDf8_;%O!|cM&5WPmbhQWh(Sz99yMvCEI ziSZ?mw_DR73kUl~ubYvn$;CuYcwQ?4AOj!>ztQ8WUdEffTPS}!{8dry*shYbP<79@ z_}*%tpl50OGJz^K}v?GHEi0M#QI@m8|COB)$|vh zrLquZ@G(YTGsXlY00^ptKbnR#LXsWh{;=Qe-+j3bARq^^M9+u!pCdl9@>c$T^ed2u zegECKz0deAz1K(2iTiVIfB)#aK7Bn$q@-O(Iz%cC>HENb;AaAis{9>xW^!Fb%jbzEu%Z9h!aSDX z0NNxXWDJT|mNbWBKoqJgAM;pZ$n-y}>qLZ^?bxNz(O|y?u7|MbMzLSP93L>3)Sn?@ zSly&*yTEkn!NVqVeaeQe7$JB?jQfSxic&&{PraE7%ZS#F{RA2t1J8jRZh*RLty=qS z)qnp_{?S)IJVS-kRUl_R8dx5B8@5cd-LYJDhy!tw~DAR;aBTg_84Ul!)N%M5~o zQKj|S$kQqZ+eUAoSrm-oQtBg%2EdSZ`i$q&y#i~a|O5Y9Iw?W=S~J2BS*@E@6Qj6d3tnpN4nlC)8mYuy^a3{OC>DOlw1$N z0rXDL{!P_mVEBehbRTo3ke9q=?`ckvBw;cir|f_8$a}8)$Kj@3*VS*ox*>4f^<~qm zc&sY-3gFi!&gKkvzIUMRFxyu8;91y~LbJ zc>e4&Afj5vN0qvIpBu+be|a^OW-=n?bF#*c@^j4F3CGwUJtxNfUM%A|IW9Vflvcyigl> ztS-kHzlF~2e4jxBKkswx=Z`&LO+Q`(okV%;(l62um+gfRy*m&reSwmQ1AD({s9n{qyS1Q@Ml59&eZ#CPpfN0sE7mZd#C0;wXU;w(#r3n;0WD@q zs|fW|UM*0w3W)#e!W3*TI<_94LeGJ=hiez*5mBWOh_s8vOfiH5w67_JOw_n#c&Q!Z zoM#`0E7RgB;_mL4!ZP85km;^ae}I+@ibebd%`XA$deUSHW z44Id*S5tZOb;9+rf%RI0Yat?fX55&`s=7dGY~;ti?j3U|*x(VQ-*j#^s3Lg5voT#@-oiykTJ^X2#lkf z1HX@(=j?OJ*T-z#p7ZJFBV^2>d6%JrwtD57%^nN&@nRUy8SfvHzop;R>dB9v^YU=T zj6TPG*C1p%L4(wd-yZ?A`T(FtzJqdDylh_(T*caiQCG%eod!K?Fc~4o%n+L)u4>P* zR2ZrzOSFMETL=SK-4d4=vLQg`mju8P2uJT*^$n2DdXLrAbzVU)d!==?Sr7!y@V+N= zh!U{$C5NtmO5Z&QXY{))=s-iAdhz)gc6qFG4@fAAsC+-a%aNJSbCbORe(>wO1c^&QAnBcVoe8JyM^W26ZP1%%PXjJAEU2r zf*i{Ftb2ceWcc)SBDPFHA=s<#lb}3+# zXQ#dwj7W|T01V7;4>S8Mkif^?oonw}DMH~&U|MTHI1BF6!T6c-370CLg@xqkBkQ~0 z`SsuW&2K*AmAAK6Qrs+7XdeV^(yRuCm?@yB7qs-yC$D*W(f8Ob)5vTBJ~HWeKq_o0 zCs0#Ih}ab!6(8L_z#y-{_MEw?Re*KZwfC3T`NI>^yM*E=9Y#S%f>Kkb zYf3F;&0Al8>G_1!kvuZkg@q06$hJ7BY3(o=>$%fK$hk=GUr;=y2o5F=ux*j}6SPR( z0e1;aPI1tgP$-G3ue+{)z&_*9&~>`{>L0Q+p3OCNACFbYnN;Mc<3h_^&z>n5%CVq7 z16C25)nF?WlHUGZ%p;4*)~u`z_wks0A(Z!{W@!wttCL;qC%|=9s*uEbZghSj*hy}p zfWvgks!kBQ9hNRR2*tElsMPy#88C7;Hpr=Zj@K`=zqi+g-n!hXz8VeW`1JOAsat}EO2 z-?*Q@m=UhS|K$COnTUQwO#S|h;R-5$8D5UOKEsZA{qE1-?jM1g-gr!{?~lma7E*k{ z*+`9W+{QuIlZphBxT49(V%sPjCe$imGOKn`qw>I*yQz9tFuPkdn>lWham4ZqWA@N) z6mt_c8aGOByIoo>XY=2|IK^zWbOTXF$SXA5!^NVrum&cB|Ji&WgKDITH4wjr^EKZT zN^_DKz>*-e2HKjRxn<@p=X;?c0X+EDA8&IZz1Rdqdm z`s)g`+1rsH{+}=NpZ&Z4^pjtFvE$L3^05x4d}^wzwK_Tppk%SKeKiu_14iChXgd@Q z2BFB*%R6DcX@5pvk@MNxQMG}!nYuwz4+QS5j7#|WMa7x1)-jLUc%^byq=%E@2`^c= zBIzl`^{FUVPe^HJ?HH&$t*iBY`_4;Eifh|K&ORHK6e{E!y}tmK^LJ_INzFLdRUyUa zXRmhnqbzd>W#oJ?4f~*PWlHwBOmlJ$dmi^G)U0v76zZCUZYbFzS3t0}%@5VdF4^ZU zyyC$>s)9D_2TIZK&K+UBklY)Adp9wNW03BQ(y=71oagh@j$z#z8#IWp`E&Nrf-KmO zS+1BKK73ap^?12o<|yvhL$wfPRxbVKvCp)uYAn3T3Mw0M&Q;y64`NKb#6Y}1 z?|#2>4u8k<$3NfhzvVZ=Gjkh<%*jS_8aC6#qmJVn&{&OwP$Z`96-A2k^kVceztRW< zh-D%3$9>mi&qWPh#Y3ga(81i3v~e#%GP7G$xo*kOgkC;A4Vep08c)L(VOhp9pdmr9 zm^N(j{>>)`ODdbo)V*4}jJ}_*zLKdVWB=}E_hjqGvwq-qKL|KW30?KVJRFxM+F}Sx zYCa9%fCk&|oG;HW#~tb5$2$Ei)uez>t|RBN@SKk9lke*WlK%S?)+DO@TdRor+@WDe67icwd;#(=i{ig?+d@WR79eJ9cN#-e(*VqYp!m6 zA^0|-{W4xXmFN!Z(=*8wWAVkHlV*B>~{PmS8ux~X${tG?i^Dc#SCzsOUvWKt7Ko3k{ZUO&KGX>2~3wPg}EwoJ!r>i|M$kaS{9??nY#)^qwL)yi_7;^M_OOa>I*8l_7BY{bl^?CEggPHc@vo zvF`U_(tGOaC1`PttL7KyF=_yU2p7Q++Mur$nhGp-w_W>?s5(95?IZ z@^;aC|`_iB{9xwVQ1*s)y8w?I4Vk3f;35PZJAG!nzwh-}>qC6wSHAXx z-}=tmk#AlZ`x?bK(VQzLS_;zxh^#lQTcpZt6uZ*#@_m+xH{&}*6+8LV3lqDX0LGBnYOePs7mzs4txGf zn?}K^kt=>oS!FNBOvZ2p>A02NC!GANQs-YC=Uly2&MIu5fC6f&M|j2UMH%<8??<@; zf>jx*B=B@f9JtdR?6+Sbng9=aQTakO0o>v>&RXa2-Oim~&NlL+(-=ALN&%>>fcCE3 z2zpL2=c~e2$jEPUBXp5TbP6~G+if^fL%8p0WtAjD33hitDj!qxUhyAFquO)9HYWPdD))juB?BvW{RM{6SZ?}BeU%tQY P5iqe4`+oiY7Enc7Mu;e#SZ+u*j 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

wGx>yWrR$XWhdQIi$Z1x&*wn96?&J%vvny2b> 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 UD(VVVW&i*H07*qoM6N<$f;ST5@c;k- diff --git a/src/gui/resources/icons/AutoLibrary_128x128.ico b/src/gui/resources/icons/AutoLibrary_128x128.ico deleted file mode 100644 index 24e2e577e21ab6f1d4d7ca72fec53ddef4d01052..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15237 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 + + + + + + + + + + + + + + From 4761cade26f051061b369ac7000b2ef38b170025 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sat, 23 May 2026 20:05:39 +0800 Subject: [PATCH 24/49] =?UTF-8?q?refactor(gui):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E8=B7=AF=E5=BE=84=E5=89=8D=E7=BC=80=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=B0=E7=89=88=20SVG=20Logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 2 +- src/Main.py | 2 +- src/gui/ALAboutDialog.py | 2 +- src/gui/ALMainWindow.py | 2 +- src/gui/resources/ALResource.qrc | 8 ++++---- src/gui/resources/icons/AutoLibrary_Logo.svg | 15 --------------- .../resources/icons/AutoLibrary_Logo_128.svg | 17 +++++++++++++++++ src/gui/resources/icons/AutoLibrary_Logo_64.svg | 17 +++++++++++++++++ 8 files changed, 42 insertions(+), 23 deletions(-) delete mode 100644 src/gui/resources/icons/AutoLibrary_Logo.svg create mode 100644 src/gui/resources/icons/AutoLibrary_Logo_128.svg create mode 100644 src/gui/resources/icons/AutoLibrary_Logo_64.svg diff --git a/readme.md b/readme.md index a308969..e6c7145 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ # AutoLibrary --- -![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_128x128.ico) +![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_Logo_128.svg) [![GitHub stars](https://img.shields.io/github/stars/KenanZhu/AutoLibrary.svg?style=social&label=Star)](https://github.com/KenanZhu/AutoLibrary) ![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary?label=license) diff --git a/src/Main.py b/src/Main.py index d2fd5f9..8716d23 100644 --- a/src/Main.py +++ b/src/Main.py @@ -22,7 +22,7 @@ def main(): app = QApplication(sys.argv) translator = QTranslator() - if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"): + if translator.load(":/res/translators/qtbase_zh_CN.ts"): app.installTranslator(translator) app.setStyle('Fusion') app.setApplicationName("AutoLibrary") diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 6769cdb..024e61d 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -43,7 +43,7 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): self ): - self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_Logo.svg").pixmap(48, 48)) + self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48)) info_text = self.generateAboutText() self.AboutInfoBrowser.setHtml(info_text) browser_font = self.AboutInfoBrowser.font() diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 2c00b1f..022e46a 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -64,7 +64,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - self.icon = QIcon(":/res/icon/icons/AutoLibrary_Logo.svg") + self.icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg") self.setWindowIcon(self.icon) self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) self.ManualAction.triggered.connect(self.onManualActionTriggered) diff --git a/src/gui/resources/ALResource.qrc b/src/gui/resources/ALResource.qrc index 3590e4b..6fad8c5 100644 --- a/src/gui/resources/ALResource.qrc +++ b/src/gui/resources/ALResource.qrc @@ -1,8 +1,8 @@ - - icons/AutoLibrary_Logo.svg - - + + icons/AutoLibrary_Logo_64.svg + icons/AutoLibrary_Logo_128.svg + translators/qtbase_zh_CN.qm diff --git a/src/gui/resources/icons/AutoLibrary_Logo.svg b/src/gui/resources/icons/AutoLibrary_Logo.svg deleted file mode 100644 index 417c038..0000000 --- a/src/gui/resources/icons/AutoLibrary_Logo.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/gui/resources/icons/AutoLibrary_Logo_128.svg b/src/gui/resources/icons/AutoLibrary_Logo_128.svg new file mode 100644 index 0000000..fef7233 --- /dev/null +++ b/src/gui/resources/icons/AutoLibrary_Logo_128.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/gui/resources/icons/AutoLibrary_Logo_64.svg b/src/gui/resources/icons/AutoLibrary_Logo_64.svg new file mode 100644 index 0000000..101c6af --- /dev/null +++ b/src/gui/resources/icons/AutoLibrary_Logo_64.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From a03ab382795d1a12d6f618c6e5b599b0803d6e4d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 24 May 2026 01:02:17 +0800 Subject: [PATCH 25/49] =?UTF-8?q?refactor(autoscript):=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=20Lua=20=E9=94=99=E8=AF=AF=E5=88=86=E7=B1=BB=E4=B8=8E?= =?UTF-8?q?=20Date/Time=20=E4=B8=A5=E6=A0=BC=E6=A0=A1=E9=AA=8C=EF=BC=8C?= =?UTF-8?q?=E6=B8=85=E7=90=86=E6=AD=BB=E4=BB=A3=E7=A0=81=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E7=B1=BB=E5=9E=8B=E6=B3=A8=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/autoscript/ASEngine.py | 159 +++++++++++++++++---- src/autoscript/__init__.py | 10 +- src/gui/ALAutoScriptEditDialog.py | 63 ++++---- src/gui/ALAutoScriptOrchDialog/_helpers.py | 4 + 4 files changed, 167 insertions(+), 69 deletions(-) diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index 129e803..feee592 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -14,6 +14,15 @@ from datetime import ( from lupa import LuaRuntime as _LuaRuntime +try: + from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError +except ImportError: + try: + from lupa.lua54 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError + except ImportError: + _LuaError = Exception + _LuaSyntaxError = Exception + __all__ = ["execute", "addTargetVar", "resetEngine"] @@ -23,11 +32,19 @@ _TARGET_VARS: dict[str, dict] = {} _lua = None # Built-in meta variable definitions (name / type / display-name) -META_VARS = { +META_VARS: dict[str, dict[str, str]] = { "CURRENT_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"}, "CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "display": "当前时间"}, } +# Per-type fallback value when target_data entry is missing. +_DEFAULT_BY_TYPE: dict[str, str | int | float | bool] = { + "String": "", + "Int": 0, + "Float": 0.0, + "Boolean": False, +} + def _getLua( ): @@ -170,6 +187,62 @@ def _assignPath( d[key_path[-1]] = value +def _pyTypeToASType( + value +) -> str: + """ + Map a Python runtime value to its AutoScript type name. + """ + + if isinstance(value, bool): + return "Boolean" + if isinstance(value, int): + return "Int" + if isinstance(value, float): + return "Float" + if isinstance(value, str): + return "String" + return "Unknown" + + +def _checkDateFormat( + date_str: str, + var_name: str = "", +) -> None: + """ + Validate that *date_str* is in YYYY-MM-DD format. + Raises ValueError with a descriptive message on failure. + """ + + prefix = f"Date 类型变量 '{var_name}' 的" if var_name else "" + try: + date.fromisoformat(date_str) + except ValueError: + raise ValueError( + f"{prefix}值 '{date_str}' 不是合法的日期格式," + f"应为 YYYY-MM-DD" + ) + + +def _checkTimeFormat( + time_str: str, + var_name: str = "", +) -> None: + """ + Validate that *time_str* is in HH:MM format. + Raises ValueError with a descriptive message on failure. + """ + + prefix = f"Time 类型变量 '{var_name}' 的" if var_name else "" + try: + datetime.strptime(time_str, "%H:%M") + except ValueError: + raise ValueError( + f"{prefix}值 '{time_str}' 不是合法的时间格式," + f"应为 HH:MM" + ) + + def _checkType( var_name: str, var_type: str, @@ -188,17 +261,17 @@ def _checkType( if not isinstance(value, str): raise ValueError( f"Date 类型变量 '{var_name}' 只能接受日期字符串," - f"不能接受 {type(value).__name__} 类型" + f"不能接受 {_pyTypeToASType(value)} 类型" ) - date.fromisoformat(value) + _checkDateFormat(value, var_name) return if var_type == "Time": if not isinstance(value, str): raise ValueError( f"Time 类型变量 '{var_name}' 只能接受时间字符串," - f"不能接受 {type(value).__name__} 类型" + f"不能接受 {_pyTypeToASType(value)} 类型" ) - datetime.strptime(value, "%H:%M") + _checkTimeFormat(value, var_name) return if var_type == "Int": if isinstance(value, bool): @@ -207,7 +280,7 @@ def _checkType( ) if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)): raise ValueError( - f"Int 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + f"Int 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" ) return if var_type == "Float": @@ -217,19 +290,19 @@ def _checkType( ) if not isinstance(value, (int, float)): raise ValueError( - f"Float 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + f"Float 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" ) return if var_type == "Boolean": if not isinstance(value, bool): raise ValueError( - f"Boolean 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + f"Boolean 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" ) return if var_type == "String": if not isinstance(value, str): raise ValueError( - f"String 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" + f"String 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" ) return @@ -238,7 +311,7 @@ def addTargetVar( name: str, var_type: str, key_path: list, - display_name: str = None, + _display_name: str = None, ) -> None: """ Register a new target variable bound to a path in the application data dict. @@ -247,7 +320,6 @@ def addTargetVar( name (str): The canonical variable name (e.g. "RESERVE_DATE"). var_type (str): "Int" | "Float" | "Boolean" | "Date" | "Time" | "String". key_path (list): Nested path into target_data, e.g. ["reserve_info", "date"]. - display_name (str): Optional Chinese alias (unused by the engine). """ upper_name = name.upper().strip() @@ -263,6 +335,7 @@ def resetEngine( Reset the engine to its initial state: clear all target variables and release the Lua runtime. """ + global _TARGET_VARS, _lua _TARGET_VARS = {} _lua = None @@ -274,6 +347,9 @@ def _push( """ Push target_data values into Lua globals. Date / Time strings are converted to native Lua types (timestamp / minutes). + + Raises ValueError for missing / malformed Date or Time values so that + execute() can surface them as user-visible AutoScript execution errors. """ lua = _getLua() @@ -285,28 +361,27 @@ def _push( key_path = info["key_path"] vt = info["type"] raw = _navigatePath(target_data, key_path) - if vt == "Date": - if raw and isinstance(raw, str): - try: - date.fromisoformat(raw.strip()) - except (ValueError, AttributeError): - raw = "2099-01-01" - else: - raw = "2099-01-01" + if not isinstance(raw, str) or not raw.strip(): + raise ValueError( + f"Date 类型变量 '{var_name}' 对应的数据为空或不是字符串类型," + f"请检查路径 {key_path} 的值是否为合法的日期字符串 (YYYY-MM-DD)" + ) + raw = raw.strip() + _checkDateFormat(raw, var_name) g[var_name] = _toDate(raw) elif vt == "Time": - if raw and isinstance(raw, str): - try: - datetime.strptime(raw.strip(), "%H:%M") - except (ValueError, AttributeError): - raw = "00:00" - else: - raw = "00:00" + if not isinstance(raw, str) or not raw.strip(): + raise ValueError( + f"Time 类型变量 '{var_name}' 对应的数据为空或不是字符串类型," + f"请检查路径 {key_path} 的值是否为合法的时间字符串 (HH:MM)" + ) + raw = raw.strip() + _checkTimeFormat(raw, var_name) g[var_name] = _toTime(raw) else: if raw is None: - raw = "" if vt == "String" else 0 if vt == "Int" else 0.0 if vt == "Float" else False + raw = _DEFAULT_BY_TYPE.get(vt, False) g[var_name] = raw @@ -326,7 +401,7 @@ def _pull( for var_name, info in _TARGET_VARS.items(): try: lua_val = g[var_name] - except (KeyError, AttributeError): + except KeyError: continue vt = info["type"] if vt == "Date": @@ -339,6 +414,20 @@ def _pull( _assignPath(target_data, info["key_path"], lua_val) +def _cleanLuaError( + raw_msg: str +) -> str: + """ + Strip internal source prefix and stack traceback from a Lua error message. + """ + + msg = raw_msg.replace('[string ""]:', "").strip() + stack_idx = msg.find("stack traceback:") + if stack_idx != -1: + msg = msg[:stack_idx].strip() + return msg + + def execute( script_text: str, target_data: dict, @@ -366,9 +455,19 @@ def execute( if not script_text or not script_text.strip(): return - _push(target_data) try: + _push(target_data) _getLua().execute(script_text) _pull(target_data) + except _LuaSyntaxError as e: + raise ValueError( + f"AutoScript 语法错误: {_cleanLuaError(str(e))}" + ) + except _LuaError as e: + raise ValueError( + f"AutoScript 运行时错误: {_cleanLuaError(str(e))}" + ) + except ValueError as e: + raise ValueError(f"AutoScript 数据错误: {e}") except Exception as e: - raise ValueError(f"AutoScript 执行错误: {e}") + raise ValueError(f"AutoScript 未知错误: {e}") diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index 480d09e..3f85baf 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -31,11 +31,11 @@ __all__ = [ # Key paths into target_data dict for each target variable. # (name, type, key_path, display_name) _TARGET_VAR_DEFS = [ - ("USERNAME", "String",["username"], "用户名"), - ("USER_ENABLE", "Boolean",["enabled"], "用户启用"), - ("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"), - ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), - ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), + ("USERNAME", "String", ["username"], "用户名"), + ("USER_ENABLE", "Boolean",["enabled"], "用户启用"), + ("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"), + ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), + ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), ] # All variables (display_name -> (name, type)), derived from target vars + meta vars. diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 0884f82..23656ac 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -169,7 +169,7 @@ class ALAutoScriptEditDialog(QDialog): ): super().__init__(parent) - self._fontSize = 19 + self._fontSize = 13 self._mockWidgets = {} self.setupUi() @@ -267,17 +267,17 @@ class ALAutoScriptEditDialog(QDialog): basicLayout.setContentsMargins(4, 4, 4, 4) basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) controlButtons = [ - ("if", "if then\n \nend"), - ("elseif", "elseif then\n "), - ("else", "else"), - ("end", "end"), - ("-- pass", "-- pass"), + ("如果 (if...)", "if then\n \nend"), + ("再如果 (elseif...)", "elseif then\n "), + ("否则 (else)", "else"), + ("结束 (end)", "end"), + ("跳过 (pass)", "-- pass"), ] - self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5) + self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3) assignButtons = [ - ("=", " = "), + ("赋值 (=)", " = "), ] - self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1) + self._addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3) tabWidget.addTab(basicWidget, "基本语法") operatorWidget = QWidget() operatorLayout = QGridLayout(operatorWidget) @@ -285,24 +285,24 @@ class ALAutoScriptEditDialog(QDialog): operatorLayout.setContentsMargins(4, 4, 4, 4) operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) arithmeticButtons = [ - ("+", " + "), - ("-", " - "), + ("加 (+)", " + "), + ("减 (-)", " - "), ] - self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2) + self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3) compareButtons = [ - ("==", " == "), - ("~=", " ~= "), - (">", " > "), - ("<", " < "), - (">=", " >= "), - ("<=", " <= "), + ("等于 (==)", " == "), + ("不等于 (~=)", " ~= "), + ("大于 (>)", " > "), + ("小于 (<)", " < "), + ("大于等于 (>=)", " >= "), + ("小于等于 (<=)", " <= "), ] - self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6) + self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3) logic_buttons = [ - ("and", " and "), - ("or", " or "), + ("且 (and)", " and "), + ("或 (or)", " or "), ] - self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2) + self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3) tabWidget.addTab(operatorWidget, "运算符") literalWidget = QWidget() literalLayout = QGridLayout(literalWidget) @@ -310,15 +310,15 @@ class ALAutoScriptEditDialog(QDialog): literalLayout.setContentsMargins(4, 4, 4, 4) literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) bool_buttons = [ - ("true", "true"), - ("false", "false"), + ("真 (true)", "true"), + ("假 (false)", "false"), ] - self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2) + self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3) dateTimeButtons = [ ("日期", '"2099-01-01"'), ("时间", '"00:00"'), ] - self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2) + self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3) hintButtons = [ ("字符串", '"请输入文本"'), ("数字", "123"), @@ -334,16 +334,15 @@ class ALAutoScriptEditDialog(QDialog): varButtons = [ (display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() ] - - self._addButtonsToGrid(varLayout, varButtons, 0, 0, 5) + self._addButtonsToGrid(varLayout, varButtons, 0, 0, 3) tabWidget.addTab(varWidget, "变量") mockPanel = self._createMockPanel() mockPanel.setMinimumWidth(260) splitter.addWidget(tabWidget) splitter.addWidget(mockPanel) splitter.setStretchFactor(0, 1) - splitter.setStretchFactor(1, 0) - splitter.setSizes([660, 400]) + splitter.setStretchFactor(1, 1) + splitter.setSizes([530, 530]) parent_layout.addWidget(splitter) @@ -367,7 +366,6 @@ class ALAutoScriptEditDialog(QDialog): btn.setFixedHeight(25) btn.setToolTip(f"插入: {template}") grid_layout.addWidget(btn, row, col) - col += 1 if col >= start_col + max_columns: col = start_col @@ -585,9 +583,6 @@ class ALAutoScriptEditDialog(QDialog): self ): - font = self.textEdit.font() - font.setPointSize(self._fontSize) - self.textEdit.setFont(font) self.textEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index 10ca230..b48ee37 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -88,6 +88,8 @@ DATE_RELATIVE_OPTIONS = [ DATE_OFFSET_UNITS = [ ("天", "days"), ("周", "weeks"), + # NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years, + # because date_add() works with second-level offsets (n * 86400). ("月", "months"), ("年", "years"), ] @@ -657,6 +659,8 @@ def _encodeDateOrTime( s = raw_value.strip() up = s.upper() + # Input comes from widget values — single binary expressions only (e.g. "A + 3", + # "CURRENT_DATE + 5"). Multi-operator expressions are not produced by the UI. m_arith_spaced = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) m_arith_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s) m_arith = m_arith_spaced or m_arith_nospace From 5e898180c72aeab0a74e006a204942032b117d16 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 24 May 2026 13:14:27 +0800 Subject: [PATCH 26/49] =?UTF-8?q?refactor(style):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=BB=A3=E7=A0=81=E9=A3=8E=E6=A0=BC=EF=BC=8C?= =?UTF-8?q?=E6=95=B4=E7=90=86=E5=AF=BC=E5=85=A5=E9=A1=BA=E5=BA=8F=E3=80=81?= =?UTF-8?q?=E9=97=B4=E8=B7=9D=E8=A7=84=E8=8C=83=E4=B8=8E=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E6=8E=92=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GUI 模块统一 QtCore → QtGui → QtWidgets 导入排列,各类独占一行按字母排序 - 统一类间两空行、类内方法间一空行、函数间一空行的间距规范 - 统一方法排列顺序:__init__ → setupUi → connectSignals → public → Slot → private - 统一 _widgets 中 ConditionRowFrame/ActionStepFrame 方法命名(populate* / toScript / updateValueWidget) - LibTimeSelector 迁入 operators/abs 抽象层 Co-Authored-By: Claude Opus 4.7 --- src/autoscript/ASEngine.py | 15 - src/autoscript/__init__.py | 2 - src/base/LibOperator.py | 1 - src/base/MsgBase.py | 4 - src/base/__init__.py | 3 +- src/boot/AppInitializer.py | 18 +- src/gui/ALAboutDialog.py | 19 +- src/gui/ALAutoScriptEditDialog.py | 252 +++++------ src/gui/ALAutoScriptOrchDialog/_blocks.py | 44 +- src/gui/ALAutoScriptOrchDialog/_dialog.py | 16 +- src/gui/ALAutoScriptOrchDialog/_helpers.py | 406 ++++++++---------- src/gui/ALAutoScriptOrchDialog/_widgets.py | 230 +++++----- src/gui/ALConfigWidget.py | 77 ++-- src/gui/ALMainWindow.py | 50 +-- src/gui/ALMainWorkers.py | 6 - src/gui/ALSeatFrame.py | 4 - src/gui/ALSeatMapSelectDialog.py | 23 +- src/gui/ALSeatMapView.py | 43 +- src/gui/ALStatusLabel.py | 20 +- src/gui/ALTimerTaskAddDialog.py | 14 +- src/gui/ALTimerTaskHistoryDialog.py | 15 +- src/gui/ALTimerTaskManageWidget.py | 56 ++- src/gui/ALUserTreeWidget.py | 22 +- src/gui/ALWebDriverDownloadDialog.py | 277 ++++++------ src/interfaces/ConfigProvider.py | 2 + src/managers/config/ConfigManager.py | 8 +- src/managers/driver/WebBrowserDetector.py | 3 +- src/managers/driver/WebDriverDownloader.py | 54 +-- src/managers/driver/WebDriverManager.py | 16 - src/managers/log/LogManager.py | 5 +- src/operators/AutoLib.py | 7 - src/operators/LibChecker.py | 12 - src/operators/LibCheckin.py | 3 - src/operators/LibCheckout.py | 1 - src/operators/LibLogin.py | 8 - src/operators/LibLogout.py | 2 - src/operators/LibRenew.py | 8 +- src/operators/LibReserve.py | 25 +- .../abs}/LibTimeSelector.py | 4 +- src/operators/abs/__init__.py | 6 + src/utils/JSONReader.py | 4 - src/utils/JSONWriter.py | 3 - 42 files changed, 780 insertions(+), 1008 deletions(-) rename src/{base => operators/abs}/LibTimeSelector.py (98%) create mode 100644 src/operators/abs/__init__.py diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index feee592..e8623a4 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -45,7 +45,6 @@ _DEFAULT_BY_TYPE: dict[str, str | int | float | bool] = { "Boolean": False, } - def _getLua( ): """ @@ -59,7 +58,6 @@ def _getLua( _registerHelpers(_lua) return _lua - def _sandbox( lua, ) -> None: @@ -92,7 +90,6 @@ def _sandbox( end """) - def _registerHelpers( lua, ) -> None: @@ -154,7 +151,6 @@ def _registerHelpers( end """) - def _navigatePath( data: dict, key_path: list, @@ -171,7 +167,6 @@ def _navigatePath( return default return d.get(key_path[-1], default) - def _assignPath( data: dict, key_path: list, @@ -186,7 +181,6 @@ def _assignPath( d = d.setdefault(key, {}) d[key_path[-1]] = value - def _pyTypeToASType( value ) -> str: @@ -204,7 +198,6 @@ def _pyTypeToASType( return "String" return "Unknown" - def _checkDateFormat( date_str: str, var_name: str = "", @@ -223,7 +216,6 @@ def _checkDateFormat( f"应为 YYYY-MM-DD" ) - def _checkTimeFormat( time_str: str, var_name: str = "", @@ -242,7 +234,6 @@ def _checkTimeFormat( f"应为 HH:MM" ) - def _checkType( var_name: str, var_type: str, @@ -306,7 +297,6 @@ def _checkType( ) return - def addTargetVar( name: str, var_type: str, @@ -328,7 +318,6 @@ def addTargetVar( "key_path": key_path, } - def resetEngine( ) -> None: """ @@ -340,7 +329,6 @@ def resetEngine( _TARGET_VARS = {} _lua = None - def _push( target_data: dict, ) -> None: @@ -384,7 +372,6 @@ def _push( raw = _DEFAULT_BY_TYPE.get(vt, False) g[var_name] = raw - def _pull( target_data: dict, ) -> None: @@ -413,7 +400,6 @@ def _pull( _checkType(var_name, vt, lua_val) _assignPath(target_data, info["key_path"], lua_val) - def _cleanLuaError( raw_msg: str ) -> str: @@ -427,7 +413,6 @@ def _cleanLuaError( msg = msg[:stack_idx].strip() return msg - def execute( script_text: str, target_data: dict, diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index 3f85baf..2315f78 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -55,7 +55,6 @@ _MOCK_TYPE_VALUES = { "Float": 0.0, } - def buildMockTargetData( ) -> dict: """ @@ -70,7 +69,6 @@ def buildMockTargetData( d[key_path[-1]] = _MOCK_TYPE_VALUES.get(var_type, "") return data - def registerDefaultTargetVars( ) -> None: """ diff --git a/src/base/LibOperator.py b/src/base/LibOperator.py index de57392..c1ae287 100644 --- a/src/base/LibOperator.py +++ b/src/base/LibOperator.py @@ -29,7 +29,6 @@ class LibOperator(MsgBase): super().__init__(input_queue, output_queue) - def _waitResponseLoad( self ) -> bool: diff --git a/src/base/MsgBase.py b/src/base/MsgBase.py index 8ac1aaf..68c9ae0 100644 --- a/src/base/MsgBase.py +++ b/src/base/MsgBase.py @@ -58,7 +58,6 @@ class MsgBase: except RuntimeError: self._logger = None - def _showMsg( self, msg: str @@ -66,7 +65,6 @@ class MsgBase: self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}") - def _showTrace( self, msg: str, @@ -79,7 +77,6 @@ class MsgBase: if self._logger and not no_log: self._logger.log(level, msg) - def _showLog( self, msg: str, @@ -89,7 +86,6 @@ class MsgBase: if self._logger: self._logger.log(level, msg) - def _waitMsg( self, timeout: float = 1.0 diff --git a/src/base/__init__.py b/src/base/__init__.py index 9f385bf..bac4fcf 100644 --- a/src/base/__init__.py +++ b/src/base/__init__.py @@ -1,8 +1,7 @@ - """ Base module for the AutoLibrary project. Here are the classes and modules in this package: - - MsgBase: Base class for messages.\ + - MsgBase: Base class for messages. - LibOperator: Base class for library operators. """ \ No newline at end of file diff --git a/src/boot/AppInitializer.py b/src/boot/AppInitializer.py index 6267892..4512368 100644 --- a/src/boot/AppInitializer.py +++ b/src/boot/AppInitializer.py @@ -16,7 +16,7 @@ from managers.config.ConfigManager import instance as configInstance from managers.driver.WebDriverManager import instance as webdriverInstance -def initializeLogManager( +def _initializeLogManager( ) -> bool: app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) @@ -27,7 +27,7 @@ def initializeLogManager( logInstance(log_dir) return True -def initializeConfigManager( +def _initializeConfigManager( ) -> bool: logger = logInstance().getLogger("AppInitializer") @@ -49,7 +49,7 @@ def initializeConfigManager( configInstance(new_config_dir) return True -def initializeWebDriverManager( +def _initializeWebDriverManager( ) -> bool: logger = logInstance().getLogger("AppInitializer") @@ -66,11 +66,17 @@ def initializeWebDriverManager( def initializeApp( ) -> bool: + """ + Initialize the application components - if not initializeLogManager(): + Order: + LogManager -> ConfigManager -> WebDriverManager + """ + + if not _initializeLogManager(): return False - if not initializeConfigManager(): + if not _initializeConfigManager(): return False - if not initializeWebDriverManager(): + if not _initializeWebDriverManager(): return False return True diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 024e61d..4441403 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -9,14 +9,17 @@ See the LICENSE file for details. """ import platform +from PySide6.QtCore import ( + Qt, + QTimer +) from PySide6.QtGui import ( - QIcon, QFont + QFont, + QIcon ) from PySide6.QtWidgets import ( - QDialog, QApplication -) -from PySide6.QtCore import ( - QTimer, Qt + QApplication, + QDialog ) from gui.ALVersionInfo import ( @@ -38,7 +41,6 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): self.modifyUi() self.connectSignals() - def modifyUi( self ): @@ -51,14 +53,12 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): self.AboutInfoBrowser.setFont(browser_font) self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) - def connectSignals( self ): self.CopyButton.clicked.connect(self.copyAboutInfo) - def generateAboutText( self ) -> str: @@ -91,7 +91,6 @@ GitHub: =)", " >= "), ("小于等于 (<=)", " <= "), ] - self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3) + self.addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3) logic_buttons = [ ("且 (and)", " and "), ("或 (or)", " or "), ] - self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3) + self.addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3) tabWidget.addTab(operatorWidget, "运算符") literalWidget = QWidget() literalLayout = QGridLayout(literalWidget) @@ -313,18 +310,18 @@ class ALAutoScriptEditDialog(QDialog): ("真 (true)", "true"), ("假 (false)", "false"), ] - self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3) + self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3) dateTimeButtons = [ ("日期", '"2099-01-01"'), ("时间", '"00:00"'), ] - self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3) + self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3) hintButtons = [ ("字符串", '"请输入文本"'), ("数字", "123"), ("注释", "-- 请输入注释"), ] - self._addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) + self.addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) tabWidget.addTab(literalWidget, "字面量") varWidget = QWidget() varLayout = QGridLayout(varWidget) @@ -334,9 +331,9 @@ class ALAutoScriptEditDialog(QDialog): varButtons = [ (display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() ] - self._addButtonsToGrid(varLayout, varButtons, 0, 0, 3) + self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3) tabWidget.addTab(varWidget, "变量") - mockPanel = self._createMockPanel() + mockPanel = self.createMockPanel() mockPanel.setMinimumWidth(260) splitter.addWidget(tabWidget) splitter.addWidget(mockPanel) @@ -345,8 +342,7 @@ class ALAutoScriptEditDialog(QDialog): splitter.setSizes([530, 530]) parent_layout.addWidget(splitter) - - def _addButtonsToGrid( + def addButtonsToGrid( self, grid_layout, buttons, @@ -361,7 +357,7 @@ class ALAutoScriptEditDialog(QDialog): for btn_text, template in buttons: btn = QPushButton(btn_text) btn.setProperty("template", template) - btn.clicked.connect(self._insertTemplate) + btn.clicked.connect(self.insertTemplate) btn.setFixedWidth(100) btn.setFixedHeight(25) btn.setToolTip(f"插入: {template}") @@ -371,22 +367,7 @@ class ALAutoScriptEditDialog(QDialog): col = start_col row += 1 - @Slot() - def _insertTemplate( - self - ): - - btn = self.sender() - if not isinstance(btn, QPushButton): - return - template = btn.property("template") - if not template: - return - cursor = self.textEdit.textCursor() - cursor.insertText(template) - - - def _createMockPanel( + def createMockPanel( self ) -> QGroupBox: @@ -397,14 +378,13 @@ class ALAutoScriptEditDialog(QDialog): self._mockWidgets = {} for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: default = _MOCK_TYPE_VALUES.get(var_type, "") - widget = self._makeMockInput(var_type, default) + widget = self.makeMockInput(var_type, default) label = QLabel(f"{display_name}: {name}({var_type})") form.addRow(label, widget) self._mockWidgets[name] = (widget, var_type, key_path) return group - - def _makeMockInput( + def makeMockInput( self, var_type: str, default @@ -447,7 +427,6 @@ class ALAutoScriptEditDialog(QDialog): w.setText(str(default)) return w - def getMockData( self ) -> dict: @@ -455,14 +434,13 @@ class ALAutoScriptEditDialog(QDialog): data = {} for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: widget, _, _ = self._mockWidgets[name] - value = self._getMockValue(widget, var_type) + value = self.getMockValue(widget, var_type) d = data for key in key_path[:-1]: d = d.setdefault(key, {}) d[key_path[-1]] = value return data - def setMockData( self, data: dict @@ -478,10 +456,9 @@ class ALAutoScriptEditDialog(QDialog): except (KeyError, TypeError): continue widget, _, _ = self._mockWidgets[name] - self._setMockValue(widget, var_type, d) + self.setMockValue(widget, var_type, d) - - def _getMockValue( + def getMockValue( self, widget: QWidget, var_type: str @@ -499,8 +476,7 @@ class ALAutoScriptEditDialog(QDialog): return widget.value() return widget.text() - - def _setMockValue( + def setMockValue( self, widget: QWidget, var_type: str, @@ -520,6 +496,103 @@ class ALAutoScriptEditDialog(QDialog): else: widget.setText(str(value)) + def connectSignals( + self + ): + + self.btnBox.accepted.connect(self.accept) + self.btnBox.rejected.connect(self.reject) + self.orchBtn.clicked.connect(self.onOpenOrchDialog) + self.debugBtn.clicked.connect(self.onDebugRun) + self.zoomInBtn.clicked.connect(self.onZoomIn) + self.zoomOutBtn.clicked.connect(self.onZoomOut) + self.zoomResetBtn.clicked.connect(self.onZoomReset) + self.copyBtn.clicked.connect(self.onCopy) + + def getScript( + self + ) -> str: + + return self.textEdit.toPlainText() + + def updateFontSize( + self + ): + + self.textEdit.setStyleSheet( + "QPlainTextEdit {" + " font-family: 'Courier New', 'Consolas', monospace;" + f" font-size: {self._fontSize}px;" + "}" + ) + self.zoomLabel.setText(f"{self._fontSize}px") + + @Slot() + def insertTemplate( + self + ): + + btn = self.sender() + if not isinstance(btn, QPushButton): + return + template = btn.property("template") + if not template: + return + cursor = self.textEdit.textCursor() + cursor.insertText(template) + + @Slot() + def onZoomIn( + self + ): + + self._fontSize = min(self._fontSize + 2, 40) + self.updateFontSize() + + @Slot() + def onZoomOut( + self + ): + + self._fontSize = max(self._fontSize - 2, 8) + self.updateFontSize() + + @Slot() + def onZoomReset( + self + ): + + self._fontSize = 21 + self.updateFontSize() + + @Slot() + def onCopy( + self + ): + + clipboard = QApplication.clipboard() + clipboard.setText(self.textEdit.toPlainText()) + original = self.copyBtn.text() + self.copyBtn.setText("已复制") + self.copyBtn.setEnabled(False) + QTimer.singleShot(2000, lambda: ( + self.copyBtn.setText(original), + self.copyBtn.setEnabled(True) + )) + + @Slot() + def onOpenOrchDialog( + self + ): + + from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog + dlg = ALAutoScriptOrchDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + script = dlg.getScript() + if script: + cursor = self.textEdit.textCursor() + cursor.insertText(script) + dlg.deleteLater() @Slot() def onDebugRun( @@ -556,90 +629,3 @@ class ALAutoScriptEditDialog(QDialog): dlg = _DebugResultDialog(changes, self) dlg.exec() dlg.deleteLater() - - - def connectSignals( - self - ): - - self.btnBox.accepted.connect(self.accept) - self.btnBox.rejected.connect(self.reject) - self.orchBtn.clicked.connect(self.onOpenOrchDialog) - self.debugBtn.clicked.connect(self.onDebugRun) - self.zoomInBtn.clicked.connect(self.onZoomIn) - self.zoomOutBtn.clicked.connect(self.onZoomOut) - self.zoomResetBtn.clicked.connect(self.onZoomReset) - self.copyBtn.clicked.connect(self.onCopy) - - - def getScript( - self - ) -> str: - - return self.textEdit.toPlainText() - - - def updateFontSize( - self - ): - - self.textEdit.setStyleSheet( - "QPlainTextEdit {" - " font-family: 'Courier New', 'Consolas', monospace;" - f" font-size: {self._fontSize}px;" - "}" - ) - self.zoomLabel.setText(f"{self._fontSize}px") - - @Slot() - def onZoomIn( - self - ): - - self._fontSize = min(self._fontSize + 2, 40) - self.updateFontSize() - - @Slot() - def onZoomOut( - self - ): - - self._fontSize = max(self._fontSize - 2, 8) - self.updateFontSize() - - @Slot() - def onZoomReset( - self - ): - - self._fontSize = 13 - self.updateFontSize() - - @Slot() - def onCopy( - self - ): - - clipboard = QApplication.clipboard() - clipboard.setText(self.textEdit.toPlainText()) - original = self.copyBtn.text() - self.copyBtn.setText("已复制") - self.copyBtn.setEnabled(False) - QTimer.singleShot(2000, lambda: ( - self.copyBtn.setText(original), - self.copyBtn.setEnabled(True) - )) - - @Slot() - def onOpenOrchDialog( - self - ): - - from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog - dlg = ALAutoScriptOrchDialog(self) - if dlg.exec() == QDialog.DialogCode.Accepted: - script = dlg.getScript() - if script: - cursor = self.textEdit.textCursor() - cursor.insertText(script) - dlg.deleteLater() diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py index 5ad7a94..de4b133 100644 --- a/src/gui/ALAutoScriptOrchDialog/_blocks.py +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -47,7 +47,6 @@ class ConditionalBlock(QGroupBox): self.connectSignals() self.addInitialConditionRow() - def setupUi( self ): @@ -103,7 +102,6 @@ class ConditionalBlock(QGroupBox): mainLayout.addWidget(self.addActionBtn) self.setUpdatesEnabled(True) - def connectSignals( self ): @@ -112,7 +110,6 @@ class ConditionalBlock(QGroupBox): self.addCondBtn.clicked.connect(self.addConditionRow) self.addActionBtn.clicked.connect(self.addActionStep) - def addInitialConditionRow( self ): @@ -124,7 +121,6 @@ class ConditionalBlock(QGroupBox): self._conditionRows.append(row) self.condRowsLayout.addWidget(row) - def addConditionRow( self ): @@ -137,7 +133,6 @@ class ConditionalBlock(QGroupBox): self._conditionRows.append(row) self.condRowsLayout.addWidget(row) - def removeConditionRow( self, row: ConditionRowFrame @@ -149,7 +144,6 @@ class ConditionalBlock(QGroupBox): row.hide() row.deleteLater() - def addActionStep( self ): @@ -159,7 +153,6 @@ class ConditionalBlock(QGroupBox): self._actionWidgets.append(step) self.actionsLayout.addWidget(step) - def removeActionStep( self, step: ActionStepFrame @@ -171,48 +164,31 @@ class ConditionalBlock(QGroupBox): step.hide() step.deleteLater() - @Slot(int) - def onTypeChanged( - self, - _idx - ): - - isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") - self.conditionWidget.setVisible(isCond) - self.actionLabel.setText( - "执行步骤:" if isCond else "ELSE 执行步骤:" - ) - - def getBlockType( self ) -> str: return self.typeCombo.currentData() - def getConditionRows( self ): return list(self._conditionRows) - def getActionSteps( self ): return list(self._actionWidgets) - def countActionSteps( self ) -> int: return len(self._actionWidgets) - - def toScriptLines( + def toScript( self ) -> list: """ @@ -223,7 +199,7 @@ class ConditionalBlock(QGroupBox): lines = [] if blockType in ("IF", "ELSE IF"): condTexts = [ - r.toConditionText() for r in self._conditionRows if r.toConditionText() + r.toScript() for r in self._conditionRows if r.toScript() ] if not condTexts: condTexts = ["true"] @@ -245,12 +221,11 @@ class ConditionalBlock(QGroupBox): else: lines.append("else") for step in self._actionWidgets: - scriptLine = step.toScriptLine() + scriptLine = step.toScript() if scriptLine: lines.append(scriptLine) return lines - def refreshVarCombos( self ): @@ -260,7 +235,6 @@ class ConditionalBlock(QGroupBox): for step in self._actionWidgets: step.refreshVarCombos() - def setPrevBlockType( self, prevType: str | None @@ -278,3 +252,15 @@ class ConditionalBlock(QGroupBox): item.setEnabled(shouldEnable) if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"): self.typeCombo.setCurrentIndex(0) + + @Slot(int) + def onTypeChanged( + self, + _idx + ): + + isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") + self.conditionWidget.setVisible(isCond) + self.actionLabel.setText( + "执行步骤:" if isCond else "ELSE 执行步骤:" + ) diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 35c3ab4..839a14b 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -42,7 +42,6 @@ class ALAutoScriptOrchDialog(QDialog): self.addBlock() self.scrollLayout.addStretch() - def setupUi( self ): @@ -70,7 +69,6 @@ class ALAutoScriptOrchDialog(QDialog): self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") mainLayout.addWidget(self.btnBox) - def connectSignals( self ): @@ -79,8 +77,7 @@ class ALAutoScriptOrchDialog(QDialog): self.btnBox.rejected.connect(self.reject) self.addBlockBtn.clicked.connect(self.addBlock) - - def _updateBlockTypeRestrictions( + def updateBlockTypeRestrictions( self ): @@ -89,7 +86,6 @@ class ALAutoScriptOrchDialog(QDialog): block.setPrevBlockType(prevType) prevType = block.getBlockType() - def addBlock( self ): @@ -98,10 +94,10 @@ class ALAutoScriptOrchDialog(QDialog): len(self._blocks), self._varMgr, parent=self ) block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block)) - block.typeCombo.currentIndexChanged.connect(self._updateBlockTypeRestrictions) + block.typeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions) block.addActionStep() self._blocks.append(block) - self._updateBlockTypeRestrictions() + self.updateBlockTypeRestrictions() if self.scrollLayout.count() > 0: lastItem = self.scrollLayout.itemAt( self.scrollLayout.count() - 1 @@ -113,7 +109,6 @@ class ALAutoScriptOrchDialog(QDialog): return self.scrollLayout.addWidget(block) - def removeBlock( self, block: ConditionalBlock @@ -135,8 +130,7 @@ class ALAutoScriptOrchDialog(QDialog): else: blk.typeCombo.setEnabled(True) blk.refreshVarCombos() - self._updateBlockTypeRestrictions() - + self.updateBlockTypeRestrictions() def getScript( self @@ -151,7 +145,7 @@ class ALAutoScriptOrchDialog(QDialog): blockType = block.getBlockType() if blockType == "IF" and prevType is not None: parts.append("end") - lines = block.toScriptLines() + lines = block.toScript() parts.extend(lines) prevType = blockType if self._blocks and self._blocks[0].getBlockType() == "IF": diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index b48ee37..bec9ee6 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -95,181 +95,6 @@ DATE_OFFSET_UNITS = [ ] -class VariableManager(QObject): - - def __init__( - self, - parent = None - ): - - super().__init__(parent) - self._vars = [] - self._nameMap = {} - - self._initPresetVars() - - - def _initPresetVars( - self - ): - - for p in PRESET_VARIABLES: - entry = {"name": p["name"], "type": p["type"], "display": p["display"]} - self._vars.append(entry) - self._nameMap[p["name"]] = entry - - - def getInfoByName( - self, - name: str - ): - - return self._nameMap.get(name.upper().strip()) - - - def populateCombo( - self, - combo: QComboBox - ): - - currentData = combo.currentData() - combo.blockSignals(True) - combo.clear() - for entry in self._vars: - combo.addItem( - entry["display"], - (entry["name"], entry["type"]) - ) - if currentData: - for i in range(combo.count()): - d = combo.itemData(i) - if d and d[0] == currentData[0]: - combo.setCurrentIndex(i) - break - combo.blockSignals(False) - - - def findExactNameEntry( - self, - combo: QComboBox, - name: str - ) -> int: - - name = name.upper().strip() - for i in range(combo.count()): - d = combo.itemData(i) - if d and len(d) >= 1 and d[0].upper().strip() == name: - return i - return -1 - - -def makeValueWidget( - var_type: str, - parent: QWidget = None -) -> QWidget: - - if var_type == "Int": - w = QSpinBox(parent) - w.setRange(-999999, 999999) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w - if var_type == "Float": - w = QDoubleSpinBox(parent) - w.setRange(-999999.0, 999999.0) - w.setDecimals(2) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w - if var_type == "String": - w = QLineEdit(parent) - w.setPlaceholderText("输入值") - w.setFixedHeight(25) - w.setMinimumWidth(120) - return w - if var_type == "Boolean": - w = QComboBox(parent) - w.addItem("是 (true)", "true") - w.addItem("否 (false)", "false") - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w - if var_type == "Date": - return _DateInputContainer(parent) - if var_type == "Time": - return _TimeInputContainer(parent) - w = QLineEdit(parent) - w.setPlaceholderText("输入值") - w.setFixedHeight(25) - w.setMinimumWidth(120) - return w - - -def makeOffsetWidget( - var_type: str, - parent: QWidget = None -) -> QWidget: - - if var_type == "Int": - w = QSpinBox(parent) - w.setRange(-999999, 999999) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w - if var_type == "Float": - w = QDoubleSpinBox(parent) - w.setRange(-999999.0, 999999.0) - w.setDecimals(2) - w.setFixedHeight(25) - w.setMinimumWidth(100) - return w - if var_type == "Date": - return _DateOffsetContainer(parent) - if var_type == "Time": - return _TimeOffsetContainer(parent) - w = QLabel("(不支持该操作)", parent) - w.setFixedHeight(25) - return w - - -def makeVarRefCombo( - parent: QWidget = None -) -> QComboBox: - - cb = QComboBox(parent) - cb.setFixedHeight(25) - cb.setMinimumWidth(120) - cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - return cb - - -def makeComboWidget( - items, - min_width: int = 80, - parent: QWidget = None -) -> QComboBox: - - cb = QComboBox(parent) - for display, data in items: - cb.addItem(display, data) - cb.setFixedHeight(25) - cb.setMinimumWidth(min_width) - return cb - - -def makeLabel( - text: str, - parent: QWidget = None, - width: int = None -) -> QLabel: - - lbl = QLabel(text, parent) - lbl.setFixedHeight(25) - if width: - lbl.setFixedWidth(width) - return lbl - - class _DateInputContainer(QWidget): def __init__( @@ -281,7 +106,6 @@ class _DateInputContainer(QWidget): self._dynamicItems = {} # index -> raw expression, for one-way parsed items self.setupUi() - def setupUi( self ): @@ -327,7 +151,6 @@ class _DateInputContainer(QWidget): return self._relCombo.currentText() return self._dateEdit.date().toString("yyyy-MM-dd") - def setValue( self, expr: str @@ -392,14 +215,12 @@ class _TimeInputContainer(QWidget): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._timeEdit) - def getValue( self ) -> str: return self._timeEdit.time().toString("HH:mm") - def setValue( self, expr: str @@ -443,14 +264,12 @@ class _DateOffsetContainer(QWidget): layout.addWidget(self._unitCombo) layout.addStretch() - def getValue( self ) -> str: return str(self.getOffsetDays()) - def setValue( self, expr: str @@ -462,7 +281,6 @@ class _DateOffsetContainer(QWidget): except ValueError: pass - def getOffsetDays( self ) -> int: @@ -477,7 +295,6 @@ class _DateOffsetContainer(QWidget): return val * 365 return val - def getRawValue( self ) -> str: @@ -502,14 +319,12 @@ class _TimeOffsetContainer(QWidget): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._spinBox) - def getValue( self ) -> str: return str(self.getOffsetHours()) - def setValue( self, expr: str @@ -521,14 +336,12 @@ class _TimeOffsetContainer(QWidget): except ValueError: pass - def getOffsetHours( self ) -> int: return self._spinBox.value() - def getRawValue( self ) -> str: @@ -536,6 +349,172 @@ class _TimeOffsetContainer(QWidget): return str(self._spinBox.value()) +class VariableManager(QObject): + + def __init__( + self, + parent = None + ): + + super().__init__(parent) + self._vars = [] + self._nameMap = {} + + self._initPresetVars() + + def _initPresetVars( + self + ): + + for p in PRESET_VARIABLES: + entry = {"name": p["name"], "type": p["type"], "display": p["display"]} + self._vars.append(entry) + self._nameMap[p["name"]] = entry + + def getInfoByName( + self, + name: str + ): + + return self._nameMap.get(name.upper().strip()) + + def populateCombo( + self, + combo: QComboBox + ): + + currentData = combo.currentData() + combo.blockSignals(True) + combo.clear() + for entry in self._vars: + combo.addItem( + entry["display"], + (entry["name"], entry["type"]) + ) + if currentData: + for i in range(combo.count()): + d = combo.itemData(i) + if d and d[0] == currentData[0]: + combo.setCurrentIndex(i) + break + combo.blockSignals(False) + + def findExactNameEntry( + self, + combo: QComboBox, + name: str + ) -> int: + + name = name.upper().strip() + for i in range(combo.count()): + d = combo.itemData(i) + if d and len(d) >= 1 and d[0].upper().strip() == name: + return i + return -1 + + +def makeValueWidget( + var_type: str, + parent: QWidget = None +) -> QWidget: + + if var_type == "Int": + w = QSpinBox(parent) + w.setRange(-999999, 999999) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Float": + w = QDoubleSpinBox(parent) + w.setRange(-999999.0, 999999.0) + w.setDecimals(2) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "String": + w = QLineEdit(parent) + w.setPlaceholderText("输入值") + w.setFixedHeight(25) + w.setMinimumWidth(120) + return w + if var_type == "Boolean": + w = QComboBox(parent) + w.addItem("是 (true)", "true") + w.addItem("否 (false)", "false") + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Date": + return _DateInputContainer(parent) + if var_type == "Time": + return _TimeInputContainer(parent) + w = QLineEdit(parent) + w.setPlaceholderText("输入值") + w.setFixedHeight(25) + w.setMinimumWidth(120) + return w + +def makeOffsetWidget( + var_type: str, + parent: QWidget = None +) -> QWidget: + + if var_type == "Int": + w = QSpinBox(parent) + w.setRange(-999999, 999999) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Float": + w = QDoubleSpinBox(parent) + w.setRange(-999999.0, 999999.0) + w.setDecimals(2) + w.setFixedHeight(25) + w.setMinimumWidth(100) + return w + if var_type == "Date": + return _DateOffsetContainer(parent) + if var_type == "Time": + return _TimeOffsetContainer(parent) + w = QLabel("(不支持该操作)", parent) + w.setFixedHeight(25) + return w + +def makeVarRefCombo( + parent: QWidget = None +) -> QComboBox: + + cb = QComboBox(parent) + cb.setFixedHeight(25) + cb.setMinimumWidth(120) + cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + return cb + +def makeComboWidget( + items, + min_width: int = 80, + parent: QWidget = None +) -> QComboBox: + + cb = QComboBox(parent) + for display, data in items: + cb.addItem(display, data) + cb.setFixedHeight(25) + cb.setMinimumWidth(min_width) + return cb + +def makeLabel( + text: str, + parent: QWidget = None, + width: int = None +) -> QLabel: + + lbl = QLabel(text, parent) + lbl.setFixedHeight(25) + if width: + lbl.setFixedWidth(width) + return lbl + def getValueFromWidget( w: QWidget ) -> str: @@ -556,8 +535,7 @@ def getValueFromWidget( return w.text() return "" - -def setWidgetValue( +def setValueToWidget( w: QWidget, var_type: str, expr: str @@ -619,7 +597,6 @@ def setWidgetValue( inner = inner[1:-1].replace('\\"', '"') w.setText(inner) - def encodeValueStr( raw_value: str, var_type: str @@ -632,7 +609,7 @@ def encodeValueStr( """ if var_type in ("Date", "Time"): - return _encodeDateOrTime(str(raw_value), var_type) + return encodeDateOrTime(str(raw_value), var_type) if isinstance(raw_value, bool): return "true" if raw_value else "false" s = str(raw_value) @@ -648,8 +625,7 @@ def encodeValueStr( return f'"{escaped}"' return s - -def _encodeDateOrTime( +def encodeDateOrTime( raw_value: str, var_type: str ) -> str: @@ -707,7 +683,6 @@ def _encodeDateOrTime( return s return f'"{s}"' - def stripOuterParens( s: str ) -> str: @@ -725,12 +700,10 @@ def stripOuterParens( return s[1:-1].strip() return s - # Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B) _RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$') _RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$') - def isArithExpr( expr: str ) -> bool: @@ -741,7 +714,6 @@ def isArithExpr( s = expr.strip() return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s)) - def isVarReference( expr: str ) -> bool: @@ -764,27 +736,7 @@ def isVarReference( return False return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up)) - -def findOperatorIn( - text: str, - operators: list -) -> tuple[int, str] | None: - - for op in operators: - op_upper = op.upper() - start = 0 - while True: - idx = text.upper().find(op_upper, start) - if idx < 0: - break - if _isInsideLiteral(text, idx): - start = idx + 1 - continue - return (idx, op) - return None - - -def _isInsideLiteral( +def isInsideLiteral( text: str, pos: int ) -> bool: @@ -799,3 +751,21 @@ def _isInsideLiteral( elif ch == '"' and not in_single: in_double = not in_double return in_single or in_double + +def findOperatorIn( + text: str, + operators: list +) -> tuple[int, str] | None: + + for op in operators: + op_upper = op.upper() + start = 0 + while True: + idx = text.upper().find(op_upper, start) + if idx < 0: + break + if isInsideLiteral(text, idx): + start = idx + 1 + continue + return (idx, op) + return None diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index c62e370..daa5710 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -58,7 +58,6 @@ class ConditionRowFrame(QFrame): self.setupUi() self.connectSignals() - def setupUi( self ): @@ -90,15 +89,7 @@ class ConditionRowFrame(QFrame): layout.addWidget(self._compTypeCombo) self.rhsStack = QStackedWidget(self) self.rhsStack.setFixedHeight(25) - self.literalStack = QStackedWidget(self) - self.literalStack.setFixedHeight(25) - self.literalWidgets = {} - for vt in VAR_TYPE_ORDER: - w = makeValueWidget(vt, self.literalStack) - self.literalWidgets[vt] = w - self.literalStack.addWidget(w) - self.literalStack.setCurrentWidget(self.literalWidgets.get("String")) - self.rhsStack.addWidget(self.literalStack) + self.initLiteralStack() self.rhsVarCombo = makeVarRefCombo(self) self.rhsStack.addWidget(self.rhsVarCombo) self.rhsStack.setCurrentIndex(0) @@ -113,7 +104,6 @@ class ConditionRowFrame(QFrame): layout.addStretch() self.setUpdatesEnabled(True) - def populateLeftVarCombo( self ): @@ -136,13 +126,25 @@ class ConditionRowFrame(QFrame): self.leftVarCombo.setCurrentIndex(ci) break - - def populateRhsVarCombo( + def populateRHSVarCombo( self ): self._varMgr.populateCombo(self.rhsVarCombo) + def initLiteralStack( + self + ): + + self.literalStack = QStackedWidget(self) + self.literalStack.setFixedHeight(25) + self._literalWidgets = {} + for vt in VAR_TYPE_ORDER: + w = makeValueWidget(vt, self.literalStack) + self._literalWidgets[vt] = w + self.literalStack.addWidget(w) + self.literalStack.setCurrentWidget(self._literalWidgets.get("String")) + self.rhsStack.addWidget(self.literalStack) def connectSignals( self @@ -151,58 +153,22 @@ class ConditionRowFrame(QFrame): self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) - @Slot(int) - def onLeftVarChanged( - self, - idx - ): - - self._rawRhsExpr = "" - if idx < 0: - return - data = self.leftVarCombo.itemData(idx) - if not data: - return - name, vartype = data - isBool = name in ("true", "false") - self._isBoolMode = isBool - self.opCombo.setVisible(not isBool) - self._compTypeCombo.setVisible(not isBool) - self.rhsStack.setVisible(not isBool) - if not isBool: - self.updateRhsLiteralWidget(vartype) - - - def updateRhsLiteralWidget( - self, - vartype: str - ): - - if vartype not in self.literalWidgets: - vartype = "String" - self.literalStack.setCurrentWidget(self.literalWidgets[vartype]) - - @Slot(int) - def onCompTypeChanged( - self, - idx - ): - - self._rawRhsExpr = "" - isVar = (self._compTypeCombo.currentData() == "variable") - self.rhsStack.setCurrentIndex(1 if isVar else 0) - if isVar: - self.populateRhsVarCombo() - - def getLogic( self ) -> str: return self.logicCombo.currentData() if self.logicCombo else "" + def updateRHSLiteralWidget( + self, + vartype: str + ): - def toConditionText( + if vartype not in self._literalWidgets: + vartype = "String" + self.literalStack.setCurrentWidget(self._literalWidgets[vartype]) + + def toScript( self ) -> str: @@ -230,20 +196,53 @@ class ConditionRowFrame(QFrame): if rhsText: return f"{name} {opSym} {rhsText}" return "" - w = self.literalWidgets.get(vartype) + w = self._literalWidgets.get(vartype) if w: rawVal = getValueFromWidget(w) encoded = encodeValueStr(rawVal, vartype) return f"{name} {opSym} {encoded}" return "" - def refreshVarCombos( self ): self.populateLeftVarCombo() - self.populateRhsVarCombo() + self.populateRHSVarCombo() + + @Slot(int) + def onLeftVarChanged( + self, + idx + ): + + self._rawRhsExpr = "" + if idx < 0: + return + data = self.leftVarCombo.itemData(idx) + if not data: + return + name, vartype = data + isBool = name in ("true", "false") + self._isBoolMode = isBool + self.opCombo.setVisible(not isBool) + self._compTypeCombo.setVisible(not isBool) + self.rhsStack.setVisible(not isBool) + if not isBool: + self.updateRHSLiteralWidget(vartype) + + @Slot(int) + def onCompTypeChanged( + self, + idx + ): + + self._rawRhsExpr = "" + isVar = (self._compTypeCombo.currentData() == "variable") + self.rhsStack.setCurrentIndex(1 if isVar else 0) + if isVar: + self.populateRHSVarCombo() + class ActionStepFrame(QFrame): @@ -262,7 +261,6 @@ class ActionStepFrame(QFrame): self.setupUi() self.connectSignals() - def setupUi( self ): @@ -280,7 +278,7 @@ class ActionStepFrame(QFrame): self.targetCombo = QComboBox(self) self.targetCombo.setFixedHeight(25) self.targetCombo.setMinimumWidth(120) - self.buildTargetCombo() + self.populateTargetCombo() layout.addWidget(self.targetCombo) layout.addWidget(makeLabel("为", self)) self.valueSrcCombo = makeComboWidget([ @@ -301,8 +299,7 @@ class ActionStepFrame(QFrame): layout.addWidget(self.deleteBtn) self.setUpdatesEnabled(True) - - def buildTargetCombo( + def populateTargetCombo( self ): @@ -319,7 +316,6 @@ class ActionStepFrame(QFrame): ) self.targetCombo.blockSignals(False) - def initValueStacks( self ): @@ -338,7 +334,6 @@ class ActionStepFrame(QFrame): self._offsetWidgets[vt] = lbl self.valueStack.addWidget(lbl) - def connectSignals( self ): @@ -347,32 +342,14 @@ class ActionStepFrame(QFrame): self.targetCombo.currentIndexChanged.connect(self.onTargetChanged) self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) - @Slot(int) - def onTargetChanged( - self, - idx - ): + def getTargetName( + self + ) -> str: - if idx < 0: - return - data = self.targetCombo.itemData(idx) - if not data: - return - _, vartype = data - self._currentTargetType = vartype - self.updateRHSWidget() - self.onValueSrcChanged(self.valueSrcCombo.currentIndex()) + data = self.targetCombo.currentData() + return data[0] if data else "" - @Slot(int) - def onOpTypeChanged( - self, - idx - ): - - self.updateRHSWidget() - - - def updateRHSWidget( + def updateValueWidget( self ): @@ -386,30 +363,7 @@ class ActionStepFrame(QFrame): else: self.valueStack.setCurrentWidget(self._literalWidgets.get("String")) - @Slot(int) - def onValueSrcChanged( - self, - idx - ): - - isVar = (self.valueSrcCombo.currentData() == "variable") - self.valueStack.setVisible(not isVar) - self.existingVarCombo.setVisible(isVar) - if isVar: - self._varMgr.populateCombo(self.existingVarCombo) - else: - self.updateRHSWidget() - - - def getTargetName( - self - ) -> str: - - data = self.targetCombo.currentData() - return data[0] if data else "" - - - def toScriptLine( + def toScript( self ) -> str: """ @@ -422,7 +376,7 @@ class ActionStepFrame(QFrame): return " -- pass" if not target: return "" - rawVal = self._getValueRaw() + rawVal = self.getValueRaw() vartype = self._currentTargetType if op == "set": encoded = encodeValueStr(rawVal, vartype) @@ -445,8 +399,7 @@ class ActionStepFrame(QFrame): return f" {target} = {target} - {rawVal}" return "" - - def _getValueRaw( + def getValueRaw( self ) -> str: @@ -458,13 +411,12 @@ class ActionStepFrame(QFrame): return getValueFromWidget(w) return "" - def refreshVarCombos( self ): currentData = self.targetCombo.currentData() - self.buildTargetCombo() + self.populateTargetCombo() if currentData: for i in range(self.targetCombo.count()): d = self.targetCombo.itemData(i) @@ -472,3 +424,41 @@ class ActionStepFrame(QFrame): self.targetCombo.setCurrentIndex(i) break self._varMgr.populateCombo(self.existingVarCombo) + + @Slot(int) + def onTargetChanged( + self, + idx + ): + + if idx < 0: + return + data = self.targetCombo.itemData(idx) + if not data: + return + _, vartype = data + self._currentTargetType = vartype + self.updateValueWidget() + self.onValueSrcChanged(self.valueSrcCombo.currentIndex()) + + @Slot(int) + def onOpTypeChanged( + self, + idx + ): + + self.updateValueWidget() + + @Slot(int) + def onValueSrcChanged( + self, + idx + ): + + isVar = (self.valueSrcCombo.currentData() == "variable") + self.valueStack.setVisible(not isVar) + self.existingVarCombo.setVisible(isVar) + if isVar: + self._varMgr.populateCombo(self.existingVarCombo) + else: + self.updateValueWidget() diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index 052b580..9c2ddfb 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -10,28 +10,46 @@ See the LICENSE file for details. import os from PySide6.QtCore import ( - Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo -) -from PySide6.QtWidgets import ( - QDialog, QWidget, QLineEdit, QMessageBox, QFileDialog, - QTreeWidgetItem, QMenu, QInputDialog + QDate, + QDir, + QFileInfo, + Qt, + QTime, + Signal, + Slot ) from PySide6.QtGui import ( - QCloseEvent, QAction + QAction, + QCloseEvent +) +from PySide6.QtWidgets import ( + QDialog, + QFileDialog, + QInputDialog, + QLineEdit, + QMenu, + QMessageBox, + QTreeWidgetItem, + QWidget ) import managers.config.ConfigManager as ConfigManager -from utils.JSONReader import JSONReader -from utils.JSONWriter import JSONWriter -from interfaces.ConfigProvider import ConfigProvider, CfgKey -from managers.config.ConfigUtils import ConfigUtils - -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.ALUserTreeWidget import ( + ALUserTreeItemType, + ALUserTreeWidget +) from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog +from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget +from interfaces.ConfigProvider import ( + CfgKey, + ConfigProvider +) +from managers.config.ConfigUtils import ConfigUtils +from utils.JSONReader import JSONReader +from utils.JSONWriter import JSONWriter class ALConfigWidget(QWidget, Ui_ALConfigWidget): @@ -54,7 +72,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): if not self.initializeConfigs(): self.close() - def modifyUi( self ): @@ -70,7 +87,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.initializeFloorRoomMap() self.initializeUserInfoWidget() - def connectSignals( self ): @@ -94,7 +110,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked) - def showEvent( self, event @@ -118,7 +133,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): return result - def closeEvent( self, event: QCloseEvent @@ -127,7 +141,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.configWidgetIsClosed.emit() super().closeEvent(event) - def initializeFloorRoomMap( self ): @@ -161,7 +174,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): "五层": ["五层考研"] } - def initializeConfigToWidget( self, which: str, @@ -176,7 +188,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.setUsersToTreeWidget(config_data) self.CurrentUserConfigEdit.setText(self.__config_paths["user"]) - def initializeConfig( self, which: str @@ -210,7 +221,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): is_success = False return is_success - def initializeConfigs( self ) -> bool: @@ -223,7 +233,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.initializeConfigToWidget(which, self.__config_data[which]) return is_success - def defaultRunConfig( self ) -> dict: @@ -247,7 +256,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): } } - def defaultUserConfig( self ) -> dict: @@ -257,7 +265,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ] } - def collectRunConfigFromWidget( self ) -> dict: @@ -279,7 +286,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): run_config["mode"]["run_mode"] = run_mode return run_config - def setRunConfigToWidget( self, run_config: dict @@ -318,7 +324,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): "文件可能被意外修改或已经损坏\n" ) - def initializeUserInfoWidget( self ): @@ -343,7 +348,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.MaxRenewTimeDiffSpinBox.setValue(30) self.PreferLateRenewTimeCheckBox.setChecked(False) - def collectUserFromWidget( self ) -> dict: @@ -376,7 +380,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): user["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked() return user - def collectUsersFromTreeWidget( self ) -> dict: @@ -399,7 +402,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): user_config["groups"].append(group_config) return user_config - def setUserToWidget( self, user: dict @@ -441,7 +443,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): "文件可能被意外修改或已经损坏\n" ) - def setUsersToTreeWidget( self, users: dict @@ -483,7 +484,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): finally: self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) - def loadRunConfig( self, run_config_path: str @@ -507,7 +507,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ) return None - def saveRunConfig( self, run_config_path: str, @@ -529,7 +528,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ) return False - def loadUserConfig( self, user_config_path: str @@ -563,7 +561,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ) return None - def saveUserConfig( self, user_config_path: str, @@ -585,7 +582,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ) return False - def saveConfigs( self, run_config_path: str, @@ -608,7 +604,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): return False return True - def loadConfig( self, config_path: str @@ -637,7 +632,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): except: return False - def addGroup( self, group_name: str = "" @@ -654,7 +648,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) return group_item - def delGroup( self, group_item: QTreeWidgetItem = None @@ -667,7 +660,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): index = self.UserTreeWidget.indexOfTopLevelItem(group_item) self.UserTreeWidget.takeTopLevelItem(index) - def addUser( self, group_item: QTreeWidgetItem = None @@ -722,7 +714,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) return user_item - def delUser( self, user_item: QTreeWidgetItem = None @@ -738,7 +729,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): if parent_item.childCount() == 0: self.UserTreeWidget.setCurrentItem(None) - def renameItem( self, item: QTreeWidgetItem, @@ -862,7 +852,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): is_checked = item.checkState(1) == Qt.CheckState.Checked item.setText(1, "" if is_checked else "跳过") - def showTreeMenu( self, menu: QMenu @@ -872,7 +861,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): add_group_action.triggered.connect(self.addGroup) menu.addAction(add_group_action) - def showGroupMenu( self, menu: QMenu, @@ -892,7 +880,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): if group_item.checkState(1) == Qt.CheckState.Unchecked: add_user_action.setEnabled(False) - def showUserMenu( self, menu: QMenu, @@ -952,7 +939,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): if browser_driver_path: self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path)) - @Slot() def onAutoDownloadWebDriverButtonClicked( self @@ -966,7 +952,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): 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/ALMainWindow.py b/src/gui/ALMainWindow.py index 022e46a..20dd3f8 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -10,24 +10,37 @@ See the LICENSE file for details. import queue from PySide6.QtCore import ( - Qt, Signal, Slot, QTimer, QUrl, -) -from PySide6.QtWidgets import ( - QMainWindow, QMenu, QSystemTrayIcon, QMessageBox + QTimer, + QUrl, + Qt, + Signal, + Slot ) from PySide6.QtGui import ( - QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices + QCloseEvent, + QDesktopServices, + QFont, + QIcon, + QTextCursor +) +from PySide6.QtWidgets import ( + QMainWindow, + QMenu, + QMessageBox, + QSystemTrayIcon ) from base.MsgBase import MsgBase -from managers.config.ConfigUtils import ConfigUtils - -from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow -from gui.resources import ALResource -from gui.ALConfigWidget import ALConfigWidget -from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget from gui.ALAboutDialog import ALAboutDialog -from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker +from gui.ALConfigWidget import ALConfigWidget +from gui.ALMainWorkers import ( + AutoLibWorker, + TimerTaskWorker +) +from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget +from gui.resources import ALResource +from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow +from managers.config.ConfigUtils import ConfigUtils class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): @@ -59,7 +72,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.startTimerTaskPolling() self._showLog("主窗口初始化完成") - def modifyUi( self ): @@ -90,7 +102,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed) self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint) - def onAboutActionTriggered( self ): @@ -98,7 +109,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): about_dialog = ALAboutDialog(self) about_dialog.exec() - def onManualActionTriggered( self ): @@ -106,7 +116,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") QDesktopServices.openUrl(url) - def setupTray( self ): @@ -128,7 +137,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.TrayIcon.activated.connect(self.onTrayIconActivated) self.TrayIcon.show() - def hideToTray( self ): @@ -141,7 +149,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): 2000 ) - def onTrayIconActivated( self, reason: QSystemTrayIcon.ActivationReason @@ -150,7 +157,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): if reason == QSystemTrayIcon.DoubleClick: self.showNormal() - def connectSignals( self ): @@ -162,7 +168,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.SendButton.clicked.connect(self.onSendButtonClicked) self.MessageEdit.returnPressed.connect(self.onSendButtonClicked) - def closeEvent( self, event: QCloseEvent @@ -188,7 +193,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self._showLog("主窗口关闭") QMainWindow.closeEvent(self, event) - def appendToTextEdit( self, text: str @@ -202,7 +206,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): scrollbar = self.MessageIOTextEdit.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) - def startMsgPolling( self ): @@ -211,7 +214,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__msg_queue_timer.timeout.connect(self.pollMsgQueue) self.__msg_queue_timer.start(100) - def startTimerTaskPolling( self ): @@ -220,7 +222,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue) self.__timer_task_timer.start(500) - def pollTimerTaskQueue( self ): @@ -254,7 +255,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self.__is_running_timer_task = False pass - def setControlButtons( self, config_button_enabled: bool, diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index f1680b0..e4f08d8 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -37,7 +37,6 @@ class AutoLibWorker(MsgBase, QThread): QThread.__init__(self) self.__config_paths = config_paths - def checkTimeAvailable( self, ) -> bool: @@ -52,7 +51,6 @@ class AutoLibWorker(MsgBase, QThread): self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO) return True - def checkConfigPaths( self, ) -> bool: @@ -68,7 +66,6 @@ class AutoLibWorker(MsgBase, QThread): self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO) return True - def loadConfigs( self ) -> bool: @@ -101,7 +98,6 @@ class AutoLibWorker(MsgBase, QThread): ) return True - def run( self ): @@ -161,7 +157,6 @@ class TimerTaskWorker(AutoLibWorker): self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished) self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError) - def run( self ): @@ -206,7 +201,6 @@ class TimerTaskWorker(AutoLibWorker): self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束") self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) - def applyRepeatAutoScript( self ): diff --git a/src/gui/ALSeatFrame.py b/src/gui/ALSeatFrame.py index 331862c..48057ee 100644 --- a/src/gui/ALSeatFrame.py +++ b/src/gui/ALSeatFrame.py @@ -31,7 +31,6 @@ class ALSeatFrame(QFrame): self.setupUi() - def setupUi( self ): @@ -55,7 +54,6 @@ class ALSeatFrame(QFrame): self.Label.setAlignment(Qt.AlignCenter) self.Label.setGeometry(0, 0, 60, 40) - def mousePressEvent( self, event @@ -65,14 +63,12 @@ class ALSeatFrame(QFrame): self.toggleSelection() self.clicked.emit(self.__seat_number) - def isSelected( self ): return self.__is_selected - def toggleSelection(self): self.__is_selected = not self.__is_selected diff --git a/src/gui/ALSeatMapSelectDialog.py b/src/gui/ALSeatMapSelectDialog.py index 032ec66..0602dc5 100644 --- a/src/gui/ALSeatMapSelectDialog.py +++ b/src/gui/ALSeatMapSelectDialog.py @@ -8,15 +8,20 @@ You may use, modify, and distribute this file under the terms of the MIT License See the LICENSE file for details. """ from PySide6.QtCore import ( - Qt, Slot, Signal -) -from PySide6.QtWidgets import ( - QDialog, QLabel, QHBoxLayout, QVBoxLayout, - QPushButton, + Qt, + Signal, + Slot ) from PySide6.QtGui import ( QCloseEvent ) +from PySide6.QtWidgets import ( + QDialog, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout +) from gui.ALSeatMapView import ALSeatMapView @@ -42,7 +47,6 @@ class ALSeatMapSelectDialog(QDialog): self.setupUi() self.connectSignals() - def setupUi( self ): @@ -85,7 +89,6 @@ class ALSeatMapSelectDialog(QDialog): self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton) self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout) - def connectSignals( self ): @@ -93,7 +96,6 @@ class ALSeatMapSelectDialog(QDialog): self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked) - def showEvent( self, event @@ -117,7 +119,6 @@ class ALSeatMapSelectDialog(QDialog): return result - def closeEvent( self, event: QCloseEvent @@ -131,7 +132,6 @@ class ALSeatMapSelectDialog(QDialog): self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats()) super().closeEvent(event) - def selectSeat( self, seat_number: str @@ -139,7 +139,6 @@ class ALSeatMapSelectDialog(QDialog): self.SeatMapGraphicsView.selectSeat(seat_number) - def selectSeats( self, seat_numbers: list[str] @@ -147,14 +146,12 @@ class ALSeatMapSelectDialog(QDialog): return self.SeatMapGraphicsView.selectSeats(seat_numbers) - def getSelectedSeats( self ) -> list[str]: return self.SeatMapGraphicsView.getSelectedSeats() - def clearSelections( self ): diff --git a/src/gui/ALSeatMapView.py b/src/gui/ALSeatMapView.py index c687b45..a215e2a 100644 --- a/src/gui/ALSeatMapView.py +++ b/src/gui/ALSeatMapView.py @@ -11,11 +11,16 @@ from PySide6.QtCore import ( Qt, Slot, QEvent ) from PySide6.QtWidgets import ( - QFrame, QWidget, - QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem + QFrame, + QWidget, + QGridLayout, + QGraphicsView, + QGraphicsScene, + QGraphicsItem ) from PySide6.QtGui import ( - QPainter, QWheelEvent + QPainter, + QWheelEvent ) from gui.ALSeatFrame import ALSeatFrame @@ -35,18 +40,6 @@ class ALSeatMapView(QGraphicsView): self.setupUi() - @staticmethod - def formatSeatNumber( - seat_number: str - ) -> str: - - if seat_number and not seat_number[-1].isdigit(): - digits = seat_number[:-1] - letter = seat_number[-1] - return digits.zfill(3) + letter - return seat_number.zfill(3) - - def eventFilter( self, watched, @@ -61,7 +54,6 @@ class ALSeatMapView(QGraphicsView): return True return super().eventFilter(watched, event) - def zoomGraphicsView( self, event: QWheelEvent @@ -80,7 +72,6 @@ class ALSeatMapView(QGraphicsView): self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.scale(zoom_factor, zoom_factor) - def setupUi( self ): @@ -100,7 +91,6 @@ class ALSeatMapView(QGraphicsView): self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget) self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False) - def setupSeatMap( self ): @@ -125,7 +115,6 @@ class ALSeatMapView(QGraphicsView): self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20) self.SeatsContainerWidget.adjustSize() - def selectSeat( self, seat_number: str @@ -142,7 +131,6 @@ class ALSeatMapView(QGraphicsView): widget.toggleSelection() self.__selected_seats.append(seat_number) - def selectSeats( self, selected_seats: list @@ -152,14 +140,12 @@ class ALSeatMapView(QGraphicsView): for seat_number in selected_seats: self.selectSeat(seat_number) - def getSelectedSeats( self ) -> list[str]: return self.__selected_seats - def clearSelections( self ): @@ -185,4 +171,15 @@ class ALSeatMapView(QGraphicsView): if len(self.__selected_seats) < 1: self.__selected_seats.append(seat_number) else: - self.__seat_frames[seat_number].toggleSelection() \ No newline at end of file + self.__seat_frames[seat_number].toggleSelection() + + @staticmethod + def formatSeatNumber( + seat_number: str + ) -> str: + + if seat_number and not seat_number[-1].isdigit(): + digits = seat_number[:-1] + letter = seat_number[-1] + return digits.zfill(3) + letter + return seat_number.zfill(3) diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py index 56791f3..aae5809 100644 --- a/src/gui/ALStatusLabel.py +++ b/src/gui/ALStatusLabel.py @@ -9,14 +9,20 @@ See the LICENSE file for details. """ from enum import Enum -from PySide6.QtWidgets import ( - QLabel -) from PySide6.QtCore import ( - Qt, Property, QPropertyAnimation, QEasingCurve + Property, + QEasingCurve, + QPropertyAnimation, + Qt ) from PySide6.QtGui import ( - QPainter, QColor, QConicalGradient, QPalette + QColor, + QConicalGradient, + QPainter, + QPalette +) +from PySide6.QtWidgets import ( + QLabel ) @@ -44,7 +50,6 @@ class ALStatusLabel(QLabel): self.setupUi() - def setupUi( self ): @@ -59,14 +64,12 @@ class ALStatusLabel(QLabel): 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: @@ -111,7 +114,6 @@ class ALStatusLabel(QLabel): self.__icon_angle = value self.update() - def paintEvent( self, event diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 019de61..1d0b600 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -49,7 +49,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): if self.__edit_timer_task: self.loadTask(self.__edit_timer_task) - def modifyUi( self ): @@ -101,10 +100,10 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3) self.AutoScriptLayout.setSpacing(3) autoScriptBtnLayout = QHBoxLayout() - self.AutoScriptPreviewButton = QPushButton("编辑") - self.AutoScriptPreviewButton.setMinimumHeight(25) - self.AutoScriptPreviewButton.setFixedWidth(60) - autoScriptBtnLayout.addWidget(self.AutoScriptPreviewButton) + self.AutoScriptEditButton = QPushButton("编辑") + self.AutoScriptEditButton.setMinimumHeight(25) + self.AutoScriptEditButton.setFixedWidth(80) + autoScriptBtnLayout.addWidget(self.AutoScriptEditButton) autoScriptBtnLayout.addStretch() self.AutoScriptHelpButton = QPushButton("?") self.AutoScriptHelpButton.setFixedSize(20, 20) @@ -133,7 +132,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.__auto_script = "" self.__mock_target_data = None - def loadTask( self, task: dict @@ -174,7 +172,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.__mock_target_data = mock_data self.ConfirmButton.setText("保存") - def connectSignals( self ): @@ -183,10 +180,9 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.ConfirmButton.clicked.connect(self.accept) self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged) self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled) - self.AutoScriptPreviewButton.clicked.connect(self.onPreviewAutoScript) + self.AutoScriptEditButton.clicked.connect(self.onPreviewAutoScript) self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp) - def getTimerTask( self ) -> dict: diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py index e656c47..03bba0d 100644 --- a/src/gui/ALTimerTaskHistoryDialog.py +++ b/src/gui/ALTimerTaskHistoryDialog.py @@ -34,7 +34,6 @@ class ALTimerTaskHistoryDialog(QDialog): self.setupUi() self.connectSignals() - def setupUi( self ): @@ -82,7 +81,6 @@ class ALTimerTaskHistoryDialog(QDialog): ButtonLayout.addWidget(self.CloseButton) MainLayout.addLayout(ButtonLayout) - def connectSignals( self ): @@ -90,7 +88,6 @@ class ALTimerTaskHistoryDialog(QDialog): self.CloseButton.clicked.connect(self.accept) self.ClearHistoryButton.clicked.connect(self.onClearHistoryButtonClicked) - def loadHistory( self ): @@ -99,6 +96,11 @@ class ALTimerTaskHistoryDialog(QDialog): for row, record in enumerate(self.__history): self.addHistoryRow(row, record) + def getHistory( + self + ) -> list: + + return self.__history def addHistoryRow( self, @@ -129,13 +131,6 @@ class ALTimerTaskHistoryDialog(QDialog): self.HistoryTableWidget.setItem(row, 2, DurationItem) self.HistoryTableWidget.setRowHeight(row, 25) - - def getHistory( - self - ) -> list: - - return self.__history - @Slot() def onClearHistoryButtonClicked( self diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index 4aef755..f2df449 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -15,24 +15,40 @@ from enum import Enum from datetime import datetime, timedelta from PySide6.QtCore import ( - Qt, Signal, Slot, QTimer -) -from PySide6.QtWidgets import ( - QDialog, QWidget, QListWidgetItem, QMessageBox, - QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QMenu + QTimer, + Qt, + Signal, + Slot ) from PySide6.QtGui import ( - QCloseEvent, QAction + QAction, + QCloseEvent +) +from PySide6.QtWidgets import ( + QDialog, + QHBoxLayout, + QLabel, + QListWidgetItem, + QMenu, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget ) import managers.config.ConfigManager as ConfigManager -from utils.TimerUtils import TimerUtils -from interfaces.ConfigProvider import ConfigProvider, CfgKey - -from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget -from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus +from gui.ALTimerTaskAddDialog import ( + ALTimerTaskAddDialog, + ALTimerTaskStatus +) from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog +from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget +from interfaces.ConfigProvider import ( + CfgKey, + ConfigProvider +) +from utils.TimerUtils import TimerUtils class ALTimerTaskItemWidget(QWidget): @@ -53,7 +69,6 @@ class ALTimerTaskItemWidget(QWidget): self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.showContextMenu) - def modifyUi( self ): @@ -204,7 +219,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): if not self.initializeTimerTasks(): raise Exception("定时任务配置文件初始化失败 !") - def connectSignals( self ): @@ -215,7 +229,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked) self.timerTasksChanged.connect(self.onTimerTasksChanged) - def setupTimer( self ): @@ -224,7 +237,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.__check_timer.timeout.connect(self.checkTasks) self.__check_timer.start(500) - def initializeTimerTasks( self ) -> bool: @@ -240,7 +252,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): return True return False - def getTimerTasks( self ) -> list: @@ -265,7 +276,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ) return None - def setTimerTasks( self, timer_tasks: list @@ -289,7 +299,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ) return False - def showEvent( self, event @@ -313,7 +322,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): return result - def closeEvent( self, event: QCloseEvent @@ -323,7 +331,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.timerTaskManageWidgetIsClosed.emit() event.ignore() - def sortTimerTasks( self, policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME, @@ -346,7 +353,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): reverse = order is Qt.SortOrder.DescendingOrder ) - def updateStat( self ): @@ -373,7 +379,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.ExecutedTaskLabel.setText(f"已执行:{executed}") self.InvalidTaskLabel.setText(f"无效的:{invalid}") - def updateTimerTaskList( self ): @@ -396,7 +401,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.TimerTasksListWidget.addItem(item) self.TimerTasksListWidget.setItemWidget(item, widget) - def addTask( self ): @@ -407,7 +411,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.__timer_tasks.append(timer_task) self.timerTasksChanged.emit() - def editTask( self, timer_task: dict @@ -440,7 +443,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): f"已记录次数:{history_count}" ) - def deleteTask( self, timer_task: dict @@ -469,7 +471,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ] self.timerTasksChanged.emit() - def clearAllTasks( self ): @@ -531,7 +532,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.__timer_tasks.clear() self.timerTasksChanged.emit() - def showTaskHistory( self, task: dict @@ -541,7 +541,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): if dialog.exec() == QDialog.DialogCode.Accepted: self.timerTasksChanged.emit() - def checkTasks( self ): @@ -615,7 +614,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): break self.timerTasksChanged.emit() - def onRepeatTimerTaskIs( self, status: ALTimerTaskStatus, diff --git a/src/gui/ALUserTreeWidget.py b/src/gui/ALUserTreeWidget.py index 15bdd2c..fdb39e6 100644 --- a/src/gui/ALUserTreeWidget.py +++ b/src/gui/ALUserTreeWidget.py @@ -10,14 +10,22 @@ See the LICENSE file for details. from enum import Enum from PySide6.QtCore import ( - Qt, QSize, QCoreApplication, QRect, QPoint + Qt, + QSize, + QCoreApplication, + QRect, + QPoint ) from PySide6.QtWidgets import ( - QAbstractScrollArea, QAbstractItemView, - QTreeWidget, QTreeWidgetItem + QAbstractScrollArea, + QAbstractItemView, + QTreeWidget, + QTreeWidgetItem ) from PySide6.QtGui import ( - QDragEnterEvent, QDragMoveEvent, QDropEvent + QDragEnterEvent, + QDragMoveEvent, + QDropEvent ) @@ -39,7 +47,6 @@ class ALUserTreeWidget(QTreeWidget): self.setupUi() self.translateUi() - def setupUi( self ): @@ -70,7 +77,6 @@ class ALUserTreeWidget(QTreeWidget): self.header().setHighlightSections(False) self.header().setProperty(u"showSortIndicator", True) - def translateUi( self ): @@ -78,7 +84,6 @@ class ALUserTreeWidget(QTreeWidget): ___qtreewidgetitem = self.headerItem() ___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None)); - @staticmethod def isDragPositionValid( target_rect: QRect, @@ -90,7 +95,6 @@ class ALUserTreeWidget(QTreeWidget): y_offset < target_rect.height()*0.8) return valid - def dragEnterEvent( self, event: QDragEnterEvent @@ -98,7 +102,6 @@ class ALUserTreeWidget(QTreeWidget): super().dragEnterEvent(event) - def dragMoveEvent( self, event: QDragMoveEvent @@ -136,7 +139,6 @@ class ALUserTreeWidget(QTreeWidget): return event.acceptProposedAction() - def dropEvent( self, event: QDropEvent diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py index 59571c5..e8cb0d9 100644 --- a/src/gui/ALWebDriverDownloadDialog.py +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -11,20 +11,30 @@ import threading from typing import Optional from PySide6.QtCore import ( - Qt, Slot, QThread, Signal + Qt, + Slot, + QThread, + Signal ) from PySide6.QtWidgets import ( - QDialog, QLabel, QComboBox, QProgressBar, - QPushButton, QVBoxLayout, QHBoxLayout, - QMessageBox, QFrame, QLineEdit -) -from PySide6.QtGui import ( - QCloseEvent + 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, + instance as webdriverManagerInstance, + WebDriverManager, + WebDriverInfo, + WebDriverType, WebDriverStatus ) from gui.ALStatusLabel import ALStatusLabel @@ -142,7 +152,6 @@ class ALWebDriverDownloadDialog(QDialog): self.initializeDriverManager() self.refreshDriverList() - def showEvent( self, event @@ -165,7 +174,6 @@ class ALWebDriverDownloadDialog(QDialog): self.move(target_pos) return result - def setupUi( self ): @@ -237,7 +245,6 @@ class ALWebDriverDownloadDialog(QDialog): self.ControlLayout.addWidget(self.ConfirmButton) self.MainLayout.addLayout(self.ControlLayout) - def connectSignals( self ): @@ -249,18 +256,16 @@ class ALWebDriverDownloadDialog(QDialog): 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) + self.__driver_manager = webdriverManagerInstance(self.__driver_dir) except ValueError as e: QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}") self.reject() - def refreshDriverList( self ): @@ -270,18 +275,20 @@ class ALWebDriverDownloadDialog(QDialog): self.__driver_manager.refresh() self.__driver_infos = self.__driver_manager.getDriverInfos() self.DriverComboBox.clear() + installed = 0 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}" + if driver_info.driver_status == WebDriverStatus.INSTALLED: + installed += 1 + installed_idx = i # get the installed driver index + display_text += " : 已安装" self.DriverComboBox.addItem(display_text) count = len(self.__driver_infos) - self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:") + self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器,{installed} 个已安装驱动:") if self.__driver_infos: self.DriverComboBox.setCurrentIndex(installed_idx) - def onDriverComboBoxChanged( self, index: int @@ -294,6 +301,116 @@ class ALWebDriverDownloadDialog(QDialog): self.updateProgressBarStates(driver_info) self.updateButtonStates(driver_info) + 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.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 + ) -> 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: + case WebDriverStatus.NOT_INSTALLED: + self.StatusLabel.status = ALStatusLabel.Status.WAITING + case WebDriverStatus.INSTALLED: + self.StatusLabel.status = ALStatusLabel.Status.SUCCESS + case WebDriverStatus.DOWNLOADING: + self.StatusLabel.status = ALStatusLabel.Status.RUNNING + 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 == 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) @Slot() def onRefreshButtonClicked( @@ -302,7 +419,6 @@ class ALWebDriverDownloadDialog(QDialog): self.refreshDriverList() - @Slot() def onDeleteButtonClicked( self @@ -355,7 +471,7 @@ class ALWebDriverDownloadDialog(QDialog): 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.finished.connect(self.onThreadFinished) self.__download_thread.start() @Slot() @@ -406,7 +522,6 @@ class ALWebDriverDownloadDialog(QDialog): self.updateButtonStates(driver_info) QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message) - @Slot() def onDownloadCancelled( self @@ -423,7 +538,6 @@ class ALWebDriverDownloadDialog(QDialog): self.updateButtonStates(driver_info) self.ProgressText.setText("下载已取消") - @Slot() def onConfirmButtonClicked( self @@ -439,7 +553,6 @@ class ALWebDriverDownloadDialog(QDialog): self.__confirmed = True self.accept() - @Slot() def onCancelButtonClicked( self @@ -458,119 +571,3 @@ class ALWebDriverDownloadDialog(QDialog): 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.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 - ) -> 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: - case WebDriverStatus.NOT_INSTALLED: - self.StatusLabel.status = ALStatusLabel.Status.WAITING - case WebDriverStatus.INSTALLED: - self.StatusLabel.status = ALStatusLabel.Status.SUCCESS - case WebDriverStatus.DOWNLOADING: - self.StatusLabel.status = ALStatusLabel.Status.RUNNING - 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 == 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/interfaces/ConfigProvider.py b/src/interfaces/ConfigProvider.py index bf5ebb3..9ee5bb4 100644 --- a/src/interfaces/ConfigProvider.py +++ b/src/interfaces/ConfigProvider.py @@ -16,6 +16,7 @@ class ConfigType(Enum): """ Config type enum. Values represent the default filename. """ + GLOBAL = "autolibrary.json" BULLETIN = "bulletin.json" TIMERTASK = "timer_task.json" @@ -30,6 +31,7 @@ class ConfigPath: Consumers pass this directly to ConfigProvider.get/set, eliminating the need to import ConfigType separately. """ + config_type: ConfigType key: str = "" diff --git a/src/managers/config/ConfigManager.py b/src/managers/config/ConfigManager.py index 1dc108f..4f89eb4 100644 --- a/src/managers/config/ConfigManager.py +++ b/src/managers/config/ConfigManager.py @@ -33,7 +33,6 @@ class ConfigTemplate: self.__config_type = config_type - def template( self ) -> dict: @@ -83,7 +82,6 @@ class ConfigManager: self.initialize() - def initialize( self ): @@ -91,7 +89,6 @@ class ConfigManager: for config_type in ConfigType: self.load(config_type) - def load( self, config_type: ConfigType @@ -108,7 +105,6 @@ class ConfigManager: self.__config_data[config_type.value] = ConfigTemplate(config_type).template() JSONWriter(config_path, self.__config_data[config_type.value]) - def get( self, key: ConfigPath, @@ -126,7 +122,6 @@ class ConfigManager: return default return config_data.get(keys[-1], default) - def set( self, key: ConfigPath, @@ -147,7 +142,6 @@ class ConfigManager: config_data[keys[-1]] = value self.save(key.config_type) - def save( self, config_type: ConfigType @@ -156,7 +150,6 @@ class ConfigManager: config_path = os.path.join(self.__config_dir, config_type.value) JSONWriter(config_path, self.__config_data[config_type.value]) - def configDir( self ) -> str: @@ -169,6 +162,7 @@ _config_manager_instance : ConfigManager | None = None # Singleton instance of ConfigManager. _instance_lock = threading.Lock() + def instance( config_dir: str = "" ) -> ConfigManager: diff --git a/src/managers/driver/WebBrowserDetector.py b/src/managers/driver/WebBrowserDetector.py index 26f246a..bcf6a64 100644 --- a/src/managers/driver/WebBrowserDetector.py +++ b/src/managers/driver/WebBrowserDetector.py @@ -41,6 +41,7 @@ class WebBrowserArch(Enum): MACX86_64 = 6 MACARM = 7 + @dataclass class WebBrowserInfo: """ @@ -70,7 +71,6 @@ class WebBrowserArchDetector: pass - def detect( self ) -> WebBrowserArch: @@ -123,7 +123,6 @@ class WebBrowserDetector: self.browser_arch = WebBrowserArchDetector().detect() self.browser_infos : list[WebBrowserInfo] = [] - def detect( self ) -> list[WebBrowserInfo]: diff --git a/src/managers/driver/WebDriverDownloader.py b/src/managers/driver/WebDriverDownloader.py index 945b103..e8189ae 100644 --- a/src/managers/driver/WebDriverDownloader.py +++ b/src/managers/driver/WebDriverDownloader.py @@ -95,7 +95,6 @@ class WebDriverName: self.driver_type = driver_type - def __str__( self ) -> str: @@ -125,7 +124,6 @@ class WebDriverExecName: self.driver_type = driver_type self.arch = arch - def __str__( self ) -> str: @@ -200,7 +198,6 @@ class WebDriverURL: self.arch = arch self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch)) - def __str__( self ) -> str: @@ -250,31 +247,6 @@ class WebDriverDownloader: 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[[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, 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 as e: - raise e - - def _download( self, progress_callback: Optional[Callable[[float, int, float, str], None]] = None, @@ -352,7 +324,6 @@ class WebDriverDownloader: continue raise e - def _verify( self, progress_callback: Optional[Callable[[float, int, float, str], None]] = None @@ -361,7 +332,6 @@ class WebDriverDownloader: progress_callback(98, 100, 0.0, "验证完成") return True - def _extract( self, progress_callback: Optional[Callable[[float, int, float, str], None]] = None @@ -397,7 +367,6 @@ class WebDriverDownloader: except Exception: return None - def _cleanup( self, driver_file: Path @@ -410,6 +379,29 @@ class WebDriverDownloader: else: item.unlink() + def download( + self, + 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, 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 as e: + raise e + class ChromeDriverDownloader(WebDriverDownloader): """ diff --git a/src/managers/driver/WebDriverManager.py b/src/managers/driver/WebDriverManager.py index 7846560..30b3645 100644 --- a/src/managers/driver/WebDriverManager.py +++ b/src/managers/driver/WebDriverManager.py @@ -81,7 +81,6 @@ class WebDriverManager: self.initialize() - def initialize( self ): @@ -93,7 +92,6 @@ class WebDriverManager: self._checkDriverStatus() self.__initialized = True - def _detectBrowsers( self ): @@ -105,7 +103,6 @@ class WebDriverManager: for info in browser_infos ] - def _checkDriverStatus( self ): @@ -117,7 +114,6 @@ class WebDriverManager: driver_info.driver_path = driver_path driver_info.driver_status = WebDriverStatus.INSTALLED - def _mapWebBrowserTypeToDriver( self, browser_type: WebBrowserType @@ -132,7 +128,6 @@ class WebDriverManager: else: raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}") - def _mapWebBrowserArchToDriver( self, browser_type: WebBrowserType, @@ -199,7 +194,6 @@ class WebDriverManager: else: raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}") - def _mapFirefoxDriverVersion( self, version: str @@ -240,7 +234,6 @@ class WebDriverManager: except Exception as e: raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e - def _getDriverInfo( self, browser_info: WebBrowserInfo @@ -256,7 +249,6 @@ class WebDriverManager: driver_info.browser_version = browser_info.browser_version return driver_info - def _getDriverPath( self, driver_info: WebDriverInfo @@ -286,7 +278,6 @@ class WebDriverManager: driver_path = driver_dir/exe_name return driver_path - def refresh( self ): @@ -294,7 +285,6 @@ class WebDriverManager: self._detectBrowsers() self._checkDriverStatus() - def getDriverInfos( self ) -> list[WebDriverInfo]: @@ -302,7 +292,6 @@ class WebDriverManager: with self.__lock: return self.__driver_infos.copy() - def getDriverInfo( self, driver_type: WebDriverType @@ -315,7 +304,6 @@ class WebDriverManager: if info.driver_type == driver_type ] - def getDriverPath( self, driver_info: WebDriverInfo @@ -325,7 +313,6 @@ class WebDriverManager: return driver_info.driver_path return None - def installDriver( self, driver_info: WebDriverInfo, @@ -390,7 +377,6 @@ class WebDriverManager: driver_info.driver_status = WebDriverStatus.ERROR raise e - def cancelDriverDownload( self, driver_info: WebDriverInfo @@ -411,7 +397,6 @@ class WebDriverManager: except Exception: return False - def uninstallDriver( self, driver_info: WebDriverInfo, @@ -441,7 +426,6 @@ class WebDriverManager: driver_info.driver_status = WebDriverStatus.ERROR raise - def driverDir( self ) -> str: diff --git a/src/managers/log/LogManager.py b/src/managers/log/LogManager.py index 21d9022..767a951 100644 --- a/src/managers/log/LogManager.py +++ b/src/managers/log/LogManager.py @@ -89,7 +89,6 @@ class LogManager: self.initialize() - def initialize( self ): @@ -139,7 +138,6 @@ class LogManager: self.__initialized = True - def getLogger( self, name: Optional[str] = None @@ -149,7 +147,6 @@ class LogManager: return self.__logger.getChild(name) return self.__logger - def setLevel( self, level: int @@ -158,7 +155,6 @@ class LogManager: if self.__logger: self.__logger.setLevel(level) - def logDir( self ) -> str: @@ -171,6 +167,7 @@ _log_manager_instance = None # Singleton instance lock. _instance_lock = threading.Lock() + def instance( log_dir: str = "" ) -> LogManager: diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py index 1972349..d3a4a85 100644 --- a/src/operators/AutoLib.py +++ b/src/operators/AutoLib.py @@ -49,7 +49,6 @@ class AutoLib(MsgBase): raise Exception("浏览器驱动URL初始化失败 !") self.__initLibOperators() - def __initBrowserDriver( self ) -> bool: @@ -142,7 +141,6 @@ class AutoLib(MsgBase): self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") return True - def __initLibOperators( self ): @@ -157,7 +155,6 @@ class AutoLib(MsgBase): self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver) self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver) - def __waitResponseLoad( self ) -> bool: @@ -184,7 +181,6 @@ class AutoLib(MsgBase): self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR) return False - def __initDriverUrl( self, ) -> bool: @@ -207,7 +203,6 @@ class AutoLib(MsgBase): return False return True - def __run( self, username: str, @@ -292,7 +287,6 @@ class AutoLib(MsgBase): return -1 return result - def run( self, user_config: dict @@ -339,7 +333,6 @@ class AutoLib(MsgBase): ) return - def close( self ) -> bool: diff --git a/src/operators/LibChecker.py b/src/operators/LibChecker.py index 7a8d52d..3dc944b 100644 --- a/src/operators/LibChecker.py +++ b/src/operators/LibChecker.py @@ -33,7 +33,6 @@ class LibChecker(LibOperator): self.__driver = driver - def _waitResponseLoad( self ) -> bool: @@ -50,7 +49,6 @@ class LibChecker(LibOperator): seconds = int(seconds%60) return f"{hours} 时 {minutes} 分 {seconds} 秒" - def __navigateToReserveRecordPage( self ) -> bool: @@ -67,7 +65,6 @@ class LibChecker(LibOperator): return False return True - def __decodeReserveTime( self, time_element @@ -105,7 +102,6 @@ class LibChecker(LibOperator): } } - def __decodeReserveInfo( self, info_elements @@ -133,7 +129,6 @@ class LibChecker(LibOperator): "status": status, } - def __decodeReserveRecord( self, reservation @@ -160,7 +155,6 @@ class LibChecker(LibOperator): "info": info } - def __loadReserveRecords( self ) -> list: @@ -177,7 +171,6 @@ class LibChecker(LibOperator): self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR) return None - def __showMoreReserveRecords( self ) -> bool: @@ -203,7 +196,6 @@ class LibChecker(LibOperator): self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR) return False - def __getReserveRecord( self, wanted_date: str, @@ -253,7 +245,6 @@ class LibChecker(LibOperator): break return None - def canReserve( self, date: str @@ -270,7 +261,6 @@ class LibChecker(LibOperator): self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约") return False - def canCheckin( self ) -> bool: @@ -307,7 +297,6 @@ class LibChecker(LibOperator): self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到") return False - def canRenew( self ) -> tuple[bool, dict]: @@ -335,7 +324,6 @@ class LibChecker(LibOperator): self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") return False, None - def postRenewCheck( self, record: dict diff --git a/src/operators/LibCheckin.py b/src/operators/LibCheckin.py index beff718..5adb21e 100644 --- a/src/operators/LibCheckin.py +++ b/src/operators/LibCheckin.py @@ -31,7 +31,6 @@ class LibCheckin(LibOperator): self.__driver = driver - def _waitResponseLoad( self ) -> bool: @@ -87,7 +86,6 @@ class LibCheckin(LibOperator): ok_btn.click() return False - def __enableCheckinBtn( self ) -> bool: @@ -112,7 +110,6 @@ class LibCheckin(LibOperator): self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING) return result - def checkin( self, username: str diff --git a/src/operators/LibCheckout.py b/src/operators/LibCheckout.py index 4e8e7a8..f55ccc8 100644 --- a/src/operators/LibCheckout.py +++ b/src/operators/LibCheckout.py @@ -33,7 +33,6 @@ class LibCheckout(LibOperator): self.__driver = driver - def _waitResponseLoad( self ) -> bool: diff --git a/src/operators/LibLogin.py b/src/operators/LibLogin.py index b5d0f7b..9fd233b 100644 --- a/src/operators/LibLogin.py +++ b/src/operators/LibLogin.py @@ -34,7 +34,6 @@ class LibLogin(LibOperator): self.__driver = driver self.__ddddocr = ddddocr.DdddOcr() - def _waitResponseLoad( self ) -> bool: @@ -58,7 +57,6 @@ class LibLogin(LibOperator): ) return False - def __fillLogInElements( self, username: str, @@ -78,7 +76,6 @@ class LibLogin(LibOperator): return False return True - def __autoRecognizeCaptcha( self ) -> str: @@ -100,7 +97,6 @@ class LibLogin(LibOperator): self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) return "" - def __manualRecognizeCaptcha( self ) -> str: @@ -118,7 +114,6 @@ class LibLogin(LibOperator): self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) return "" - def __refreshCaptcha( self ): @@ -134,7 +129,6 @@ class LibLogin(LibOperator): self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR) return False - def __solveCaptcha( self, auto_captcha: bool = True @@ -158,7 +152,6 @@ class LibLogin(LibOperator): ) return "" - def __fillCaptchaElement( self, captcha_text: str @@ -173,7 +166,6 @@ class LibLogin(LibOperator): self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR) return False - def login( self, username: str, diff --git a/src/operators/LibLogout.py b/src/operators/LibLogout.py index 63cc63e..14bd02d 100644 --- a/src/operators/LibLogout.py +++ b/src/operators/LibLogout.py @@ -28,14 +28,12 @@ class LibLogout(LibOperator): self.__driver = driver - def _waitResponseLoad( self ) -> bool: return True - def logout( self, username: str diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py index 4213168..2d9eec6 100644 --- a/src/operators/LibRenew.py +++ b/src/operators/LibRenew.py @@ -14,7 +14,7 @@ from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from base.LibTimeSelector import LibTimeSelector +from operators.abs.LibTimeSelector import LibTimeSelector class LibRenew(LibTimeSelector): @@ -30,7 +30,6 @@ class LibRenew(LibTimeSelector): self.__driver = driver - def _waitResponseLoad( self ) -> bool: @@ -38,7 +37,6 @@ class LibRenew(LibTimeSelector): self.__driver.refresh() return True - def __waitRenewDialog( self ) -> bool: @@ -77,7 +75,6 @@ class LibRenew(LibTimeSelector): return False return True - def __selectNearestTime( self, record: dict, @@ -116,7 +113,6 @@ class LibRenew(LibTimeSelector): self._showTrace(f"当前可供续约的时间有: {free_times}") return False - def __validateAndAdjustRenewTime( self, end_time: str, @@ -139,7 +135,6 @@ class LibRenew(LibTimeSelector): return True return True - def __confirmRenewal( self, best_opt, @@ -167,7 +162,6 @@ class LibRenew(LibTimeSelector): self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR) return False - def renew( self, username: str, diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py index 23f84d8..930db0a 100644 --- a/src/operators/LibReserve.py +++ b/src/operators/LibReserve.py @@ -15,7 +15,7 @@ from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from base.LibTimeSelector import LibTimeSelector +from operators.abs.LibTimeSelector import LibTimeSelector class LibReserve(LibTimeSelector): @@ -48,7 +48,6 @@ class LibReserve(LibTimeSelector): "8": "五层考研" } - def _waitResponseLoad( self, ) -> bool: @@ -99,7 +98,6 @@ class LibReserve(LibTimeSelector): self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR) return False - def __containRequiredInfo( self, reserve_info: dict @@ -135,7 +133,6 @@ class LibReserve(LibTimeSelector): ) return False - def __isValidDate( self, reserve_info: dict @@ -157,7 +154,6 @@ class LibReserve(LibTimeSelector): reserve_info["date"] = cur_date_str return True - def __isValidBeginTime( self, reserve_info: dict @@ -177,7 +173,6 @@ class LibReserve(LibTimeSelector): self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True") return True - def __isValidExpectDuration( self, reserve_info: dict @@ -192,7 +187,6 @@ class LibReserve(LibTimeSelector): self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时") return True - def __isValidEndTime( self, reserve_info: dict @@ -222,7 +216,6 @@ class LibReserve(LibTimeSelector): self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True") return True - def __finalCheck( self, reserve_info: dict @@ -275,7 +268,6 @@ class LibReserve(LibTimeSelector): reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60) return True - def __checkReserveInfo( self, reserve_info: dict @@ -305,7 +297,6 @@ class LibReserve(LibTimeSelector): ) return True - def __clickElement( self, trigger_locator: tuple, @@ -330,7 +321,6 @@ class LibReserve(LibTimeSelector): self._showTrace(fail_msg) return False - def __clickElementByJS( self, trigger_locator_id: str, @@ -364,7 +354,6 @@ class LibReserve(LibTimeSelector): self._showTrace(fail_msg) return result - def __selectDate( self, date_str: str @@ -384,7 +373,6 @@ class LibReserve(LibTimeSelector): fail_msg=f"选择日期失败 ! : {date_str} 不可用" ) - def __selectPlace( self, place: str @@ -406,7 +394,6 @@ class LibReserve(LibTimeSelector): fail_msg=f"选择预约场所失败 ! : {display_place} 不可用" ) - def __selectFloor( self, floor: str @@ -427,7 +414,6 @@ class LibReserve(LibTimeSelector): fail_msg=f"选择楼层失败 ! : {display_floor} 不可用" ) - def __selectRoom( self, room: str @@ -453,7 +439,6 @@ class LibReserve(LibTimeSelector): self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) return False - def __selectSeat( self, seat_id: str @@ -492,7 +477,6 @@ class LibReserve(LibTimeSelector): self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR) return False - def __selectNearestTime( self, time_id: str, @@ -547,7 +531,6 @@ class LibReserve(LibTimeSelector): self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") return -1 - def __selectSeatTime( self, begin_time: dict, @@ -583,7 +566,7 @@ 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, expect_duration)) + exp_end_mins = int(self.__validateAndAdjustEndTime(act_beg_mins, expect_duration)) exp_end_tm_str = self._minsToTimeStr(exp_end_mins) self._showTrace( f"需要满足期望预约持续时间: {expect_duration} 小时, " @@ -607,8 +590,7 @@ class LibReserve(LibTimeSelector): ) return True - - def validateAndAdjustEndTime( + def __validateAndAdjustEndTime( self, begin_mins: int, duration: int @@ -627,7 +609,6 @@ class LibReserve(LibTimeSelector): ) return expect_end_mins - def reserve( self, username: str, diff --git a/src/base/LibTimeSelector.py b/src/operators/abs/LibTimeSelector.py similarity index 98% rename from src/base/LibTimeSelector.py rename to src/operators/abs/LibTimeSelector.py index 737a708..472d6e4 100644 --- a/src/base/LibTimeSelector.py +++ b/src/operators/abs/LibTimeSelector.py @@ -16,7 +16,7 @@ from base.LibOperator import LibOperator class LibTimeSelector(LibOperator): """ - Base class for time selection operations. + Abstract base class for time selection operations. This class provides common time selection logic for reservation and renewal operations, including time conversion utilities and best time option finding. @@ -60,7 +60,6 @@ class LibTimeSelector(LibOperator): hour, minute = divmod(int(mins), 60) return f"{hour:02d}:{minute:02d}" - def _formatTimeRelation( self, abs_diff: int, @@ -78,7 +77,6 @@ class LibTimeSelector(LibOperator): else: return f"正好等于 {time_type}" - def _findBestTimeOption( self, time_options: list, diff --git a/src/operators/abs/__init__.py b/src/operators/abs/__init__.py new file mode 100644 index 0000000..e6f47db --- /dev/null +++ b/src/operators/abs/__init__.py @@ -0,0 +1,6 @@ +""" + Abstract layer class of the LibOperator + + Here are the classes and modules in this package: + - LibTimeSelector: Abstract base class for time selection operations. +""" \ No newline at end of file diff --git a/src/utils/JSONReader.py b/src/utils/JSONReader.py index 04677a3..f8f918e 100644 --- a/src/utils/JSONReader.py +++ b/src/utils/JSONReader.py @@ -42,7 +42,6 @@ class JSONReader: self.__json_data = None self.__read() - def __read( self ): @@ -59,7 +58,6 @@ class JSONReader: except Exception as e: raise Exception(f"读取文件时发生未知错误: {e}") from e - def read( self ) -> bool: @@ -70,14 +68,12 @@ class JSONReader: return False return True - def data( self ) -> dict: return self.__json_data.copy() - def path( self ) -> str: diff --git a/src/utils/JSONWriter.py b/src/utils/JSONWriter.py index baa4d27..8767b2f 100644 --- a/src/utils/JSONWriter.py +++ b/src/utils/JSONWriter.py @@ -46,7 +46,6 @@ class JSONWriter: self.__json_data = json_data.copy() if json_data is not None else {} self.__write() - def __write( self ): @@ -63,7 +62,6 @@ class JSONWriter: except Exception as e: raise Exception(f"写入文件时发生未知错误: {e}") from e - def write( self ) -> bool: @@ -74,7 +72,6 @@ class JSONWriter: return False return True - def path( self ) -> str: From 106463b9e580f0d59e267dbdc50e8f8f86709883 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Mon, 25 May 2026 19:10:07 +0800 Subject: [PATCH 27/49] =?UTF-8?q?refactor(autoscript):=20=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E5=8C=96=20ASEngine=E3=80=81=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=97=A7=E5=8F=98=E9=87=8F=E5=AF=BC=E5=87=BA=E3=80=81=E6=B8=85?= =?UTF-8?q?=E7=90=86=E7=BC=96=E6=8E=92=E7=AA=97=E5=8F=A3=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ASEngine 转为类,目标变量注册作为 __init__ 接口,配套函数提取到 _helpers.py - Lua 函数重命名 CURRENT_DATE→datenow, CURRENT_TIME→timenow, date_add→dateadd 等 - __init__.py 移除 ALL_VARIABLES/_TARGET_VAR_DEFS/_MOCK_TYPE_VALUES 导出,替换为接口函数 - 编排窗口移除脚本→控件的反向解析逻辑,合并常量定义为查询接口 - 编辑窗口新增工具函数 Tab、Tab 键插入 4 空格、图标改用 setIcon 加载 Co-Authored-By: Claude Opus 4.7 --- src/autoscript/ASEngine.py | 594 +++++++-------------- src/autoscript/__init__.py | 82 ++- src/autoscript/_helpers.py | 153 ++++++ src/gui/ALAboutDialog.py | 5 +- src/gui/ALAutoScriptEditDialog.py | 98 ++-- src/gui/ALAutoScriptOrchDialog/_helpers.py | 355 ++---------- src/gui/ALAutoScriptOrchDialog/_widgets.py | 26 +- src/gui/ALMainWorkers.py | 6 +- src/gui/ALTimerTaskAddDialog.py | 2 +- src/gui/ALWebDriverDownloadDialog.py | 4 +- src/gui/resources/ALResource.qrc | 3 + src/gui/resources/icons/Copy.svg | 6 + src/gui/resources/icons/Reset.svg | 3 + 13 files changed, 536 insertions(+), 801 deletions(-) create mode 100644 src/autoscript/_helpers.py create mode 100644 src/gui/resources/icons/Copy.svg create mode 100644 src/gui/resources/icons/Reset.svg diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index e8623a4..7c5916b 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -14,6 +14,16 @@ from datetime import ( from lupa import LuaRuntime as _LuaRuntime +from autoscript._helpers import ( + _TYPE_DEFAULT_VAR, + _assignPath, + _checkDateFormat, + _checkTimeFormat, + _checkType, + _cleanLuaError, + _navigatePath, +) + try: from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError except ImportError: @@ -24,435 +34,229 @@ except ImportError: _LuaSyntaxError = Exception -__all__ = ["execute", "addTargetVar", "resetEngine"] +__all__ = ["ASEngine"] -# Engine state -_TARGET_VARS: dict[str, dict] = {} -_lua = None +class ASEngine: -# Built-in meta variable definitions (name / type / display-name) -META_VARS: dict[str, dict[str, str]] = { - "CURRENT_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"}, - "CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "display": "当前时间"}, -} + @staticmethod + def getCurrentDate( + ) -> str: -# Per-type fallback value when target_data entry is missing. -_DEFAULT_BY_TYPE: dict[str, str | int | float | bool] = { - "String": "", - "Int": 0, - "Float": 0.0, - "Boolean": False, -} + return date.today().isoformat() -def _getLua( -): - """ - Return the sandboxed Lua runtime singleton. - """ + @staticmethod + def getCurrentTime( + ) -> str: - global _lua - if _lua is None: - _lua = _LuaRuntime(unpack_returned_tuples = True) - _sandbox(_lua) - _registerHelpers(_lua) - return _lua + return datetime.now().strftime("%H:%M") -def _sandbox( - lua, -) -> None: - """ - Remove dangerous Lua globals while keeping os.date / os.time for date-time helpers. - """ + @staticmethod + def _sandbox( + lua, + ) -> None: - lua.execute(""" - io = nil - require = nil - dofile = nil - loadfile = nil - load = nil - package = nil - rawget = nil - rawset = nil - rawequal = nil - getfenv = nil - setfenv = nil - debug = nil - -- selectively disable dangerous os functions, keep date / time - if os then - os.execute = nil - os.exit = nil - os.getenv = nil - os.remove = nil - os.rename = nil - os.tmpname = nil - os.setlocale = nil - end - """) + lua.execute(""" + io = nil + require = nil + dofile = nil + loadfile = nil + load = nil + package = nil + rawget = nil + rawset = nil + rawequal = nil + getfenv = nil + setfenv = nil + debug = nil + if os then + os.execute = nil + os.exit = nil + os.getenv = nil + os.remove = nil + os.rename = nil + os.tmpname = nil + os.setlocale = nil + end + """) -def _registerHelpers( - lua, -) -> None: - """ - Inject Date / Time helpers as pure Lua functions. + @staticmethod + def _registerHelpers( + lua, + ) -> None: - Date values are os.time timestamps (seconds since epoch). - Time values are minutes since midnight (0-1439). + lua.execute(""" + function date(y, m, d) + return os.time({year = y, month = m, day = d}) + end - This keeps Date / Time as native Lua numbers during script execution, - enabling type-safe arithmetic (+, -) and comparisons (<, <=, ==, ~=). - """ + function time(h, m) + return h * 60 + m + end - lua.execute(""" - function date(y, m, d) - return os.time({year = y, month = m, day = d}) - end + function datenow() + local now = os.date("*t") + return os.time({year = now.year, month = now.month, day = now.day}) + end - function time(h, m) - return h * 60 + m - end + function timenow() + local now = os.date("*t") + return now.hour * 60 + now.min + end - function CURRENT_DATE() - local now = os.date("*t") - return os.time({year = now.year, month = now.month, day = now.day}) - end + function dateadd(date_val, n) + return date_val + n * 86400 + end - function CURRENT_TIME() - local now = os.date("*t") - return now.hour * 60 + now.min - end + function timeadd(time_val, n) + return (time_val + n * 60) % 1440 + end - function date_add(date_val, n) - return date_val + n * 86400 - end + function strtodate(iso_str) + local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)") + return os.time({year = y, month = m, day = d}) + end - function time_add(time_val, n) - return (time_val + n * 60) % 1440 - end + function strtotime(hm_str) + local h, m = hm_str:match("(%d+):(%d+)") + return h * 60 + m + end - -- push helpers: string -> native type - function _to_date(iso_str) - local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)") - return os.time({year = y, month = m, day = d}) - end + function datetostr(ts) + return os.date("%Y-%m-%d", ts) + end - function _to_time(hm_str) - local h, m = hm_str:match("(%d+):(%d+)") - return h * 60 + m - end + function timetostr(m) + return string.format("%02d:%02d", math.floor(m / 60), m % 60) + end + """) - -- pull helpers: native type -> string - function _from_date(ts) - return os.date("%Y-%m-%d", ts) - end + def __init__( + self, + targetVars: list[tuple] = None, + ): - function _from_time(m) - return string.format("%02d:%02d", math.floor(m / 60), m % 60) - end - """) + self._targetVars: dict[str, dict] = {} + self._lua = None -def _navigatePath( - data: dict, - key_path: list, - default = None, -): - """ - Walk *key_path* into *data* and return the value at the leaf. - """ + if targetVars: + for item in targetVars: + name, varType, keyPath = item[0], item[1], item[2] + self.addTargetVar(name, varType, keyPath) - d = data - for key in key_path[:-1]: - d = d.get(key, {}) - if not isinstance(d, dict): - return default - return d.get(key_path[-1], default) + def _getLua( + self, + ): -def _assignPath( - data: dict, - key_path: list, - value, -) -> None: - """ - Walk *key_path* into *data* and set *value* at the leaf. - """ + if self._lua is None: + self._lua = _LuaRuntime(unpack_returned_tuples=True) + self._sandbox(self._lua) + self._registerHelpers(self._lua) + return self._lua - d = data - for key in key_path[:-1]: - d = d.setdefault(key, {}) - d[key_path[-1]] = value + def _push( + self, + targetData: dict, + ) -> None: -def _pyTypeToASType( - value -) -> str: - """ - Map a Python runtime value to its AutoScript type name. - """ + lua = self._getLua() + g = lua.globals() + strToDate = g["strtodate"] + strToTime = g["strtotime"] - if isinstance(value, bool): - return "Boolean" - if isinstance(value, int): - return "Int" - if isinstance(value, float): - return "Float" - if isinstance(value, str): - return "String" - return "Unknown" + for varName, info in self._targetVars.items(): + keyPath = info["keyPath"] + vt = info["type"] + raw = _navigatePath(targetData, keyPath) + if vt == "Date": + if not isinstance(raw, str) or not raw.strip(): + raise ValueError( + f"Date 类型变量 '{varName}' 对应的数据为空或不是字符串类型," + f"请检查路径 {keyPath} 的值是否为合法的日期字符串 (YYYY-MM-DD)" + ) + raw = raw.strip() + _checkDateFormat(raw, varName) + g[varName] = strToDate(raw) + elif vt == "Time": + if not isinstance(raw, str) or not raw.strip(): + raise ValueError( + f"Time 类型变量 '{varName}' 对应的数据为空或不是字符串类型," + f"请检查路径 {keyPath} 的值是否为合法的时间字符串 (HH:MM)" + ) + raw = raw.strip() + _checkTimeFormat(raw, varName) + g[varName] = strToTime(raw) + else: + if raw is None: + raw = _TYPE_DEFAULT_VAR.get(vt, False) + g[varName] = raw -def _checkDateFormat( - date_str: str, - var_name: str = "", -) -> None: - """ - Validate that *date_str* is in YYYY-MM-DD format. - Raises ValueError with a descriptive message on failure. - """ + def _pull( + self, + targetData: dict, + ) -> None: - prefix = f"Date 类型变量 '{var_name}' 的" if var_name else "" - try: - date.fromisoformat(date_str) - except ValueError: - raise ValueError( - f"{prefix}值 '{date_str}' 不是合法的日期格式," - f"应为 YYYY-MM-DD" - ) + lua = self._getLua() + g = lua.globals() + dateToStr = g["datetostr"] + timeToStr = g["timetostr"] -def _checkTimeFormat( - time_str: str, - var_name: str = "", -) -> None: - """ - Validate that *time_str* is in HH:MM format. - Raises ValueError with a descriptive message on failure. - """ + for varName, info in self._targetVars.items(): + try: + luaVal = g[varName] + except KeyError: + continue + vt = info["type"] + if vt == "Date": + luaVal = dateToStr(luaVal) + elif vt == "Time": + luaVal = timeToStr(luaVal) + elif vt == "Float" and isinstance(luaVal, int) and not isinstance(luaVal, bool): + luaVal = float(luaVal) + _checkType(varName, vt, luaVal) + _assignPath(targetData, info["keyPath"], luaVal) - prefix = f"Time 类型变量 '{var_name}' 的" if var_name else "" - try: - datetime.strptime(time_str, "%H:%M") - except ValueError: - raise ValueError( - f"{prefix}值 '{time_str}' 不是合法的时间格式," - f"应为 HH:MM" - ) + def addTargetVar( + self, + name: str, + varType: str, + keyPath: list, + ) -> None: -def _checkType( - var_name: str, - var_type: str, - value, -) -> None: - """ - Validate that *value* matches the declared variable type. + upperName = name.upper().strip() + self._targetVars[upperName] = { + "type": varType, + "keyPath": keyPath, + } - Date / Time values arrive as ISO / HH:MM strings (already converted - from Lua native types during the pull phase). - Int / Float / Boolean / String check Python type identity. - Int -> Float widening is allowed. - """ + def execute( + self, + scriptText: str, + targetData: dict, + ) -> None: - if var_type == "Date": - if not isinstance(value, str): - raise ValueError( - f"Date 类型变量 '{var_name}' 只能接受日期字符串," - f"不能接受 {_pyTypeToASType(value)} 类型" - ) - _checkDateFormat(value, var_name) - return - if var_type == "Time": - if not isinstance(value, str): - raise ValueError( - f"Time 类型变量 '{var_name}' 只能接受时间字符串," - f"不能接受 {_pyTypeToASType(value)} 类型" - ) - _checkTimeFormat(value, var_name) - return - if var_type == "Int": - if isinstance(value, bool): - raise ValueError( - f"Int 类型变量 '{var_name}' 不能接受 Boolean 类型的值" - ) - if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)): - raise ValueError( - f"Int 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" - ) - return - if var_type == "Float": - if isinstance(value, bool): - raise ValueError( - f"Float 类型变量 '{var_name}' 不能接受 Boolean 类型的值" - ) - if not isinstance(value, (int, float)): - raise ValueError( - f"Float 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" - ) - return - if var_type == "Boolean": - if not isinstance(value, bool): - raise ValueError( - f"Boolean 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" - ) - return - if var_type == "String": - if not isinstance(value, str): - raise ValueError( - f"String 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值" - ) - return - -def addTargetVar( - name: str, - var_type: str, - key_path: list, - _display_name: str = None, -) -> None: - """ - Register a new target variable bound to a path in the application data dict. - - Args: - name (str): The canonical variable name (e.g. "RESERVE_DATE"). - var_type (str): "Int" | "Float" | "Boolean" | "Date" | "Time" | "String". - key_path (list): Nested path into target_data, e.g. ["reserve_info", "date"]. - """ - - upper_name = name.upper().strip() - _TARGET_VARS[upper_name] = { - "type": var_type, - "key_path": key_path, - } - -def resetEngine( -) -> None: - """ - Reset the engine to its initial state: clear all target variables - and release the Lua runtime. - """ - - global _TARGET_VARS, _lua - _TARGET_VARS = {} - _lua = None - -def _push( - target_data: dict, -) -> None: - """ - Push target_data values into Lua globals. - Date / Time strings are converted to native Lua types (timestamp / minutes). - - Raises ValueError for missing / malformed Date or Time values so that - execute() can surface them as user-visible AutoScript execution errors. - """ - - lua = _getLua() - g = lua.globals() - _toDate = g["_to_date"] - _toTime = g["_to_time"] - - for var_name, info in _TARGET_VARS.items(): - key_path = info["key_path"] - vt = info["type"] - raw = _navigatePath(target_data, key_path) - if vt == "Date": - if not isinstance(raw, str) or not raw.strip(): - raise ValueError( - f"Date 类型变量 '{var_name}' 对应的数据为空或不是字符串类型," - f"请检查路径 {key_path} 的值是否为合法的日期字符串 (YYYY-MM-DD)" - ) - raw = raw.strip() - _checkDateFormat(raw, var_name) - g[var_name] = _toDate(raw) - elif vt == "Time": - if not isinstance(raw, str) or not raw.strip(): - raise ValueError( - f"Time 类型变量 '{var_name}' 对应的数据为空或不是字符串类型," - f"请检查路径 {key_path} 的值是否为合法的时间字符串 (HH:MM)" - ) - raw = raw.strip() - _checkTimeFormat(raw, var_name) - g[var_name] = _toTime(raw) - else: - if raw is None: - raw = _DEFAULT_BY_TYPE.get(vt, False) - g[var_name] = raw - -def _pull( - target_data: dict, -) -> None: - """ - Pull Lua global values back into target_data. - Date / Time native types are converted back to ISO / HH:MM strings. - """ - - lua = _getLua() - g = lua.globals() - _fromDate = g["_from_date"] - _fromTime = g["_from_time"] - - for var_name, info in _TARGET_VARS.items(): + if not scriptText or not scriptText.strip(): + return try: - lua_val = g[var_name] - except KeyError: - continue - vt = info["type"] - if vt == "Date": - lua_val = _fromDate(lua_val) - elif vt == "Time": - lua_val = _fromTime(lua_val) - elif vt == "Float" and isinstance(lua_val, int) and not isinstance(lua_val, bool): - lua_val = float(lua_val) - _checkType(var_name, vt, lua_val) - _assignPath(target_data, info["key_path"], lua_val) + self._push(targetData) + self._getLua().execute(scriptText) + self._pull(targetData) + except _LuaSyntaxError as e: + raise ValueError( + f"AutoScript 语法错误: {_cleanLuaError(str(e))}" + ) + except _LuaError as e: + raise ValueError( + f"AutoScript 运行时错误: {_cleanLuaError(str(e))}" + ) + except ValueError as e: + raise ValueError(f"AutoScript 数据错误: {e}") + except Exception as e: + raise ValueError(f"AutoScript 未知错误: {e}") -def _cleanLuaError( - raw_msg: str -) -> str: - """ - Strip internal source prefix and stack traceback from a Lua error message. - """ + def reset( + self, + ) -> None: - msg = raw_msg.replace('[string ""]:', "").strip() - stack_idx = msg.find("stack traceback:") - if stack_idx != -1: - msg = msg[:stack_idx].strip() - return msg - -def execute( - script_text: str, - target_data: dict, -) -> None: - """ - Execute an AutoScript (Lua) on the given target data. - - The script runs in a sandboxed Lua environment with target variables - exposed as globals. The following helpers are available as Lua functions: - - date(y, m, d) -> timestamp (os.time seconds) - time(h, m) -> minutes since midnight (0-1439) - CURRENT_DATE() -> today's timestamp - CURRENT_TIME() -> current minutes since midnight - date_add(ts, n) -> ts + n * 86400 - time_add(m, n) -> (m + n * 60) % 1440 - - Date and Time values are native Lua numbers during execution. - Arithmetic (+, -) and comparisons (<, <=, ==, ~=, >, >=) work - with strong type safety — no implicit string coercion. - - Raises: - ValueError: On Lua compilation/runtime errors or type mismatches. - """ - - if not script_text or not script_text.strip(): - return - try: - _push(target_data) - _getLua().execute(script_text) - _pull(target_data) - except _LuaSyntaxError as e: - raise ValueError( - f"AutoScript 语法错误: {_cleanLuaError(str(e))}" - ) - except _LuaError as e: - raise ValueError( - f"AutoScript 运行时错误: {_cleanLuaError(str(e))}" - ) - except ValueError as e: - raise ValueError(f"AutoScript 数据错误: {e}") - except Exception as e: - raise ValueError(f"AutoScript 未知错误: {e}") + self._targetVars = {} + self._lua = None diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index 2315f78..a3397b9 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -7,45 +7,25 @@ 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. """ -from autoscript.ASEngine import ( - execute, - addTargetVar, - resetEngine, - META_VARS, -) +from autoscript.ASEngine import ASEngine __all__ = [ - "execute", - "addTargetVar", - "resetEngine", - "registerDefaultTargetVars", - "buildMockTargetData", - "META_VARS", - "ALL_VARIABLES", - "_TARGET_VAR_DEFS", - "_MOCK_TYPE_VALUES", + "ASEngine", + "createEngine", + "createMockTargetData", + "createAllVariablesTable", + "createTargetVarDefs", ] -# Key paths into target_data dict for each target variable. -# (name, type, key_path, display_name) _TARGET_VAR_DEFS = [ - ("USERNAME", "String", ["username"], "用户名"), - ("USER_ENABLE", "Boolean",["enabled"], "用户启用"), - ("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"), - ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), - ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), + ("USERNAME", "String", ["username"], "用户名"), + ("USER_ENABLE", "Boolean", ["enabled"], "用户启用"), + ("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"), + ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), + ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), ] - -# All variables (display_name -> (name, type)), derived from target vars + meta vars. -ALL_VARIABLES = { - display_name: (name, var_type) - for name, var_type, _, display_name in _TARGET_VAR_DEFS -} | { - v["display"]: (v["name"], v["type"]) - for v in META_VARS.values() -} _MOCK_TYPE_VALUES = { "String": "__mock__", "Boolean": True, @@ -55,26 +35,32 @@ _MOCK_TYPE_VALUES = { "Float": 0.0, } -def buildMockTargetData( + +def createAllVariablesTable( ) -> dict: - """ - Build a target_data dict filled with type-appropriate mock values - for all registered target variables. - """ + + return { + displayName: (name, varType) + for name, varType, _, displayName in _TARGET_VAR_DEFS + } + +def createTargetVarDefs( +) -> list: + + return list(_TARGET_VAR_DEFS) + +def createMockTargetData( +) -> dict: + data = {} - for _, var_type, key_path, _ in _TARGET_VAR_DEFS: + for _, varType, keyPath, _ in _TARGET_VAR_DEFS: d = data - for key in key_path[:-1]: + for key in keyPath[:-1]: d = d.setdefault(key, {}) - d[key_path[-1]] = _MOCK_TYPE_VALUES.get(var_type, "") + d[keyPath[-1]] = _MOCK_TYPE_VALUES.get(varType, "") return data -def registerDefaultTargetVars( -) -> None: - """ - Register all built-in target variables with the engine. - This must be called before any script execution. - Calling multiple times is idempotent (re-registers same keys). - """ - for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: - addTargetVar(name, var_type, key_path, display_name) +def createEngine( +) -> ASEngine: + + return ASEngine(_TARGET_VAR_DEFS) diff --git a/src/autoscript/_helpers.py b/src/autoscript/_helpers.py new file mode 100644 index 0000000..2b7fc27 --- /dev/null +++ b/src/autoscript/_helpers.py @@ -0,0 +1,153 @@ +# -*- 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. +""" +from datetime import ( + date, + datetime, +) + + +_TYPE_DEFAULT_VAR: dict[str, str | int | float | bool] = { + "String": "", + "Int": 0, + "Float": 0.0, + "Boolean": False, +} + + +def _navigatePath( + data: dict, + keyPath: list, + default=None, +): + + d = data + for key in keyPath[:-1]: + d = d.get(key, {}) + if not isinstance(d, dict): + return default + return d.get(keyPath[-1], default) + +def _assignPath( + data: dict, + keyPath: list, + value, +) -> None: + + d = data + for key in keyPath[:-1]: + d = d.setdefault(key, {}) + d[keyPath[-1]] = value + +def _checkDateFormat( + dateStr: str, + varName: str = "", +) -> None: + + prefix = f"Date 类型变量 '{varName}' 的" if varName else "" + try: + date.fromisoformat(dateStr) + except ValueError: + raise ValueError( + f"{prefix}值 '{dateStr}' 不是合法的日期格式," + f"应为 YYYY-MM-DD" + ) + +def _checkTimeFormat( + timeStr: str, + varName: str = "", +) -> None: + + prefix = f"Time 类型变量 '{varName}' 的" if varName else "" + try: + datetime.strptime(timeStr, "%H:%M") + except ValueError: + raise ValueError( + f"{prefix}值 '{timeStr}' 不是合法的时间格式," + f"应为 HH:MM" + ) + +def _checkType( + varName: str, + varType: str, + value, +) -> None: + + if varType == "Date": + if not isinstance(value, str): + raise ValueError( + f"Date 类型变量 '{varName}' 只能接受日期字符串," + f"不能接受 {_pyTypeToASType(value)} 类型" + ) + _checkDateFormat(value, varName) + return + if varType == "Time": + if not isinstance(value, str): + raise ValueError( + f"Time 类型变量 '{varName}' 只能接受时间字符串," + f"不能接受 {_pyTypeToASType(value)} 类型" + ) + _checkTimeFormat(value, varName) + return + if varType == "Int": + if isinstance(value, bool): + raise ValueError( + f"Int 类型变量 '{varName}' 不能接受 Boolean 类型的值" + ) + if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)): + raise ValueError( + f"Int 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值" + ) + return + if varType == "Float": + if isinstance(value, bool): + raise ValueError( + f"Float 类型变量 '{varName}' 不能接受 Boolean 类型的值" + ) + if not isinstance(value, (int, float)): + raise ValueError( + f"Float 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值" + ) + return + if varType == "Boolean": + if not isinstance(value, bool): + raise ValueError( + f"Boolean 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值" + ) + return + if varType == "String": + if not isinstance(value, str): + raise ValueError( + f"String 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值" + ) + return + +def _pyTypeToASType( + value, +) -> str: + + if isinstance(value, bool): + return "Boolean" + if isinstance(value, int): + return "Int" + if isinstance(value, float): + return "Float" + if isinstance(value, str): + return "String" + return "Unknown" + +def _cleanLuaError( + rawMsg: str, +) -> str: + + msg = rawMsg.replace('[string ""]:', "").strip() + stackIdx = msg.find("stack traceback:") + if stackIdx != -1: + msg = msg[:stackIdx].strip() + return msg diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 4441403..94c9c7d 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -13,10 +13,7 @@ from PySide6.QtCore import ( Qt, QTimer ) -from PySide6.QtGui import ( - QFont, - QIcon -) +from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QApplication, QDialog diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 86f5a03..82b7fe9 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -9,10 +9,18 @@ See the LICENSE file for details. """ from copy import deepcopy -from PySide6.QtCore import QDate, Qt, QTime, QTimer, Slot +from PySide6.QtCore import ( + QDate, + QSize, + Qt, + QTime, + QTimer, + Slot +) from PySide6.QtGui import ( QColor, QFont, + QIcon, QSyntaxHighlighter, QTextCharFormat, ) @@ -46,11 +54,10 @@ from PySide6.QtWidgets import ( ) from autoscript import ( - ALL_VARIABLES, - _MOCK_TYPE_VALUES, - _TARGET_VAR_DEFS, - execute, - registerDefaultTargetVars, + createAllVariablesTable, + createMockTargetData, + createTargetVarDefs, + createEngine, ) @@ -94,12 +101,12 @@ class ALScriptHighlighter(QSyntaxHighlighter): funcFmt = QTextCharFormat() funcFmt.setForeground(QColor("#DCDCAA")) funcFmt.setFontWeight(QFont.Weight.Normal) - for fn in ["CURRENT_DATE", "CURRENT_TIME", "date_add", "time_add"]: + for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]: self._rules.append((r"\b" + fn + r"\b", funcFmt)) varFmt = QTextCharFormat() varFmt.setForeground(QColor("#9CDCFE")) varFmt.setFontWeight(QFont.Weight.Normal) - var_names = [name for _, (name, _) in ALL_VARIABLES.items()] + var_names = [name for _, (name, _) in createAllVariablesTable().items()] for var in var_names: self._rules.append((r"\b" + var + r"\b", varFmt)) strFmt = QTextCharFormat() @@ -158,6 +165,19 @@ class _DebugResultDialog(QDialog): layout.addWidget(btnBox) +class _TabToSpacesEditor(QPlainTextEdit): + + def keyPressEvent( + self, + event + ): + + if event.key() == Qt.Key.Key_Tab: + self.insertPlainText(" ") + return + super().keyPressEvent(event) + + class ALAutoScriptEditDialog(QDialog): def __init__( @@ -194,11 +214,9 @@ class ALAutoScriptEditDialog(QDialog): self.zoomInBtn.setFixedSize(25, 25) self.zoomOutBtn = QPushButton("-") self.zoomOutBtn.setFixedSize(25, 25) - self.zoomResetBtn = QPushButton( - QApplication.style().standardIcon( - QStyle.StandardPixmap.SP_BrowserReload - ), "" - ) + self.zoomResetBtn = QPushButton("") + self.zoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg")) + self.zoomResetBtn.setIconSize(QSize(20, 20)) self.zoomResetBtn.setFixedSize(25, 25) self.zoomResetBtn.setToolTip("重置缩放") self.zoomLabel = QLabel(f"{self._fontSize}px") @@ -221,16 +239,15 @@ class ALAutoScriptEditDialog(QDialog): toolbarLayout.addWidget(self.zoomResetBtn) toolbarLayout.addWidget(self.zoomLabel) toolbarLayout.addStretch() - self.copyBtn = QPushButton( - QApplication.style().standardIcon( - QStyle.StandardPixmap.SP_FileDialogDetailedView - ), "" - ) + self.copyBtn = QPushButton("") + self.copyBtn.setIcon(QIcon(":/res/icons/Copy.svg")) + self.copyBtn.setIconSize(QSize(20, 20)) self.copyBtn.setFixedSize(25, 25) self.copyBtn.setToolTip("复制脚本") toolbarLayout.addWidget(self.copyBtn) layout.addLayout(toolbarLayout) - self.textEdit = QPlainTextEdit(self) + self.textEdit = _TabToSpacesEditor(self) + self.textEdit.setTabStopDistance(40) self.textEdit.setLineWrapMode( QPlainTextEdit.LineWrapMode.NoWrap ) @@ -329,10 +346,30 @@ class ALAutoScriptEditDialog(QDialog): varLayout.setContentsMargins(4, 4, 4, 4) varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) varButtons = [ - (display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() + (display_name, name) for display_name, (name, _) in createAllVariablesTable().items() ] self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3) tabWidget.addTab(varWidget, "变量") + funcWidget = QWidget() + funcLayout = QGridLayout(funcWidget) + funcLayout.setSpacing(4) + funcLayout.setContentsMargins(4, 4, 4, 4) + funcLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + funcButtons = [ + ("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"), + ("timenow()", "timenow()", "返回当前时间在一天中的分钟数"), + ("dateadd(d, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"), + ("timeadd(t, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), + ] + for i, (text, template, tooltip) in enumerate(funcButtons): + btn = QPushButton(text) + btn.setProperty("template", template) + btn.clicked.connect(self.insertTemplate) + btn.setFixedWidth(100) + btn.setFixedHeight(25) + btn.setToolTip(tooltip) + funcLayout.addWidget(btn, i // 2, i % 2) + tabWidget.addTab(funcWidget, "工具函数") mockPanel = self.createMockPanel() mockPanel.setMinimumWidth(260) splitter.addWidget(tabWidget) @@ -376,8 +413,12 @@ class ALAutoScriptEditDialog(QDialog): form.setSpacing(4) form.setContentsMargins(5, 10, 5, 5) self._mockWidgets = {} - for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: - default = _MOCK_TYPE_VALUES.get(var_type, "") + mockData = createMockTargetData() + for name, var_type, key_path, display_name in createTargetVarDefs(): + d = mockData + for key in key_path: + d = d[key] + default = d widget = self.makeMockInput(var_type, default) label = QLabel(f"{display_name}: {name}({var_type})") form.addRow(label, widget) @@ -432,7 +473,7 @@ class ALAutoScriptEditDialog(QDialog): ) -> dict: data = {} - for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + for name, var_type, key_path, display_name in createTargetVarDefs(): widget, _, _ = self._mockWidgets[name] value = self.getMockValue(widget, var_type) d = data @@ -448,7 +489,7 @@ class ALAutoScriptEditDialog(QDialog): if not data: return - for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + for name, var_type, key_path, display_name in createTargetVarDefs(): d = data try: for key in key_path: @@ -572,11 +613,8 @@ class ALAutoScriptEditDialog(QDialog): clipboard = QApplication.clipboard() clipboard.setText(self.textEdit.toPlainText()) - original = self.copyBtn.text() - self.copyBtn.setText("已复制") self.copyBtn.setEnabled(False) QTimer.singleShot(2000, lambda: ( - self.copyBtn.setText(original), self.copyBtn.setEnabled(True) )) @@ -606,13 +644,13 @@ class ALAutoScriptEditDialog(QDialog): target_data = self.getMockData() before = deepcopy(target_data) try: - registerDefaultTargetVars() - execute(script, target_data) + engine = createEngine() + engine.execute(script, target_data) except ValueError as e: QMessageBox.warning(self, "运行错误", str(e)) return changes = [] - for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: + for name, var_type, key_path, display_name in createTargetVarDefs(): before_val = before after_val = target_data try: diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index bec9ee6..e287560 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -12,11 +12,7 @@ See the LICENSE file for details. """ import re -from PySide6.QtCore import ( - QObject, - QDate, - QTime -) +from PySide6.QtCore import QObject from PySide6.QtWidgets import ( QComboBox, QDateEdit, @@ -31,61 +27,66 @@ from PySide6.QtWidgets import ( QWidget, ) -from autoscript import ( - ALL_VARIABLES, -) +from autoscript import createAllVariablesTable -# Types that support arithmetic operations (add/sub) -ARITH_TYPES = {"Date", "Time", "Int", "Float"} -VAR_TYPE_ORDER = [ - "String", - "Int", - "Float", - "Boolean", - "Date", - "Time" +VARTYPE_INFOS = [ + # varType, isArithType + ("String", False), + ("Int", True), + ("Float", True), + ("Boolean", False), + ("Date", True), + ("Time", True), ] -PRESET_VARIABLES = [ - { - "name": name.upper(), - "type": vtype, - "display": display - } - for display, (name, vtype) in ALL_VARIABLES.items() + + +def getTypeOrder( +) -> list: + + return [t for t, _ in VARTYPE_INFOS] + +def getArithType( + varType: str +) -> bool: + + for t, a in VARTYPE_INFOS: + if t == varType: + return a + +def getPresetVars( +) -> list: + + return [ + {"name": name.upper(), "type": vtype, "display": display} + for display, (name, vtype) in createAllVariablesTable().items() + ] + + +COMPARE_OPTIONS = [ + ("等于", "=="), + ("不等于", "~="), + ("大于", ">"), + ("小于", "<"), + ("大于等于", ">="), + ("小于等于", "<="), ] -PRESET_NAMES = { - p["name"] for p in PRESET_VARIABLES -} -# Operator display names (UI-specific), using Lua operator symbols -_COMPARE_DISPLAY_MAP = { - "==": "等于", - "~=": "不等于", - ">": "大于", - "<": "小于", - ">=": "大于等于", - "<=": "小于等于", -} -COMPARE_OPERATORS = sorted( - [(name, op) for op, name in _COMPARE_DISPLAY_MAP.items()], - key=lambda x: len(x[1]), reverse=True -) -LOGIC_OPERATORS = [ +LOGIC_OPTIONS = [ ("并且 (and)", "and"), ("或者 (or)", "or"), ] -ACTION_TYPES = [ +ACTION_OPTIONS = [ ("设置为", "set"), ("增加", "add"), ("减少", "sub"), ] -DATE_RELATIVE_OPTIONS = [ +DATE_OPTIONS = [ ("前天", "day_before_yesterday"), ("昨天", "yesterday"), ("今天", "today"), ("明天", "tomorrow"), ("后天", "day_after_tomorrow") ] -DATE_OFFSET_UNITS = [ +DATE_OFFSET_OPTIONS = [ ("天", "days"), ("周", "weeks"), # NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years, @@ -103,7 +104,6 @@ class _DateInputContainer(QWidget): ): super().__init__(parent) - self._dynamicItems = {} # index -> raw expression, for one-way parsed items self.setupUi() def setupUi( @@ -119,7 +119,7 @@ class _DateInputContainer(QWidget): self._modeCombo.setFixedHeight(25) self._stack = QStackedWidget(self) self._relCombo = QComboBox(self) - for display, data in DATE_RELATIVE_OPTIONS: + for display, data in DATE_OPTIONS: self._relCombo.addItem(display, data) self._relCombo.setFixedHeight(25) self._stack.addWidget(self._relCombo) @@ -135,69 +135,15 @@ class _DateInputContainer(QWidget): layout.addWidget(self._stack) layout.addStretch() - _RE_DATE_ADD_CURRENT = re.compile( - r'^date_add\(CURRENT_DATE\(\),\s*(-?\d+)\)$', re.IGNORECASE - ) - def getValue( self ) -> str: mode = self._modeCombo.currentData() if mode == "relative": - idx = self._relCombo.currentIndex() - if idx in self._dynamicItems: - return self._dynamicItems[idx] return self._relCombo.currentText() return self._dateEdit.date().toString("yyyy-MM-dd") - def setValue( - self, - expr: str - ): - - s = expr.strip() - up = s.upper() - if up == "CURRENT_DATE()": - self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(2) - return - m_add = self._RE_DATE_ADD_CURRENT.match(up) - if m_add: - n = int(m_add.group(1)) - _OFFSET_IDX = {-2: 0, -1: 1, 0: 2, 1: 3, 2: 4} - idx = _OFFSET_IDX.get(n) - if idx is not None: - self._modeCombo.setCurrentIndex(0) - self._relCombo.setCurrentIndex(idx) - return - label = f"{n}天后" if n >= 0 else f"{-n}天前" - raw = f"CURRENT_DATE {'+' if n >= 0 else '-'} {abs(n)}" - self._modeCombo.setCurrentIndex(0) - for ci in range(self._relCombo.count()): - if ci in self._dynamicItems and self._dynamicItems[ci] == raw: - self._relCombo.setCurrentIndex(ci) - return - idx = self._relCombo.count() - self._relCombo.addItem(label) - self._dynamicItems[idx] = raw - self._relCombo.setCurrentIndex(idx) - return - m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up) - if m_date_ctor: - self._modeCombo.setCurrentIndex(1) - self._dateEdit.setDate(QDate( - int(m_date_ctor.group(1)), - int(m_date_ctor.group(2)), - int(m_date_ctor.group(3)), - )) - return - m_date = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s) - if m_date: - self._modeCombo.setCurrentIndex(1) - parts = m_date.group(1).split("-") - self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) - class _TimeInputContainer(QWidget): @@ -221,25 +167,6 @@ class _TimeInputContainer(QWidget): return self._timeEdit.time().toString("HH:mm") - def setValue( - self, - expr: str - ): - - s = expr.strip() - up = s.upper() - m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up) - if m_time_ctor: - self._timeEdit.setTime(QTime( - int(m_time_ctor.group(1)), - int(m_time_ctor.group(2)), - )) - return - m = re.match(r'^"(\d{1,2}:\d{2})"$', s) - if m: - parts = m.group(1).split(":") - self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1]))) - class _DateOffsetContainer(QWidget): @@ -253,7 +180,7 @@ class _DateOffsetContainer(QWidget): self._spinBox.setRange(0, 99999) self._spinBox.setFixedHeight(25) self._unitCombo = QComboBox(self) - for display, data in DATE_OFFSET_UNITS: + for display, data in DATE_OFFSET_OPTIONS: self._unitCombo.addItem(display, data) self._unitCombo.setFixedHeight(25) @@ -270,17 +197,6 @@ class _DateOffsetContainer(QWidget): return str(self.getOffsetDays()) - def setValue( - self, - expr: str - ): - - s = expr.strip().lstrip("+") - try: - self._spinBox.setValue(int(s)) - except ValueError: - pass - def getOffsetDays( self ) -> int: @@ -295,12 +211,6 @@ class _DateOffsetContainer(QWidget): return val * 365 return val - def getRawValue( - self - ) -> str: - - return str(self._spinBox.value()) - class _TimeOffsetContainer(QWidget): @@ -325,29 +235,12 @@ class _TimeOffsetContainer(QWidget): return str(self.getOffsetHours()) - def setValue( - self, - expr: str - ): - - s = expr.strip().lstrip("+") - try: - self._spinBox.setValue(int(s)) - except ValueError: - pass - def getOffsetHours( self ) -> int: return self._spinBox.value() - def getRawValue( - self - ) -> str: - - return str(self._spinBox.value()) - class VariableManager(QObject): @@ -360,13 +253,13 @@ class VariableManager(QObject): self._vars = [] self._nameMap = {} - self._initPresetVars() + self.initPresetVars() - def _initPresetVars( + def initPresetVars( self ): - for p in PRESET_VARIABLES: + for p in getPresetVars(): entry = {"name": p["name"], "type": p["type"], "display": p["display"]} self._vars.append(entry) self._nameMap[p["name"]] = entry @@ -399,19 +292,6 @@ class VariableManager(QObject): break combo.blockSignals(False) - def findExactNameEntry( - self, - combo: QComboBox, - name: str - ) -> int: - - name = name.upper().strip() - for i in range(combo.count()): - d = combo.itemData(i) - if d and len(d) >= 1 and d[0].upper().strip() == name: - return i - return -1 - def makeValueWidget( var_type: str, @@ -535,68 +415,6 @@ def getValueFromWidget( return w.text() return "" -def setValueToWidget( - w: QWidget, - var_type: str, - expr: str -): - """ - Set a widget's value from a Lua script expression. - """ - - if hasattr(w, "setValue"): - w.setValue(expr) - return - s = expr.strip() - up = s.upper() - if isinstance(w, QTimeEdit): - m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up) - if m_time_ctor: - w.setTime(QTime(int(m_time_ctor.group(1)), int(m_time_ctor.group(2)))) - else: - m = re.match(r'^"(\d{1,2}:\d{2})"$', s) - if m: - parts = m.group(1).split(":") - w.setTime(QTime(int(parts[0]), int(parts[1]))) - elif isinstance(w, QDateEdit): - m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up) - if m_date_ctor: - w.setDate(QDate( - int(m_date_ctor.group(1)), - int(m_date_ctor.group(2)), - int(m_date_ctor.group(3)), - )) - else: - m = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s) - if m: - parts = m.group(1).split("-") - w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) - elif isinstance(w, QComboBox): - for i in range(w.count()): - d = w.itemData(i) - if d is not None: - if str(d).upper() == up: - w.setCurrentIndex(i) - return - if w.itemText(i).upper() == up: - w.setCurrentIndex(i) - return - elif isinstance(w, QSpinBox): - try: - w.setValue(int(expr)) - except ValueError: - pass - elif isinstance(w, QDoubleSpinBox): - try: - w.setValue(float(expr)) - except ValueError: - pass - elif isinstance(w, QLineEdit): - inner = expr.strip() - if inner.startswith('"') and inner.endswith('"'): - inner = inner[1:-1].replace('\\"', '"') - w.setText(inner) - def encodeValueStr( raw_value: str, var_type: str @@ -683,23 +501,6 @@ def encodeDateOrTime( return s return f'"{s}"' -def stripOuterParens( - s: str -) -> str: - - s = s.strip() - if s.startswith("(") and s.endswith(")"): - depth = 0 - for i, ch in enumerate(s): - if ch == "(": - depth += 1 - elif ch == ")": - depth -= 1 - if depth == 0 and i < len(s) - 1: - return s - return s[1:-1].strip() - return s - # Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B) _RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$') _RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$') @@ -713,59 +514,3 @@ def isArithExpr( s = expr.strip() return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s)) - -def isVarReference( - expr: str -) -> bool: - """ - Return True if *expr* looks like a variable name reference - (as opposed to a literal value or function call). - """ - - s = expr.strip() - up = s.upper() - if up in ("TRUE", "FALSE"): - return False - if re.match(r"^DATE\(|^TIME\(|^DATE_ADD\(|^TIME_ADD\(|^CURRENT_DATE\(|^CURRENT_TIME\(|^CURRENT_", up): - return False - if up.startswith('"') or up.startswith("'"): - return False - if re.match(r"^[+-]?\d", s): - return False - if isArithExpr(s): - return False - return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up)) - -def isInsideLiteral( - text: str, - pos: int -) -> bool: - - in_single = False - in_double = False - for i, ch in enumerate(text): - if i >= pos: - break - if ch == "'" and not in_double: - in_single = not in_single - elif ch == '"' and not in_single: - in_double = not in_double - return in_single or in_double - -def findOperatorIn( - text: str, - operators: list -) -> tuple[int, str] | None: - - for op in operators: - op_upper = op.upper() - start = 0 - while True: - idx = text.upper().find(op_upper, start) - if idx < 0: - break - if isInsideLiteral(text, idx): - start = idx + 1 - continue - return (idx, op) - return None diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index daa5710..f29f82f 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -22,14 +22,14 @@ from PySide6.QtWidgets import ( ) from gui.ALAutoScriptOrchDialog._helpers import ( - ACTION_TYPES, - ARITH_TYPES, - COMPARE_OPERATORS, - LOGIC_OPERATORS, - PRESET_VARIABLES, - VAR_TYPE_ORDER, + ACTION_OPTIONS, + COMPARE_OPTIONS, + LOGIC_OPTIONS, encodeValueStr, + getPresetVars, + getTypeOrder, getValueFromWidget, + getArithType, makeComboWidget, makeLabel, makeOffsetWidget, @@ -72,7 +72,7 @@ class ConditionRowFrame(QFrame): if self._isFirst: self.logicCombo = None else: - self.logicCombo = makeComboWidget(LOGIC_OPERATORS, min_width=110, parent=self) + self.logicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self) layout.addWidget(self.logicCombo) self.leftVarCombo = QComboBox(self) self.leftVarCombo.setFixedHeight(25) @@ -80,7 +80,7 @@ class ConditionRowFrame(QFrame): self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.populateLeftVarCombo() layout.addWidget(self.leftVarCombo) - self.opCombo = makeComboWidget(COMPARE_OPERATORS, min_width=80, parent=self) + self.opCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self) layout.addWidget(self.opCombo) self._compTypeCombo = makeComboWidget([ ("特定值", "literal"), @@ -139,7 +139,7 @@ class ConditionRowFrame(QFrame): self.literalStack = QStackedWidget(self) self.literalStack.setFixedHeight(25) self._literalWidgets = {} - for vt in VAR_TYPE_ORDER: + for vt in getTypeOrder(): w = makeValueWidget(vt, self.literalStack) self._literalWidgets[vt] = w self.literalStack.addWidget(w) @@ -272,7 +272,7 @@ class ActionStepFrame(QFrame): layout = QHBoxLayout(self) layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(4) - self.opTypeCombo = makeComboWidget(ACTION_TYPES, min_width=70, parent=self) + self.opTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self) layout.addWidget(self.opTypeCombo) layout.addWidget(makeLabel("设置", self)) self.targetCombo = QComboBox(self) @@ -305,7 +305,7 @@ class ActionStepFrame(QFrame): self.targetCombo.blockSignals(True) self.targetCombo.clear() - for p in PRESET_VARIABLES: + for p in getPresetVars(): if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): continue info = self._varMgr.getInfoByName(p["name"]) @@ -322,10 +322,10 @@ class ActionStepFrame(QFrame): self._literalWidgets = {} self._offsetWidgets = {} - for vt in VAR_TYPE_ORDER: + for vt in getTypeOrder(): self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack) self.valueStack.addWidget(self._literalWidgets[vt]) - if vt in ARITH_TYPES: + if getArithType(vt): self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack) self.valueStack.addWidget(self._offsetWidgets[vt]) else: diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index e4f08d8..9f684fa 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -18,7 +18,7 @@ from PySide6.QtCore import ( from base.MsgBase import MsgBase from operators.AutoLib import AutoLib from utils.JSONReader import JSONReader -from autoscript import execute, registerDefaultTargetVars +from autoscript import createEngine class AutoLibWorker(MsgBase, QThread): @@ -219,8 +219,8 @@ class TimerTaskWorker(AutoLibWorker): continue for user in group.get("users", []): try: - registerDefaultTargetVars() - execute(auto_script, user) + engine = createEngine() + engine.execute(auto_script, user) affected_count += 1 except ValueError as e: self._showTrace( diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 1d0b600..2252909 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -108,7 +108,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.AutoScriptHelpButton = QPushButton("?") self.AutoScriptHelpButton.setFixedSize(20, 20) self.AutoScriptHelpButton.setToolTip( - "AutoScript 是一种轻量级 DSL\n" + "AutoScript 是一种轻量级 DSL 语言,基于 Lua 实现。\n" "用于在重复定时任务执行前,对用户的预约数据进行预处理\n" "\n" "点击查看完整在线文档" diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py index e8cb0d9..43a40d8 100644 --- a/src/gui/ALWebDriverDownloadDialog.py +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -31,7 +31,7 @@ from PySide6.QtWidgets import ( from PySide6.QtGui import QCloseEvent from managers.driver.WebDriverManager import ( - instance as webdriverManagerInstance, + instance as webdriverInstance, WebDriverManager, WebDriverInfo, WebDriverType, @@ -261,7 +261,7 @@ class ALWebDriverDownloadDialog(QDialog): ): try: - self.__driver_manager = webdriverManagerInstance(self.__driver_dir) + self.__driver_manager = webdriverInstance(self.__driver_dir) except ValueError as e: QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}") self.reject() diff --git a/src/gui/resources/ALResource.qrc b/src/gui/resources/ALResource.qrc index 6fad8c5..a66d94c 100644 --- a/src/gui/resources/ALResource.qrc +++ b/src/gui/resources/ALResource.qrc @@ -3,6 +3,9 @@ icons/AutoLibrary_Logo_64.svg icons/AutoLibrary_Logo_128.svg + icons/Copy.svg + icons/Reset.svg + translators/qtbase_zh_CN.qm diff --git a/src/gui/resources/icons/Copy.svg b/src/gui/resources/icons/Copy.svg new file mode 100644 index 0000000..aa98267 --- /dev/null +++ b/src/gui/resources/icons/Copy.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/gui/resources/icons/Reset.svg b/src/gui/resources/icons/Reset.svg new file mode 100644 index 0000000..da2b511 --- /dev/null +++ b/src/gui/resources/icons/Reset.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 2226e8ac908709ea78ee55d33028cbd1b04bed97 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 26 May 2026 12:39:21 +0800 Subject: [PATCH 28/49] =?UTF-8?q?refactor(pages):=20=E5=BC=95=E5=85=A5=20P?= =?UTF-8?q?age=20Object=20=E6=A8=A1=E5=BC=8F=E9=87=8D=E6=9E=84=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E9=A1=B5=E9=9D=A2=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E7=BB=9F=E4=B8=80=E4=B8=BA=20snake=5Fcase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原始 Selenium 操作脚本重构为三层 Page Object 架构: - Page Objects(LoginPage/ReserveView/RecordsView/MainShell) - Component Objects(Overlay 基类 + SeatMapOverlay/ReserveResultDialog 等对话框) - Flow 状态机(ReserveFlow/CheckinFlow/RenewFlow) - Services(CaptchaHandler/ReserveValidator/RecordChecker) 变量命名统一为 snake_case,方法名保持 camelCase,类名保持 PascalCase。 Co-Authored-By: Claude Opus 4.7 --- src/pages/AutoLibPages.py | 398 +++++++++++++++++++++++++ src/pages/LoginPage.py | 209 +++++++++++++ src/pages/MainShell.py | 166 +++++++++++ src/pages/RecordsView.py | 93 ++++++ src/pages/ReserveView.py | 266 +++++++++++++++++ src/pages/__init__.py | 34 +++ src/pages/_dialogs.py | 302 +++++++++++++++++++ src/pages/_overlay.py | 110 +++++++ src/pages/flows/CheckinFlow.py | 94 ++++++ src/pages/flows/RenewFlow.py | 156 ++++++++++ src/pages/flows/ReserveFlow.py | 272 +++++++++++++++++ src/pages/flows/__init__.py | 18 ++ src/pages/flows/_helpers.py | 85 ++++++ src/pages/services/CaptchaHandler.py | 101 +++++++ src/pages/services/RecordChecker.py | 302 +++++++++++++++++++ src/pages/services/ReserveValidator.py | 221 ++++++++++++++ src/pages/services/__init__.py | 18 ++ src/test_pages_refactor.py | 162 ++++++++++ 18 files changed, 3007 insertions(+) create mode 100644 src/pages/AutoLibPages.py create mode 100644 src/pages/LoginPage.py create mode 100644 src/pages/MainShell.py create mode 100644 src/pages/RecordsView.py create mode 100644 src/pages/ReserveView.py create mode 100644 src/pages/__init__.py create mode 100644 src/pages/_dialogs.py create mode 100644 src/pages/_overlay.py create mode 100644 src/pages/flows/CheckinFlow.py create mode 100644 src/pages/flows/RenewFlow.py create mode 100644 src/pages/flows/ReserveFlow.py create mode 100644 src/pages/flows/__init__.py create mode 100644 src/pages/flows/_helpers.py create mode 100644 src/pages/services/CaptchaHandler.py create mode 100644 src/pages/services/RecordChecker.py create mode 100644 src/pages/services/ReserveValidator.py create mode 100644 src/pages/services/__init__.py create mode 100644 src/test_pages_refactor.py diff --git a/src/pages/AutoLibPages.py b/src/pages/AutoLibPages.py new file mode 100644 index 0000000..a200a10 --- /dev/null +++ b/src/pages/AutoLibPages.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2025 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 queue + +from selenium import webdriver +from selenium.common.exceptions import ( + TimeoutException, + WebDriverException, +) +from selenium.webdriver.edge.service import Service as EdgeService +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.firefox.service import Service as FirefoxService + +from base.MsgBase import MsgBase +from pages.LoginPage import LoginPage +from pages.MainShell import MainShell +from pages.flows.ReserveFlow import ReserveFlow, ReserveContext +from pages.flows.CheckinFlow import CheckinFlow +from pages.flows.RenewFlow import RenewFlow +from pages.services.CaptchaHandler import CaptchaHandler +from pages.services.ReserveValidator import ReserveValidator +from pages.services.RecordChecker import RecordChecker + + +class AutoLibPages(MsgBase): + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + run_config: dict, + ) -> None: + super().__init__(input_queue, output_queue) + + self.__run_config: dict = run_config + self.__user_config: dict | None = None + self.__driver = None + self.__driver_type: str = "" + self.__driver_path: str = "" + self.__login_page: LoginPage = None + self.__shell: MainShell = None + self.__captcha_handler: CaptchaHandler = None + self.__record_checker: RecordChecker = None + self.__reserve_validator: ReserveValidator = None + self.__reserve_flow: ReserveFlow = None + self.__checkin_flow: CheckinFlow = None + self.__renew_flow: RenewFlow = None + + if not self.__initBrowserDriver(): + raise Exception("浏览器驱动初始化失败 !") + else: + if not self.__initDriverUrl(): + self.close() + raise Exception("浏览器驱动URL初始化失败 !") + self.__initPagesServices() + self.__initPagesFlows() + + def __initBrowserDriver( + self, + ) -> bool: + + self._showTrace("正在初始化浏览器驱动......", no_log=True) + web_driver_config: dict = self.__run_config.get("web_driver", None) + self.__driver_type = web_driver_config.get("driver_type") + match self.__driver_type.lower(): + case "edge": + driver_options = webdriver.EdgeOptions() + case "chrome": + driver_options = webdriver.ChromeOptions() + case "firefox": + driver_options = webdriver.FirefoxOptions() + case _: + self._showTrace( + f"不支持的浏览器驱动类型: {self.__driver_type} !", + self.TraceLevel.WARNING, + ) + return False + if not web_driver_config: + self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR) + return False + if web_driver_config.get("headless"): + driver_options.add_argument("--headless") + driver_options.add_argument("--disable-gpu") + driver_options.add_argument("--no-sandbox") + driver_options.add_argument("--disable-dev-shm-usage") + + # must be 1920x1080, otherwise the page will cause some elements not accessible + driver_options.add_argument("--window-size=1920,1080") + + # omit ssl errors and verbose log level + driver_options.add_argument("--ignore-certificate-errors") + driver_options.add_argument("--ignore-ssl-errors") + driver_options.add_argument("--log-level=OFF") + driver_options.add_argument("--silent") + + # set options for chrome and edge + if self.__driver_type.lower() in ["edge", "chrome"]: + driver_options.add_argument("--remote-allow-origins=*") + driver_options.add_experimental_option("excludeSwitches", ["enable-automation"]) + driver_options.add_experimental_option("useAutomationExtension", False) + driver_options.add_argument("--disable-blink-features=AutomationControlled") + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\ + "AppleWebKit/537.36 (KHTML, like Gecko) "\ + "Chrome/120.0.0.0 "\ + "Safari/537.36" + if self.__driver_type.lower() == "edge": + user_agent += " Edg/120.0.0.0" + + # set options for firefox + elif self.__driver_type.lower() == "firefox": + driver_options.set_preference("dom.webdriver.enabled", False) + driver_options.set_preference("useAutomationExtension", False) + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\ + "Gecko/20100101 Firefox/120.0" + driver_options.add_argument(f"user-agent={user_agent}") + + # init browser driver + self.__driver_path = web_driver_config.get("driver_path") + if not self.__driver_path: + self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING) + return False + self.__driver_path = os.path.abspath(self.__driver_path) + try: + service = None + match self.__driver_type.lower(): + case "edge": + service = EdgeService(executable_path=self.__driver_path) + self.__driver = webdriver.Edge(service=service, options=driver_options) + case "chrome": + service = ChromeService(executable_path=self.__driver_path) + self.__driver = webdriver.Chrome(service=service, options=driver_options) + case "firefox": + self._showTrace("Firefox 浏览器驱动初始化略慢, 请耐心等待...", no_log=True) + service = FirefoxService(executable_path=self.__driver_path) + self.__driver = webdriver.Firefox(service=service, options=driver_options) + case _: + raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type} !") + self.__driver.implicitly_wait(1) + self.__driver.execute_script( + "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" + ) + except WebDriverException as e: + self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR) + return False + except Exception as e: + self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR) + return False + self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") + return True + + def __initDriverUrl( + self, + ) -> bool: + + lib_config: dict = self.__run_config.get("library", None) + if not lib_config: + self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR) + return False + url: str = lib_config.get("host_url") + lib_config.get("login_url") + self.__login_page = LoginPage(self.__driver) + self.__driver.set_page_load_timeout(5) + try: + self.__driver.get(url) + except TimeoutException: + self.__login_page.stopPageLoad() + self._showTrace( + "图书馆登录页面加载超时 ! 请检查网络环境是否正常", self.TraceLevel.ERROR + ) + return False + except WebDriverException as e: + self._showTrace(f"图书馆页面加载失败: {e}", self.TraceLevel.ERROR) + return False + if not self.__login_page.waitUntilLoaded(): + return False + return True + + def __initPagesServices( + self, + ) -> None: + + if not self.__driver: + self._showTrace("浏览器驱动未初始化, 请先初始化浏览器驱动 !", self.TraceLevel.WARNING) + return + self.__shell = MainShell(self.__driver) + self.__captcha_handler = CaptchaHandler( + input_queue=self._input_queue, + output_queue=self._output_queue, + login_page=self.__login_page, + ) + self.__record_checker = RecordChecker( + input_queue=self._input_queue, + output_queue=self._output_queue, + shell=self.__shell, + ) + self.__reserve_validator = ReserveValidator( + input_queue=self._input_queue, + output_queue=self._output_queue, + ) + + def __initPagesFlows( + self, + ) -> None: + + self.__reserve_flow = ReserveFlow( + input_queue=self._input_queue, + output_queue=self._output_queue, + driver=self.__driver, + shell=self.__shell, + ) + self.__checkin_flow = CheckinFlow( + input_queue=self._input_queue, + output_queue=self._output_queue, + driver=self.__driver, + shell=self.__shell, + ) + self.__renew_flow = RenewFlow( + input_queue=self._input_queue, + output_queue=self._output_queue, + driver=self.__driver, + shell=self.__shell, + ) + + def __run( + self, + username: str, + password: str, + login_config: dict, + run_mode_config: dict, + reserve_info: dict, + ) -> int: + + # result : -1 - terminate, 0 - success, 1 - failed, 2 - passed + result: int = 2 + + # login + auto_captcha: bool = login_config.get("auto_captcha", True) + if not self.__login_page.login( + username, + password, + captcha_solver=lambda: self.__captcha_handler.solveCaptcha(auto_captcha), + tracer=self._showTrace, + log_level=self.TraceLevel, + max_attempts=login_config.get("max_attempt", 3), + ): + return 1 + run_mode_raw: int = run_mode_config.get("run_mode", 0) + run_mode: dict[str, bool] = { + "auto_reserve": run_mode_raw & 0x1, + "auto_checkin": run_mode_raw & 0x2, + "auto_renewal": run_mode_raw & 0x4, + } + # reserve + if run_mode["auto_reserve"]: + if self.__record_checker.canReserve(reserve_info.get("date")): + if self.__reserve_validator.validate(reserve_info): + ctx = ReserveContext( + username=username, + date=reserve_info["date"], + floor=reserve_info["floor"], + room=reserve_info["room"], + seat_id=reserve_info["seat_id"], + begin_time=reserve_info["begin_time"]["time"], + end_time=reserve_info["end_time"]["time"], + begin_max_diff=reserve_info["begin_time"]["max_diff"], + end_max_diff=reserve_info["end_time"]["max_diff"], + begin_prefer_early=reserve_info["begin_time"]["prefer_early"], + end_prefer_early=reserve_info["end_time"]["prefer_early"], + expect_duration=reserve_info["expect_duration"], + satisfy_duration=reserve_info["satisfy_duration"], + ) + if self.__reserve_flow.execute(ctx): + result = 0 + else: + result = 1 + else: + result = 1 + else: + self._showTrace(f"用户 {username} 无法预约, 已跳过") + result = 2 + + # checkin + last_result: int = result + if run_mode["auto_checkin"] and last_result != 1: + if self.__record_checker.canCheckin(): + if self.__checkin_flow.execute(username): + result = 0 + else: + result = 1 + else: + self._showTrace(f"用户 {username} 无法签到, 已跳过") + result = 2 + if last_result == 0: # partly success + result = 0 + + # renewal + last_result = result + if run_mode["auto_renewal"] and last_result != 1: + can_renew, record = self.__record_checker.canRenew() + if can_renew: + renew_info: dict = reserve_info.get("renew_time", {}) + if self.__renew_flow.execute(username, record, renew_info): + if self.__record_checker.postRenewCheck(record): + self._showTrace(f"用户 {username} 续约成功 !") + result = 0 + else: + 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.__shell.logout(): + if not self.__initDriverUrl(): + return -1 + return result + + def run( + self, + user_config: dict, + ) -> None: + + self.__user_config = user_config + + user_counter: dict[str, int] = {"current": 0, "success": 0, "failed": 0, "passed": 0} + users: list = self.__user_config["users"] + self._showTrace(f"共发现 {len(users)} 个用户") + for user in users: + user_counter["current"] += 1 + self._showTrace( + f"正在处理第 {user_counter['current']}/{len(users)} 个用户: {user['username']}......", + no_log=True, + ) + if not user["enabled"]: + self._showTrace(f"用户 {user['username']} 已跳过") + user_counter["passed"] += 1 + continue + r: int = self.__run( + username=user["username"], + password=user["password"], + login_config=self.__run_config["login"], + run_mode_config=self.__run_config["mode"], + reserve_info=user["reserve_info"], + ) + if r == -1: + self._showTrace( + f"用户 {user['username']} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !", + self.TraceLevel.WARNING, + ) + break + elif r == 0: + user_counter["success"] += 1 + elif r == 1: + user_counter["failed"] += 1 + elif r == 2: + user_counter["passed"] += 1 + self._showTrace( + f"处理完成, 共计 {user_counter['current']} 个用户, " + f"成功 {user_counter['success']} 个用户, " + f"失败 {user_counter['failed']} 个用户, " + f"跳过 {user_counter['passed']} 个用户" + ) + return + + def close( + self, + ) -> bool: + + if self.__driver: + if self.__driver_type.lower() == "firefox": + self._showTrace( + "Firefox 浏览器驱动关闭略慢, 请耐心等待...", + no_log=True, + ) + try: + self.__driver.quit() + except WebDriverException as e: + self._showTrace(f"浏览器驱动关闭时发生异常: {e}", self.TraceLevel.WARNING) + self.__driver = None + self._showTrace("浏览器驱动已关闭") + return True + else: + self._showTrace("浏览器驱动未初始化, 无需关闭", no_log=True) + return False diff --git a/src/pages/LoginPage.py b/src/pages/LoginPage.py new file mode 100644 index 0000000..24c9b1b --- /dev/null +++ b/src/pages/LoginPage.py @@ -0,0 +1,209 @@ +# -*- 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. +""" +from typing import Callable + +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + TimeoutException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class LoginPage: + + USERNAME_INPUT = (By.NAME, "username") + PASSWORD_INPUT = (By.NAME, "password") + CAPTCHA_INPUT = (By.NAME, "answer") + CAPTCHA_IMG = (By.ID, "loadImgId") + LOGIN_BUTTON = (By.XPATH, "//input[@type='button' and @value='登录']") + + SUCCESS_INDICATOR_SEARCH = (By.ID, "search") + SUCCESS_INDICATOR_CONTENT = (By.CLASS_NAME, "selectContent") + SUCCESS_TITLE_KEYWORD = "自选座位 :: 座位预约系统" + + PAGE_LOAD_TIMEOUT = 5 + + def __init__( + self, + driver: WebDriver, + ) -> None: + + self._driver: WebDriver = driver + + def navigate( + self, + url: str, + ) -> bool: + + self._driver.set_page_load_timeout(self.PAGE_LOAD_TIMEOUT) + self._driver.get(url) + if not self.waitUntilLoaded(): + return False + return True + + def waitUntilLoaded( + self, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.title_contains("首页") + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.USERNAME_INPUT) + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.PASSWORD_INPUT) + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.CAPTCHA_INPUT) + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.CAPTCHA_IMG) + ) + return True + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + + def fillCredentials( + self, + username: str, + password: str, + ) -> bool: + + try: + el = self._driver.find_element(*self.USERNAME_INPUT) + el.clear() + el.send_keys(username) + el = self._driver.find_element(*self.PASSWORD_INPUT) + el.clear() + el.send_keys(password) + return True + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + + def getCaptchaImageSrc( + self, + ) -> str: + + captcha_el = self._driver.find_element(*self.CAPTCHA_IMG) + return captcha_el.get_attribute("src") + + def refreshCaptcha( + self, + ) -> bool: + + try: + self._driver.find_element(*self.CAPTCHA_IMG).click() + return True + except (NoSuchElementException, TimeoutException, + ElementNotInteractableException): + return False + except Exception: + return False + + def fillCaptcha( + self, + captcha_text: str, + ) -> bool: + + try: + el = self._driver.find_element(*self.CAPTCHA_INPUT) + el.clear() + el.send_keys(captcha_text) + return True + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + + def clickLogin( + self, + ) -> bool: + + try: + self._driver.find_element(*self.LOGIN_BUTTON).click() + return True + except (NoSuchElementException, TimeoutException, + ElementNotInteractableException): + return False + except Exception: + return False + + def waitLoginSuccess( + self, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.title_contains(self.SUCCESS_TITLE_KEYWORD) + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.SUCCESS_INDICATOR_SEARCH) + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.SUCCESS_INDICATOR_CONTENT) + ) + return True + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + + def stopPageLoad( + self, + ) -> None: + + self._driver.execute_script("window.stop();") + + def login( + self, + username: str, + password: str, + captcha_solver: Callable[[], str], + tracer: Callable[..., None], + log_level: type, + max_attempts: int = 5, + ) -> bool: + + ERR = log_level.ERROR + for attempt in range(max_attempts): + tracer( + f"用户 {username} 第 {attempt + 1} 次尝试登录......", + 20, no_log=True, + ) + if not self.fillCredentials(username, password): + continue + captcha_text = captcha_solver() + if not captcha_text: + continue + if not self.fillCaptcha(captcha_text): + continue + tracer("尝试登录...", 20, no_log=True) + if not self.clickLogin(): + continue + if self.waitLoginSuccess(): + tracer(f"用户 {username} 第 {attempt + 1} 次登录成功 !") + return True + else: + err_msg = ( + "登录页面加载失败 ! : " + "用户账号或者密码错误/验证码错误, 具体以页面提示为准" + ) + tracer(err_msg, ERR) + return False diff --git a/src/pages/MainShell.py b/src/pages/MainShell.py new file mode 100644 index 0000000..a9b6247 --- /dev/null +++ b/src/pages/MainShell.py @@ -0,0 +1,166 @@ +# -*- 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 time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.common.exceptions import ( + NoSuchElementException, + TimeoutException, +) + +from pages.ReserveView import ReserveView +from pages.RecordsView import RecordsView + + +class MainShell: + + TAB_RESERVE = (By.XPATH, "//a[@href='/map']") + TAB_HISTORY = (By.XPATH, "//a[@href='/history?type=SEAT']") + TAB_LOGOUT = (By.XPATH, "//a[@href='/logout']") + + BTN_CHECKIN = (By.ID, "btnCheckIn") + BTN_EXTEND = (By.ID, "btnExtend") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + self._driver = driver + + def gotoReserveView( + self, + ) -> ReserveView: + + self._clickTab(self.TAB_RESERVE) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located((By.ID, "seatLayout")) + ) + return ReserveView(self._driver) + + def gotoRecordsView( + self, + ) -> RecordsView: + + self._clickTab(self.TAB_HISTORY) + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located((By.CLASS_NAME, "myReserveList")) + ) + return RecordsView(self._driver) + + def logout( + self, + ) -> bool: + + try: + self._driver.find_element(*self.TAB_LOGOUT).click() + return True + except NoSuchElementException: + return False + except Exception: + return False + + def waitCheckinButton( + self, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.BTN_CHECKIN) + ) + return True + except TimeoutException: + return False + except Exception: + return False + + def waitExtendButton( + self, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.BTN_EXTEND) + ) + return True + except TimeoutException: + return False + except Exception: + return False + + def isCheckinButtonDisabled( + self, + ) -> bool: + + btn = self._driver.find_element(*self.BTN_CHECKIN) + return "disabled" in btn.get_attribute("class") + + def isExtendButtonDisabled( + self, + ) -> bool: + + btn = self._driver.find_element(*self.BTN_EXTEND) + return "disabled" in btn.get_attribute("class") + + def clickCheckinButton( + self, + ) -> None: + + btn = WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.BTN_CHECKIN) + ) + btn.click() + + def clickExtendButton( + self, + ) -> None: + + btn = WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.BTN_EXTEND) + ) + btn.click() + + def enableCheckinButtonByJS( + self, + ) -> bool: + + script = """ + try { + var checkin_btn = document.getElementById('btnCheckIn'); + if (checkin_btn) { + checkin_btn.classList.remove('disabled'); + return true; + } + return false; + } catch (e) { + return false; + } + """ + result = self._driver.execute_script(script) + time.sleep(0.1) + return result + + def refresh( + self, + ) -> None: + + self._driver.refresh() + + def _clickTab( + self, + locator: tuple, + ) -> None: + + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(locator) + ).click() diff --git a/src/pages/RecordsView.py b/src/pages/RecordsView.py new file mode 100644 index 0000000..d4f838c --- /dev/null +++ b/src/pages/RecordsView.py @@ -0,0 +1,93 @@ +# -*- 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. +""" +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement +from selenium.common.exceptions import ( + NoSuchElementException, + StaleElementReferenceException, + TimeoutException, +) + + +class RecordsView: + + RECORDS_LIST = (By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)") + MORE_BTN = (By.ID, "more_btn") + RECORD_TIME = (By.CSS_SELECTOR, "dt") + RECORD_INFO = (By.CSS_SELECTOR, "a") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + self._driver = driver + + def loadRecords( + self, + ) -> list | None: + + try: + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.RECORDS_LIST) + ) + return self._driver.find_elements(*self.RECORDS_LIST) + except TimeoutException: + return None + except Exception: + return None + + def getRecordTimeElement( + self, + record: WebElement, + ) -> WebElement: + + return record.find_element(*self.RECORD_TIME) + + def getRecordInfoElements( + self, + record: WebElement, + ) -> list[WebElement]: + + return record.find_elements(*self.RECORD_INFO) + + def showMoreRecords( + self, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.MORE_BTN) + ) + except TimeoutException: + return False + except Exception: + return False + try: + more_btn = self._driver.find_element(*self.MORE_BTN) + if more_btn.is_displayed() and more_btn.is_enabled(): + self._driver.execute_script("arguments[0].scrollIntoView(true);", more_btn) + self._driver.execute_script("arguments[0].click();", more_btn) + return True + return False + except (NoSuchElementException, StaleElementReferenceException): + return False + except Exception: + return False + + def getRecordText( + self, + record: WebElement, + ) -> str: + + return record.text.strip() diff --git a/src/pages/ReserveView.py b/src/pages/ReserveView.py new file mode 100644 index 0000000..13c4cb7 --- /dev/null +++ b/src/pages/ReserveView.py @@ -0,0 +1,266 @@ +# -*- 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 time + +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, + TimeoutException, +) + +from pages._dialogs import SeatMapOverlay, ReserveResultDialog + + +class ReserveView: + + DATE_SELECT = (By.ID, "onDate_select") + DATE_OPTION_FMT = "p#options_onDate a[value='{value}']" + DATE_XPATH_FMT = "//p[@id='options_onDate']/a[@value='{value}']" + + PLACE_SELECT = (By.ID, "display_building") + PLACE_OPTION_FMT = "p#options_building a[value='{value}']" + PLACE_XPATH_FMT = "//p[@id='options_building']/a[@value='{value}']" + + FLOOR_SELECT = (By.ID, "floor_select") + FLOOR_OPTION_FMT = "p#options_floor a[value='{value}']" + FLOOR_XPATH_FMT = "//p[@id='options_floor']/a[@value='{value}']" + + FIND_ROOM_BTN = (By.ID, "findRoom") + ROOM_BTN_FMT = "room_{room}" + + SEAT_LAYOUT = (By.ID, "seatLayout") + SEAT_ITEMS = (By.CSS_SELECTOR, "li[id^='seat_']") + RESERVE_BTN = (By.ID, "reserveBtn") + + START_TIME_OPTS = (By.CSS_SELECTOR, "#startTime ul li a") + END_TIME_OPTS = (By.CSS_SELECTOR, "#endTime ul li a") + + RESULT_DIALOG = (By.CLASS_NAME, "layoutSeat") + RESULT_TITLE = (By.CSS_SELECTOR, ".layoutSeat dt") + RESULT_DETAIL = (By.CSS_SELECTOR, ".layoutSeat dd") + + FLOOR_MAP = {"2": "二层", "3": "三层", "4": "四层", "5": "五层"} + ROOM_MAP = { + "1": "二层内环", "2": "二层西区", "3": "三层内环", "4": "三层外环", + "5": "四层内环", "6": "四层外环", "7": "四层期刊", "8": "五层考研", + } + + def __init__( + self, + driver: WebDriver, + ) -> None: + + self._driver = driver + + def selectDate( + self, + date_str: str, + ) -> bool: + + if self._clickOptionByJS( + trigger_id="onDate_select", + option_css=self.DATE_OPTION_FMT.format(value=date_str), + ): + return True + return self._clickOption( + trigger=self.DATE_SELECT, + option=(By.XPATH, self.DATE_XPATH_FMT.format(value=date_str)), + ) + + def selectPlace( + self, + place: str = "1", + ) -> bool: + + if self._clickOptionByJS( + trigger_id="display_building", + option_css=self.PLACE_OPTION_FMT.format(value=place), + ): + return True + return self._clickOption( + trigger=self.PLACE_SELECT, + option=(By.XPATH, self.PLACE_XPATH_FMT.format(value=place)), + ) + + def selectFloor( + self, + floor: str, + ) -> bool: + + if self._clickOptionByJS( + trigger_id="floor_select", + option_css=self.FLOOR_OPTION_FMT.format(value=floor), + ): + return True + return self._clickOption( + trigger=self.FLOOR_SELECT, + option=(By.XPATH, self.FLOOR_XPATH_FMT.format(value=floor)), + ) + + def selectRoom( + self, + room: str, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.FIND_ROOM_BTN) + ).click() + except (TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room))) + ).click() + return True + except (TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False + + def openSeatMap( + self, + ) -> SeatMapOverlay: + + return SeatMapOverlay(self._driver) + + def selectSeat( + self, + seat_id: str, + ) -> str | None: + + try: + WebDriverWait(self._driver, 2).until( + EC.presence_of_element_located(self.SEAT_LAYOUT) + ) + WebDriverWait(self._driver, 2).until( + EC.presence_of_all_elements_located(self.SEAT_ITEMS) + ) + except TimeoutException: + return None + except Exception: + return None + try: + all_seats = self._driver.find_elements(*self.SEAT_ITEMS) + seat_id_upper = seat_id.lstrip('0').upper() + for seat in all_seats: + if not seat_id_upper == seat.text.lstrip('0'): + continue + seat_link = seat.find_element(By.TAG_NAME, "a") + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(seat_link) + ) + seat_link.click() + return seat_link.get_attribute("title") + return None + except (NoSuchElementException, TimeoutException, + StaleElementReferenceException, ElementNotInteractableException): + return None + except Exception: + return None + + def submitReserve( + self, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.RESERVE_BTN) + ).click() + return True + except (TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False + + def waitResultDialog( + self, + ) -> ReserveResultDialog: + + return ReserveResultDialog(self._driver) + + def getAvailableTimeOptions( + self, + time_id: str, + ) -> list: + + try: + WebDriverWait(self._driver, 2).until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, f"#{time_id} ul li a") + ) + ) + except TimeoutException: + return [] + except Exception: + return [] + return self._driver.find_elements( + By.CSS_SELECTOR, + f"#{time_id} ul li a", + ) + + def refresh( + self, + ) -> None: + + self._driver.refresh() + + def _clickOptionByJS( + self, + trigger_id: str, + option_css: str, + ) -> bool: + + script = f""" + try {{ + var trigger = document.getElementById('{trigger_id}'); + if (trigger) {{ + trigger.click(); + var option = document.querySelector("{option_css}"); + if (option) {{ + option.click(); + return true; + }} + return false; + }} + return false; + }} catch (e) {{ + return false; + }} + """ + result = self._driver.execute_script(script) + time.sleep(0.1) + return result + + def _clickOption( + self, + trigger: tuple, + option: tuple, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(trigger) + ).click() + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(option) + ).click() + return True + except (TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False diff --git a/src/pages/__init__.py b/src/pages/__init__.py new file mode 100644 index 0000000..36f0450 --- /dev/null +++ b/src/pages/__init__.py @@ -0,0 +1,34 @@ +# -*- 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. +""" +from pages.AutoLibPages import AutoLibPages +from pages.LoginPage import LoginPage +from pages.MainShell import MainShell +from pages.ReserveView import ReserveView +from pages.RecordsView import RecordsView +from pages._dialogs import ( + SeatMapOverlay, + TimeSelectDialog, + ReserveResultDialog, + CheckinResultDialog, + RenewDialog, +) + +__all__ = [ + "AutoLibPages", + "LoginPage", + "MainShell", + "ReserveView", + "RecordsView", + "SeatMapOverlay", + "TimeSelectDialog", + "ReserveResultDialog", + "CheckinResultDialog", + "RenewDialog", +] diff --git a/src/pages/_dialogs.py b/src/pages/_dialogs.py new file mode 100644 index 0000000..dc52602 --- /dev/null +++ b/src/pages/_dialogs.py @@ -0,0 +1,302 @@ +# -*- 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. +""" +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, + TimeoutException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from pages._overlay import Overlay + + +class SeatMapOverlay(Overlay): + """ + Seat selection overlay that opens after choosing a floor and room. + """ + + ROOT = (By.ID, "seatLayout") + SEAT_ITEMS = (By.CSS_SELECTOR, "li[id^='seat_']") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT) + + def selectSeat( + self, + seat_id: str, + ) -> str | None: + + try: + self._waitAllPresence(self.SEAT_ITEMS) + except (NoSuchElementException, TimeoutException): + return None + except Exception: + return None + try: + all_seats = self._findAll(*self.SEAT_ITEMS) + seat_id_upper = seat_id.lstrip('0').upper() + for seat in all_seats: + if not seat_id_upper == seat.text.lstrip('0'): + continue + seat_link = seat.find_element(By.TAG_NAME, "a") + self._waitClickable((By.TAG_NAME, "a")) + seat_link.click() + return seat_link.get_attribute("title") + return None + except (NoSuchElementException, TimeoutException, + ElementNotInteractableException, StaleElementReferenceException): + return None + except Exception: + return None + + +class TimeSelectDialog(Overlay): + """ + Time selection panel that appears after selecting a seat. + + Contains start-time and end-time option lists. + Does NOT auto-close — the reserve submission handles cleanup. + """ + + ROOT = (By.CSS_SELECTOR, "#startTime ul") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def getTimeOptions( + self, + time_id: str, + ) -> list[WebElement]: + + try: + self._waitAllPresence( + (By.CSS_SELECTOR, f"#{time_id} ul li a") + ) + except (NoSuchElementException, TimeoutException): + return [] + except Exception: + return [] + return self._findAll( + By.CSS_SELECTOR, + f"#{time_id} ul li a", + ) + + +class ReserveResultDialog(Overlay): + """ + Reservation result dialog shown after submitting a reserve request. + """ + + ROOT = (By.CLASS_NAME, "layoutSeat") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def getTitle( + self, + ) -> str: + + try: + return self._find(*self._titleLocator()).text + except (NoSuchElementException, StaleElementReferenceException): + return "" + except Exception: + return "" + + def isSuccess( + self, + ) -> bool: + + title = self.getTitle() + return any( + kw in title + for kw in ("预定好了", "预约成功", "操作成功") + ) + + def isFailure( + self, + ) -> bool: + + contents = self.getDetailTexts() + return any( + "预约失败" in msg or "已有1个有效预约" in msg + for msg in contents + ) + + def getDetailTexts( + self, + ) -> list[str]: + + try: + elements = self._findAll(By.CSS_SELECTOR, ".layoutSeat dd") + return [el.text for el in elements if el.text.strip()] + except (NoSuchElementException, StaleElementReferenceException): + return [] + except Exception: + return [] + + def _titleLocator( + self, + ) -> tuple: + + return (By.CSS_SELECTOR, ".layoutSeat dt") + + +class CheckinResultDialog(Overlay): + """ + Check-in result dialog. + """ + + ROOT = (By.CLASS_NAME, "ui_dialog") + + RESULT_MSG = (By.CLASS_NAME, "resultMessage") + OK_BTN = (By.CLASS_NAME, "btnOK") + DETAIL_DD = (By.CSS_SELECTOR, ".resultMessage dd") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def getResultMessage( + self, + ) -> str: + + try: + self._waitPresence(self.RESULT_MSG) + el = self._find(*self.RESULT_MSG) + return el.text + except (TimeoutException, NoSuchElementException, StaleElementReferenceException): + return "" + except Exception: + return "" + + def getDetails( + self, + ) -> list[str]: + + try: + elements = self._findAll(*self.DETAIL_DD) + return [el.text for el in elements if el.text.strip()] + except (NoSuchElementException, StaleElementReferenceException): + return [] + except Exception: + return [] + + def clickOk( + self, + ) -> bool: + + try: + self._waitClickable(self.OK_BTN).click() + return True + except (NoSuchElementException, TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False + + +class RenewDialog(Overlay): + """ + Renewal time selection dialog. + """ + + ROOT = (By.ID, "extendDiv") + + MESSAGE_HEAD = (By.CSS_SELECTOR, "#extendDiv p.messageHead") + RESULT_MSG = (By.CSS_SELECTOR, "#extendDiv div.resultMessage") + TIME_OPTS = (By.CSS_SELECTOR, "#extendDiv .renewal_List li") + OK_BTN = (By.CSS_SELECTOR, "#extendDiv .btnOK") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def waitUntilReady( + self, + ) -> bool: + + try: + self._waitVisible(self.ROOT) + self._waitPresence(self.MESSAGE_HEAD) + self._waitPresence(self.RESULT_MSG) + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + head_msg = self._find(*self.MESSAGE_HEAD).text.strip() + if "警告" in head_msg: + return False + try: + self._waitAllPresence(self.TIME_OPTS) + self._waitPresence(self.OK_BTN) + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + return True + + def getHeadMessage( + self, + ) -> str: + + return self._find(*self.MESSAGE_HEAD).text.strip() + + def getResultMessage( + self, + ) -> str: + + return self._find(*self.RESULT_MSG).text.strip() + + def getTimeOptions( + self, + ) -> list[WebElement]: + + return self._findAll(*self.TIME_OPTS) + + def getOkButton( + self, + ) -> WebElement: + + return self._find(*self.OK_BTN) + + def clickOk( + self, + ) -> bool: + + try: + self._find(*self.OK_BTN).click() + return True + except (NoSuchElementException, TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False diff --git a/src/pages/_overlay.py b/src/pages/_overlay.py new file mode 100644 index 0000000..12041e6 --- /dev/null +++ b/src/pages/_overlay.py @@ -0,0 +1,110 @@ +# -*- 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. +""" +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class Overlay: + """ + Context-managed overlay / modal / dialog on a page. + + Automates the lifecycle: wait for appearance on enter, + optionally wait for disappearance on exit. + """ + + def __init__( + self, + driver: WebDriver, + root_locator: tuple, + auto_close_on_exit: bool = True, + wait_timeout: float = 3.0, + ) -> None: + + self._driver: WebDriver = driver + self._root_locator: tuple = root_locator + self._auto_close: bool = auto_close_on_exit + self._timeout: float = wait_timeout + + def __enter__( + self, + ) -> "Overlay": + + WebDriverWait(self._driver, self._timeout).until( + EC.visibility_of_element_located(self._root_locator) + ) + return self + + def __exit__( + self, + *args: object, + ) -> None: + + if self._auto_close: + WebDriverWait(self._driver, self._timeout).until( + EC.invisibility_of_element_located(self._root_locator) + ) + + def _find( + self, + by: str, + value: str, + ) -> WebElement: + + return self._driver.find_element(by, value) + + def _findAll( + self, + by: str, + value: str, + ) -> list[WebElement]: + + return self._driver.find_elements(by, value) + + def _waitClickable( + self, + locator: tuple, + timeout: float = 2.0, + ) -> WebElement: + + return WebDriverWait(self._driver, timeout).until( + EC.element_to_be_clickable(locator) + ) + + def _waitPresence( + self, + locator: tuple, + timeout: float = 2.0, + ) -> WebElement: + + return WebDriverWait(self._driver, timeout).until( + EC.presence_of_element_located(locator) + ) + + def _waitVisible( + self, + locator: tuple, + timeout: float = 2.0, + ) -> WebElement: + + return WebDriverWait(self._driver, timeout).until( + EC.visibility_of_element_located(locator) + ) + + def _waitAllPresence( + self, + locator: tuple, + timeout: float = 2.0, + ) -> list[WebElement]: + + return WebDriverWait(self._driver, timeout).until( + EC.presence_of_all_elements_located(locator) + ) diff --git a/src/pages/flows/CheckinFlow.py b/src/pages/flows/CheckinFlow.py new file mode 100644 index 0000000..299ab04 --- /dev/null +++ b/src/pages/flows/CheckinFlow.py @@ -0,0 +1,94 @@ +# -*- 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 queue + +from selenium.common.exceptions import ( + NoSuchElementException, + TimeoutException, +) +from selenium.webdriver.remote.webdriver import WebDriver + +from base.MsgBase import MsgBase +from pages.MainShell import MainShell +from pages._dialogs import CheckinResultDialog + + +class CheckinFlow(MsgBase): + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + driver: WebDriver, + shell: MainShell, + ) -> None: + + super().__init__(input_queue, output_queue) + self._driver: WebDriver = driver + self._shell: MainShell = shell + + def execute( + self, + username: str, + ) -> bool: + + if not self._shell.waitCheckinButton(): + self._showTrace(f"用户 {username} 签到界面加载失败 !", self.TraceLevel.ERROR) + return False + + if self._shell.isCheckinButtonDisabled(): + self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......") + if not self._shell.enableCheckinButtonByJS(): + self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR) + return False + self._showTrace("签到按钮已启用") + + self._shell.clickCheckinButton() + + try: + with CheckinResultDialog(self._driver) as dialog: + result_msg = dialog.getResultMessage() + if "签到成功" in result_msg: + details = dialog.getDetails() + if details: + if len(details) >= 5: + self._showTrace( + f"\n" + f" 签到成功 !\n" + f" {details[1]}\n" + f" {details[2]}\n" + f" {details[3]}\n" + f" {details[4]}" + ) + else: + self._showTrace( + "\n" + " 签到成功 !\n" + " 未获取到签到详情 !" + ) + dialog.clickOk() + self._showTrace(f"用户 {username} 签到成功 !") + return True + else: + failure_reason = result_msg.replace("签到失败", "").strip() + self._showTrace( + f"\n" + " 签到失败 !\n" + f" {failure_reason}" + ) + dialog.clickOk() + self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR) + return False + except (NoSuchElementException, TimeoutException): + self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) + return False + except Exception: + self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) + return False diff --git a/src/pages/flows/RenewFlow.py b/src/pages/flows/RenewFlow.py new file mode 100644 index 0000000..35b48f1 --- /dev/null +++ b/src/pages/flows/RenewFlow.py @@ -0,0 +1,156 @@ +# -*- 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 queue + +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + TimeoutException, +) +from selenium.webdriver.remote.webdriver import WebDriver + +from base.MsgBase import MsgBase +from pages.MainShell import MainShell +from pages._dialogs import RenewDialog +from pages.flows._helpers import ( + timeStrToMins, + minsToTimeStr, + findBestTimeOption, +) + + +class RenewFlow(MsgBase): + + LIBRARY_CLOSE_MINS = 1410 + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + driver: WebDriver, + shell: MainShell, + ) -> None: + + super().__init__(input_queue, output_queue) + self._driver: WebDriver = driver + self._shell: MainShell = shell + + def execute( + self, + username: str, + record: dict, + renew_info: dict, + ) -> bool: + + max_diff = renew_info["max_diff"] + prefer_earlier = renew_info["prefer_early"] + end_time = record["time"]["end"] + target_renew_mins = timeStrToMins(end_time) + renew_info["expect_duration"] * 60 + + if not self._validateRenewTime(end_time, target_renew_mins): + return False + + if not self._shell.waitExtendButton(): + self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR) + return False + + if self._shell.isExtendButtonDisabled(): + self._showTrace( + f"用户 {username} 续约按钮不可用, 可能不在场馆内, " + f"请连接图书馆网络后重试" + ) + return False + + self._shell.clickExtendButton() + + try: + with RenewDialog(self._driver) as dialog: + if not dialog.waitUntilReady(): + result_msg = dialog.getResultMessage() + self._showTrace( + f"\n" + f" 续约失败 !\n" + f" {result_msg}" + ) + self._shell.refresh() + self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR) + return False + + renew_ok_btn = dialog.getOkButton() + renew_time_opts = dialog.getTimeOptions() + if not renew_time_opts: + self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING) + self._shell.refresh() + return False + + best_opt, best_text, actual_diff, free_times = findBestTimeOption( + renew_time_opts, target_renew_mins, max_diff, prefer_earlier, + is_reserve=False, + ) + if best_opt is not None: + best_opt.click() + abs_diff = abs(actual_diff) + if actual_diff < 0: + relation = f"早了 {abs_diff} 分钟" + elif actual_diff > 0: + relation = f"晚了 {abs_diff} 分钟" + else: + relation = "正好等于 续约时间" + self._showTrace( + f"选择距离期望续约时间最近的 {best_text}, " + f"与期望续约时间相比 {relation}" + ) + record["time"]["end"] = best_text.strip() + renew_ok_btn.click() + self._shell.refresh() + return True + + self._showTrace( + "无法选择最近的可用续约时间 ! " + f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", + self.TraceLevel.WARNING, + ) + self._showTrace(f"当前可供续约的时间有: {free_times}") + self._shell.refresh() + return False + except (NoSuchElementException, TimeoutException) as e: + self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) + self._shell.refresh() + return False + except (ElementNotInteractableException) as e: + self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) + self._shell.refresh() + return False + except Exception as e: + self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) + self._shell.refresh() + return False + + def _validateRenewTime( + self, + end_time: str, + target_renew_mins: int, + ) -> bool: + + if target_renew_mins > self.LIBRARY_CLOSE_MINS: + actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time) + if actual_renew_duration <= 0: + self._showTrace( + f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR + ) + return False + self._showTrace( + f"续约时间已调整至闭馆时间 " + f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)}," + f"实际续约时长为 " + f"{actual_renew_duration // 60} 小时 " + f"{actual_renew_duration % 60} 分钟" + ) + return True diff --git a/src/pages/flows/ReserveFlow.py b/src/pages/flows/ReserveFlow.py new file mode 100644 index 0000000..037e36c --- /dev/null +++ b/src/pages/flows/ReserveFlow.py @@ -0,0 +1,272 @@ +# -*- 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 queue +from dataclasses import dataclass +from typing import Optional + +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + TimeoutException, +) +from selenium.webdriver.remote.webdriver import WebDriver + +from base.MsgBase import MsgBase +from pages.MainShell import MainShell +from pages.flows._helpers import ( + timeStrToMins, + minsToTimeStr, + findBestTimeOption, +) +from pages.ReserveView import ReserveView +from pages._dialogs import ReserveResultDialog + + +@dataclass +class ReserveContext: + + username: str + date: str + floor: str + room: str + seat_id: str + begin_time: str + end_time: str + begin_max_diff: int = 30 + end_max_diff: int = 30 + begin_prefer_early: bool = True + end_prefer_early: bool = False + expect_duration: int = 4 + satisfy_duration: bool = True + + +class ReserveFlow(MsgBase): + + LIBRARY_CLOSE_MINS = timeStrToMins("23:30") + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + driver: WebDriver, + shell: MainShell, + ) -> None: + + super().__init__(input_queue, output_queue) + self._driver: WebDriver = driver + self._shell: MainShell = shell + self._ctx: Optional[ReserveContext] = None + + def execute( + self, + ctx: ReserveContext, + ) -> bool: + + self._ctx = ctx + submit_reserve = False + reserve_success = False + have_hover_on_page = False + + try: + view = self._shell.gotoReserveView() + except (NoSuchElementException, TimeoutException) as e: + self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR) + return False + except Exception as e: + self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR) + return False + + if not view.selectDate(ctx.date): + self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR) + return False + self._showTrace(f"日期 {ctx.date} 选择成功 !") + + if not view.selectPlace("1"): + self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR) + return False + self._showTrace("预约场所 图书馆 选择成功 !") + + if not view.selectFloor(ctx.floor): + display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor) + self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR) + return False + self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !") + + if not view.selectRoom(ctx.room): + display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room) + self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) + return False + self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !") + have_hover_on_page = True + + seat_status = view.selectSeat(ctx.seat_id) + if seat_status is None: + self._showTrace( + f"座位 {ctx.seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", + self.TraceLevel.WARNING, + ) + else: + self._showTrace(f"座位 {ctx.seat_id} 选择成功 ! : 当前状态 - '{seat_status}'") + + select_time_ok = self._selectSeatTime(view) + if not select_time_ok: + self._showTrace("选择时间失败 !", self.TraceLevel.ERROR) + else: + try: + view.submitReserve() + submit_reserve = True + with ReserveResultDialog(self._driver) as result: + if result.isFailure(): + self._showTrace("预约失败", self.TraceLevel.ERROR) + elif result.isSuccess(): + details = result.getDetailTexts() + if len(details) >= 6: + self._showTrace( + f"\n" + f" 预约成功 !\n" + f" {details[1]}\n" + f" {details[2]}\n" + f" {details[3]}\n" + f" 签到时间 :{details[5]}" + ) + else: + self._showTrace( + "\n" + " 预约成功 !\n" + " 未找获取到详细信息" + ) + reserve_success = True + else: + self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR) + except (TimeoutException, ElementNotInteractableException): + self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) + except Exception: + self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) + + if not submit_reserve and have_hover_on_page: + view.refresh() + if reserve_success: + self._showTrace(f"用户 {ctx.username} 预约成功 !") + else: + self._showTrace(f"用户 {ctx.username} 预约失败 !", self.TraceLevel.ERROR) + return reserve_success + + def _selectSeatTime( + self, + view: ReserveView, + ) -> bool: + + ctx = self._ctx + exp_beg_tm_str = ctx.begin_time + exp_end_tm_str = ctx.end_time + exp_beg_mins = timeStrToMins(exp_beg_tm_str) + exp_end_mins = timeStrToMins(exp_end_tm_str) + act_beg_mins = exp_beg_mins + act_beg_tm_str = exp_beg_tm_str + act_end_mins = exp_end_mins + act_end_tm_str = exp_end_tm_str + + act_beg_mins = self._selectNearestTime( + view, + time_id="startTime", + time_type="开始时间", + target_time=exp_beg_mins, + max_time_diff=ctx.begin_max_diff, + prefer_earlier=ctx.begin_prefer_early, + ) + if act_beg_mins == -1: + return False + act_beg_tm_str = minsToTimeStr(act_beg_mins) + + if ctx.satisfy_duration: + exp_end_mins = self._calcEndTime(act_beg_mins, ctx.expect_duration) + exp_end_tm_str = minsToTimeStr(exp_end_mins) + self._showTrace( + f"需要满足期望预约持续时间: {ctx.expect_duration} 小时, " + f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}" + ) + + act_end_mins = self._selectNearestTime( + view, + time_id="end_time", + time_type="结束时间", + target_time=exp_end_mins, + max_time_diff=ctx.end_max_diff, + prefer_earlier=ctx.end_prefer_early, + ) + if act_end_mins == -1: + return False + act_end_tm_str = minsToTimeStr(act_end_mins) + + self._showTrace( + f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, " + f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}" + ) + return True + + def _selectNearestTime( + self, + view: ReserveView, + time_id: str, + time_type: str, + target_time: int, + max_time_diff: int, + prefer_earlier: bool, + ) -> int: + + all_time_opts = view.getAvailableTimeOptions(time_id) + if not all_time_opts: + self._showTrace( + f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR + ) + return -1 + + best_opt, best_text, actual_diff, free_times = findBestTimeOption( + all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True + ) + if best_opt is not None: + best_opt.click() + abs_diff = abs(actual_diff) + if actual_diff < 0: + relation = f"早了 {abs_diff} 分钟" + elif actual_diff > 0: + relation = f"晚了 {abs_diff} 分钟" + else: + relation = f"正好等于 {time_type}" + self._showTrace( + f"选择距离期望 {time_type} 最近的 {best_text}, " + f"与期望 {time_type} 相比 {relation}" + ) + return target_time + actual_diff + + target_time_str = minsToTimeStr(target_time) + self._showTrace( + f"无法选择最近的 {time_type} {target_time_str}, " + f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", + self.TraceLevel.WARNING, + ) + self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") + return -1 + + def _calcEndTime( + self, + begin_mins: int, + duration: int, + ) -> int: + + expect_end_mins = int(begin_mins + duration * 60) + if expect_end_mins > self.LIBRARY_CLOSE_MINS: + expect_end_mins = self.LIBRARY_CLOSE_MINS + self._showTrace( + f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, " + f"自动调整为 23:30", + self.TraceLevel.WARNING, + ) + return expect_end_mins diff --git a/src/pages/flows/__init__.py b/src/pages/flows/__init__.py new file mode 100644 index 0000000..c8f1d9f --- /dev/null +++ b/src/pages/flows/__init__.py @@ -0,0 +1,18 @@ +# -*- 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. +""" +from pages.flows.ReserveFlow import ReserveFlow +from pages.flows.CheckinFlow import CheckinFlow +from pages.flows.RenewFlow import RenewFlow + +__all__ = [ + "ReserveFlow", + "CheckinFlow", + "RenewFlow", +] diff --git a/src/pages/flows/_helpers.py b/src/pages/flows/_helpers.py new file mode 100644 index 0000000..1b2fcf5 --- /dev/null +++ b/src/pages/flows/_helpers.py @@ -0,0 +1,85 @@ +# -*- 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. +""" +from datetime import datetime + + +def timeStrToMins( + time_str: str, +) -> int: + + hour, minute = map(int, time_str.split(":")) + return hour * 60 + minute + + +def minsToTimeStr( + mins: int, +) -> str: + + hour, minute = divmod(int(mins), 60) + return f"{hour:02d}:{minute:02d}" + + +def findBestTimeOption( + time_options: list, + target_time: int, + max_time_diff: int, + prefer_earlier: bool, + is_reserve: bool = True, +) -> tuple: + """ + Find the best time option from available WebElement options. + + Returns: + (bestElement, bestText, actual_diff, freeTimesList) + or (None, None, None, freeTimesList) if no suitable option. + """ + + free_times = [] + best_time_diff = max_time_diff + best_actual_diff = None + best_time_opt = None + + for time_opt in time_options: + if is_reserve: + time_attr = time_opt.get_attribute("time") + if time_attr == "now": + now = datetime.now() + time_val = now.hour * 60 + now.minute + elif time_attr and time_attr.isdigit(): + time_val = int(time_attr) + else: + continue + else: + time_attr = time_opt.get_attribute("id") + 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 minsToTimeStr(time_val) + ) + actual_diff = time_val - target_time + abs_diff = abs(actual_diff) + + if abs_diff < best_time_diff or ( + 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/pages/services/CaptchaHandler.py b/src/pages/services/CaptchaHandler.py new file mode 100644 index 0000000..2aec267 --- /dev/null +++ b/src/pages/services/CaptchaHandler.py @@ -0,0 +1,101 @@ +# -*- 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 base64 +import queue + +import ddddocr +from selenium.common.exceptions import ( + NoSuchElementException, + TimeoutException, +) + +from base.MsgBase import MsgBase +from pages.LoginPage import LoginPage + + +class CaptchaHandler(MsgBase): + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + login_page: LoginPage, + ) -> None: + + super().__init__(input_queue, output_queue) + self._login_page = login_page + self._ocr = ddddocr.DdddOcr() + + def solveCaptcha( + self, + auto_captcha: bool = True, + ) -> str: + + max_attempts = 3 + for _ in range(max_attempts): + if auto_captcha: + captcha_text = self._autoRecognize() + else: + self._showTrace("用户未配置自动识别验证码, 请手动输入验证码 !", 20, no_log=True) + captcha_text = self._manualRecognize() + if captcha_text: + return captcha_text + else: + if not self._login_page.refreshCaptcha(): + return "" + self._showTrace( + f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !", + self.TraceLevel.WARNING, + ) + return "" + + def _autoRecognize( + self, + ) -> str: + + try: + img_src = self._login_page.getCaptchaImageSrc() + base64_str = img_src.split(',', 1)[1] + captcha_img = base64.b64decode(base64_str) + captcha_text = self._ocr.classification(captcha_img) + captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower() + self._showTrace(f"识别到验证码为 : '{captcha_text}'", 20, no_log=True) + if len(captcha_text) != 4: + self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) + raise Exception("识别到的验证码长度不等于 4 个字符 !") + return captcha_text + except (NoSuchElementException, TimeoutException) as e: + self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) + return "" + except (ValueError, OSError) as e: + self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) + return "" + except Exception as e: + self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) + return "" + + def _manualRecognize( + self, + ) -> str: + + try: + self._showMsg("请输入验证码:") + captcha_text = self._waitMsg(timeout=15) + self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True) + if len(captcha_text) != 4: + self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) + raise Exception("输入的验证码长度不等于 4 个字符 !") + return captcha_text + except ValueError as e: + self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) + return "" + except Exception as e: + self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) + return "" diff --git a/src/pages/services/RecordChecker.py b/src/pages/services/RecordChecker.py new file mode 100644 index 0000000..e00536a --- /dev/null +++ b/src/pages/services/RecordChecker.py @@ -0,0 +1,302 @@ +# -*- 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 queue +import re +import time +from datetime import datetime, timedelta + +from selenium.common.exceptions import ( + NoSuchElementException, + StaleElementReferenceException, + TimeoutException, +) + +from base.MsgBase import MsgBase +from pages.MainShell import MainShell +from pages.RecordsView import RecordsView + + +class RecordChecker(MsgBase): + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + shell: MainShell, + ) -> None: + + super().__init__(input_queue, output_queue) + self._shell = shell + + @staticmethod + def _formatDiffTime( + seconds: float, + ) -> str: + + hours = int(seconds // 3600) + minutes = int(seconds % 3600 // 60) + seconds = int(seconds % 60) + return f"{hours} 时 {minutes} 分 {seconds} 秒" + + def canReserve( + self, + date: str, + ) -> bool: + + if self._getReserveRecord(date, "已预约") is None: + if self._getReserveRecord(date, "使用中") is None: + self._showTrace(f"用户在 {date} 可以预约") + return True + self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约") + return False + self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约") + return False + + def canCheckin( + self, + ) -> bool: + + date = time.strftime("%Y-%m-%d", time.localtime()) + record = self._getReserveRecord(date, "已预约") + if record is not None: + begin_time = record["time"]["begin"] + begin_time = datetime.strptime( + f"{date} {begin_time}", "%Y-%m-%d %H:%M" + ) + time_diff = datetime.now() - begin_time + time_diff_seconds = time_diff.total_seconds() + if time_diff_seconds < -30 * 60: + self._showTrace( + f"用户在 {date} 的预约开始时间为 {begin_time}, " + f"当前距离预约开始时间还有 " + f"{self._formatDiffTime(abs(time_diff_seconds))}, 无法签到" + ) + return False + elif -30 * 60 <= time_diff_seconds < 0: + self._showTrace( + f"用户在 {date} 的预约开始时间为 {begin_time}, " + f"当前距离预约开始时间还有 " + f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" + ) + return True + elif 0 <= time_diff_seconds < 30 * 60 - 5: + self._showTrace( + f"用户在 {date} 的预约开始时间为 {begin_time}, " + f"当前距离预约开始时间已经过去 " + f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" + ) + return True + self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到") + return False + + def canRenew( + self, + ) -> tuple[bool, dict]: + + date = time.strftime("%Y-%m-%d", time.localtime()) + record = self._getReserveRecord(date, "使用中") + if record is not None: + end_time = record["time"]["end"] + end_time = datetime.strptime( + f"{date} {end_time}", "%Y-%m-%d %H:%M" + ) + time_diff = end_time - datetime.now() + time_diff_seconds = time_diff.total_seconds() + trace_msg = ( + f"用户在 {date} 的预约结束时间为 {end_time}, " + f"当前距离预约结束时间还有 " + f"{self._formatDiffTime(abs(time_diff_seconds))}" + ) + if abs(time_diff_seconds) < 120 * 60: + self._showTrace(f"{trace_msg}, 可以续约") + return True, record + else: + self._showTrace(f"{trace_msg}, 无法续约") + return False, None + self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") + return False, None + + def postRenewCheck( + self, + record: dict, + ) -> bool: + + date = record["date"] + act_record = self._getReserveRecord(date, "使用中") + if act_record is not None: + if ( + act_record["time"]["begin"] == record["time"]["begin"] + and act_record["time"]["end"] == record["time"]["end"] + ): + self._showTrace( + f"\n" + f" 续约成功 !\n" + f" 日 期 :{date}\n" + f" 时 间 :{act_record['time']['begin']}" + f" - {act_record['time']['end']}\n" + f" 位 置 :{act_record['info']['location']}\n" + f" 状 态 :{act_record['info']['status']}" + ) + return True + else: + self._showTrace( + f"\n" + f" 续约失败 !\n" + f" 续约后结束时间为 {act_record['time']['end']}," + f"与预期结束时间 {record['time']['end']} 不符 !" + ) + return False + self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") + return False + + def _getReserveRecord( + self, + wanted_date: str, + wanted_status: str, + ) -> dict | None: + + if wanted_date is None: + self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING) + return None + self._showTrace( + f"正在检查用户在 {wanted_date} 是否有预约状态为 " + f"{wanted_status} 的预约记录......", 20, no_log=True + ) + + checked_count = 0 + max_check_times = 6 + + records_view = self._shell.gotoRecordsView() + for _ in range(max_check_times): + reservations = records_view.loadRecords() + if reservations is None: + return None + for reservation in reservations[checked_count:]: + record = self._decodeReserveRecord(reservation, records_view) + checked_count += 1 + if record is None: + continue + if record["date"] == "": + continue + if record["time"] == {"begin": "", "end": ""}: + continue + if ( + datetime.strptime(record["date"], "%Y-%m-%d").date() + > datetime.strptime(wanted_date, "%Y-%m-%d").date() + ): + continue + if ( + datetime.strptime(record["date"], "%Y-%m-%d").date() + < datetime.strptime(wanted_date, "%Y-%m-%d").date() + ): + return None + if record["info"]["status"] == wanted_status: + self._showTrace( + f"寻找到用户第 {checked_count} 条状态为 " + f"{wanted_status} 的预约记录, " + f"详细信息: {record['date']} " + f"{record['time']['begin']} - " + f"{record['time']['end']} " + f"{record['info']['location']}", + 20, no_log=True, + ) + return record + if not records_view.showMoreRecords(): + break + return None + + def _decodeReserveRecord( + self, + reservation, + records_view: RecordsView, + ) -> dict: + + try: + time_element = records_view.getRecordTimeElement(reservation) + info_elements = records_view.getRecordInfoElements(reservation) + except (NoSuchElementException, TimeoutException, StaleElementReferenceException): + return { + "date": "", + "time": {"begin": "", "end": ""}, + "info": {"location": "", "status": ""}, + } + except Exception: + return { + "date": "", + "time": {"begin": "", "end": ""}, + "info": {"location": "", "status": ""}, + } + time_data = self._decodeReserveTime(time_element) + info_data = self._decodeReserveInfo(info_elements) + return { + "date": time_data["date"], + "time": time_data["time"], + "info": info_data, + } + + def _decodeReserveTime( + self, + time_element, + ) -> dict: + + time_str = time_element.text.strip() + today = datetime.now().date() + if "明天" in time_str: + target_date = today + timedelta(days=1) + date = target_date.strftime("%Y-%m-%d") + elif "今天" in time_str: + target_date = today + date = target_date.strftime("%Y-%m-%d") + elif "昨天" in time_str: + target_date = today - timedelta(days=1) + date = target_date.strftime("%Y-%m-%d") + else: + date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str) + if date_match: + date = date_match.group(1) + else: + date = "" + time_match = re.search( + r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str + ) + if time_match: + begin_time = time_match.group(1) + end_time = time_match.group(2) + else: + begin_time = "" + end_time = "" + return { + "date": date, + "time": {"begin": begin_time, "end": end_time}, + } + + def _decodeReserveInfo( + self, + info_elements, + ) -> dict: + + location = "" + status = "" + for info in info_elements: + if "已预约" in info.text: + status = "已预约" + elif "使用中" in info.text: + status = "使用中" + elif "已完成" in info.text: + status = "已完成" + elif "已结束使用" in info.text: + status = "已结束使用" + elif "已取消" in info.text: + status = "已取消" + elif "失约" in info.text: + status = "失约" + elif "图书馆" in info.text: + location = info.text.strip() + return {"location": location, "status": status} diff --git a/src/pages/services/ReserveValidator.py b/src/pages/services/ReserveValidator.py new file mode 100644 index 0000000..5587839 --- /dev/null +++ b/src/pages/services/ReserveValidator.py @@ -0,0 +1,221 @@ +# -*- 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 queue +import time + +from base.MsgBase import MsgBase +from pages.ReserveView import ReserveView +from pages.flows._helpers import timeStrToMins, minsToTimeStr + + +class ReserveValidator(MsgBase): + + def __init__( + self, + input_queue: queue.Queue, + output_queue: queue.Queue, + ) -> None: + + super().__init__(input_queue, output_queue) + + def validate( + self, + reserve_info: dict, + ) -> bool: + + if not self._containRequiredInfo(reserve_info): + return False + if not self._isValidDate(reserve_info): + return False + if not self._isValidBeginTime(reserve_info): + return False + if not self._isValidExpectDuration(reserve_info): + return False + if not self._isValidEndTime(reserve_info): + return False + if not self._finalCheck(reserve_info): + return False + self._showTrace( + f"预约信息检查完成, 准备预约 " + f"{reserve_info['date']} " + f"{reserve_info['begin_time']['time']} - " + f"{reserve_info['end_time']['time']} " + f"图书馆 " + f"{ReserveView.FLOOR_MAP[reserve_info['floor']]} " + f"{ReserveView.ROOM_MAP[reserve_info['room']]} " + f"的座位 {reserve_info['seat_id']}" + ) + return True + + def _containRequiredInfo( + self, + reserve_info: dict, + ) -> bool: + + floor_map = ReserveView.FLOOR_MAP + room_map = ReserveView.ROOM_MAP + try: + if reserve_info.get("floor") is None: + raise ValueError("未指定楼层") + if reserve_info["floor"] not in floor_map: + raise ValueError(f"该楼层 '{reserve_info['floor']}' 不存在") + if reserve_info.get("room") is None: + raise ValueError("未指定房间") + if reserve_info["room"] not in room_map: + raise ValueError(f"该房间 '{reserve_info['room']}' 不存在") + if reserve_info.get("seat_id") is None: + raise ValueError("未指定座位") + if reserve_info["seat_id"] == "": + raise ValueError("未指定座位号") + return True + except ValueError as e: + msg = ( + f"预约信息错误 ! : {e}, " + f"由于缺少必要的预约信息, 无法开始预约流程" + ) + self._showTrace(msg, self.TraceLevel.ERROR) + self._showTrace( + f"预约信息错误 ! : {e}, " + f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整", + 20, + no_log=True, + ) + return False + + def _isValidDate( + self, + reserve_info: dict, + ) -> bool: + + 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_str + self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date_str}") + else: + 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_str}, 自动设置为当前日期", + self.TraceLevel.WARNING, + ) + reserve_info["date"] = cur_date_str + return True + + def _isValidBeginTime( + self, + reserve_info: dict, + ) -> bool: + + cur_time = time.strftime("%H:%M", time.localtime()) + if reserve_info.get("begin_time") is None: + reserve_info["begin_time"] = {} + if "time" not in reserve_info["begin_time"]: + reserve_info["begin_time"]["time"] = cur_time + self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}") + if "max_diff" not in reserve_info["begin_time"]: + reserve_info["begin_time"]["max_diff"] = 30 + self._showTrace("开始时间最大时间差未指定, 自动设置为 30 分钟") + if "prefer_early" not in reserve_info["begin_time"]: + reserve_info["begin_time"]["prefer_early"] = True + self._showTrace("是否优先选择更早开始时间未指定, 自动设置为 True") + return True + + def _isValidExpectDuration( + self, + reserve_info: dict, + ) -> bool: + + if reserve_info.get("satisfy_duration") is None: + reserve_info["satisfy_duration"] = True + self._showTrace("预约满足时长要求未指定, 默认满足") + if reserve_info["satisfy_duration"]: + if reserve_info.get("expect_duration") is None: + reserve_info["expect_duration"] = 4 + self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时") + return True + + def _isValidEndTime( + self, + reserve_info: dict, + ) -> bool: + + if reserve_info.get("end_time") is None: + reserve_info["end_time"] = {} + if "time" not in reserve_info["end_time"]: + end_mins = timeStrToMins(reserve_info["begin_time"]["time"]) + end_mins = end_mins + int(reserve_info["expect_duration"] * 60) + reserve_info["end_time"] = { + "time": minsToTimeStr(end_mins), + "max_diff": 30, + "prefer_early": False, + } + self._showTrace( + f"结束时间未指定, 自动设置为开始时间加上期望时长: " + f"{reserve_info['end_time']['time']}" + ) + if "max_diff" not in reserve_info["end_time"]: + reserve_info["end_time"]["max_diff"] = 30 + self._showTrace("结束时间最大时间差未指定, 自动设置为 30 分钟") + if "prefer_early" not in reserve_info["end_time"]: + reserve_info["end_time"]["prefer_early"] = False + self._showTrace("是否优先选择较晚结束时间未指定, 自动设置为 True") + return True + + def _finalCheck( + self, + reserve_info: dict, + ) -> bool: + + begin_time = reserve_info["begin_time"] + end_time = reserve_info["end_time"] + begin_mins = timeStrToMins(begin_time["time"]) + end_mins = timeStrToMins(end_time["time"]) + + if end_mins < begin_mins and reserve_info["satisfy_duration"] is False: + self._showTrace( + f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, " + f"尝试交换时间", + self.TraceLevel.WARNING, + ) + reserve_info["end_time"], reserve_info["begin_time"] = begin_time, end_time + begin_time, end_time = end_time, begin_time + begin_mins = timeStrToMins(begin_time["time"]) + end_mins = timeStrToMins(end_time["time"]) + + max_end_mins = timeStrToMins("23:30") + if end_mins > max_end_mins: + self._showTrace( + f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30", + self.TraceLevel.WARNING, + ) + reserve_info["end_time"]["time"] = "23:30" + end_mins = max_end_mins + + if reserve_info["satisfy_duration"]: + if reserve_info["expect_duration"] > 8: + self._showTrace( + f"该用户设置了优先满足时长要求, 但是预约期望持续时间 " + f"{reserve_info['expect_duration']} 小时 " + f"超出最大时长 8 小时, 自动设置为 8 小时", + self.TraceLevel.WARNING, + ) + reserve_info["expect_duration"] = 8 + else: + if end_mins - begin_mins > 8 * 60: + self._showTrace( + f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 " + f"{float((end_mins - begin_mins) / 60)} 小时 " + f"超出最大时长 8 小时, 自动设置为 8 小时", + self.TraceLevel.WARNING, + ) + reserve_info["end_time"]["time"] = minsToTimeStr(begin_mins + 8 * 60) + return True diff --git a/src/pages/services/__init__.py b/src/pages/services/__init__.py new file mode 100644 index 0000000..8545fc0 --- /dev/null +++ b/src/pages/services/__init__.py @@ -0,0 +1,18 @@ +# -*- 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. +""" +from pages.services.CaptchaHandler import CaptchaHandler +from pages.services.ReserveValidator import ReserveValidator +from pages.services.RecordChecker import RecordChecker + +__all__ = [ + "CaptchaHandler", + "ReserveValidator", + "RecordChecker", +] diff --git a/src/test_pages_refactor.py b/src/test_pages_refactor.py new file mode 100644 index 0000000..bb6f28e --- /dev/null +++ b/src/test_pages_refactor.py @@ -0,0 +1,162 @@ +# -*- 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. + +AutoLibrary 真实运行测试脚本。 +在 venv 中运行: + py -3 test_pages_refactor.py [--mode MODE] + +MODE 可选值 (默认 1): + 1 = 只预约 + 2 = 只签到 + 3 = 预约 + 签到 + 4 = 只续约 + 7 = 全部 (预约 + 签到 + 续约) +""" +import os +import sys +import argparse + +SRC = os.path.dirname(os.path.abspath(__file__)) +if SRC not in sys.path: + sys.path.insert(0, SRC) + + +def getAppConfigDir() -> str: + appData = os.environ.get("APPDATA", "") + if not appData: + appData = os.path.join(os.path.expanduser("~"), "AppData", "Roaming") + return os.path.join(appData, "AutoLibrary", "configs") + + +def main(): + parser = argparse.ArgumentParser(description="AutoLibrary 真实运行测试") + parser.add_argument( + "--mode", type=int, default=1, + help="运行模式 bitmask: 1=预约 2=签到 4=续约 (默认 1)" + ) + parser.add_argument( + "--group", type=int, default=0, + help="只运行第 N 个启用的任务组 (0=全部, 默认 0)" + ) + parser.add_argument( + "--headless", action="store_true", + help="使用 headless 模式运行浏览器" + ) + args = parser.parse_args() + + # ---- 1. 初始化 ConfigManager ---- + from managers.config.ConfigManager import instance as configInstance + from managers.config.ConfigUtils import ConfigUtils + from utils.JSONReader import JSONReader + + configDir = getAppConfigDir() + if not os.path.isdir(configDir): + print(f"[FAIL] 配置目录不存在: {configDir}") + print("请先启动一次 AutoLibrary GUI 以生成配置文件。") + return 1 + + try: + configInstance(configDir) + except ValueError: + pass + + configPaths = ConfigUtils.getAutomationConfigPaths() + runPath = configPaths.get("run") + userPath = configPaths.get("user") + + if not runPath or not os.path.isfile(runPath): + print(f"[FAIL] run.json 不存在: {runPath}") + return 1 + if not userPath or not os.path.isfile(userPath): + print(f"[FAIL] user.json 不存在: {userPath}") + return 1 + + print(f"[INFO] run : {runPath}") + print(f"[INFO] user : {userPath}") + + # ---- 2. 加载配置 ---- + runConfig = JSONReader(runPath).data() + userConfig = JSONReader(userPath).data() + + if args.mode is not None: + runConfig["mode"]["run_mode"] = args.mode + if args.headless: + runConfig["web_driver"]["headless"] = True + + groups = userConfig.get("groups", []) + if not groups: + print("[FAIL] user.json 中没有任务组") + return 1 + + print(f"[INFO] 运行模式: {runConfig['mode']['run_mode']}") + if args.headless: + print("[INFO] Headless 模式已启用") + + # ---- 3. 创建 AutoLib 并运行 ---- + from pages.AutoLibPages import AutoLibPages + import queue + import threading + + for gi, group in enumerate(groups): + if args.group > 0 and gi + 1 != args.group: + continue + if not group.get("enabled", True): + print(f"[SKIP] 任务组 {gi + 1} '{group.get('name', '未命名')}' 已禁用") + continue + + users = group.get("users", []) + enabledUsers = [u for u in users if u.get("enabled", True)] + if not enabledUsers: + print(f"[SKIP] 任务组 {gi + 1} 没有启用的用户") + continue + + print(f"\n{'=' * 60}") + print(f"任务组 {gi + 1}/{len(groups)}: '{group.get('name', '未命名')}'") + print(f"启用的用户: {len(enabledUsers)}/{len(users)}") + print(f"{'=' * 60}") + + outputQueue = queue.Queue() + stopConsumer = threading.Event() + traceLines = [] + + def consumeTrace(): + while not stopConsumer.is_set(): + try: + msg = outputQueue.get(timeout=0.3) + traceLines.append(msg) + print(msg) + except queue.Empty: + continue + + consumer = threading.Thread(target=consumeTrace, daemon=True) + consumer.start() + + try: + autoLib = AutoLibPages( + input_queue=queue.Queue(), + output_queue=outputQueue, + run_config=runConfig, + ) + autoLib.run({"users": enabledUsers}) + autoLib.close() + except Exception as e: + print(f"[FAIL] 运行异常: {e}") + import traceback + traceback.print_exc() + return 1 + finally: + stopConsumer.set() + consumer.join(timeout=2) + + print("\n[OK] 测试完成") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From a6bc103c73a549aca4c934799adecb944c6dc272 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 26 May 2026 13:41:55 +0800 Subject: [PATCH 29/49] =?UTF-8?q?refactor(pages):=20=E6=8B=86=E5=88=86=20?= =?UTF-8?q?=5Fdialogs=20=E4=B8=BA=E7=8B=AC=E7=AB=8B=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E8=A7=A3=E8=80=A6=20Service=20?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=EF=BC=8C=E6=B6=88=E9=99=A4?= =?UTF-8?q?=20PageObject=20=E9=87=8D=E5=A4=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 _dialogs.py 拆分为 pages/components/ 下的独立文件,Overlay 基类同步移入 - CaptchaHandler / RecordChecker 构造函数不再持有 PageObject,改为方法参数注入 - LoginPage.login() 直接接收 auto_captcha 参数,简化 captcha_solver 调用链 - SeatMapOverlay.selectSeat 引入两层查找:先按 ID 直查,失败后遍历匹配 - 移除 ReserveView 中与 Dialog/Overlay 重复的方法(selectSeat、getAvailableTimeOptions) - AutoLibPages 拆分 __initPagesServices / __initPagesFlows - 修复 RecordsView.MORE_BTN 选择器被错误 snake_case 化(more_btn → moreBtn) Co-Authored-By: Claude Opus 4.7 --- src/pages/AutoLibPages.py | 14 +- src/pages/LoginPage.py | 5 +- src/pages/RecordsView.py | 2 +- src/pages/ReserveView.py | 69 +--- src/pages/__init__.py | 12 +- src/pages/_dialogs.py | 302 ------------------ src/pages/components/CheckinResultDialog.py | 75 +++++ .../{_overlay.py => components/Overlay.py} | 0 src/pages/components/RenewDialog.py | 99 ++++++ src/pages/components/ReserveResultDialog.py | 81 +++++ src/pages/components/SeatMapOverlay.py | 74 +++++ src/pages/components/TimeSelectDialog.py | 54 ++++ src/pages/components/__init__.py | 22 ++ src/pages/flows/CheckinFlow.py | 2 +- src/pages/flows/RenewFlow.py | 2 +- src/pages/flows/ReserveFlow.py | 34 +- src/pages/services/CaptchaHandler.py | 52 +-- src/pages/services/RecordChecker.py | 231 +++++++------- src/pages/services/ReserveValidator.py | 58 ++-- 19 files changed, 608 insertions(+), 580 deletions(-) delete mode 100644 src/pages/_dialogs.py create mode 100644 src/pages/components/CheckinResultDialog.py rename src/pages/{_overlay.py => components/Overlay.py} (100%) create mode 100644 src/pages/components/RenewDialog.py create mode 100644 src/pages/components/ReserveResultDialog.py create mode 100644 src/pages/components/SeatMapOverlay.py create mode 100644 src/pages/components/TimeSelectDialog.py create mode 100644 src/pages/components/__init__.py diff --git a/src/pages/AutoLibPages.py b/src/pages/AutoLibPages.py index a200a10..faab6e7 100644 --- a/src/pages/AutoLibPages.py +++ b/src/pages/AutoLibPages.py @@ -9,7 +9,6 @@ See the LICENSE file for details. """ import os import queue - from selenium import webdriver from selenium.common.exceptions import ( TimeoutException, @@ -193,12 +192,10 @@ class AutoLibPages(MsgBase): self.__captcha_handler = CaptchaHandler( input_queue=self._input_queue, output_queue=self._output_queue, - login_page=self.__login_page, ) self.__record_checker = RecordChecker( input_queue=self._input_queue, output_queue=self._output_queue, - shell=self.__shell, ) self.__reserve_validator = ReserveValidator( input_queue=self._input_queue, @@ -245,7 +242,8 @@ class AutoLibPages(MsgBase): if not self.__login_page.login( username, password, - captcha_solver=lambda: self.__captcha_handler.solveCaptcha(auto_captcha), + captcha_solver=self.__captcha_handler.solveCaptcha, + auto_captcha=auto_captcha, tracer=self._showTrace, log_level=self.TraceLevel, max_attempts=login_config.get("max_attempt", 3), @@ -259,7 +257,7 @@ class AutoLibPages(MsgBase): } # reserve if run_mode["auto_reserve"]: - if self.__record_checker.canReserve(reserve_info.get("date")): + if self.__record_checker.canReserve(self.__shell, reserve_info.get("date")): if self.__reserve_validator.validate(reserve_info): ctx = ReserveContext( username=username, @@ -289,7 +287,7 @@ class AutoLibPages(MsgBase): # checkin last_result: int = result if run_mode["auto_checkin"] and last_result != 1: - if self.__record_checker.canCheckin(): + if self.__record_checker.canCheckin(self.__shell): if self.__checkin_flow.execute(username): result = 0 else: @@ -303,11 +301,11 @@ class AutoLibPages(MsgBase): # renewal last_result = result if run_mode["auto_renewal"] and last_result != 1: - can_renew, record = self.__record_checker.canRenew() + can_renew, record = self.__record_checker.canRenew(self.__shell) if can_renew: renew_info: dict = reserve_info.get("renew_time", {}) if self.__renew_flow.execute(username, record, renew_info): - if self.__record_checker.postRenewCheck(record): + if self.__record_checker.postRenewCheck(self.__shell, record): self._showTrace(f"用户 {username} 续约成功 !") result = 0 else: diff --git a/src/pages/LoginPage.py b/src/pages/LoginPage.py index 24c9b1b..f4f231d 100644 --- a/src/pages/LoginPage.py +++ b/src/pages/LoginPage.py @@ -175,7 +175,8 @@ class LoginPage: self, username: str, password: str, - captcha_solver: Callable[[], str], + captcha_solver: Callable[["LoginPage", bool], str], + auto_captcha: bool, tracer: Callable[..., None], log_level: type, max_attempts: int = 5, @@ -189,7 +190,7 @@ class LoginPage: ) if not self.fillCredentials(username, password): continue - captcha_text = captcha_solver() + captcha_text = captcha_solver(self, auto_captcha) if not captcha_text: continue if not self.fillCaptcha(captcha_text): diff --git a/src/pages/RecordsView.py b/src/pages/RecordsView.py index d4f838c..dc42faa 100644 --- a/src/pages/RecordsView.py +++ b/src/pages/RecordsView.py @@ -22,7 +22,7 @@ from selenium.common.exceptions import ( class RecordsView: RECORDS_LIST = (By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)") - MORE_BTN = (By.ID, "more_btn") + MORE_BTN = (By.ID, "moreBtn") RECORD_TIME = (By.CSS_SELECTOR, "dt") RECORD_INFO = (By.CSS_SELECTOR, "a") diff --git a/src/pages/ReserveView.py b/src/pages/ReserveView.py index 13c4cb7..f0710d8 100644 --- a/src/pages/ReserveView.py +++ b/src/pages/ReserveView.py @@ -15,12 +15,11 @@ from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.remote.webdriver import WebDriver from selenium.common.exceptions import ( ElementNotInteractableException, - NoSuchElementException, - StaleElementReferenceException, TimeoutException, ) -from pages._dialogs import SeatMapOverlay, ReserveResultDialog +from pages.components.SeatMapOverlay import SeatMapOverlay +from pages.components.ReserveResultDialog import ReserveResultDialog class ReserveView: @@ -40,17 +39,8 @@ class ReserveView: FIND_ROOM_BTN = (By.ID, "findRoom") ROOM_BTN_FMT = "room_{room}" - SEAT_LAYOUT = (By.ID, "seatLayout") - SEAT_ITEMS = (By.CSS_SELECTOR, "li[id^='seat_']") RESERVE_BTN = (By.ID, "reserveBtn") - START_TIME_OPTS = (By.CSS_SELECTOR, "#startTime ul li a") - END_TIME_OPTS = (By.CSS_SELECTOR, "#endTime ul li a") - - RESULT_DIALOG = (By.CLASS_NAME, "layoutSeat") - RESULT_TITLE = (By.CSS_SELECTOR, ".layoutSeat dt") - RESULT_DETAIL = (By.CSS_SELECTOR, ".layoutSeat dd") - FLOOR_MAP = {"2": "二层", "3": "三层", "4": "四层", "5": "五层"} ROOM_MAP = { "1": "二层内环", "2": "二层西区", "3": "三层内环", "4": "三层外环", @@ -138,41 +128,6 @@ class ReserveView: return SeatMapOverlay(self._driver) - def selectSeat( - self, - seat_id: str, - ) -> str | None: - - try: - WebDriverWait(self._driver, 2).until( - EC.presence_of_element_located(self.SEAT_LAYOUT) - ) - WebDriverWait(self._driver, 2).until( - EC.presence_of_all_elements_located(self.SEAT_ITEMS) - ) - except TimeoutException: - return None - except Exception: - return None - try: - all_seats = self._driver.find_elements(*self.SEAT_ITEMS) - seat_id_upper = seat_id.lstrip('0').upper() - for seat in all_seats: - if not seat_id_upper == seat.text.lstrip('0'): - continue - seat_link = seat.find_element(By.TAG_NAME, "a") - WebDriverWait(self._driver, 2).until( - EC.element_to_be_clickable(seat_link) - ) - seat_link.click() - return seat_link.get_attribute("title") - return None - except (NoSuchElementException, TimeoutException, - StaleElementReferenceException, ElementNotInteractableException): - return None - except Exception: - return None - def submitReserve( self, ) -> bool: @@ -193,26 +148,6 @@ class ReserveView: return ReserveResultDialog(self._driver) - def getAvailableTimeOptions( - self, - time_id: str, - ) -> list: - - try: - WebDriverWait(self._driver, 2).until( - EC.presence_of_all_elements_located( - (By.CSS_SELECTOR, f"#{time_id} ul li a") - ) - ) - except TimeoutException: - return [] - except Exception: - return [] - return self._driver.find_elements( - By.CSS_SELECTOR, - f"#{time_id} ul li a", - ) - def refresh( self, ) -> None: diff --git a/src/pages/__init__.py b/src/pages/__init__.py index 36f0450..391cc81 100644 --- a/src/pages/__init__.py +++ b/src/pages/__init__.py @@ -12,13 +12,11 @@ from pages.LoginPage import LoginPage from pages.MainShell import MainShell from pages.ReserveView import ReserveView from pages.RecordsView import RecordsView -from pages._dialogs import ( - SeatMapOverlay, - TimeSelectDialog, - ReserveResultDialog, - CheckinResultDialog, - RenewDialog, -) +from pages.components.SeatMapOverlay import SeatMapOverlay +from pages.components.TimeSelectDialog import TimeSelectDialog +from pages.components.ReserveResultDialog import ReserveResultDialog +from pages.components.CheckinResultDialog import CheckinResultDialog +from pages.components.RenewDialog import RenewDialog __all__ = [ "AutoLibPages", diff --git a/src/pages/_dialogs.py b/src/pages/_dialogs.py deleted file mode 100644 index dc52602..0000000 --- a/src/pages/_dialogs.py +++ /dev/null @@ -1,302 +0,0 @@ -# -*- 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. -""" -from selenium.common.exceptions import ( - ElementNotInteractableException, - NoSuchElementException, - StaleElementReferenceException, - TimeoutException, -) -from selenium.webdriver.common.by import By -from selenium.webdriver.remote.webdriver import WebDriver -from selenium.webdriver.remote.webelement import WebElement -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from pages._overlay import Overlay - - -class SeatMapOverlay(Overlay): - """ - Seat selection overlay that opens after choosing a floor and room. - """ - - ROOT = (By.ID, "seatLayout") - SEAT_ITEMS = (By.CSS_SELECTOR, "li[id^='seat_']") - - def __init__( - self, - driver: WebDriver, - ) -> None: - - super().__init__(driver, self.ROOT) - - def selectSeat( - self, - seat_id: str, - ) -> str | None: - - try: - self._waitAllPresence(self.SEAT_ITEMS) - except (NoSuchElementException, TimeoutException): - return None - except Exception: - return None - try: - all_seats = self._findAll(*self.SEAT_ITEMS) - seat_id_upper = seat_id.lstrip('0').upper() - for seat in all_seats: - if not seat_id_upper == seat.text.lstrip('0'): - continue - seat_link = seat.find_element(By.TAG_NAME, "a") - self._waitClickable((By.TAG_NAME, "a")) - seat_link.click() - return seat_link.get_attribute("title") - return None - except (NoSuchElementException, TimeoutException, - ElementNotInteractableException, StaleElementReferenceException): - return None - except Exception: - return None - - -class TimeSelectDialog(Overlay): - """ - Time selection panel that appears after selecting a seat. - - Contains start-time and end-time option lists. - Does NOT auto-close — the reserve submission handles cleanup. - """ - - ROOT = (By.CSS_SELECTOR, "#startTime ul") - - def __init__( - self, - driver: WebDriver, - ) -> None: - - super().__init__(driver, self.ROOT, auto_close_on_exit=False) - - def getTimeOptions( - self, - time_id: str, - ) -> list[WebElement]: - - try: - self._waitAllPresence( - (By.CSS_SELECTOR, f"#{time_id} ul li a") - ) - except (NoSuchElementException, TimeoutException): - return [] - except Exception: - return [] - return self._findAll( - By.CSS_SELECTOR, - f"#{time_id} ul li a", - ) - - -class ReserveResultDialog(Overlay): - """ - Reservation result dialog shown after submitting a reserve request. - """ - - ROOT = (By.CLASS_NAME, "layoutSeat") - - def __init__( - self, - driver: WebDriver, - ) -> None: - - super().__init__(driver, self.ROOT, auto_close_on_exit=False) - - def getTitle( - self, - ) -> str: - - try: - return self._find(*self._titleLocator()).text - except (NoSuchElementException, StaleElementReferenceException): - return "" - except Exception: - return "" - - def isSuccess( - self, - ) -> bool: - - title = self.getTitle() - return any( - kw in title - for kw in ("预定好了", "预约成功", "操作成功") - ) - - def isFailure( - self, - ) -> bool: - - contents = self.getDetailTexts() - return any( - "预约失败" in msg or "已有1个有效预约" in msg - for msg in contents - ) - - def getDetailTexts( - self, - ) -> list[str]: - - try: - elements = self._findAll(By.CSS_SELECTOR, ".layoutSeat dd") - return [el.text for el in elements if el.text.strip()] - except (NoSuchElementException, StaleElementReferenceException): - return [] - except Exception: - return [] - - def _titleLocator( - self, - ) -> tuple: - - return (By.CSS_SELECTOR, ".layoutSeat dt") - - -class CheckinResultDialog(Overlay): - """ - Check-in result dialog. - """ - - ROOT = (By.CLASS_NAME, "ui_dialog") - - RESULT_MSG = (By.CLASS_NAME, "resultMessage") - OK_BTN = (By.CLASS_NAME, "btnOK") - DETAIL_DD = (By.CSS_SELECTOR, ".resultMessage dd") - - def __init__( - self, - driver: WebDriver, - ) -> None: - - super().__init__(driver, self.ROOT, auto_close_on_exit=False) - - def getResultMessage( - self, - ) -> str: - - try: - self._waitPresence(self.RESULT_MSG) - el = self._find(*self.RESULT_MSG) - return el.text - except (TimeoutException, NoSuchElementException, StaleElementReferenceException): - return "" - except Exception: - return "" - - def getDetails( - self, - ) -> list[str]: - - try: - elements = self._findAll(*self.DETAIL_DD) - return [el.text for el in elements if el.text.strip()] - except (NoSuchElementException, StaleElementReferenceException): - return [] - except Exception: - return [] - - def clickOk( - self, - ) -> bool: - - try: - self._waitClickable(self.OK_BTN).click() - return True - except (NoSuchElementException, TimeoutException, ElementNotInteractableException): - return False - except Exception: - return False - - -class RenewDialog(Overlay): - """ - Renewal time selection dialog. - """ - - ROOT = (By.ID, "extendDiv") - - MESSAGE_HEAD = (By.CSS_SELECTOR, "#extendDiv p.messageHead") - RESULT_MSG = (By.CSS_SELECTOR, "#extendDiv div.resultMessage") - TIME_OPTS = (By.CSS_SELECTOR, "#extendDiv .renewal_List li") - OK_BTN = (By.CSS_SELECTOR, "#extendDiv .btnOK") - - def __init__( - self, - driver: WebDriver, - ) -> None: - - super().__init__(driver, self.ROOT, auto_close_on_exit=False) - - def waitUntilReady( - self, - ) -> bool: - - try: - self._waitVisible(self.ROOT) - self._waitPresence(self.MESSAGE_HEAD) - self._waitPresence(self.RESULT_MSG) - except (NoSuchElementException, TimeoutException): - return False - except Exception: - return False - head_msg = self._find(*self.MESSAGE_HEAD).text.strip() - if "警告" in head_msg: - return False - try: - self._waitAllPresence(self.TIME_OPTS) - self._waitPresence(self.OK_BTN) - except (NoSuchElementException, TimeoutException): - return False - except Exception: - return False - return True - - def getHeadMessage( - self, - ) -> str: - - return self._find(*self.MESSAGE_HEAD).text.strip() - - def getResultMessage( - self, - ) -> str: - - return self._find(*self.RESULT_MSG).text.strip() - - def getTimeOptions( - self, - ) -> list[WebElement]: - - return self._findAll(*self.TIME_OPTS) - - def getOkButton( - self, - ) -> WebElement: - - return self._find(*self.OK_BTN) - - def clickOk( - self, - ) -> bool: - - try: - self._find(*self.OK_BTN).click() - return True - except (NoSuchElementException, TimeoutException, ElementNotInteractableException): - return False - except Exception: - return False diff --git a/src/pages/components/CheckinResultDialog.py b/src/pages/components/CheckinResultDialog.py new file mode 100644 index 0000000..edd4d48 --- /dev/null +++ b/src/pages/components/CheckinResultDialog.py @@ -0,0 +1,75 @@ +# -*- 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. +""" +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, + TimeoutException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver + +from pages.components.Overlay import Overlay + + +class CheckinResultDialog(Overlay): + """ + Check-in result dialog. + """ + + ROOT = (By.CLASS_NAME, "ui_dialog") + + RESULT_MSG = (By.CLASS_NAME, "resultMessage") + OK_BTN = (By.CLASS_NAME, "btnOK") + DETAIL_DD = (By.CSS_SELECTOR, ".resultMessage dd") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def getResultMessage( + self, + ) -> str: + + try: + self._waitPresence(self.RESULT_MSG) + el = self._find(*self.RESULT_MSG) + return el.text + except (TimeoutException, NoSuchElementException, StaleElementReferenceException): + return "" + except Exception: + return "" + + def getDetails( + self, + ) -> list[str]: + + try: + elements = self._findAll(*self.DETAIL_DD) + return [el.text for el in elements if el.text.strip()] + except (NoSuchElementException, StaleElementReferenceException): + return [] + except Exception: + return [] + + def clickOk( + self, + ) -> bool: + + try: + self._waitClickable(self.OK_BTN).click() + return True + except (NoSuchElementException, TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False diff --git a/src/pages/_overlay.py b/src/pages/components/Overlay.py similarity index 100% rename from src/pages/_overlay.py rename to src/pages/components/Overlay.py diff --git a/src/pages/components/RenewDialog.py b/src/pages/components/RenewDialog.py new file mode 100644 index 0000000..7eb36ac --- /dev/null +++ b/src/pages/components/RenewDialog.py @@ -0,0 +1,99 @@ +# -*- 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. +""" +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + TimeoutException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement + +from pages.components.Overlay import Overlay + + +class RenewDialog(Overlay): + """ + Renewal time selection dialog. + """ + + ROOT = (By.ID, "extendDiv") + + MESSAGE_HEAD = (By.CSS_SELECTOR, "#extendDiv p.messageHead") + RESULT_MSG = (By.CSS_SELECTOR, "#extendDiv div.resultMessage") + TIME_OPTS = (By.CSS_SELECTOR, "#extendDiv .renewal_List li") + OK_BTN = (By.CSS_SELECTOR, "#extendDiv .btnOK") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def waitUntilReady( + self, + ) -> bool: + + try: + self._waitVisible(self.ROOT) + self._waitPresence(self.MESSAGE_HEAD) + self._waitPresence(self.RESULT_MSG) + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + head_msg = self._find(*self.MESSAGE_HEAD).text.strip() + if "警告" in head_msg: + return False + try: + self._waitAllPresence(self.TIME_OPTS) + self._waitPresence(self.OK_BTN) + except (NoSuchElementException, TimeoutException): + return False + except Exception: + return False + return True + + def getHeadMessage( + self, + ) -> str: + + return self._find(*self.MESSAGE_HEAD).text.strip() + + def getResultMessage( + self, + ) -> str: + + return self._find(*self.RESULT_MSG).text.strip() + + def getTimeOptions( + self, + ) -> list[WebElement]: + + return self._findAll(*self.TIME_OPTS) + + def getOkButton( + self, + ) -> WebElement: + + return self._find(*self.OK_BTN) + + def clickOk( + self, + ) -> bool: + + try: + self._find(*self.OK_BTN).click() + return True + except (NoSuchElementException, TimeoutException, ElementNotInteractableException): + return False + except Exception: + return False diff --git a/src/pages/components/ReserveResultDialog.py b/src/pages/components/ReserveResultDialog.py new file mode 100644 index 0000000..c85cab1 --- /dev/null +++ b/src/pages/components/ReserveResultDialog.py @@ -0,0 +1,81 @@ +# -*- 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. +""" +from selenium.common.exceptions import ( + NoSuchElementException, + StaleElementReferenceException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver + +from pages.components.Overlay import Overlay + + +class ReserveResultDialog(Overlay): + """ + Reservation result dialog shown after submitting a reserve request. + """ + + ROOT = (By.CLASS_NAME, "layoutSeat") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def getTitle( + self, + ) -> str: + + try: + return self._find(*self._title_locator()).text + except (NoSuchElementException, StaleElementReferenceException): + return "" + except Exception: + return "" + + def isSuccess( + self, + ) -> bool: + + title = self.getTitle() + return any( + kw in title + for kw in ("预定好了", "预约成功", "操作成功") + ) + + def isFailure( + self, + ) -> bool: + + contents = self.getDetailTexts() + return any( + "预约失败" in msg or "已有1个有效预约" in msg + for msg in contents + ) + + def getDetailTexts( + self, + ) -> list[str]: + + try: + elements = self._findAll(By.CSS_SELECTOR, ".layoutSeat dd") + return [el.text for el in elements if el.text.strip()] + except (NoSuchElementException, StaleElementReferenceException): + return [] + except Exception: + return [] + + def _title_locator( + self, + ) -> tuple: + + return (By.CSS_SELECTOR, ".layoutSeat dt") diff --git a/src/pages/components/SeatMapOverlay.py b/src/pages/components/SeatMapOverlay.py new file mode 100644 index 0000000..42f1137 --- /dev/null +++ b/src/pages/components/SeatMapOverlay.py @@ -0,0 +1,74 @@ +# -*- 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. +""" +from selenium.common.exceptions import ( + ElementNotInteractableException, + NoSuchElementException, + StaleElementReferenceException, + TimeoutException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver + +from pages.components.Overlay import Overlay + + +class SeatMapOverlay(Overlay): + """ + Seat selection overlay that opens after choosing a floor and room. + """ + + ROOT = (By.ID, "seatLayout") + SEAT_ITEMS = (By.CSS_SELECTOR, "li[id^='seat_']") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT) + + def selectSeat( + self, + seat_id: str, + ) -> str | None: + + try: + self._waitAllPresence(self.SEAT_ITEMS) + except (NoSuchElementException, TimeoutException): + return None + except Exception: + return None + try: + seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}") + seat_link = seat_el.find_element(By.TAG_NAME, "a") + self._waitClickable((By.TAG_NAME, "a")) + seat_link.click() + return seat_link.get_attribute("title") + except (NoSuchElementException, ValueError, TimeoutException, + ElementNotInteractableException, StaleElementReferenceException): + pass + except Exception: + pass + try: + all_seats = self._findAll(*self.SEAT_ITEMS) + seat_id_upper = seat_id.lstrip('0').upper() + for seat in all_seats: + if not seat_id_upper == seat.text.lstrip('0'): + continue + seat_link = seat.find_element(By.TAG_NAME, "a") + self._waitClickable((By.TAG_NAME, "a")) + seat_link.click() + return seat_link.get_attribute("title") + return None + except (NoSuchElementException, TimeoutException, + ElementNotInteractableException, StaleElementReferenceException): + return None + except Exception: + return None diff --git a/src/pages/components/TimeSelectDialog.py b/src/pages/components/TimeSelectDialog.py new file mode 100644 index 0000000..0782aef --- /dev/null +++ b/src/pages/components/TimeSelectDialog.py @@ -0,0 +1,54 @@ +# -*- 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. +""" +from selenium.common.exceptions import ( + NoSuchElementException, + TimeoutException, +) +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement + +from pages.components.Overlay import Overlay + + +class TimeSelectDialog(Overlay): + """ + Time selection panel that appears after selecting a seat. + + Contains start-time and end-time option lists. + Does NOT auto-close — the reserve submission handles cleanup. + """ + + ROOT = (By.CSS_SELECTOR, "#startTime ul") + + def __init__( + self, + driver: WebDriver, + ) -> None: + + super().__init__(driver, self.ROOT, auto_close_on_exit=False) + + def getTimeOptions( + self, + time_id: str, + ) -> list[WebElement]: + + try: + self._waitAllPresence( + (By.CSS_SELECTOR, f"#{time_id} ul li a") + ) + except (NoSuchElementException, TimeoutException): + return [] + except Exception: + return [] + return self._findAll( + By.CSS_SELECTOR, + f"#{time_id} ul li a", + ) diff --git a/src/pages/components/__init__.py b/src/pages/components/__init__.py new file mode 100644 index 0000000..f0ac233 --- /dev/null +++ b/src/pages/components/__init__.py @@ -0,0 +1,22 @@ +# -*- 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. +""" +from pages.components.SeatMapOverlay import SeatMapOverlay +from pages.components.TimeSelectDialog import TimeSelectDialog +from pages.components.ReserveResultDialog import ReserveResultDialog +from pages.components.CheckinResultDialog import CheckinResultDialog +from pages.components.RenewDialog import RenewDialog + +__all__ = [ + "SeatMapOverlay", + "TimeSelectDialog", + "ReserveResultDialog", + "CheckinResultDialog", + "RenewDialog", +] diff --git a/src/pages/flows/CheckinFlow.py b/src/pages/flows/CheckinFlow.py index 299ab04..fc0a7c6 100644 --- a/src/pages/flows/CheckinFlow.py +++ b/src/pages/flows/CheckinFlow.py @@ -17,7 +17,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from base.MsgBase import MsgBase from pages.MainShell import MainShell -from pages._dialogs import CheckinResultDialog +from pages.components.CheckinResultDialog import CheckinResultDialog class CheckinFlow(MsgBase): diff --git a/src/pages/flows/RenewFlow.py b/src/pages/flows/RenewFlow.py index 35b48f1..abde378 100644 --- a/src/pages/flows/RenewFlow.py +++ b/src/pages/flows/RenewFlow.py @@ -18,7 +18,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from base.MsgBase import MsgBase from pages.MainShell import MainShell -from pages._dialogs import RenewDialog +from pages.components.RenewDialog import RenewDialog from pages.flows._helpers import ( timeStrToMins, minsToTimeStr, diff --git a/src/pages/flows/ReserveFlow.py b/src/pages/flows/ReserveFlow.py index 037e36c..c3313bf 100644 --- a/src/pages/flows/ReserveFlow.py +++ b/src/pages/flows/ReserveFlow.py @@ -26,7 +26,8 @@ from pages.flows._helpers import ( findBestTimeOption, ) from pages.ReserveView import ReserveView -from pages._dialogs import ReserveResultDialog +from pages.components.ReserveResultDialog import ReserveResultDialog +from pages.components.TimeSelectDialog import TimeSelectDialog @dataclass @@ -82,31 +83,27 @@ class ReserveFlow(MsgBase): except Exception as e: self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR) return False - if not view.selectDate(ctx.date): self._showTrace(f"选择日期失败 ! : {ctx.date} 不可用", self.TraceLevel.ERROR) return False self._showTrace(f"日期 {ctx.date} 选择成功 !") - if not view.selectPlace("1"): self._showTrace("选择预约场所失败 ! : 图书馆 不可用", self.TraceLevel.ERROR) return False self._showTrace("预约场所 图书馆 选择成功 !") - if not view.selectFloor(ctx.floor): display_floor = ReserveView.FLOOR_MAP.get(ctx.floor, ctx.floor) self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR) return False self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !") - if not view.selectRoom(ctx.room): display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room) self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) return False self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !") have_hover_on_page = True - - seat_status = view.selectSeat(ctx.seat_id) + seat_map = view.openSeatMap() + seat_status = seat_map.selectSeat(ctx.seat_id) if seat_status is None: self._showTrace( f"座位 {ctx.seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", @@ -114,8 +111,8 @@ class ReserveFlow(MsgBase): ) else: self._showTrace(f"座位 {ctx.seat_id} 选择成功 ! : 当前状态 - '{seat_status}'") - - select_time_ok = self._selectSeatTime(view) + time_dialog = TimeSelectDialog(self._driver) + select_time_ok = self._selectSeatTime(time_dialog) if not select_time_ok: self._showTrace("选择时间失败 !", self.TraceLevel.ERROR) else: @@ -149,7 +146,6 @@ class ReserveFlow(MsgBase): self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) except Exception: self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) - if not submit_reserve and have_hover_on_page: view.refresh() if reserve_success: @@ -160,7 +156,7 @@ class ReserveFlow(MsgBase): def _selectSeatTime( self, - view: ReserveView, + time_dialog: TimeSelectDialog, ) -> bool: ctx = self._ctx @@ -172,9 +168,8 @@ class ReserveFlow(MsgBase): act_beg_tm_str = exp_beg_tm_str act_end_mins = exp_end_mins act_end_tm_str = exp_end_tm_str - act_beg_mins = self._selectNearestTime( - view, + time_dialog, time_id="startTime", time_type="开始时间", target_time=exp_beg_mins, @@ -184,7 +179,6 @@ class ReserveFlow(MsgBase): if act_beg_mins == -1: return False act_beg_tm_str = minsToTimeStr(act_beg_mins) - if ctx.satisfy_duration: exp_end_mins = self._calcEndTime(act_beg_mins, ctx.expect_duration) exp_end_tm_str = minsToTimeStr(exp_end_mins) @@ -192,10 +186,9 @@ class ReserveFlow(MsgBase): f"需要满足期望预约持续时间: {ctx.expect_duration} 小时, " f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}" ) - act_end_mins = self._selectNearestTime( - view, - time_id="end_time", + time_dialog, + time_id="endTime", time_type="结束时间", target_time=exp_end_mins, max_time_diff=ctx.end_max_diff, @@ -204,7 +197,6 @@ class ReserveFlow(MsgBase): if act_end_mins == -1: return False act_end_tm_str = minsToTimeStr(act_end_mins) - self._showTrace( f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, " f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}" @@ -213,7 +205,7 @@ class ReserveFlow(MsgBase): def _selectNearestTime( self, - view: ReserveView, + time_dialog: TimeSelectDialog, time_id: str, time_type: str, target_time: int, @@ -221,13 +213,12 @@ class ReserveFlow(MsgBase): prefer_earlier: bool, ) -> int: - all_time_opts = view.getAvailableTimeOptions(time_id) + all_time_opts = time_dialog.getTimeOptions(time_id) if not all_time_opts: self._showTrace( f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR ) return -1 - best_opt, best_text, actual_diff, free_times = findBestTimeOption( all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True ) @@ -245,7 +236,6 @@ class ReserveFlow(MsgBase): f"与期望 {time_type} 相比 {relation}" ) return target_time + actual_diff - target_time_str = minsToTimeStr(target_time) self._showTrace( f"无法选择最近的 {time_type} {target_time_str}, " diff --git a/src/pages/services/CaptchaHandler.py b/src/pages/services/CaptchaHandler.py index 2aec267..1e941bd 100644 --- a/src/pages/services/CaptchaHandler.py +++ b/src/pages/services/CaptchaHandler.py @@ -26,42 +26,18 @@ class CaptchaHandler(MsgBase): self, input_queue: queue.Queue, output_queue: queue.Queue, - login_page: LoginPage, ) -> None: super().__init__(input_queue, output_queue) - self._login_page = login_page self._ocr = ddddocr.DdddOcr() - def solveCaptcha( - self, - auto_captcha: bool = True, - ) -> str: - - max_attempts = 3 - for _ in range(max_attempts): - if auto_captcha: - captcha_text = self._autoRecognize() - else: - self._showTrace("用户未配置自动识别验证码, 请手动输入验证码 !", 20, no_log=True) - captcha_text = self._manualRecognize() - if captcha_text: - return captcha_text - else: - if not self._login_page.refreshCaptcha(): - return "" - self._showTrace( - f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !", - self.TraceLevel.WARNING, - ) - return "" - def _autoRecognize( self, + login_page: LoginPage, ) -> str: try: - img_src = self._login_page.getCaptchaImageSrc() + img_src = login_page.getCaptchaImageSrc() base64_str = img_src.split(',', 1)[1] captcha_img = base64.b64decode(base64_str) captcha_text = self._ocr.classification(captcha_img) @@ -99,3 +75,27 @@ class CaptchaHandler(MsgBase): except Exception as e: self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) return "" + + def solveCaptcha( + self, + login_page: LoginPage, + auto_captcha: bool = True, + ) -> str: + + max_attempts = 3 + for _ in range(max_attempts): + if auto_captcha: + captcha_text = self._autoRecognize(login_page) + else: + self._showTrace("用户未配置自动识别验证码, 请手动输入验证码 !", 20, no_log=True) + captcha_text = self._manualRecognize() + if captcha_text: + return captcha_text + else: + if not login_page.refreshCaptcha(): + return "" + self._showTrace( + f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !", + self.TraceLevel.WARNING, + ) + return "" diff --git a/src/pages/services/RecordChecker.py b/src/pages/services/RecordChecker.py index e00536a..106ad33 100644 --- a/src/pages/services/RecordChecker.py +++ b/src/pages/services/RecordChecker.py @@ -29,11 +29,9 @@ class RecordChecker(MsgBase): self, input_queue: queue.Queue, output_queue: queue.Queue, - shell: MainShell, ) -> None: super().__init__(input_queue, output_queue) - self._shell = shell @staticmethod def _formatDiffTime( @@ -45,119 +43,9 @@ class RecordChecker(MsgBase): seconds = int(seconds % 60) return f"{hours} 时 {minutes} 分 {seconds} 秒" - def canReserve( - self, - date: str, - ) -> bool: - - if self._getReserveRecord(date, "已预约") is None: - if self._getReserveRecord(date, "使用中") is None: - self._showTrace(f"用户在 {date} 可以预约") - return True - self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约") - return False - self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约") - return False - - def canCheckin( - self, - ) -> bool: - - date = time.strftime("%Y-%m-%d", time.localtime()) - record = self._getReserveRecord(date, "已预约") - if record is not None: - begin_time = record["time"]["begin"] - begin_time = datetime.strptime( - f"{date} {begin_time}", "%Y-%m-%d %H:%M" - ) - time_diff = datetime.now() - begin_time - time_diff_seconds = time_diff.total_seconds() - if time_diff_seconds < -30 * 60: - self._showTrace( - f"用户在 {date} 的预约开始时间为 {begin_time}, " - f"当前距离预约开始时间还有 " - f"{self._formatDiffTime(abs(time_diff_seconds))}, 无法签到" - ) - return False - elif -30 * 60 <= time_diff_seconds < 0: - self._showTrace( - f"用户在 {date} 的预约开始时间为 {begin_time}, " - f"当前距离预约开始时间还有 " - f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" - ) - return True - elif 0 <= time_diff_seconds < 30 * 60 - 5: - self._showTrace( - f"用户在 {date} 的预约开始时间为 {begin_time}, " - f"当前距离预约开始时间已经过去 " - f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" - ) - return True - self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到") - return False - - def canRenew( - self, - ) -> tuple[bool, dict]: - - date = time.strftime("%Y-%m-%d", time.localtime()) - record = self._getReserveRecord(date, "使用中") - if record is not None: - end_time = record["time"]["end"] - end_time = datetime.strptime( - f"{date} {end_time}", "%Y-%m-%d %H:%M" - ) - time_diff = end_time - datetime.now() - time_diff_seconds = time_diff.total_seconds() - trace_msg = ( - f"用户在 {date} 的预约结束时间为 {end_time}, " - f"当前距离预约结束时间还有 " - f"{self._formatDiffTime(abs(time_diff_seconds))}" - ) - if abs(time_diff_seconds) < 120 * 60: - self._showTrace(f"{trace_msg}, 可以续约") - return True, record - else: - self._showTrace(f"{trace_msg}, 无法续约") - return False, None - self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") - return False, None - - def postRenewCheck( - self, - record: dict, - ) -> bool: - - date = record["date"] - act_record = self._getReserveRecord(date, "使用中") - if act_record is not None: - if ( - act_record["time"]["begin"] == record["time"]["begin"] - and act_record["time"]["end"] == record["time"]["end"] - ): - self._showTrace( - f"\n" - f" 续约成功 !\n" - f" 日 期 :{date}\n" - f" 时 间 :{act_record['time']['begin']}" - f" - {act_record['time']['end']}\n" - f" 位 置 :{act_record['info']['location']}\n" - f" 状 态 :{act_record['info']['status']}" - ) - return True - else: - self._showTrace( - f"\n" - f" 续约失败 !\n" - f" 续约后结束时间为 {act_record['time']['end']}," - f"与预期结束时间 {record['time']['end']} 不符 !" - ) - return False - self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") - return False - def _getReserveRecord( self, + shell: MainShell, wanted_date: str, wanted_status: str, ) -> dict | None: @@ -173,7 +61,7 @@ class RecordChecker(MsgBase): checked_count = 0 max_check_times = 6 - records_view = self._shell.gotoRecordsView() + records_view = shell.gotoRecordsView() for _ in range(max_check_times): reservations = records_view.loadRecords() if reservations is None: @@ -300,3 +188,118 @@ class RecordChecker(MsgBase): elif "图书馆" in info.text: location = info.text.strip() return {"location": location, "status": status} + + def canReserve( + self, + shell: MainShell, + date: str, + ) -> bool: + + if self._getReserveRecord(shell, date, "已预约") is None: + if self._getReserveRecord(shell, date, "使用中") is None: + self._showTrace(f"用户在 {date} 可以预约") + return True + self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约") + return False + self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约") + return False + + def canCheckin( + self, + shell: MainShell, + ) -> bool: + + date = time.strftime("%Y-%m-%d", time.localtime()) + record = self._getReserveRecord(shell, date, "已预约") + if record is not None: + begin_time = record["time"]["begin"] + begin_time = datetime.strptime( + f"{date} {begin_time}", "%Y-%m-%d %H:%M" + ) + time_diff = datetime.now() - begin_time + time_diff_seconds = time_diff.total_seconds() + if time_diff_seconds < -30 * 60: + self._showTrace( + f"用户在 {date} 的预约开始时间为 {begin_time}, " + f"当前距离预约开始时间还有 " + f"{self._formatDiffTime(abs(time_diff_seconds))}, 无法签到" + ) + return False + elif -30 * 60 <= time_diff_seconds < 0: + self._showTrace( + f"用户在 {date} 的预约开始时间为 {begin_time}, " + f"当前距离预约开始时间还有 " + f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" + ) + return True + elif 0 <= time_diff_seconds < 30 * 60 - 5: + self._showTrace( + f"用户在 {date} 的预约开始时间为 {begin_time}, " + f"当前距离预约开始时间已经过去 " + f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" + ) + return True + self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到") + return False + + def canRenew( + self, + shell: MainShell, + ) -> tuple[bool, dict]: + + date = time.strftime("%Y-%m-%d", time.localtime()) + record = self._getReserveRecord(shell, date, "使用中") + if record is not None: + end_time = record["time"]["end"] + end_time = datetime.strptime( + f"{date} {end_time}", "%Y-%m-%d %H:%M" + ) + time_diff = end_time - datetime.now() + time_diff_seconds = time_diff.total_seconds() + trace_msg = ( + f"用户在 {date} 的预约结束时间为 {end_time}, " + f"当前距离预约结束时间还有 " + f"{self._formatDiffTime(abs(time_diff_seconds))}" + ) + if abs(time_diff_seconds) < 120 * 60: + self._showTrace(f"{trace_msg}, 可以续约") + return True, record + else: + self._showTrace(f"{trace_msg}, 无法续约") + return False, None + self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") + return False, None + + def postRenewCheck( + self, + shell: MainShell, + record: dict, + ) -> bool: + + date = record["date"] + act_record = self._getReserveRecord(shell, date, "使用中") + if act_record is not None: + if ( + act_record["time"]["begin"] == record["time"]["begin"] + and act_record["time"]["end"] == record["time"]["end"] + ): + self._showTrace( + f"\n" + f" 续约成功 !\n" + f" 日 期 :{date}\n" + f" 时 间 :{act_record['time']['begin']}" + f" - {act_record['time']['end']}\n" + f" 位 置 :{act_record['info']['location']}\n" + f" 状 态 :{act_record['info']['status']}" + ) + return True + else: + self._showTrace( + f"\n" + f" 续约失败 !\n" + f" 续约后结束时间为 {act_record['time']['end']}," + f"与预期结束时间 {record['time']['end']} 不符 !" + ) + return False + self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") + return False diff --git a/src/pages/services/ReserveValidator.py b/src/pages/services/ReserveValidator.py index 5587839..c458dc0 100644 --- a/src/pages/services/ReserveValidator.py +++ b/src/pages/services/ReserveValidator.py @@ -25,35 +25,6 @@ class ReserveValidator(MsgBase): super().__init__(input_queue, output_queue) - def validate( - self, - reserve_info: dict, - ) -> bool: - - if not self._containRequiredInfo(reserve_info): - return False - if not self._isValidDate(reserve_info): - return False - if not self._isValidBeginTime(reserve_info): - return False - if not self._isValidExpectDuration(reserve_info): - return False - if not self._isValidEndTime(reserve_info): - return False - if not self._finalCheck(reserve_info): - return False - self._showTrace( - f"预约信息检查完成, 准备预约 " - f"{reserve_info['date']} " - f"{reserve_info['begin_time']['time']} - " - f"{reserve_info['end_time']['time']} " - f"图书馆 " - f"{ReserveView.FLOOR_MAP[reserve_info['floor']]} " - f"{ReserveView.ROOM_MAP[reserve_info['room']]} " - f"的座位 {reserve_info['seat_id']}" - ) - return True - def _containRequiredInfo( self, reserve_info: dict, @@ -219,3 +190,32 @@ class ReserveValidator(MsgBase): ) reserve_info["end_time"]["time"] = minsToTimeStr(begin_mins + 8 * 60) return True + + def validate( + self, + reserve_info: dict, + ) -> bool: + + if not self._containRequiredInfo(reserve_info): + return False + if not self._isValidDate(reserve_info): + return False + if not self._isValidBeginTime(reserve_info): + return False + if not self._isValidExpectDuration(reserve_info): + return False + if not self._isValidEndTime(reserve_info): + return False + if not self._finalCheck(reserve_info): + return False + self._showTrace( + f"预约信息检查完成, 准备预约 " + f"{reserve_info['date']} " + f"{reserve_info['begin_time']['time']} - " + f"{reserve_info['end_time']['time']} " + f"图书馆 " + f"{ReserveView.FLOOR_MAP[reserve_info['floor']]} " + f"{ReserveView.ROOM_MAP[reserve_info['room']]} " + f"的座位 {reserve_info['seat_id']}" + ) + return True \ No newline at end of file From 280028259f3703ed4144ed73eed50e45a677d17d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 26 May 2026 18:01:25 +0800 Subject: [PATCH 30/49] =?UTF-8?q?refactor(pages):=20=E5=B0=86=20LoginPage?= =?UTF-8?q?=20=E6=97=A5=E5=BF=97=E5=9B=9E=E8=B0=83=E4=BB=8E=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=8F=82=E6=95=B0=E6=94=B9=E4=B8=BA=E6=9E=84=E9=80=A0?= =?UTF-8?q?=E5=99=A8=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 消除 login() 方法签名中的 tracer/log_level 参数,通过构造器可选注入 tracer 统一日志模式,避免 Page Object 对外暴露 MsgBase 内部细节。 Co-Authored-By: Claude Opus 4.7 --- src/pages/AutoLibPages.py | 4 +- src/pages/LoginPage.py | 31 ++++--- src/test_pages_refactor.py | 162 ------------------------------------- 3 files changed, 21 insertions(+), 176 deletions(-) delete mode 100644 src/test_pages_refactor.py diff --git a/src/pages/AutoLibPages.py b/src/pages/AutoLibPages.py index faab6e7..3c73ff0 100644 --- a/src/pages/AutoLibPages.py +++ b/src/pages/AutoLibPages.py @@ -164,7 +164,7 @@ class AutoLibPages(MsgBase): self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR) return False url: str = lib_config.get("host_url") + lib_config.get("login_url") - self.__login_page = LoginPage(self.__driver) + self.__login_page = LoginPage(self.__driver, tracer=self._showTrace) self.__driver.set_page_load_timeout(5) try: self.__driver.get(url) @@ -244,8 +244,6 @@ class AutoLibPages(MsgBase): password, captcha_solver=self.__captcha_handler.solveCaptcha, auto_captcha=auto_captcha, - tracer=self._showTrace, - log_level=self.TraceLevel, max_attempts=login_config.get("max_attempt", 3), ): return 1 diff --git a/src/pages/LoginPage.py b/src/pages/LoginPage.py index f4f231d..db34756 100644 --- a/src/pages/LoginPage.py +++ b/src/pages/LoginPage.py @@ -7,7 +7,7 @@ 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. """ -from typing import Callable +from typing import Callable, Optional from selenium.common.exceptions import ( ElementNotInteractableException, @@ -37,9 +37,21 @@ class LoginPage: def __init__( self, driver: WebDriver, + tracer: Optional[Callable[..., None]] = None, ) -> None: self._driver: WebDriver = driver + self._tracer: Optional[Callable[..., None]] = tracer + + def _trace( + self, + msg: str, + level: int = 20, + no_log: bool = False, + ) -> None: + + if self._tracer: + self._tracer(msg, level, no_log) def navigate( self, @@ -177,16 +189,13 @@ class LoginPage: password: str, captcha_solver: Callable[["LoginPage", bool], str], auto_captcha: bool, - tracer: Callable[..., None], - log_level: type, max_attempts: int = 5, ) -> bool: - ERR = log_level.ERROR for attempt in range(max_attempts): - tracer( + self._trace( f"用户 {username} 第 {attempt + 1} 次尝试登录......", - 20, no_log=True, + no_log=True, ) if not self.fillCredentials(username, password): continue @@ -195,16 +204,16 @@ class LoginPage: continue if not self.fillCaptcha(captcha_text): continue - tracer("尝试登录...", 20, no_log=True) + self._trace("尝试登录...", no_log=True) if not self.clickLogin(): continue if self.waitLoginSuccess(): - tracer(f"用户 {username} 第 {attempt + 1} 次登录成功 !") + self._trace(f"用户 {username} 第 {attempt + 1} 次登录成功 !") return True else: - err_msg = ( + self._trace( "登录页面加载失败 ! : " - "用户账号或者密码错误/验证码错误, 具体以页面提示为准" + "用户账号或者密码错误/验证码错误, 具体以页面提示为准", + level=40, ) - tracer(err_msg, ERR) return False diff --git a/src/test_pages_refactor.py b/src/test_pages_refactor.py deleted file mode 100644 index bb6f28e..0000000 --- a/src/test_pages_refactor.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- 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. - -AutoLibrary 真实运行测试脚本。 -在 venv 中运行: - py -3 test_pages_refactor.py [--mode MODE] - -MODE 可选值 (默认 1): - 1 = 只预约 - 2 = 只签到 - 3 = 预约 + 签到 - 4 = 只续约 - 7 = 全部 (预约 + 签到 + 续约) -""" -import os -import sys -import argparse - -SRC = os.path.dirname(os.path.abspath(__file__)) -if SRC not in sys.path: - sys.path.insert(0, SRC) - - -def getAppConfigDir() -> str: - appData = os.environ.get("APPDATA", "") - if not appData: - appData = os.path.join(os.path.expanduser("~"), "AppData", "Roaming") - return os.path.join(appData, "AutoLibrary", "configs") - - -def main(): - parser = argparse.ArgumentParser(description="AutoLibrary 真实运行测试") - parser.add_argument( - "--mode", type=int, default=1, - help="运行模式 bitmask: 1=预约 2=签到 4=续约 (默认 1)" - ) - parser.add_argument( - "--group", type=int, default=0, - help="只运行第 N 个启用的任务组 (0=全部, 默认 0)" - ) - parser.add_argument( - "--headless", action="store_true", - help="使用 headless 模式运行浏览器" - ) - args = parser.parse_args() - - # ---- 1. 初始化 ConfigManager ---- - from managers.config.ConfigManager import instance as configInstance - from managers.config.ConfigUtils import ConfigUtils - from utils.JSONReader import JSONReader - - configDir = getAppConfigDir() - if not os.path.isdir(configDir): - print(f"[FAIL] 配置目录不存在: {configDir}") - print("请先启动一次 AutoLibrary GUI 以生成配置文件。") - return 1 - - try: - configInstance(configDir) - except ValueError: - pass - - configPaths = ConfigUtils.getAutomationConfigPaths() - runPath = configPaths.get("run") - userPath = configPaths.get("user") - - if not runPath or not os.path.isfile(runPath): - print(f"[FAIL] run.json 不存在: {runPath}") - return 1 - if not userPath or not os.path.isfile(userPath): - print(f"[FAIL] user.json 不存在: {userPath}") - return 1 - - print(f"[INFO] run : {runPath}") - print(f"[INFO] user : {userPath}") - - # ---- 2. 加载配置 ---- - runConfig = JSONReader(runPath).data() - userConfig = JSONReader(userPath).data() - - if args.mode is not None: - runConfig["mode"]["run_mode"] = args.mode - if args.headless: - runConfig["web_driver"]["headless"] = True - - groups = userConfig.get("groups", []) - if not groups: - print("[FAIL] user.json 中没有任务组") - return 1 - - print(f"[INFO] 运行模式: {runConfig['mode']['run_mode']}") - if args.headless: - print("[INFO] Headless 模式已启用") - - # ---- 3. 创建 AutoLib 并运行 ---- - from pages.AutoLibPages import AutoLibPages - import queue - import threading - - for gi, group in enumerate(groups): - if args.group > 0 and gi + 1 != args.group: - continue - if not group.get("enabled", True): - print(f"[SKIP] 任务组 {gi + 1} '{group.get('name', '未命名')}' 已禁用") - continue - - users = group.get("users", []) - enabledUsers = [u for u in users if u.get("enabled", True)] - if not enabledUsers: - print(f"[SKIP] 任务组 {gi + 1} 没有启用的用户") - continue - - print(f"\n{'=' * 60}") - print(f"任务组 {gi + 1}/{len(groups)}: '{group.get('name', '未命名')}'") - print(f"启用的用户: {len(enabledUsers)}/{len(users)}") - print(f"{'=' * 60}") - - outputQueue = queue.Queue() - stopConsumer = threading.Event() - traceLines = [] - - def consumeTrace(): - while not stopConsumer.is_set(): - try: - msg = outputQueue.get(timeout=0.3) - traceLines.append(msg) - print(msg) - except queue.Empty: - continue - - consumer = threading.Thread(target=consumeTrace, daemon=True) - consumer.start() - - try: - autoLib = AutoLibPages( - input_queue=queue.Queue(), - output_queue=outputQueue, - run_config=runConfig, - ) - autoLib.run({"users": enabledUsers}) - autoLib.close() - except Exception as e: - print(f"[FAIL] 运行异常: {e}") - import traceback - traceback.print_exc() - return 1 - finally: - stopConsumer.set() - consumer.join(timeout=2) - - print("\n[OK] 测试完成") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) From caa563e7708e7e314a0031010f523c244941c9e0 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Tue, 26 May 2026 20:52:52 +0800 Subject: [PATCH 31/49] =?UTF-8?q?refactor(pages):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20SeatMapOverlay=20=E5=85=83=E7=B4=A0=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AutoLibPages → AutoLib(移除实现细节后缀) - ReserveValidator → ReserveChecker(与 RecordChecker 命名一致) - CaptchaHandler → CaptchaSolver(语义更准确,职责是"求解"验证码) - ReserveChecker.validate() → check()(与 RecordChecker 风格统一) - 修复 SeatMapOverlay.selectSeat() 中 _waitClickable 等待页面全局 而非具体 seat_link 元素的时序缺陷 - ALMainWorkers 切换为 pages.AutoLib 新版实现 Co-Authored-By: Claude Opus 4.7 --- src/gui/ALMainWorkers.py | 2 +- src/pages/{AutoLibPages.py => AutoLib.py} | 18 +++++++++--------- src/pages/__init__.py | 4 ++-- src/pages/components/SeatMapOverlay.py | 10 ++++++++-- src/pages/flows/CheckinFlow.py | 3 --- src/pages/flows/RenewFlow.py | 8 -------- src/pages/flows/_helpers.py | 2 -- .../{CaptchaHandler.py => CaptchaSolver.py} | 2 +- .../{ReserveValidator.py => ReserveChecker.py} | 7 ++----- src/pages/services/__init__.py | 8 ++++---- 10 files changed, 27 insertions(+), 37 deletions(-) rename src/pages/{AutoLibPages.py => AutoLib.py} (96%) rename src/pages/services/{CaptchaHandler.py => CaptchaSolver.py} (99%) rename src/pages/services/{ReserveValidator.py => ReserveChecker.py} (99%) diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index 9f684fa..66d5319 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -16,7 +16,7 @@ from PySide6.QtCore import ( ) from base.MsgBase import MsgBase -from operators.AutoLib import AutoLib +from pages.AutoLib import AutoLib from utils.JSONReader import JSONReader from autoscript import createEngine diff --git a/src/pages/AutoLibPages.py b/src/pages/AutoLib.py similarity index 96% rename from src/pages/AutoLibPages.py rename to src/pages/AutoLib.py index 3c73ff0..bfac01a 100644 --- a/src/pages/AutoLibPages.py +++ b/src/pages/AutoLib.py @@ -24,12 +24,12 @@ from pages.MainShell import MainShell from pages.flows.ReserveFlow import ReserveFlow, ReserveContext from pages.flows.CheckinFlow import CheckinFlow from pages.flows.RenewFlow import RenewFlow -from pages.services.CaptchaHandler import CaptchaHandler -from pages.services.ReserveValidator import ReserveValidator +from pages.services.CaptchaSolver import CaptchaSolver +from pages.services.ReserveChecker import ReserveChecker from pages.services.RecordChecker import RecordChecker -class AutoLibPages(MsgBase): +class AutoLib(MsgBase): def __init__( self, @@ -46,9 +46,9 @@ class AutoLibPages(MsgBase): self.__driver_path: str = "" self.__login_page: LoginPage = None self.__shell: MainShell = None - self.__captcha_handler: CaptchaHandler = None + self.__captcha_solver: CaptchaSolver = None self.__record_checker: RecordChecker = None - self.__reserve_validator: ReserveValidator = None + self.__reserve_checker: ReserveChecker = None self.__reserve_flow: ReserveFlow = None self.__checkin_flow: CheckinFlow = None self.__renew_flow: RenewFlow = None @@ -189,7 +189,7 @@ class AutoLibPages(MsgBase): self._showTrace("浏览器驱动未初始化, 请先初始化浏览器驱动 !", self.TraceLevel.WARNING) return self.__shell = MainShell(self.__driver) - self.__captcha_handler = CaptchaHandler( + self.__captcha_solver = CaptchaSolver( input_queue=self._input_queue, output_queue=self._output_queue, ) @@ -197,7 +197,7 @@ class AutoLibPages(MsgBase): input_queue=self._input_queue, output_queue=self._output_queue, ) - self.__reserve_validator = ReserveValidator( + self.__reserve_checker = ReserveChecker( input_queue=self._input_queue, output_queue=self._output_queue, ) @@ -242,7 +242,7 @@ class AutoLibPages(MsgBase): if not self.__login_page.login( username, password, - captcha_solver=self.__captcha_handler.solveCaptcha, + captcha_solver=self.__captcha_solver.solveCaptcha, auto_captcha=auto_captcha, max_attempts=login_config.get("max_attempt", 3), ): @@ -256,7 +256,7 @@ class AutoLibPages(MsgBase): # reserve if run_mode["auto_reserve"]: if self.__record_checker.canReserve(self.__shell, reserve_info.get("date")): - if self.__reserve_validator.validate(reserve_info): + if self.__reserve_checker.check(reserve_info): ctx = ReserveContext( username=username, date=reserve_info["date"], diff --git a/src/pages/__init__.py b/src/pages/__init__.py index 391cc81..e8ad247 100644 --- a/src/pages/__init__.py +++ b/src/pages/__init__.py @@ -7,7 +7,7 @@ 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. """ -from pages.AutoLibPages import AutoLibPages +from pages.AutoLib import AutoLib from pages.LoginPage import LoginPage from pages.MainShell import MainShell from pages.ReserveView import ReserveView @@ -19,7 +19,7 @@ from pages.components.CheckinResultDialog import CheckinResultDialog from pages.components.RenewDialog import RenewDialog __all__ = [ - "AutoLibPages", + "AutoLib", "LoginPage", "MainShell", "ReserveView", diff --git a/src/pages/components/SeatMapOverlay.py b/src/pages/components/SeatMapOverlay.py index 42f1137..1512a36 100644 --- a/src/pages/components/SeatMapOverlay.py +++ b/src/pages/components/SeatMapOverlay.py @@ -15,6 +15,8 @@ from selenium.common.exceptions import ( ) from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC from pages.components.Overlay import Overlay @@ -48,7 +50,9 @@ class SeatMapOverlay(Overlay): try: seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}") seat_link = seat_el.find_element(By.TAG_NAME, "a") - self._waitClickable((By.TAG_NAME, "a")) + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(seat_link) + ) seat_link.click() return seat_link.get_attribute("title") except (NoSuchElementException, ValueError, TimeoutException, @@ -63,7 +67,9 @@ class SeatMapOverlay(Overlay): if not seat_id_upper == seat.text.lstrip('0'): continue seat_link = seat.find_element(By.TAG_NAME, "a") - self._waitClickable((By.TAG_NAME, "a")) + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(seat_link) + ) seat_link.click() return seat_link.get_attribute("title") return None diff --git a/src/pages/flows/CheckinFlow.py b/src/pages/flows/CheckinFlow.py index fc0a7c6..bb0689e 100644 --- a/src/pages/flows/CheckinFlow.py +++ b/src/pages/flows/CheckinFlow.py @@ -42,16 +42,13 @@ class CheckinFlow(MsgBase): if not self._shell.waitCheckinButton(): self._showTrace(f"用户 {username} 签到界面加载失败 !", self.TraceLevel.ERROR) return False - if self._shell.isCheckinButtonDisabled(): self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......") if not self._shell.enableCheckinButtonByJS(): self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR) return False self._showTrace("签到按钮已启用") - self._shell.clickCheckinButton() - try: with CheckinResultDialog(self._driver) as dialog: result_msg = dialog.getResultMessage() diff --git a/src/pages/flows/RenewFlow.py b/src/pages/flows/RenewFlow.py index abde378..4d4a36a 100644 --- a/src/pages/flows/RenewFlow.py +++ b/src/pages/flows/RenewFlow.py @@ -53,23 +53,18 @@ class RenewFlow(MsgBase): prefer_earlier = renew_info["prefer_early"] end_time = record["time"]["end"] target_renew_mins = timeStrToMins(end_time) + renew_info["expect_duration"] * 60 - if not self._validateRenewTime(end_time, target_renew_mins): return False - if not self._shell.waitExtendButton(): self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR) return False - if self._shell.isExtendButtonDisabled(): self._showTrace( f"用户 {username} 续约按钮不可用, 可能不在场馆内, " f"请连接图书馆网络后重试" ) return False - self._shell.clickExtendButton() - try: with RenewDialog(self._driver) as dialog: if not dialog.waitUntilReady(): @@ -82,14 +77,12 @@ class RenewFlow(MsgBase): self._shell.refresh() self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR) return False - renew_ok_btn = dialog.getOkButton() renew_time_opts = dialog.getTimeOptions() if not renew_time_opts: self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING) self._shell.refresh() return False - best_opt, best_text, actual_diff, free_times = findBestTimeOption( renew_time_opts, target_renew_mins, max_diff, prefer_earlier, is_reserve=False, @@ -111,7 +104,6 @@ class RenewFlow(MsgBase): renew_ok_btn.click() self._shell.refresh() return True - self._showTrace( "无法选择最近的可用续约时间 ! " f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", diff --git a/src/pages/flows/_helpers.py b/src/pages/flows/_helpers.py index 1b2fcf5..488615a 100644 --- a/src/pages/flows/_helpers.py +++ b/src/pages/flows/_helpers.py @@ -68,7 +68,6 @@ def findBestTimeOption( ) actual_diff = time_val - target_time abs_diff = abs(actual_diff) - if abs_diff < best_time_diff or ( abs_diff == best_time_diff and ( @@ -79,7 +78,6 @@ def findBestTimeOption( 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/pages/services/CaptchaHandler.py b/src/pages/services/CaptchaSolver.py similarity index 99% rename from src/pages/services/CaptchaHandler.py rename to src/pages/services/CaptchaSolver.py index 1e941bd..1544bee 100644 --- a/src/pages/services/CaptchaHandler.py +++ b/src/pages/services/CaptchaSolver.py @@ -20,7 +20,7 @@ from base.MsgBase import MsgBase from pages.LoginPage import LoginPage -class CaptchaHandler(MsgBase): +class CaptchaSolver(MsgBase): def __init__( self, diff --git a/src/pages/services/ReserveValidator.py b/src/pages/services/ReserveChecker.py similarity index 99% rename from src/pages/services/ReserveValidator.py rename to src/pages/services/ReserveChecker.py index c458dc0..f38f449 100644 --- a/src/pages/services/ReserveValidator.py +++ b/src/pages/services/ReserveChecker.py @@ -15,7 +15,7 @@ from pages.ReserveView import ReserveView from pages.flows._helpers import timeStrToMins, minsToTimeStr -class ReserveValidator(MsgBase): +class ReserveChecker(MsgBase): def __init__( self, @@ -150,7 +150,6 @@ class ReserveValidator(MsgBase): end_time = reserve_info["end_time"] begin_mins = timeStrToMins(begin_time["time"]) end_mins = timeStrToMins(end_time["time"]) - if end_mins < begin_mins and reserve_info["satisfy_duration"] is False: self._showTrace( f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, " @@ -161,7 +160,6 @@ class ReserveValidator(MsgBase): begin_time, end_time = end_time, begin_time begin_mins = timeStrToMins(begin_time["time"]) end_mins = timeStrToMins(end_time["time"]) - max_end_mins = timeStrToMins("23:30") if end_mins > max_end_mins: self._showTrace( @@ -170,7 +168,6 @@ class ReserveValidator(MsgBase): ) reserve_info["end_time"]["time"] = "23:30" end_mins = max_end_mins - if reserve_info["satisfy_duration"]: if reserve_info["expect_duration"] > 8: self._showTrace( @@ -191,7 +188,7 @@ class ReserveValidator(MsgBase): reserve_info["end_time"]["time"] = minsToTimeStr(begin_mins + 8 * 60) return True - def validate( + def check( self, reserve_info: dict, ) -> bool: diff --git a/src/pages/services/__init__.py b/src/pages/services/__init__.py index 8545fc0..cee361c 100644 --- a/src/pages/services/__init__.py +++ b/src/pages/services/__init__.py @@ -7,12 +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. """ -from pages.services.CaptchaHandler import CaptchaHandler -from pages.services.ReserveValidator import ReserveValidator +from pages.services.CaptchaSolver import CaptchaSolver +from pages.services.ReserveChecker import ReserveChecker from pages.services.RecordChecker import RecordChecker __all__ = [ - "CaptchaHandler", - "ReserveValidator", + "CaptchaSolver", + "ReserveChecker", "RecordChecker", ] From 345cb95b98b6eceb85e3b433b64dcb4ead06b63f Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 27 May 2026 13:13:43 +0800 Subject: [PATCH 32/49] =?UTF-8?q?refactor(pages):=20=E6=8A=BD=E5=8F=96?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E9=80=89=E6=8B=A9=E7=AD=96=E7=95=A5=E4=B8=BA?= =?UTF-8?q?=20TimeSelectMaker=EF=BC=8C=E5=B0=86=20Overlay=20=E5=9F=BA?= =?UTF-8?q?=E7=B1=BB=E6=9B=B4=E5=90=8D=E4=B8=BA=20Dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 findBestTimeOption 中的预约/续约双分支逻辑抽象为策略模式: - TimeOptionReader 负责从 WebElement 提取时间数据(ReserveTimeReader / RenewTimeReader) - TimeDecisionMaker 执行纯决策算法,零 Selenium 依赖 - TimeSelectMaker 作为工厂统一创建配置好的决策器 - 共享常量 LIBRARY_CLOSE_MINS 统一收敛至 TimeSelectMaker 同时将 Overlay 基类重命名为 Dialog,SeatMapOverlay 同步更名为 SeatMapDialog,保持命名一致性。 Co-Authored-By: Claude Opus 4.7 --- src/pages/ReserveView.py | 6 +- src/pages/__init__.py | 4 +- src/pages/components/CheckinResultDialog.py | 4 +- .../components/{Overlay.py => Dialog.py} | 4 +- src/pages/components/RenewDialog.py | 4 +- src/pages/components/ReserveResultDialog.py | 4 +- .../{SeatMapOverlay.py => SeatMapDialog.py} | 4 +- src/pages/components/TimeSelectDialog.py | 4 +- src/pages/components/__init__.py | 4 +- src/pages/flows/RenewFlow.py | 33 ++-- src/pages/flows/ReserveFlow.py | 34 ++-- src/pages/flows/_helpers.py | 61 ------- src/pages/strategies/__init__.py | 28 +++ src/pages/strategies/timeSelectMaker.py | 164 ++++++++++++++++++ 14 files changed, 244 insertions(+), 114 deletions(-) rename src/pages/components/{Overlay.py => Dialog.py} (98%) rename src/pages/components/{SeatMapOverlay.py => SeatMapDialog.py} (97%) create mode 100644 src/pages/strategies/__init__.py create mode 100644 src/pages/strategies/timeSelectMaker.py diff --git a/src/pages/ReserveView.py b/src/pages/ReserveView.py index f0710d8..eda9b54 100644 --- a/src/pages/ReserveView.py +++ b/src/pages/ReserveView.py @@ -18,7 +18,7 @@ from selenium.common.exceptions import ( TimeoutException, ) -from pages.components.SeatMapOverlay import SeatMapOverlay +from pages.components.SeatMapDialog import SeatMapDialog from pages.components.ReserveResultDialog import ReserveResultDialog @@ -124,9 +124,9 @@ class ReserveView: def openSeatMap( self, - ) -> SeatMapOverlay: + ) -> SeatMapDialog: - return SeatMapOverlay(self._driver) + return SeatMapDialog(self._driver) def submitReserve( self, diff --git a/src/pages/__init__.py b/src/pages/__init__.py index e8ad247..5b0d495 100644 --- a/src/pages/__init__.py +++ b/src/pages/__init__.py @@ -12,7 +12,7 @@ from pages.LoginPage import LoginPage from pages.MainShell import MainShell from pages.ReserveView import ReserveView from pages.RecordsView import RecordsView -from pages.components.SeatMapOverlay import SeatMapOverlay +from pages.components.SeatMapDialog import SeatMapDialog from pages.components.TimeSelectDialog import TimeSelectDialog from pages.components.ReserveResultDialog import ReserveResultDialog from pages.components.CheckinResultDialog import CheckinResultDialog @@ -24,7 +24,7 @@ __all__ = [ "MainShell", "ReserveView", "RecordsView", - "SeatMapOverlay", + "SeatMapDialog", "TimeSelectDialog", "ReserveResultDialog", "CheckinResultDialog", diff --git a/src/pages/components/CheckinResultDialog.py b/src/pages/components/CheckinResultDialog.py index edd4d48..7843f41 100644 --- a/src/pages/components/CheckinResultDialog.py +++ b/src/pages/components/CheckinResultDialog.py @@ -16,10 +16,10 @@ from selenium.common.exceptions import ( from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver -from pages.components.Overlay import Overlay +from pages.components.Dialog import Dialog -class CheckinResultDialog(Overlay): +class CheckinResultDialog(Dialog): """ Check-in result dialog. """ diff --git a/src/pages/components/Overlay.py b/src/pages/components/Dialog.py similarity index 98% rename from src/pages/components/Overlay.py rename to src/pages/components/Dialog.py index 12041e6..95d1bfe 100644 --- a/src/pages/components/Overlay.py +++ b/src/pages/components/Dialog.py @@ -13,7 +13,7 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -class Overlay: +class Dialog: """ Context-managed overlay / modal / dialog on a page. @@ -36,7 +36,7 @@ class Overlay: def __enter__( self, - ) -> "Overlay": + ) -> "Dialog": WebDriverWait(self._driver, self._timeout).until( EC.visibility_of_element_located(self._root_locator) diff --git a/src/pages/components/RenewDialog.py b/src/pages/components/RenewDialog.py index 7eb36ac..d663a1c 100644 --- a/src/pages/components/RenewDialog.py +++ b/src/pages/components/RenewDialog.py @@ -16,10 +16,10 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webelement import WebElement -from pages.components.Overlay import Overlay +from pages.components.Dialog import Dialog -class RenewDialog(Overlay): +class RenewDialog(Dialog): """ Renewal time selection dialog. """ diff --git a/src/pages/components/ReserveResultDialog.py b/src/pages/components/ReserveResultDialog.py index c85cab1..d1ac48e 100644 --- a/src/pages/components/ReserveResultDialog.py +++ b/src/pages/components/ReserveResultDialog.py @@ -14,10 +14,10 @@ from selenium.common.exceptions import ( from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver -from pages.components.Overlay import Overlay +from pages.components.Dialog import Dialog -class ReserveResultDialog(Overlay): +class ReserveResultDialog(Dialog): """ Reservation result dialog shown after submitting a reserve request. """ diff --git a/src/pages/components/SeatMapOverlay.py b/src/pages/components/SeatMapDialog.py similarity index 97% rename from src/pages/components/SeatMapOverlay.py rename to src/pages/components/SeatMapDialog.py index 1512a36..9f9a5d5 100644 --- a/src/pages/components/SeatMapOverlay.py +++ b/src/pages/components/SeatMapDialog.py @@ -18,10 +18,10 @@ from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from pages.components.Overlay import Overlay +from pages.components.Dialog import Dialog -class SeatMapOverlay(Overlay): +class SeatMapDialog(Dialog): """ Seat selection overlay that opens after choosing a floor and room. """ diff --git a/src/pages/components/TimeSelectDialog.py b/src/pages/components/TimeSelectDialog.py index 0782aef..643769f 100644 --- a/src/pages/components/TimeSelectDialog.py +++ b/src/pages/components/TimeSelectDialog.py @@ -15,10 +15,10 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webelement import WebElement -from pages.components.Overlay import Overlay +from pages.components.Dialog import Dialog -class TimeSelectDialog(Overlay): +class TimeSelectDialog(Dialog): """ Time selection panel that appears after selecting a seat. diff --git a/src/pages/components/__init__.py b/src/pages/components/__init__.py index f0ac233..3d61a36 100644 --- a/src/pages/components/__init__.py +++ b/src/pages/components/__init__.py @@ -7,14 +7,14 @@ 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. """ -from pages.components.SeatMapOverlay import SeatMapOverlay +from pages.components.SeatMapDialog import SeatMapDialog from pages.components.TimeSelectDialog import TimeSelectDialog from pages.components.ReserveResultDialog import ReserveResultDialog from pages.components.CheckinResultDialog import CheckinResultDialog from pages.components.RenewDialog import RenewDialog __all__ = [ - "SeatMapOverlay", + "SeatMapDialog", "TimeSelectDialog", "ReserveResultDialog", "CheckinResultDialog", diff --git a/src/pages/flows/RenewFlow.py b/src/pages/flows/RenewFlow.py index 4d4a36a..fb06111 100644 --- a/src/pages/flows/RenewFlow.py +++ b/src/pages/flows/RenewFlow.py @@ -19,16 +19,13 @@ from selenium.webdriver.remote.webdriver import WebDriver from base.MsgBase import MsgBase from pages.MainShell import MainShell from pages.components.RenewDialog import RenewDialog -from pages.flows._helpers import ( - timeStrToMins, - minsToTimeStr, - findBestTimeOption, -) +from pages.flows._helpers import timeStrToMins, minsToTimeStr +from pages.strategies.timeSelectMaker import TimeSelectMaker class RenewFlow(MsgBase): - LIBRARY_CLOSE_MINS = 1410 + LIBRARY_CLOSE_MINS = TimeSelectMaker.LIBRARY_CLOSE_MINS def __init__( self, @@ -83,24 +80,26 @@ class RenewFlow(MsgBase): self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING) self._shell.refresh() return False - best_opt, best_text, actual_diff, free_times = findBestTimeOption( - renew_time_opts, target_renew_mins, max_diff, prefer_earlier, - is_reserve=False, + result = TimeSelectMaker.forRenew().decide( + renew_time_opts, + target_renew_mins, + max_diff, + prefer_earlier ) - if best_opt is not None: - best_opt.click() - abs_diff = abs(actual_diff) - if actual_diff < 0: + if result.selected_index >= 0: + renew_time_opts[result.selected_index].click() + abs_diff = abs(result.actual_diff) + if result.actual_diff < 0: relation = f"早了 {abs_diff} 分钟" - elif actual_diff > 0: + elif result.actual_diff > 0: relation = f"晚了 {abs_diff} 分钟" else: relation = "正好等于 续约时间" self._showTrace( - f"选择距离期望续约时间最近的 {best_text}, " + f"选择距离期望续约时间最近的 {result.display_text}, " f"与期望续约时间相比 {relation}" ) - record["time"]["end"] = best_text.strip() + record["time"]["end"] = result.display_text.strip() renew_ok_btn.click() self._shell.refresh() return True @@ -109,7 +108,7 @@ class RenewFlow(MsgBase): f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", self.TraceLevel.WARNING, ) - self._showTrace(f"当前可供续约的时间有: {free_times}") + self._showTrace(f"当前可供续约的时间有: {result.free_times}") self._shell.refresh() return False except (NoSuchElementException, TimeoutException) as e: diff --git a/src/pages/flows/ReserveFlow.py b/src/pages/flows/ReserveFlow.py index c3313bf..e26eab8 100644 --- a/src/pages/flows/ReserveFlow.py +++ b/src/pages/flows/ReserveFlow.py @@ -20,11 +20,8 @@ from selenium.webdriver.remote.webdriver import WebDriver from base.MsgBase import MsgBase from pages.MainShell import MainShell -from pages.flows._helpers import ( - timeStrToMins, - minsToTimeStr, - findBestTimeOption, -) +from pages.flows._helpers import timeStrToMins, minsToTimeStr +from pages.strategies.timeSelectMaker import TimeSelectMaker from pages.ReserveView import ReserveView from pages.components.ReserveResultDialog import ReserveResultDialog from pages.components.TimeSelectDialog import TimeSelectDialog @@ -50,7 +47,7 @@ class ReserveContext: class ReserveFlow(MsgBase): - LIBRARY_CLOSE_MINS = timeStrToMins("23:30") + LIBRARY_CLOSE_MINS = TimeSelectMaker.LIBRARY_CLOSE_MINS def __init__( self, @@ -219,30 +216,33 @@ class ReserveFlow(MsgBase): f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR ) return -1 - best_opt, best_text, actual_diff, free_times = findBestTimeOption( - all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True + result = TimeSelectMaker.forReserve().decide( + all_time_opts, + target_time, + max_time_diff, + prefer_earlier ) - if best_opt is not None: - best_opt.click() - abs_diff = abs(actual_diff) - if actual_diff < 0: + if result.selected_index >= 0: + all_time_opts[result.selected_index].click() + abs_diff = abs(result.actual_diff) + if result.actual_diff < 0: relation = f"早了 {abs_diff} 分钟" - elif actual_diff > 0: + elif result.actual_diff > 0: relation = f"晚了 {abs_diff} 分钟" else: relation = f"正好等于 {time_type}" self._showTrace( - f"选择距离期望 {time_type} 最近的 {best_text}, " + f"选择距离期望 {time_type} 最近的 {result.display_text}, " f"与期望 {time_type} 相比 {relation}" ) - return target_time + actual_diff + return target_time + result.actual_diff target_time_str = minsToTimeStr(target_time) self._showTrace( f"无法选择最近的 {time_type} {target_time_str}, " f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", self.TraceLevel.WARNING, ) - self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") + self._showTrace(f"当前可供预约的 {time_type} 有: {result.free_times}") return -1 def _calcEndTime( @@ -251,7 +251,7 @@ class ReserveFlow(MsgBase): duration: int, ) -> int: - expect_end_mins = int(begin_mins + duration * 60) + expect_end_mins = int(begin_mins + duration*60) if expect_end_mins > self.LIBRARY_CLOSE_MINS: expect_end_mins = self.LIBRARY_CLOSE_MINS self._showTrace( diff --git a/src/pages/flows/_helpers.py b/src/pages/flows/_helpers.py index 488615a..936ccc5 100644 --- a/src/pages/flows/_helpers.py +++ b/src/pages/flows/_helpers.py @@ -7,9 +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. """ -from datetime import datetime - - def timeStrToMins( time_str: str, ) -> int: @@ -17,67 +14,9 @@ def timeStrToMins( hour, minute = map(int, time_str.split(":")) return hour * 60 + minute - def minsToTimeStr( mins: int, ) -> str: hour, minute = divmod(int(mins), 60) return f"{hour:02d}:{minute:02d}" - - -def findBestTimeOption( - time_options: list, - target_time: int, - max_time_diff: int, - prefer_earlier: bool, - is_reserve: bool = True, -) -> tuple: - """ - Find the best time option from available WebElement options. - - Returns: - (bestElement, bestText, actual_diff, freeTimesList) - or (None, None, None, freeTimesList) if no suitable option. - """ - - free_times = [] - best_time_diff = max_time_diff - best_actual_diff = None - best_time_opt = None - - for time_opt in time_options: - if is_reserve: - time_attr = time_opt.get_attribute("time") - if time_attr == "now": - now = datetime.now() - time_val = now.hour * 60 + now.minute - elif time_attr and time_attr.isdigit(): - time_val = int(time_attr) - else: - continue - else: - time_attr = time_opt.get_attribute("id") - 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 minsToTimeStr(time_val) - ) - actual_diff = time_val - target_time - abs_diff = abs(actual_diff) - if abs_diff < best_time_diff or ( - 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/pages/strategies/__init__.py b/src/pages/strategies/__init__.py new file mode 100644 index 0000000..6599a5f --- /dev/null +++ b/src/pages/strategies/__init__.py @@ -0,0 +1,28 @@ +# -*- 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. +""" +from pages.strategies.timeSelectMaker import ( + TimeSelectMaker, + TimeDecisionMaker, + TimeOptionReader, + ReserveTimeReader, + RenewTimeReader, + TimeOption, + TimeSelectionResult, +) + +__all__ = [ + "TimeSelectMaker", + "TimeDecisionMaker", + "TimeOptionReader", + "ReserveTimeReader", + "RenewTimeReader", + "TimeOption", + "TimeSelectionResult", +] diff --git a/src/pages/strategies/timeSelectMaker.py b/src/pages/strategies/timeSelectMaker.py new file mode 100644 index 0000000..ada47cf --- /dev/null +++ b/src/pages/strategies/timeSelectMaker.py @@ -0,0 +1,164 @@ +# -*- 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. +""" +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from datetime import datetime + +from pages.flows._helpers import minsToTimeStr + + +@dataclass +class TimeOption: + + value: int + element_text: str + + +@dataclass +class TimeSelectionResult: + + selected_index: int = -1 + selected_value: int = 0 + display_text: str = "" + actual_diff: int = 0 + free_times: list[str] = field(default_factory=list) + + +class TimeOptionReader(ABC): + + @abstractmethod + def readOptions( + self, + elements: list, + ) -> list[TimeOption]: + ... + + def formatFreeTime( + self, + opt: TimeOption, + ) -> str: + + return opt.element_text + + +class ReserveTimeReader(TimeOptionReader): + """ + Reads the ``time`` HTML attribute for the reserve flow. + Special value ``"now"`` is resolved to the current wall-clock minute. + """ + + def readOptions( + self, + elements: list, + ) -> list[TimeOption]: + + options: list[TimeOption] = [] + for el in elements: + time_attr = el.get_attribute("time") + if time_attr == "now": + now = datetime.now() + value = now.hour * 60 + now.minute + elif time_attr and time_attr.isdigit(): + value = int(time_attr) + else: + continue + options.append(TimeOption(value=value, element_text=el.text.strip())) + return options + + def formatFreeTime( + self, + opt: TimeOption, + ) -> str: + + return minsToTimeStr(opt.value) + + +class RenewTimeReader(TimeOptionReader): + """ + Reads the ``id`` HTML attribute for the renewal flow. + """ + + def readOptions( + self, + elements: list, + ) -> list[TimeOption]: + + options: list[TimeOption] = [] + for el in elements: + time_attr = el.get_attribute("id") + if not (time_attr and time_attr.isdigit()): + continue + options.append(TimeOption(value=int(time_attr), element_text=el.text.strip())) + return options + + +class TimeDecisionMaker: + + def __init__( + self, + reader: TimeOptionReader, + ) -> None: + + self._reader = reader + + def decide( + self, + elements: list, + target_time: int, + max_time_diff: int, + prefer_earlier: bool, + ) -> TimeSelectionResult: + + options = self._reader.readOptions(elements) + free_times = [self._reader.formatFreeTime(o) for o in options] + best_diff = max_time_diff + best_actual_diff = None + best_index = -1 + for i, opt in enumerate(options): + actual_diff = opt.value - target_time + abs_diff = abs(actual_diff) + if abs_diff < best_diff or ( + abs_diff == best_diff + and ( + (prefer_earlier and actual_diff <= 0) + or (not prefer_earlier and actual_diff >= 0) + ) + ): + best_diff = abs_diff + best_actual_diff = actual_diff + best_index = i + if best_index == -1: + return TimeSelectionResult(free_times=free_times) + chosen = options[best_index] + return TimeSelectionResult( + selected_index=best_index, + selected_value=chosen.value, + display_text=chosen.element_text, + actual_diff=best_actual_diff or 0, + free_times=free_times, + ) + + +class TimeSelectMaker: + + LIBRARY_CLOSE_MINS = 1410 + MAX_DURATION_HOURS = 8 + + @staticmethod + def forReserve( + ) -> TimeDecisionMaker: + + return TimeDecisionMaker(ReserveTimeReader()) + + @staticmethod + def forRenew( + ) -> TimeDecisionMaker: + + return TimeDecisionMaker(RenewTimeReader()) From e77c561685534daac7e200783d3698b515a85d9d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 27 May 2026 19:54:26 +0800 Subject: [PATCH 33/49] =?UTF-8?q?refactor:=20=E6=97=B6=E9=97=B4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E9=80=BB=E8=BE=91=E4=B8=8B=E6=B2=89=E8=87=B3=20Dialog?= =?UTF-8?q?=E3=80=81Worker=20=E6=A8=A1=E6=9D=BF=E6=96=B9=E6=B3=95=E6=8A=BD?= =?UTF-8?q?=E8=B1=A1=E3=80=81=E9=85=8D=E7=BD=AE=E8=AE=BF=E9=97=AE=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=8C=96=E4=B8=8E=E4=BB=A3=E7=A0=81=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=20Co-Authored-By:=20Claude=20Opus=204.7=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Main.py | 2 +- src/gui/ALAutoScriptOrchDialog/_helpers.py | 6 +- src/gui/ALMainWorkers.py | 203 ++++++++++---------- src/gui/ALSeatMapView.py | 4 +- src/gui/ALTimerTaskAddDialog.py | 18 +- src/pages/AutoLib.py | 40 ++-- src/pages/ReserveView.py | 30 ++- src/pages/components/Dialog.py | 10 +- src/pages/components/RenewDialog.py | 27 ++- src/pages/components/ReserveResultDialog.py | 14 +- src/pages/components/TimeSelectDialog.py | 178 +++++++++++++++++ src/pages/flows/RenewFlow.py | 37 ++-- src/pages/flows/ReserveFlow.py | 189 ++++-------------- src/pages/flows/_helpers.py | 19 +- src/pages/services/RecordChecker.py | 192 +++++++++--------- src/pages/services/ReserveChecker.py | 30 +-- src/pages/strategies/__init__.py | 4 +- src/pages/strategies/timeSelectMaker.py | 61 +++++- 18 files changed, 599 insertions(+), 465 deletions(-) diff --git a/src/Main.py b/src/Main.py index 8716d23..c9e50ca 100644 --- a/src/Main.py +++ b/src/Main.py @@ -24,7 +24,7 @@ def main(): translator = QTranslator() if translator.load(":/res/translators/qtbase_zh_CN.ts"): app.installTranslator(translator) - app.setStyle('Fusion') + app.setStyle("Fusion") app.setApplicationName("AutoLibrary") if not initializeApp(): sys.exit(-1) diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index e287560..16a53c5 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -204,11 +204,11 @@ class _DateOffsetContainer(QWidget): val = self._spinBox.value() unit = self._unitCombo.currentData() if unit == "weeks": - return val * 7 + return val*7 if unit == "months": - return val * 30 + return val*30 if unit == "years": - return val * 365 + return val*365 return val diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index 66d5319..343acfa 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -12,7 +12,8 @@ import time import queue from PySide6.QtCore import ( - Slot, Signal, QThread + Signal, + QThread, ) from base.MsgBase import MsgBase @@ -30,7 +31,7 @@ class AutoLibWorker(MsgBase, QThread): self, input_queue: queue.Queue, output_queue: queue.Queue, - config_paths: dict + config_paths: dict, ): MsgBase.__init__(self, input_queue, output_queue) @@ -45,7 +46,7 @@ class AutoLibWorker(MsgBase, QThread): if current_time >= "23:30" or current_time <= "07:30": self._showTrace( "当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试", - self.TraceLevel.WARNING + self.TraceLevel.WARNING, ) return False self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO) @@ -60,83 +61,113 @@ class AutoLibWorker(MsgBase, QThread): ): self._showTrace( "配置文件路径不存在, 请检查配置文件路径是否正确", - self.TraceLevel.ERROR + self.TraceLevel.ERROR, ) return False - self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO) + self._showLog( + f"配置文件路径检查通过, 路径: {self.__config_paths}", + self.TraceLevel.INFO, + ) return True def loadConfigs( - self + self, ) -> bool: self._showTrace( f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}", - no_log=True + no_log=True, ) self._run_config = JSONReader(self.__config_paths["run"]).data() self._showTrace( f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}", - no_log=True + 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.TraceLevel.ERROR + self.TraceLevel.ERROR, ) return False if not self._user_config.get("groups"): self._showTrace( "用户配置文件中无有效任务组, 请检查用户配置文件是否正确", - self.TraceLevel.WARNING + self.TraceLevel.WARNING, ) return False self._showLog( - f"配置文件加载成功, 任务组数量: {len(self._user_config.get('groups', []))}", - self.TraceLevel.INFO + f"配置文件加载成功, 任务组数量: {len(self._user_config.get("groups"))}", + self.TraceLevel.INFO, ) return True + def _runName( + self, + ) -> str: + + return "常规任务" + + def _beforeCreateAutoLib( + self, + ): + + return + + def _onChecksFailed( + self, + ) -> bool: + + return True + + def _onFinished( + self, + ): + + self.autoLibWorkerIsFinished.emit() + + def _onError( + self, + error_msg: str, + ): + + self._showTrace(error_msg, self.TraceLevel.ERROR) + self.autoLibWorkerFinishedWithError.emit() + def run( - self + self, ): auto_lib = None - self._showTrace("AutoLibrary 开始运行") - if not self.checkTimeAvailable()\ - or not self.checkConfigPaths(): - # time or config existence check failed, skip and finish - pass + self._showTrace(f"{self._runName()} 开始运行") + + if not self.checkTimeAvailable() or not self.checkConfigPaths(): + if not self._onChecksFailed(): + return else: try: if not self.loadConfigs(): raise Exception("配置文件加载失败") + self._beforeCreateAutoLib() auto_lib = AutoLib( self._input_queue, self._output_queue, - self._run_config + self._run_config, ) groups = self._user_config.get("groups") for group in groups: - if not group["enabled"]: - self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True) + if not group.get("enabled", False): + self._showTrace(f"任务组 {group.get("name", "未知")} 已跳过", no_log=True) continue - self._showTrace(f"正在运行任务组 {group["name"]}", no_log=True) - auto_lib.run( - { "users": group.get("users", []) } - ) + self._showTrace(f"正在运行任务组 {group.get("name", "未知")}", no_log=True) + auto_lib.run({"users": group.get("users", [])}) except Exception as e: - self._showTrace( - f"AutoLibrary 运行时发生异常 : {e}", - self.TraceLevel.ERROR - ) - self.autoLibWorkerFinishedWithError.emit() + self._onError(f"{self._runName()} 运行时发生异常 : {e}") return if auto_lib: auto_lib.close() - self._showTrace("AutoLibrary 运行结束") - self.autoLibWorkerIsFinished.emit() + self._showTrace(f"{self._runName()} 运行结束") + self._onFinished() class TimerTaskWorker(AutoLibWorker): @@ -148,70 +179,54 @@ class TimerTaskWorker(AutoLibWorker): timer_task: dict, input_queue: queue.Queue, output_queue: queue.Queue, - config_paths: dict + config_paths: dict, ): super().__init__(input_queue, output_queue, config_paths) self.__timer_task = timer_task - self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished) - self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError) + def _runName( + self, + ) -> str: - def run( - self + return f"定时任务 '{self.__timer_task.get("name", "未知")}'" + + def _beforeCreateAutoLib( + self, + ): + + self.applyRepeatAutoScript() + + def _onChecksFailed( + self, + ) -> bool: + + self._showTrace("定时任务跳过执行: 时间或配置文件检查未通过") + self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) + return False + + def _onFinished( + self, ): - self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行") - if not self.checkTimeAvailable() or not self.checkConfigPaths(): - self._showTrace("定时任务跳过执行 (时间或配置文件检查未通过)") - self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) - return - try: - if not self.loadConfigs(): - raise Exception("配置文件加载失败") - self.applyRepeatAutoScript() - auto_lib = AutoLib( - self._input_queue, - self._output_queue, - self._run_config - ) - groups = self._user_config.get("groups") - for group in groups: - if not group["enabled"]: - self._showTrace( - f"任务组 {group['name']} 已跳过", - no_log=True - ) - continue - self._showTrace( - f"正在运行任务组 {group['name']}", - no_log=True - ) - auto_lib.run( - {"users": group.get("users", [])} - ) - auto_lib.close() - except Exception as e: - self._showTrace( - f"定时任务 {self.__timer_task['name']} 运行时发生异常: {e}", - self.TraceLevel.ERROR - ) - self.timerTaskWorkerIsFinished.emit(True, self.__timer_task) - return - self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束") self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) + def _onError( + self, + error_msg: str, + ): + + self._showTrace(error_msg, self.TraceLevel.ERROR) + self.timerTaskWorkerIsFinished.emit(True, self.__timer_task) + def applyRepeatAutoScript( - self + self, ): auto_script = self.__timer_task.get("repeat_auto_script", "") if not auto_script or not auto_script.strip(): return - self._showTrace( - f"检测到重复定时任务 AutoScript, 开始执行...", - no_log=True - ) + self._showTrace("检测到重复定时任务 AutoScript, 开始执行...", no_log=True) groups = self._user_config.get("groups", []) affected_count = 0 for group in groups: @@ -224,30 +239,10 @@ class TimerTaskWorker(AutoLibWorker): affected_count += 1 except ValueError as e: self._showTrace( - f"AutoScript 执行错误 (用户 {user['username']}): {e}", - self.TraceLevel.ERROR + f"AutoScript 执行错误 (用户 {user.get("username", "未知")}): {e}", + self.TraceLevel.ERROR, ) self._showLog( - f"AutoScript 执行完毕, " - f"影响 {affected_count} 个用户", - self.TraceLevel.INFO + f"AutoScript 执行完毕, 影响 {affected_count} 个用户", + self.TraceLevel.INFO, ) - - @Slot() - def onTimerTaskIsFinished( - self - ): - - self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束") - self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) - - @Slot() - def onTimerTaskFinishedWithError( - self - ): - - self._showTrace( - f"定时任务 {self.__timer_task['name']} 运行时发生异常", - self.TraceLevel.ERROR - ) - self.timerTaskWorkerIsFinished.emit(True, self.__timer_task) diff --git a/src/gui/ALSeatMapView.py b/src/gui/ALSeatMapView.py index a215e2a..2fd1e2c 100644 --- a/src/gui/ALSeatMapView.py +++ b/src/gui/ALSeatMapView.py @@ -8,7 +8,9 @@ You may use, modify, and distribute this file under the terms of the MIT License See the LICENSE file for details. """ from PySide6.QtCore import ( - Qt, Slot, QEvent + Qt, + Slot, + QEvent ) from PySide6.QtWidgets import ( QFrame, diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 2252909..10b4d65 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -12,9 +12,23 @@ import uuid from enum import Enum from datetime import datetime, timedelta -from PySide6.QtCore import Slot, QDateTime, QUrl +from PySide6.QtCore import ( + Slot, + QDateTime, + QUrl +) from PySide6.QtGui import QDesktopServices -from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton +from PySide6.QtWidgets import ( + QLabel, + QDialog, + QWidget, + QSpinBox, + QHBoxLayout, + QVBoxLayout, + QDateTimeEdit, + QGroupBox, + QPushButton +) from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog from utils.TimerUtils import TimerUtils diff --git a/src/pages/AutoLib.py b/src/pages/AutoLib.py index bfac01a..fab83e4 100644 --- a/src/pages/AutoLib.py +++ b/src/pages/AutoLib.py @@ -68,7 +68,7 @@ class AutoLib(MsgBase): self._showTrace("正在初始化浏览器驱动......", no_log=True) web_driver_config: dict = self.__run_config.get("web_driver", None) - self.__driver_type = web_driver_config.get("driver_type") + self.__driver_type = web_driver_config.get("driver_type", "none") match self.__driver_type.lower(): case "edge": driver_options = webdriver.EdgeOptions() @@ -85,7 +85,7 @@ class AutoLib(MsgBase): if not web_driver_config: self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR) return False - if web_driver_config.get("headless"): + if web_driver_config.get("headless", False): driver_options.add_argument("--headless") driver_options.add_argument("--disable-gpu") driver_options.add_argument("--no-sandbox") @@ -122,12 +122,12 @@ class AutoLib(MsgBase): driver_options.add_argument(f"user-agent={user_agent}") # init browser driver - self.__driver_path = web_driver_config.get("driver_path") + self.__driver_path = web_driver_config.get("driver_path", "") if not self.__driver_path: self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING) return False - self.__driver_path = os.path.abspath(self.__driver_path) try: + self.__driver_path = os.path.abspath(self.__driver_path) service = None match self.__driver_type.lower(): case "edge": @@ -236,7 +236,6 @@ class AutoLib(MsgBase): # result : -1 - terminate, 0 - success, 1 - failed, 2 - passed result: int = 2 - # login auto_captcha: bool = login_config.get("auto_captcha", True) if not self.__login_page.login( @@ -255,7 +254,7 @@ class AutoLib(MsgBase): } # reserve if run_mode["auto_reserve"]: - if self.__record_checker.canReserve(self.__shell, reserve_info.get("date")): + if self.__record_checker.canReserve(self.__shell, reserve_info["date"]): if self.__reserve_checker.check(reserve_info): ctx = ReserveContext( username=username, @@ -331,30 +330,29 @@ class AutoLib(MsgBase): ) -> None: self.__user_config = user_config - user_counter: dict[str, int] = {"current": 0, "success": 0, "failed": 0, "passed": 0} - users: list = self.__user_config["users"] + users: list = self.__user_config.get("users", []) self._showTrace(f"共发现 {len(users)} 个用户") 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.get("username", "未知")}......", no_log=True, ) - if not user["enabled"]: - self._showTrace(f"用户 {user['username']} 已跳过") + if not user.get("enabled", False): + self._showTrace(f"用户 {user.get("username", "未知")} 已跳过") user_counter["passed"] += 1 continue r: int = self.__run( - username=user["username"], - password=user["password"], - login_config=self.__run_config["login"], - run_mode_config=self.__run_config["mode"], - reserve_info=user["reserve_info"], + username=user.get("username", ""), + password=user.get("password", ""), + login_config=self.__run_config.get("login", {}), + run_mode_config=self.__run_config.get("mode", {}), + reserve_info=user.get("reserve_info", {}), ) if r == -1: self._showTrace( - f"用户 {user['username']} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !", + f"用户 {user.get("username", "未知")} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !", self.TraceLevel.WARNING, ) break @@ -365,10 +363,10 @@ class AutoLib(MsgBase): elif r == 2: user_counter["passed"] += 1 self._showTrace( - f"处理完成, 共计 {user_counter['current']} 个用户, " - f"成功 {user_counter['success']} 个用户, " - f"失败 {user_counter['failed']} 个用户, " - f"跳过 {user_counter['passed']} 个用户" + f"处理完成, 共计 {user_counter["current"]} 个用户, " + f"成功 {user_counter["success"]} 个用户, " + f"失败 {user_counter["failed"]} 个用户, " + f"跳过 {user_counter["passed"]} 个用户" ) return diff --git a/src/pages/ReserveView.py b/src/pages/ReserveView.py index eda9b54..1f7e459 100644 --- a/src/pages/ReserveView.py +++ b/src/pages/ReserveView.py @@ -19,7 +19,6 @@ from selenium.common.exceptions import ( ) from pages.components.SeatMapDialog import SeatMapDialog -from pages.components.ReserveResultDialog import ReserveResultDialog class ReserveView: @@ -102,31 +101,30 @@ class ReserveView: def selectRoom( self, room: str, - ) -> bool: + ) -> SeatMapDialog | None: try: WebDriverWait(self._driver, 2).until( EC.element_to_be_clickable(self.FIND_ROOM_BTN) ).click() except (TimeoutException, ElementNotInteractableException): - return False + return None except Exception: - return False + return None try: WebDriverWait(self._driver, 2).until( EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room))) ).click() - return True except (TimeoutException, ElementNotInteractableException): - return False + return None except Exception: - return False - - def openSeatMap( - self, - ) -> SeatMapDialog: - - return SeatMapDialog(self._driver) + return None + try: + return SeatMapDialog(self._driver) + except (TimeoutException): + return None + except Exception: + return None def submitReserve( self, @@ -142,12 +140,6 @@ class ReserveView: except Exception: return False - def waitResultDialog( - self, - ) -> ReserveResultDialog: - - return ReserveResultDialog(self._driver) - def refresh( self, ) -> None: diff --git a/src/pages/components/Dialog.py b/src/pages/components/Dialog.py index 95d1bfe..9f4e268 100644 --- a/src/pages/components/Dialog.py +++ b/src/pages/components/Dialog.py @@ -17,6 +17,9 @@ class Dialog: """ Context-managed overlay / modal / dialog on a page. + The constructor verifies that the root element is visible — if not, + the dialog is not on screen and a :exc:`TimeoutException` is raised. + Automates the lifecycle: wait for appearance on enter, optionally wait for disappearance on exit. """ @@ -34,13 +37,14 @@ class Dialog: self._auto_close: bool = auto_close_on_exit self._timeout: float = wait_timeout + WebDriverWait(self._driver, self._timeout).until( + EC.visibility_of_element_located(self._root_locator) + ) + def __enter__( self, ) -> "Dialog": - WebDriverWait(self._driver, self._timeout).until( - EC.visibility_of_element_located(self._root_locator) - ) return self def __exit__( diff --git a/src/pages/components/RenewDialog.py b/src/pages/components/RenewDialog.py index d663a1c..150eca1 100644 --- a/src/pages/components/RenewDialog.py +++ b/src/pages/components/RenewDialog.py @@ -17,6 +17,10 @@ from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webelement import WebElement from pages.components.Dialog import Dialog +from pages.strategies.TimeSelectMaker import ( + TimeSelectionResult, + TimeSelectMaker, +) class RenewDialog(Dialog): @@ -80,6 +84,26 @@ class RenewDialog(Dialog): return self._findAll(*self.TIME_OPTS) + def selectBestTime( + self, + target_time: int, + max_time_diff: int, + prefer_earlier: bool, + ) -> TimeSelectionResult: + + all_time_opts = self.getTimeOptions() + if not all_time_opts: + return TimeSelectionResult() + result = TimeSelectMaker.forRenew().decide( + all_time_opts, + target_time, + max_time_diff, + prefer_earlier, + ) + if result.selected_index >= 0: + all_time_opts[result.selected_index].click() + return result + def getOkButton( self, ) -> WebElement: @@ -93,7 +117,8 @@ class RenewDialog(Dialog): try: self._find(*self.OK_BTN).click() return True - except (NoSuchElementException, TimeoutException, ElementNotInteractableException): + except (NoSuchElementException, TimeoutException, + ElementNotInteractableException): return False except Exception: return False diff --git a/src/pages/components/ReserveResultDialog.py b/src/pages/components/ReserveResultDialog.py index d1ac48e..8ddb858 100644 --- a/src/pages/components/ReserveResultDialog.py +++ b/src/pages/components/ReserveResultDialog.py @@ -31,12 +31,18 @@ class ReserveResultDialog(Dialog): super().__init__(driver, self.ROOT, auto_close_on_exit=False) + def _titleLocator( + self, + ) -> tuple: + + return (By.CSS_SELECTOR, ".layoutSeat dt") + def getTitle( self, ) -> str: try: - return self._find(*self._title_locator()).text + return self._find(*self._titleLocator()).text except (NoSuchElementException, StaleElementReferenceException): return "" except Exception: @@ -73,9 +79,3 @@ class ReserveResultDialog(Dialog): return [] except Exception: return [] - - def _title_locator( - self, - ) -> tuple: - - return (By.CSS_SELECTOR, ".layoutSeat dt") diff --git a/src/pages/components/TimeSelectDialog.py b/src/pages/components/TimeSelectDialog.py index 643769f..1a843a3 100644 --- a/src/pages/components/TimeSelectDialog.py +++ b/src/pages/components/TimeSelectDialog.py @@ -7,6 +7,11 @@ 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. """ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Callable, Optional + from selenium.common.exceptions import ( NoSuchElementException, TimeoutException, @@ -16,6 +21,16 @@ from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webelement import WebElement from pages.components.Dialog import Dialog +from pages.strategies.TimeSelectMaker import ( + TimeRangeResult, + TimeSelectionResult, + TimeSelectMaker, + minsToTimeStr, + timeStrToMins, +) + +if TYPE_CHECKING: + from pages.flows.ReserveFlow import ReserveContext class TimeSelectDialog(Dialog): @@ -31,9 +46,56 @@ class TimeSelectDialog(Dialog): def __init__( self, driver: WebDriver, + tracer: Optional[Callable[[str, int], None]] = None, ) -> None: super().__init__(driver, self.ROOT, auto_close_on_exit=False) + self._tracer = tracer + + def _trace( + self, + msg: str, + level: int = logging.INFO, + ) -> None: + + if self._tracer is not None: + self._tracer(msg, level) + + def _logTimeStep( + self, + time_type: str, + target_mins: int, + max_diff: int, + step_result: TimeSelectionResult, + ) -> bool: + + if step_result.selected_index >= 0: + abs_diff = abs(step_result.actual_diff) + if step_result.actual_diff < 0: + relation = f"早了 {abs_diff} 分钟" + elif step_result.actual_diff > 0: + relation = f"晚了 {abs_diff} 分钟" + else: + relation = f"正好等于 {time_type}" + self._trace( + f"选择距离期望 {time_type} 最近的 {step_result.display_text}, " + f"与期望 {time_type} 相比 {relation}" + ) + return True + if not step_result.free_times: + self._trace( + f"{time_type} 选择失败 ! : 当前未查询到可用时间", + logging.ERROR, + ) + else: + target_str = minsToTimeStr(target_mins) + self._trace( + f"无法选择最近的 {time_type} {target_str}, " + f"所有可选时间与目标时间相差都超过 {max_diff} 分钟", + logging.WARNING, + ) + self._trace(f"当前可供预约的 {time_type} 有: {step_result.free_times}") + return False def getTimeOptions( self, @@ -52,3 +114,119 @@ class TimeSelectDialog(Dialog): By.CSS_SELECTOR, f"#{time_id} ul li a", ) + + def selectNearestTime( + self, + time_id: str, + target_time: int, + max_time_diff: int, + prefer_earlier: bool, + ) -> TimeSelectionResult: + + all_time_opts = self.getTimeOptions(time_id) + if not all_time_opts: + return TimeSelectionResult() + result = TimeSelectMaker.forReserve().decide( + all_time_opts, + target_time, + max_time_diff, + prefer_earlier, + ) + if result.selected_index >= 0: + all_time_opts[result.selected_index].click() + return result + + def selectTimeRange( + self, + begin_target: int, + end_target: int, + begin_max_diff: int = 30, + end_max_diff: int = 30, + begin_prefer_early: bool = True, + end_prefer_early: bool = False, + satisfy_duration: bool = True, + expect_duration: int = 4, + library_close_mins: int = TimeSelectMaker.LIBRARY_CLOSE_MINS, + ) -> TimeRangeResult: + + begin_result = self.selectNearestTime( + "startTime", + begin_target, + begin_max_diff, + begin_prefer_early, + ) + if begin_result.selected_index < 0: + return TimeRangeResult(begin_result=begin_result) + actual_begin = begin_result.selected_value + if satisfy_duration: + end_target = TimeSelectMaker.calcEndTime( + actual_begin, + expect_duration, + library_close_mins, + ) + end_result = self.selectNearestTime( + "endTime", + end_target, + end_max_diff, + end_prefer_early, + ) + if end_result.selected_index < 0: + return TimeRangeResult( + begin_result=begin_result, + actual_begin_mins=actual_begin, + end_result=end_result, + expect_end_mins=end_target, + ) + return TimeRangeResult( + begin_result=begin_result, + end_result=end_result, + actual_begin_mins=actual_begin, + actual_end_mins=end_result.selected_value, + expect_end_mins=end_target, + ) + + def selectSeatTime( + self, + ctx: ReserveContext, + library_close_mins: int = TimeSelectMaker.LIBRARY_CLOSE_MINS, + ) -> bool: + + exp_beg_mins = timeStrToMins(ctx.begin_time) + exp_end_mins = timeStrToMins(ctx.end_time) + result = self.selectTimeRange( + begin_target=exp_beg_mins, + end_target=exp_end_mins, + begin_max_diff=ctx.begin_max_diff, + end_max_diff=ctx.end_max_diff, + begin_prefer_early=ctx.begin_prefer_early, + end_prefer_early=ctx.end_prefer_early, + satisfy_duration=ctx.satisfy_duration, + expect_duration=ctx.expect_duration, + library_close_mins=library_close_mins, + ) + if not self._logTimeStep("开始时间", exp_beg_mins, ctx.begin_max_diff, result.begin_result): + return False + if ctx.satisfy_duration: + unclipped = result.actual_begin_mins + ctx.expect_duration*60 + if unclipped > library_close_mins: + self._trace( + f"预约持续时间 {ctx.expect_duration} 小时, 超过最大预约时间 {minsToTimeStr(library_close_mins)}, " + f"自动调整为 {minsToTimeStr(library_close_mins)}", + logging.WARNING, + ) + act_beg_str = minsToTimeStr(result.actual_begin_mins) + exp_end_str = minsToTimeStr(result.expect_end_mins) + self._trace( + f"需要满足期望预约持续时间: {ctx.expect_duration} 小时, " + f"根据开始时间 {act_beg_str} 计算结束时间: {exp_end_str}" + ) + if not self._logTimeStep("结束时间", result.expect_end_mins, ctx.end_max_diff, result.end_result): + return False + act_beg_str = minsToTimeStr(result.actual_begin_mins) + act_end_str = minsToTimeStr(result.actual_end_mins) + exp_end_str = minsToTimeStr(result.expect_end_mins) + self._trace( + f"期望预约时间段: {ctx.begin_time} - {exp_end_str}, " + f"实际预约时间段: {act_beg_str} - {act_end_str}" + ) + return True diff --git a/src/pages/flows/RenewFlow.py b/src/pages/flows/RenewFlow.py index fb06111..b8f6421 100644 --- a/src/pages/flows/RenewFlow.py +++ b/src/pages/flows/RenewFlow.py @@ -20,7 +20,7 @@ from base.MsgBase import MsgBase from pages.MainShell import MainShell from pages.components.RenewDialog import RenewDialog from pages.flows._helpers import timeStrToMins, minsToTimeStr -from pages.strategies.timeSelectMaker import TimeSelectMaker +from pages.strategies.TimeSelectMaker import TimeSelectMaker class RenewFlow(MsgBase): @@ -46,10 +46,10 @@ class RenewFlow(MsgBase): renew_info: dict, ) -> bool: - max_diff = renew_info["max_diff"] - prefer_earlier = renew_info["prefer_early"] + max_diff = renew_info.get("max_diff", 30) + prefer_earlier = renew_info.get("prefer_early", True) end_time = record["time"]["end"] - target_renew_mins = timeStrToMins(end_time) + renew_info["expect_duration"] * 60 + target_renew_mins = timeStrToMins(end_time) + renew_info.get("expect_duration", 2) * 60 if not self._validateRenewTime(end_time, target_renew_mins): return False if not self._shell.waitExtendButton(): @@ -74,20 +74,12 @@ class RenewFlow(MsgBase): self._shell.refresh() self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR) return False - renew_ok_btn = dialog.getOkButton() - renew_time_opts = dialog.getTimeOptions() - if not renew_time_opts: - self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING) - self._shell.refresh() - return False - result = TimeSelectMaker.forRenew().decide( - renew_time_opts, + result = dialog.selectBestTime( target_renew_mins, max_diff, - prefer_earlier + prefer_earlier, ) if result.selected_index >= 0: - renew_time_opts[result.selected_index].click() abs_diff = abs(result.actual_diff) if result.actual_diff < 0: relation = f"早了 {abs_diff} 分钟" @@ -100,15 +92,18 @@ class RenewFlow(MsgBase): f"与期望续约时间相比 {relation}" ) record["time"]["end"] = result.display_text.strip() - renew_ok_btn.click() + dialog.clickOk() self._shell.refresh() return True - self._showTrace( - "无法选择最近的可用续约时间 ! " - f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", - self.TraceLevel.WARNING, - ) - self._showTrace(f"当前可供续约的时间有: {result.free_times}") + if not result.free_times: + self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING) + else: + self._showTrace( + "无法选择最近的可用续约时间 ! " + f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", + self.TraceLevel.WARNING, + ) + self._showTrace(f"当前可供续约的时间有: {result.free_times}") self._shell.refresh() return False except (NoSuchElementException, TimeoutException) as e: diff --git a/src/pages/flows/ReserveFlow.py b/src/pages/flows/ReserveFlow.py index e26eab8..1722903 100644 --- a/src/pages/flows/ReserveFlow.py +++ b/src/pages/flows/ReserveFlow.py @@ -9,7 +9,6 @@ See the LICENSE file for details. """ import queue from dataclasses import dataclass -from typing import Optional from selenium.common.exceptions import ( ElementNotInteractableException, @@ -20,8 +19,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from base.MsgBase import MsgBase from pages.MainShell import MainShell -from pages.flows._helpers import timeStrToMins, minsToTimeStr -from pages.strategies.timeSelectMaker import TimeSelectMaker +from pages.strategies.TimeSelectMaker import TimeSelectMaker from pages.ReserveView import ReserveView from pages.components.ReserveResultDialog import ReserveResultDialog from pages.components.TimeSelectDialog import TimeSelectDialog @@ -60,14 +58,12 @@ class ReserveFlow(MsgBase): super().__init__(input_queue, output_queue) self._driver: WebDriver = driver self._shell: MainShell = shell - self._ctx: Optional[ReserveContext] = None def execute( self, ctx: ReserveContext, ) -> bool: - self._ctx = ctx submit_reserve = False reserve_success = False have_hover_on_page = False @@ -93,13 +89,13 @@ class ReserveFlow(MsgBase): self._showTrace(f"选择楼层失败 ! : {display_floor} 不可用", self.TraceLevel.ERROR) return False self._showTrace(f"楼层 {ReserveView.FLOOR_MAP.get(ctx.floor)} 选择成功 !") - if not view.selectRoom(ctx.room): + seat_map = view.selectRoom(ctx.room) + if seat_map is None: display_room = ReserveView.ROOM_MAP.get(ctx.room, ctx.room) self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) return False self._showTrace(f"房间 {ReserveView.ROOM_MAP.get(ctx.room)} 选择成功 !") have_hover_on_page = True - seat_map = view.openSeatMap() seat_status = seat_map.selectSeat(ctx.seat_id) if seat_status is None: self._showTrace( @@ -108,41 +104,44 @@ class ReserveFlow(MsgBase): ) else: self._showTrace(f"座位 {ctx.seat_id} 选择成功 ! : 当前状态 - '{seat_status}'") - time_dialog = TimeSelectDialog(self._driver) - select_time_ok = self._selectSeatTime(time_dialog) - if not select_time_ok: - self._showTrace("选择时间失败 !", self.TraceLevel.ERROR) + try: + time_dialog = TimeSelectDialog(self._driver, tracer=self._showTrace) + except TimeoutException: + self._showTrace("时间选择面板未出现 !", self.TraceLevel.ERROR) else: - try: - view.submitReserve() - submit_reserve = True - with ReserveResultDialog(self._driver) as result: - if result.isFailure(): - self._showTrace("预约失败", self.TraceLevel.ERROR) - elif result.isSuccess(): - details = result.getDetailTexts() - if len(details) >= 6: - self._showTrace( - f"\n" - f" 预约成功 !\n" - f" {details[1]}\n" - f" {details[2]}\n" - f" {details[3]}\n" - f" 签到时间 :{details[5]}" - ) + if not time_dialog.selectSeatTime(ctx): + self._showTrace("选择时间失败 !", self.TraceLevel.ERROR) + else: + try: + view.submitReserve() + submit_reserve = True + with ReserveResultDialog(self._driver) as result: + if result.isFailure(): + self._showTrace("预约失败", self.TraceLevel.ERROR) + elif result.isSuccess(): + details = result.getDetailTexts() + if len(details) >= 6: + self._showTrace( + f"\n" + f" 预约成功 !\n" + f" {details[1]}\n" + f" {details[2]}\n" + f" {details[3]}\n" + f" 签到时间 :{details[5]}" + ) + else: + self._showTrace( + "\n" + " 预约成功 !\n" + " 未找获取到详细信息" + ) + reserve_success = True else: - self._showTrace( - "\n" - " 预约成功 !\n" - " 未找获取到详细信息" - ) - reserve_success = True - else: - self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR) - except (TimeoutException, ElementNotInteractableException): - self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) - except Exception: - self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) + self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR) + except (TimeoutException, ElementNotInteractableException): + self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) + except Exception: + self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) if not submit_reserve and have_hover_on_page: view.refresh() if reserve_success: @@ -150,113 +149,3 @@ class ReserveFlow(MsgBase): else: self._showTrace(f"用户 {ctx.username} 预约失败 !", self.TraceLevel.ERROR) return reserve_success - - def _selectSeatTime( - self, - time_dialog: TimeSelectDialog, - ) -> bool: - - ctx = self._ctx - exp_beg_tm_str = ctx.begin_time - exp_end_tm_str = ctx.end_time - exp_beg_mins = timeStrToMins(exp_beg_tm_str) - exp_end_mins = timeStrToMins(exp_end_tm_str) - act_beg_mins = exp_beg_mins - act_beg_tm_str = exp_beg_tm_str - act_end_mins = exp_end_mins - act_end_tm_str = exp_end_tm_str - act_beg_mins = self._selectNearestTime( - time_dialog, - time_id="startTime", - time_type="开始时间", - target_time=exp_beg_mins, - max_time_diff=ctx.begin_max_diff, - prefer_earlier=ctx.begin_prefer_early, - ) - if act_beg_mins == -1: - return False - act_beg_tm_str = minsToTimeStr(act_beg_mins) - if ctx.satisfy_duration: - exp_end_mins = self._calcEndTime(act_beg_mins, ctx.expect_duration) - exp_end_tm_str = minsToTimeStr(exp_end_mins) - self._showTrace( - f"需要满足期望预约持续时间: {ctx.expect_duration} 小时, " - f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}" - ) - act_end_mins = self._selectNearestTime( - time_dialog, - time_id="endTime", - time_type="结束时间", - target_time=exp_end_mins, - max_time_diff=ctx.end_max_diff, - prefer_earlier=ctx.end_prefer_early, - ) - if act_end_mins == -1: - return False - act_end_tm_str = minsToTimeStr(act_end_mins) - self._showTrace( - f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, " - f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}" - ) - return True - - def _selectNearestTime( - self, - time_dialog: TimeSelectDialog, - time_id: str, - time_type: str, - target_time: int, - max_time_diff: int, - prefer_earlier: bool, - ) -> int: - - all_time_opts = time_dialog.getTimeOptions(time_id) - if not all_time_opts: - self._showTrace( - f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR - ) - return -1 - result = TimeSelectMaker.forReserve().decide( - all_time_opts, - target_time, - max_time_diff, - prefer_earlier - ) - if result.selected_index >= 0: - all_time_opts[result.selected_index].click() - abs_diff = abs(result.actual_diff) - if result.actual_diff < 0: - relation = f"早了 {abs_diff} 分钟" - elif result.actual_diff > 0: - relation = f"晚了 {abs_diff} 分钟" - else: - relation = f"正好等于 {time_type}" - self._showTrace( - f"选择距离期望 {time_type} 最近的 {result.display_text}, " - f"与期望 {time_type} 相比 {relation}" - ) - return target_time + result.actual_diff - target_time_str = minsToTimeStr(target_time) - self._showTrace( - f"无法选择最近的 {time_type} {target_time_str}, " - f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", - self.TraceLevel.WARNING, - ) - self._showTrace(f"当前可供预约的 {time_type} 有: {result.free_times}") - return -1 - - def _calcEndTime( - self, - begin_mins: int, - duration: int, - ) -> int: - - expect_end_mins = int(begin_mins + duration*60) - if expect_end_mins > self.LIBRARY_CLOSE_MINS: - expect_end_mins = self.LIBRARY_CLOSE_MINS - self._showTrace( - f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, " - f"自动调整为 23:30", - self.TraceLevel.WARNING, - ) - return expect_end_mins diff --git a/src/pages/flows/_helpers.py b/src/pages/flows/_helpers.py index 936ccc5..886d17c 100644 --- a/src/pages/flows/_helpers.py +++ b/src/pages/flows/_helpers.py @@ -7,16 +7,13 @@ 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. """ -def timeStrToMins( - time_str: str, -) -> int: +from pages.strategies.TimeSelectMaker import ( + minsToTimeStr, + timeStrToMins +) - hour, minute = map(int, time_str.split(":")) - return hour * 60 + minute -def minsToTimeStr( - mins: int, -) -> str: - - hour, minute = divmod(int(mins), 60) - return f"{hour:02d}:{minute:02d}" +__all__ = [ + "minsToTimeStr", + "timeStrToMins", +] \ No newline at end of file diff --git a/src/pages/services/RecordChecker.py b/src/pages/services/RecordChecker.py index 106ad33..c7cef7f 100644 --- a/src/pages/services/RecordChecker.py +++ b/src/pages/services/RecordChecker.py @@ -38,97 +38,11 @@ class RecordChecker(MsgBase): seconds: float, ) -> str: - hours = int(seconds // 3600) - minutes = int(seconds % 3600 // 60) - seconds = int(seconds % 60) + hours = int(seconds//3600) + minutes = int(seconds%3600//60) + seconds = int(seconds%60) return f"{hours} 时 {minutes} 分 {seconds} 秒" - def _getReserveRecord( - self, - shell: MainShell, - wanted_date: str, - wanted_status: str, - ) -> dict | None: - - if wanted_date is None: - self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING) - return None - self._showTrace( - f"正在检查用户在 {wanted_date} 是否有预约状态为 " - f"{wanted_status} 的预约记录......", 20, no_log=True - ) - - checked_count = 0 - max_check_times = 6 - - records_view = shell.gotoRecordsView() - for _ in range(max_check_times): - reservations = records_view.loadRecords() - if reservations is None: - return None - for reservation in reservations[checked_count:]: - record = self._decodeReserveRecord(reservation, records_view) - checked_count += 1 - if record is None: - continue - if record["date"] == "": - continue - if record["time"] == {"begin": "", "end": ""}: - continue - if ( - datetime.strptime(record["date"], "%Y-%m-%d").date() - > datetime.strptime(wanted_date, "%Y-%m-%d").date() - ): - continue - if ( - datetime.strptime(record["date"], "%Y-%m-%d").date() - < datetime.strptime(wanted_date, "%Y-%m-%d").date() - ): - return None - if record["info"]["status"] == wanted_status: - self._showTrace( - f"寻找到用户第 {checked_count} 条状态为 " - f"{wanted_status} 的预约记录, " - f"详细信息: {record['date']} " - f"{record['time']['begin']} - " - f"{record['time']['end']} " - f"{record['info']['location']}", - 20, no_log=True, - ) - return record - if not records_view.showMoreRecords(): - break - return None - - def _decodeReserveRecord( - self, - reservation, - records_view: RecordsView, - ) -> dict: - - try: - time_element = records_view.getRecordTimeElement(reservation) - info_elements = records_view.getRecordInfoElements(reservation) - except (NoSuchElementException, TimeoutException, StaleElementReferenceException): - return { - "date": "", - "time": {"begin": "", "end": ""}, - "info": {"location": "", "status": ""}, - } - except Exception: - return { - "date": "", - "time": {"begin": "", "end": ""}, - "info": {"location": "", "status": ""}, - } - time_data = self._decodeReserveTime(time_element) - info_data = self._decodeReserveInfo(info_elements) - return { - "date": time_data["date"], - "time": time_data["time"], - "info": info_data, - } - def _decodeReserveTime( self, time_element, @@ -189,6 +103,92 @@ class RecordChecker(MsgBase): location = info.text.strip() return {"location": location, "status": status} + def _decodeReserveRecord( + self, + reservation, + records_view: RecordsView, + ) -> dict: + + try: + time_element = records_view.getRecordTimeElement(reservation) + info_elements = records_view.getRecordInfoElements(reservation) + except (NoSuchElementException, TimeoutException, StaleElementReferenceException): + return { + "date": "", + "time": {"begin": "", "end": ""}, + "info": {"location": "", "status": ""}, + } + except Exception: + return { + "date": "", + "time": {"begin": "", "end": ""}, + "info": {"location": "", "status": ""}, + } + time_data = self._decodeReserveTime(time_element) + info_data = self._decodeReserveInfo(info_elements) + return { + "date": time_data["date"], + "time": time_data["time"], + "info": info_data, + } + + def _getReserveRecord( + self, + shell: MainShell, + wanted_date: str, + wanted_status: str, + ) -> dict | None: + + if wanted_date is None: + self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING) + return None + self._showTrace( + f"正在检查用户在 {wanted_date} 是否有预约状态为 " + f"{wanted_status} 的预约记录......", 20, no_log=True + ) + + checked_count = 0 + max_check_times = 6 + + records_view = shell.gotoRecordsView() + for _ in range(max_check_times): + reservations = records_view.loadRecords() + if reservations is None: + return None + for reservation in reservations[checked_count:]: + record = self._decodeReserveRecord(reservation, records_view) + checked_count += 1 + if record is None: + continue + if record["date"] == "": + continue + if record["time"] == {"begin": "", "end": ""}: + continue + if ( + datetime.strptime(record["date"], "%Y-%m-%d").date() + > datetime.strptime(wanted_date, "%Y-%m-%d").date() + ): + continue + if ( + datetime.strptime(record["date"], "%Y-%m-%d").date() + < datetime.strptime(wanted_date, "%Y-%m-%d").date() + ): + return None + if record["info"]["status"] == wanted_status: + self._showTrace( + f"寻找到用户第 {checked_count} 条状态为 " + f"{wanted_status} 的预约记录, " + f"详细信息: {record["date"]} " + f"{record["time"]["begin"]} - " + f"{record["time"]["end"]} " + f"{record["info"]["location"]}", + 20, no_log=True, + ) + return record + if not records_view.showMoreRecords(): + break + return None + def canReserve( self, shell: MainShell, @@ -232,7 +232,7 @@ class RecordChecker(MsgBase): f"{self._formatDiffTime(abs(time_diff_seconds))}, 可以签到" ) return True - elif 0 <= time_diff_seconds < 30 * 60 - 5: + elif 0 <= time_diff_seconds < 30*60 - 5: self._showTrace( f"用户在 {date} 的预约开始时间为 {begin_time}, " f"当前距离预约开始时间已经过去 " @@ -287,18 +287,18 @@ class RecordChecker(MsgBase): f"\n" f" 续约成功 !\n" f" 日 期 :{date}\n" - f" 时 间 :{act_record['time']['begin']}" - f" - {act_record['time']['end']}\n" - f" 位 置 :{act_record['info']['location']}\n" - f" 状 态 :{act_record['info']['status']}" + f" 时 间 :{act_record["time"]["begin"]}" + f" - {act_record["time"]["end"]}\n" + f" 位 置 :{act_record["info"]["location"]}\n" + f" 状 态 :{act_record["info"]["status"]}" ) return True else: self._showTrace( f"\n" f" 续约失败 !\n" - f" 续约后结束时间为 {act_record['time']['end']}," - f"与预期结束时间 {record['time']['end']} 不符 !" + f" 续约后结束时间为 {act_record["time"]["end"]}," + f"与预期结束时间 {record["time"]["end"]} 不符 !" ) return False self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") diff --git a/src/pages/services/ReserveChecker.py b/src/pages/services/ReserveChecker.py index f38f449..d5785e0 100644 --- a/src/pages/services/ReserveChecker.py +++ b/src/pages/services/ReserveChecker.py @@ -36,11 +36,11 @@ class ReserveChecker(MsgBase): if reserve_info.get("floor") is None: raise ValueError("未指定楼层") if reserve_info["floor"] not in floor_map: - raise ValueError(f"该楼层 '{reserve_info['floor']}' 不存在") + raise ValueError(f"该楼层 '{reserve_info["floor"]}' 不存在") if reserve_info.get("room") is None: raise ValueError("未指定房间") if reserve_info["room"] not in room_map: - raise ValueError(f"该房间 '{reserve_info['room']}' 不存在") + raise ValueError(f"该房间 '{reserve_info["room"]}' 不存在") if reserve_info.get("seat_id") is None: raise ValueError("未指定座位") if reserve_info["seat_id"] == "": @@ -75,7 +75,7 @@ class ReserveChecker(MsgBase): 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 @@ -131,7 +131,7 @@ class ReserveChecker(MsgBase): } self._showTrace( f"结束时间未指定, 自动设置为开始时间加上期望时长: " - f"{reserve_info['end_time']['time']}" + f"{reserve_info["end_time"]["time"]}" ) if "max_diff" not in reserve_info["end_time"]: reserve_info["end_time"]["max_diff"] = 30 @@ -152,7 +152,7 @@ class ReserveChecker(MsgBase): end_mins = timeStrToMins(end_time["time"]) 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"]}, " f"尝试交换时间", self.TraceLevel.WARNING, ) @@ -163,7 +163,7 @@ class ReserveChecker(MsgBase): max_end_mins = 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" @@ -172,20 +172,20 @@ class ReserveChecker(MsgBase): if reserve_info["expect_duration"] > 8: self._showTrace( f"该用户设置了优先满足时长要求, 但是预约期望持续时间 " - f"{reserve_info['expect_duration']} 小时 " + f"{reserve_info["expect_duration"]} 小时 " f"超出最大时长 8 小时, 自动设置为 8 小时", self.TraceLevel.WARNING, ) reserve_info["expect_duration"] = 8 else: - if end_mins - begin_mins > 8 * 60: + if end_mins - begin_mins > 8*60: self._showTrace( f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 " f"{float((end_mins - begin_mins) / 60)} 小时 " f"超出最大时长 8 小时, 自动设置为 8 小时", self.TraceLevel.WARNING, ) - reserve_info["end_time"]["time"] = minsToTimeStr(begin_mins + 8 * 60) + reserve_info["end_time"]["time"] = minsToTimeStr(begin_mins + 8*60) return True def check( @@ -207,12 +207,12 @@ class ReserveChecker(MsgBase): return False self._showTrace( f"预约信息检查完成, 准备预约 " - f"{reserve_info['date']} " - f"{reserve_info['begin_time']['time']} - " - f"{reserve_info['end_time']['time']} " + f"{reserve_info["date"]} " + f"{reserve_info["begin_time"]["time"]} - " + f"{reserve_info["end_time"]["time"]} " f"图书馆 " - f"{ReserveView.FLOOR_MAP[reserve_info['floor']]} " - f"{ReserveView.ROOM_MAP[reserve_info['room']]} " - f"的座位 {reserve_info['seat_id']}" + f"{ReserveView.FLOOR_MAP[reserve_info["floor"]]} " + f"{ReserveView.ROOM_MAP[reserve_info["room"]]} " + f"的座位 {reserve_info["seat_id"]}" ) return True \ No newline at end of file diff --git a/src/pages/strategies/__init__.py b/src/pages/strategies/__init__.py index 6599a5f..23597e4 100644 --- a/src/pages/strategies/__init__.py +++ b/src/pages/strategies/__init__.py @@ -7,7 +7,7 @@ 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. """ -from pages.strategies.timeSelectMaker import ( +from pages.strategies.TimeSelectMaker import ( TimeSelectMaker, TimeDecisionMaker, TimeOptionReader, @@ -15,6 +15,7 @@ from pages.strategies.timeSelectMaker import ( RenewTimeReader, TimeOption, TimeSelectionResult, + TimeRangeResult, ) __all__ = [ @@ -25,4 +26,5 @@ __all__ = [ "RenewTimeReader", "TimeOption", "TimeSelectionResult", + "TimeRangeResult", ] diff --git a/src/pages/strategies/timeSelectMaker.py b/src/pages/strategies/timeSelectMaker.py index ada47cf..92428b6 100644 --- a/src/pages/strategies/timeSelectMaker.py +++ b/src/pages/strategies/timeSelectMaker.py @@ -11,8 +11,20 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime -from pages.flows._helpers import minsToTimeStr +def timeStrToMins( + time_str: str, +) -> int: + + hour, minute = map(int, time_str.split(":")) + return hour*60 + minute + +def minsToTimeStr( + mins: int, +) -> str: + + hour, minute = divmod(int(mins), 60) + return f"{hour:02d}:{minute:02d}" @dataclass class TimeOption: @@ -31,18 +43,28 @@ class TimeSelectionResult: free_times: list[str] = field(default_factory=list) +@dataclass +class TimeRangeResult: + + begin_result: TimeSelectionResult = field(default_factory=TimeSelectionResult) + end_result: TimeSelectionResult = field(default_factory=TimeSelectionResult) + actual_begin_mins: int = -1 + actual_end_mins: int = -1 + expect_end_mins: int = 0 + + class TimeOptionReader(ABC): @abstractmethod def readOptions( self, - elements: list, + elements: list ) -> list[TimeOption]: ... def formatFreeTime( self, - opt: TimeOption, + opt: TimeOption ) -> str: return opt.element_text @@ -56,7 +78,7 @@ class ReserveTimeReader(TimeOptionReader): def readOptions( self, - elements: list, + elements: list ) -> list[TimeOption]: options: list[TimeOption] = [] @@ -74,7 +96,7 @@ class ReserveTimeReader(TimeOptionReader): def formatFreeTime( self, - opt: TimeOption, + opt: TimeOption ) -> str: return minsToTimeStr(opt.value) @@ -87,7 +109,7 @@ class RenewTimeReader(TimeOptionReader): def readOptions( self, - elements: list, + elements: list ) -> list[TimeOption]: options: list[TimeOption] = [] @@ -103,7 +125,7 @@ class TimeDecisionMaker: def __init__( self, - reader: TimeOptionReader, + reader: TimeOptionReader ) -> None: self._reader = reader @@ -113,7 +135,7 @@ class TimeDecisionMaker: elements: list, target_time: int, max_time_diff: int, - prefer_earlier: bool, + prefer_earlier: bool ) -> TimeSelectionResult: options = self._reader.readOptions(elements) @@ -148,9 +170,30 @@ class TimeDecisionMaker: class TimeSelectMaker: - LIBRARY_CLOSE_MINS = 1410 + LIBRARY_CLOSE_MINS = 1350 # 22:30 MAX_DURATION_HOURS = 8 + @staticmethod + def calcEndTime( + begin_mins: int, + duration: int, + library_close_mins: int = LIBRARY_CLOSE_MINS + ) -> int: + + expect_end_mins = int(begin_mins + duration*60) + if expect_end_mins > library_close_mins: + return library_close_mins + return expect_end_mins + + @staticmethod + def calcRemainingDuration( + end_time_str: str, + target_mins: int, + library_close_mins: int = LIBRARY_CLOSE_MINS + ) -> int: + + return library_close_mins - timeStrToMins(end_time_str) + @staticmethod def forReserve( ) -> TimeDecisionMaker: From 43336f98d2b87e5b369564a1665a558f17bbe1d4 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 27 May 2026 20:03:35 +0800 Subject: [PATCH 34/49] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E9=97=AD?= =?UTF-8?q?=E9=A6=86=E6=97=B6=E9=97=B4=E4=B8=BA=20TimeSelectMaker.LIBRARY?= =?UTF-8?q?=5FCLOSE=5FMINS=20(22:30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReserveChecker._finalCheck 中存在硬编码的 "23:30",与 TimeSelectMaker.LIBRARY_CLOSE_MINS (22:30) 不一致,导致校验阶段与选时阶段使用不同的闭馆时间上限。 Co-Authored-By: Claude Opus 4.7 --- src/pages/services/ReserveChecker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/services/ReserveChecker.py b/src/pages/services/ReserveChecker.py index d5785e0..9be86e2 100644 --- a/src/pages/services/ReserveChecker.py +++ b/src/pages/services/ReserveChecker.py @@ -13,6 +13,7 @@ import time from base.MsgBase import MsgBase from pages.ReserveView import ReserveView from pages.flows._helpers import timeStrToMins, minsToTimeStr +from pages.strategies.TimeSelectMaker import TimeSelectMaker class ReserveChecker(MsgBase): @@ -160,13 +161,15 @@ class ReserveChecker(MsgBase): begin_time, end_time = end_time, begin_time begin_mins = timeStrToMins(begin_time["time"]) end_mins = timeStrToMins(end_time["time"]) - max_end_mins = timeStrToMins("23:30") + max_end_mins = TimeSelectMaker.LIBRARY_CLOSE_MINS if end_mins > max_end_mins: + close_time_str = minsToTimeStr(TimeSelectMaker.LIBRARY_CLOSE_MINS) self._showTrace( - f"结束时间 {end_time["time"]} 晚于 23:30, 自动设置为 23:30", + f"结束时间 {end_time["time"]} 晚于 {close_time_str}, " + f"自动设置为 {close_time_str}", self.TraceLevel.WARNING, ) - reserve_info["end_time"]["time"] = "23:30" + reserve_info["end_time"]["time"] = close_time_str end_mins = max_end_mins if reserve_info["satisfy_duration"]: if reserve_info["expect_duration"] > 8: From b279b51b420b60ec70b3860327ab5709f4a4e4b4 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 27 May 2026 20:05:24 +0800 Subject: [PATCH 35/49] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E6=97=A7?= =?UTF-8?q?=20operators/=20=E6=A8=A1=E5=9D=97=E5=92=8C=20base/=20=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit operators/ 模块已被 pages/ 模块完全替代,base/ 中的 LibOperator 和 MsgBase 不再被任何新代码引用。 Co-Authored-By: Claude Opus 4.7 --- src/base/LibOperator.py | 36 -- src/base/__init__.py | 1 - src/operators/AutoLib.py | 352 -------------- src/operators/LibChecker.py | 365 --------------- src/operators/LibCheckin.py | 139 ------ src/operators/LibCheckout.py | 40 -- src/operators/LibLogin.py | 207 -------- src/operators/LibLogout.py | 53 --- src/operators/LibRenew.py | 199 -------- src/operators/LibReserve.py | 674 --------------------------- src/operators/__init__.py | 13 - src/operators/abs/LibTimeSelector.py | 139 ------ src/operators/abs/__init__.py | 6 - 13 files changed, 2224 deletions(-) delete mode 100644 src/base/LibOperator.py delete mode 100644 src/operators/AutoLib.py delete mode 100644 src/operators/LibChecker.py delete mode 100644 src/operators/LibCheckin.py delete mode 100644 src/operators/LibCheckout.py delete mode 100644 src/operators/LibLogin.py delete mode 100644 src/operators/LibLogout.py delete mode 100644 src/operators/LibRenew.py delete mode 100644 src/operators/LibReserve.py delete mode 100644 src/operators/__init__.py delete mode 100644 src/operators/abs/LibTimeSelector.py delete mode 100644 src/operators/abs/__init__.py diff --git a/src/base/LibOperator.py b/src/base/LibOperator.py deleted file mode 100644 index c1ae287..0000000 --- a/src/base/LibOperator.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 queue - -from base.MsgBase import MsgBase - - -class LibOperator(MsgBase): - """ - Base abstract class for library operation. - - This class provides the foundation for library-related operations, inheriting - message handling and tracing abilities from MsgBase. It serves as an abstract - base class that must be subclassed to implement specific library functionality. - """ - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue - ): - - super().__init__(input_queue, output_queue) - - def _waitResponseLoad( - self - ) -> bool: - - pass diff --git a/src/base/__init__.py b/src/base/__init__.py index bac4fcf..7becebc 100644 --- a/src/base/__init__.py +++ b/src/base/__init__.py @@ -3,5 +3,4 @@ Here are the classes and modules in this package: - MsgBase: Base class for messages. - - LibOperator: Base class for library operators. """ \ No newline at end of file diff --git a/src/operators/AutoLib.py b/src/operators/AutoLib.py deleted file mode 100644 index d3a4a85..0000000 --- a/src/operators/AutoLib.py +++ /dev/null @@ -1,352 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 queue - -from selenium import webdriver -from selenium.common.exceptions import TimeoutException -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.edge.service import Service as EdgeService -from selenium.webdriver.chrome.service import Service as ChromeService -from selenium.webdriver.firefox.service import Service as FirefoxService - -from base.MsgBase import MsgBase -from operators.LibChecker import LibChecker -from operators.LibLogin import LibLogin -from operators.LibLogout import LibLogout -from operators.LibReserve import LibReserve -from operators.LibCheckin import LibCheckin -from operators.LibRenew import LibRenew - - -class AutoLib(MsgBase): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - run_config: dict - ): - super().__init__(input_queue, output_queue) - - self.__run_config = run_config - self.__user_config = None - self.__driver = None - if not self.__initBrowserDriver(): - raise Exception("浏览器驱动初始化失败 !") - else: - if not self.__initDriverUrl(): - self.close() - raise Exception("浏览器驱动URL初始化失败 !") - self.__initLibOperators() - - def __initBrowserDriver( - self - ) -> bool: - - self._showTrace("正在初始化浏览器驱动......", no_log=True) - - web_driver_config = self.__run_config.get("web_driver", None) - self.__driver_type = web_driver_config.get("driver_type") - match self.__driver_type.lower(): - case "edge": - driver_options = webdriver.EdgeOptions() - case "chrome": - driver_options = webdriver.ChromeOptions() - case "firefox": - driver_options = webdriver.FirefoxOptions() - case _: - self._showTrace( - f"不支持的浏览器驱动类型: {self.__driver_type} !", - self.TraceLevel.WARNING - ) - return False - - if not web_driver_config: - self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR) - return False - if web_driver_config.get("headless"): - driver_options.add_argument("--headless") - driver_options.add_argument("--disable-gpu") - driver_options.add_argument("--no-sandbox") - driver_options.add_argument("--disable-dev-shm-usage") - - # must be 1920x1080, otherwise the page will cause some elements not accessible - driver_options.add_argument("--window-size=1920,1080") - - # omit ssl errors and verbose log level - driver_options.add_argument("--ignore-certificate-errors") - driver_options.add_argument("--ignore-ssl-errors") - driver_options.add_argument("--log-level=OFF") - driver_options.add_argument("--silent") - - # set options for chrome and edge - if self.__driver_type.lower() in ["edge", "chrome"]: - driver_options.add_argument("--remote-allow-origins=*") - driver_options.add_experimental_option("excludeSwitches", ["enable-automation"]) - driver_options.add_experimental_option("useAutomationExtension", False) - driver_options.add_argument("--disable-blink-features=AutomationControlled") - user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\ - "AppleWebKit/537.36 (KHTML, like Gecko) "\ - "Chrome/120.0.0.0 "\ - "Safari/537.36" - if self.__driver_type.lower() == "edge": - user_agent += " Edg/120.0.0.0" - # set options for firefox - elif self.__driver_type.lower() == "firefox": - driver_options.set_preference("dom.webdriver.enabled", False) - driver_options.set_preference("useAutomationExtension", False) - user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\ - "Gecko/20100101 Firefox/120.0" - driver_options.add_argument(f"user-agent={user_agent}") - - # init browser driver - self.__driver_path = web_driver_config.get("driver_path") - if not self.__driver_path: - self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING) - return False - self.__driver_path = os.path.abspath(self.__driver_path) - try: - service = None - match self.__driver_type.lower(): - case "edge": - service = EdgeService(executable_path=self.__driver_path) - self.__driver = webdriver.Edge(service=service, options=driver_options) - case "chrome": - service = ChromeService(executable_path=self.__driver_path) - self.__driver = webdriver.Chrome(service=service, options=driver_options) - case "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 - # of 'driver_options' - 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.TraceLevel.ERROR) - return False - self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") - return True - - def __initLibOperators( - self - ): - - if not self.__driver: - 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) - self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver) - self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver) - self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver) - self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver) - - def __waitResponseLoad( - self - ) -> bool: - - # wait for page load - try: - WebDriverWait(self.__driver, 2).until( # title contains "首页" - EC.title_contains("首页") - ) - WebDriverWait(self.__driver, 2).until( # username field presence - EC.presence_of_element_located((By.NAME, "username")) - ) - WebDriverWait(self.__driver, 2).until( # password field presence - EC.presence_of_element_located((By.NAME, "password")) - ) - WebDriverWait(self.__driver, 2).until( # captcha field presence - EC.presence_of_element_located((By.NAME, "answer")) - ) - WebDriverWait(self.__driver, 2).until( # captcha image presence - EC.presence_of_element_located((By.ID, "loadImgId")) - ) - return True - except: - self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR) - return False - - def __initDriverUrl( - self, - ) -> bool: - - lib_config = self.__run_config.get("library", None) - if not lib_config: - 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) - try: - self.__driver.get(url) - except TimeoutException: - self.__driver.execute_script("window.stop();") - self._showTrace( - f"图书馆登录页面加载超时 ! 请检查网络环境是否正常", self.TraceLevel.ERROR - ) - return False - if not self.__waitResponseLoad(): - return False - return True - - def __run( - self, - username: str, - password: str, - login_config: dict, - run_mode_config: dict, - reserve_info: dict - ) -> int: - - # result : -1 - terminate, 0 - success, 1 - failed, 2 - passed - result = 2 - - # login - if not self.__lib_login.login( - username, - password, - login_config.get("max_attempt", 3), - login_config.get("auto_captcha", True), - ): - return 1 - # Here, we collect the run mode from the run config. - run_mode = run_mode_config.get("run_mode", 0) - run_mode = { - "auto_reserve": run_mode&0x1, - "auto_checkin": run_mode&0x2, - "auto_renewal": run_mode&0x4, - } - # reserve - if run_mode["auto_reserve"]: - if self.__lib_checker.canReserve(reserve_info.get("date")): - if self.__lib_reserve.reserve(username, reserve_info): - result = 0 - else: - result = 1 - else: - self._showTrace(f"用户 {username} 无法预约, 已跳过") - result = 2 - - # checkin - 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 - else: - result = 1 - else: - self._showTrace(f"用户 {username} 无法签到, 已跳过") - result = 2 - if last_result == 0: # partly success - result = 0 - - # renewal - 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): - if self.__lib_checker.postRenewCheck(record): - self._showTrace(f"用户 {username} 续约成功 !") - result = 0 - else: - 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 - ): - # if logout is failed, we must make sure the host to be reloaded - # otherwise, the next login may fail - if not self.__initDriverUrl(): - return -1 - return result - - def run( - self, - user_config: dict - ): - - self.__user_config = user_config - - user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0} - users = self.__user_config["users"] - self._showTrace(f"共发现 {len(users)} 个用户") - for user in users: - user_counter["current"] += 1 - self._showTrace( - f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......", - no_log=True - ) - if not user["enabled"]: - self._showTrace(f"用户 {user["username"]} 已跳过") - user_counter["passed"] += 1 - continue - r = self.__run( - username=user["username"], - password=user["password"], - login_config=self.__run_config["login"], - run_mode_config=self.__run_config["mode"], - reserve_info=user["reserve_info"], - ) - if r == -1: - self._showTrace( - f"用户 {user["username"]} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !", - self.TraceLevel.WARNING - ) - break - elif r == 0: - user_counter["success"] += 1 - elif r == 1: - user_counter["failed"] += 1 - elif r == 2: - user_counter["passed"] += 1 - self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\ - f"成功 {user_counter["success"]} 个用户, "\ - f"失败 {user_counter["failed"]} 个用户, "\ - f"跳过 {user_counter["passed"]} 个用户" - ) - return - - def close( - self - ) -> bool: - - if self.__driver: - if self.__driver_type.lower() == "firefox": - self._showTrace( - f"Firefox 浏览器驱动关闭略慢, 请耐心等待...", - no_log=True - ) - self.__driver.quit() - self.__driver = None - self._showTrace(f"浏览器驱动已关闭") - return True - else: - 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 deleted file mode 100644 index 3dc944b..0000000 --- a/src/operators/LibChecker.py +++ /dev/null @@ -1,365 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 re -import time -import queue - -from datetime import datetime, timedelta -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from base.LibOperator import LibOperator - - -class LibChecker(LibOperator): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - - def _waitResponseLoad( - self - ) -> bool: - - pass - - @staticmethod - def __formatDiffTime( - seconds: float - ) -> str: - - hours = int(seconds//3600) - minutes = int(seconds%3600//60) - seconds = int(seconds%60) - return f"{hours} 时 {minutes} 分 {seconds} 秒" - - def __navigateToReserveRecordPage( - self - ) -> bool: - - try: - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']")) - ).click() - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CLASS_NAME, "myReserveList")) - ) - except: - self._showTrace("加载预约记录页面失败 !", self.TraceLevel.ERROR) - return False - return True - - def __decodeReserveTime( - self, - time_element - ) -> dict: - - time_str = time_element.text.strip() - today = datetime.now().date() - if "明天" in time_str: - target_date = today + timedelta(days=1) - date = target_date.strftime("%Y-%m-%d") - elif "今天" in time_str: - target_date = today - date = target_date.strftime("%Y-%m-%d") - elif "昨天" in time_str: - target_date = today - timedelta(days=1) - date = target_date.strftime("%Y-%m-%d") - else: - date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str) - if date_match: - date = date_match.group(1) - else: - date = "" - time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str) - if time_match: - begin_time = time_match.group(1) - end_time = time_match.group(2) - else: - begin_time = "" - end_time = "" - return { - "date": date, - "time": { - "begin": begin_time, - "end": end_time - } - } - - def __decodeReserveInfo( - self, - info_elements - ) -> str: - - location = "" - status = "" - for info in info_elements: - if "已预约" in info.text: - status = "已预约" - elif "使用中" in info.text: - status = "使用中" - elif "已完成" in info.text: - status = "已完成" - elif "已结束使用" in info.text: - status = "已结束使用" - elif "已取消" in info.text: - status = "已取消" - elif "失约" in info.text: - status = "失约" - elif "图书馆" in info.text: - location = info.text.strip() - return { - "location": location, - "status": status, - } - - def __decodeReserveRecord( - self, - reservation - ) -> dict: - - try: - time_element = reservation.find_element( - By.CSS_SELECTOR, "dt" - ) - info_elements = reservation.find_elements( - By.CSS_SELECTOR, "a" - ) - except: - return { - "date": "", - "time": {"begin": "", "end": ""}, - "info": {"location": "", "status": ""} - } - time = self.__decodeReserveTime(time_element) - info = self.__decodeReserveInfo(info_elements) - return { - "date": time["date"], - "time": time["time"], - "info": info - } - - def __loadReserveRecords( - self - ) -> list: - try: - # check if there's any reservation on the date - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl")) - ) - reservations = self.__driver.find_elements( - By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)" - ) - return reservations - except: - self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR) - return None - - def __showMoreReserveRecords( - self - ) -> bool: - - # load new reservations if still not sure - try: - WebDriverWait(self.__driver, 0.1).until( - EC.element_to_be_clickable((By.ID, "moreBtn")) - ) - except: - # the reservation is the last one - return False - try: - more_btn = self.__driver.find_element(By.ID, "moreBtn") - if more_btn.is_displayed() and more_btn.is_enabled(): - self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn) - self.__driver.execute_script("arguments[0].click();", more_btn) - return True - else: - self._showTrace("用户无法加载更多预约记录", self.TraceLevel.WARNING) - return False - except: - self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR) - return False - - def __getReserveRecord( - self, - wanted_date: str, - wanted_status: str - ) -> dict: - - if wanted_date is None: - self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING) - return None - 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 - - if not self.__navigateToReserveRecordPage(): - return None - for _ in range(max_check_times): - reservations = self.__loadReserveRecords() - if reservations is None: - return None - for reservation in reservations[checked_count:]: - record = self.__decodeReserveRecord(reservation) - checked_count += 1 - if record is None: - continue - if record["date"] == "": - continue - if record["time"] == {"begin": "", "end": ""}: - continue - # record date is later than the given date, check the next one - if datetime.strptime(record["date"], "%Y-%m-%d").date() >\ - datetime.strptime(wanted_date, "%Y-%m-%d").date(): - continue - # record date is earlier than the given date, so there is no wanted record - if datetime.strptime(record["date"], "%Y-%m-%d").date() <\ - datetime.strptime(wanted_date, "%Y-%m-%d").date(): - return None - if record["info"]["status"] == wanted_status: - self._showTrace( - f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, " - f"详细信息: {record["date"]} " - f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}", - no_log=True - ) - return record - if not self.__showMoreReserveRecords(): - break - return None - - def canReserve( - self, - date: str - ) -> bool: - - # no reserved or using record in the given date - # then can reserve - if self.__getReserveRecord(date, "已预约") is None: - if self.__getReserveRecord(date, "使用中") is None: - self._showTrace(f"用户在 {date} 可以预约") - return True - self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约") - return False - self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约") - return False - - def canCheckin( - self - ) -> bool: - - # only check the current date - date = time.strftime("%Y-%m-%d", time.localtime()) - record = self.__getReserveRecord(date, "已预约") - if record is not None: - begin_time = record["time"]["begin"] - begin_time = datetime.strptime(f"{date} {begin_time}", "%Y-%m-%d %H:%M") - time_diff = datetime.now() - begin_time - time_diff_seconds = time_diff.total_seconds() - # before 30 minutes, cant checkin - if time_diff_seconds < -30*60: - self._showTrace( - f"用户在 {date} 的预约开始时间为 {begin_time}, " - f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 无法签到" - ) - return False - # before in 30 minutes, can checkin - elif -30*60 <= time_diff_seconds < 0: - self._showTrace( - f"用户在 {date} 的预约开始时间为 {begin_time}, " - f"当前距离预约开始时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到" - ) - return True - # past less than 30 minutes, can checkin - elif 0 <= time_diff_seconds < 30*60 - 5: # spare 5 seconds for the checkin process - self._showTrace( - f"用户在 {date} 的预约开始时间为 {begin_time}, " - f"当前距离预约开始时间已经过去 {self.__formatDiffTime(abs(time_diff_seconds))}, 可以签到" - ) - return True - self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到") - return False - - def canRenew( - self - ) -> tuple[bool, dict]: - - # only check the current date - date = time.strftime("%Y-%m-%d", time.localtime()) - record = self.__getReserveRecord(date, "使用中") - if record is not None: - end_time = record["time"]["end"] - end_time = datetime.strptime(f"{date} {end_time}", "%Y-%m-%d %H:%M") - time_diff = end_time - datetime.now() - time_diff_seconds = time_diff.total_seconds() - # a using record is definitely after the begin time - trace_msg = ( - f"用户在 {date} 的预约结束时间为 {end_time}, " - f"当前距离预约结束时间还有 {self.__formatDiffTime(abs(time_diff_seconds))}" - ) - if abs(time_diff_seconds) < 120*60: - self._showTrace(f"{trace_msg}, 可以续约") - return True, record - else: - self._showTrace(f"{trace_msg}, 无法续约") - return False, None # we do not need to return the record, because if current - # time is not available for renewal, the record is not required - self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") - return False, None - - def postRenewCheck( - self, - record: dict - ) -> bool: - """ - Check if the renew operation is successful - - Args: - record (dict): The expected record after renewal - - Returns: - bool: True if the renew operation is successful, False otherwise - """ - # because the special circumstance that the renew operation - # do not show the success message or anything else, - # we need to check the record data to make sure the renew operation is successful. - - # only check the given record date - date = record["date"] - act_record = self.__getReserveRecord(date, "使用中") - if act_record is not None: - if act_record["time"]["begin"] == record["time"]["begin"] and\ - act_record["time"]["end"] == record["time"]["end"]: - self._showTrace(f"\n"\ - f" 续约成功 !\n"\ - f" 日 期 :{date}\n"\ - f" 时 间 :{act_record["time"]["begin"]} - {act_record["time"]["end"]}\n"\ - f" 位 置 :{act_record["info"]["location"]}\n" - f" 状 态 :{act_record["info"]["status"]}" - ) - return True - else: - self._showTrace(f"\n"\ - f" 续约失败 !\n"\ - f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !" - ) - return False - self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") - return False diff --git a/src/operators/LibCheckin.py b/src/operators/LibCheckin.py deleted file mode 100644 index 5adb21e..0000000 --- a/src/operators/LibCheckin.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 time -import queue - -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from base.LibOperator import LibOperator - - -class LibCheckin(LibOperator): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - - def _waitResponseLoad( - self - ) -> bool: - - try: - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CLASS_NAME, "ui_dialog")) - ) - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CLASS_NAME, "resultMessage")) - ) - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable((By.CLASS_NAME, "btnOK")) - ) - result_message_element = self.__driver.find_element( - By.CLASS_NAME, "resultMessage" - ) - ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK") - except: - self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) - return False - result_message = result_message_element.text - if "签到成功" in result_message: - try: - detail_elements = self.__driver.find_elements( - By.CSS_SELECTOR, ".resultMessage dd" - ) - except: - pass - if detail_elements: - details = [element.text for element in detail_elements if element.text.strip()] - if len(details) >= 5: - self._showTrace(f"\n"\ - f" 签到成功 !\n"\ - f" {details[1]}\n"\ - f" {details[2]}\n"\ - f" {details[3]}\n"\ - f" {details[4]}" - ) - else: - self._showTrace(f"\n"\ - " 签到成功 !\n"\ - " 未获取到签到详情 !" - ) - ok_btn.click() - return True - else: - failure_reason = result_message.replace("签到失败", "").strip() - self._showTrace(f"\n"\ - " 签到失败 !\n"\ - f" {failure_reason}" - ) - ok_btn.click() - return False - - def __enableCheckinBtn( - self - ) -> bool: - - script = """ - try { - var checkin_btn = document.getElementById('btnCheckIn'); - if (checkin_btn) { - checkin_btn.classList.remove('disabled'); - return true; - } - return false; - } catch (e) { - return false; - } - """ - result = self.__driver.execute_script(script) - time.sleep(0.1) - if result: - self._showTrace("签到按钮已启用", no_log=True) - else: - self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING) - return result - - def checkin( - self, - username: str - ) -> bool: - - if self.__driver is None: - 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.TraceLevel.ERROR) - return False - if "disabled" in checkin_btn.get_attribute("class"): - 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} 签到成功 !", no_log=True) - return True - else: - self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR) - return False diff --git a/src/operators/LibCheckout.py b/src/operators/LibCheckout.py deleted file mode 100644 index f55ccc8..0000000 --- a/src/operators/LibCheckout.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 re -import time -import queue - -from datetime import datetime, timedelta -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from base.LibOperator import LibOperator - - -class LibCheckout(LibOperator): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - - def _waitResponseLoad( - self - ) -> bool: - - pass \ No newline at end of file diff --git a/src/operators/LibLogin.py b/src/operators/LibLogin.py deleted file mode 100644 index 9fd233b..0000000 --- a/src/operators/LibLogin.py +++ /dev/null @@ -1,207 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 queue -import base64 - -import ddddocr - -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from base.LibOperator import LibOperator - - -class LibLogin(LibOperator): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - self.__ddddocr = ddddocr.DdddOcr() - - def _waitResponseLoad( - self - ) -> bool: - - # wait to verify login success - try: - WebDriverWait(self.__driver, 2).until( # title contains "自选座位 :: 座位预约系统" - EC.title_contains("自选座位 :: 座位预约系统") - ) - WebDriverWait(self.__driver, 2).until( # search button presence - EC.presence_of_element_located((By.ID, "search")) - ) - WebDriverWait(self.__driver, 2).until( # select content presence - EC.presence_of_element_located((By.CLASS_NAME, "selectContent")) - ) - return True - except: - self._showTrace( - f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准", - self.TraceLevel.ERROR - ) - return False - - def __fillLogInElements( - self, - username: str, - password: str - ) -> bool: - - # ensure elements presence and fill them - try: - username_element = self.__driver.find_element(By.NAME, "username") - username_element.clear() - username_element.send_keys(username) - password_element = self.__driver.find_element(By.NAME, "password") - password_element.clear() - password_element.send_keys(password) - except Exception as e: - self._showTrace(f"用户名或密码填写失败 ! : {e}", self.TraceLevel.ERROR) - return False - return True - - def __autoRecognizeCaptcha( - self - ) -> str: - - # auto recognize captcha - try: - captcha_img = self.__driver.find_element(By.ID, "loadImgId") - img_src = captcha_img.get_attribute("src") - base64_str = img_src.split(',', 1)[1] - 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}'", no_log=True) - 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.TraceLevel.ERROR) - return "" - - def __manualRecognizeCaptcha( - self - ) -> str: - - # manual recognize captcha - try: - self._showMsg("请输入验证码:") - captcha_text = self._waitMsg(timeout=15) - self._showTrace(f"输入的验证码为 : '{captcha_text}'", no_log=True) - 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.TraceLevel.ERROR) - return "" - - def __refreshCaptcha( - self - ): - - # refresh captcha - try: - self._showTrace("刷新验证码......", no_log=True) - self.__driver.find_element( - By.ID, "loadImgId" - ).click() - return True - except Exception as e: - self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR) - return False - - def __solveCaptcha( - self, - auto_captcha: bool = True - ) -> str: - - max_attempts = 3 # the possibility of 3 times failed is less than (10%^3) - for _ in range(max_attempts): - if auto_captcha: - captcha_text = self.__autoRecognizeCaptcha() - else: - self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !", no_log=True) - captcha_text = self.__manualRecognizeCaptcha() - if captcha_text: - return captcha_text - else: - if not self.__refreshCaptcha(): - return "" - self._showTrace( - f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !", - self.TraceLevel.WARNING - ) - return "" - - def __fillCaptchaElement( - self, - captcha_text: str - ) -> bool: - - try: - captcha_element = self.__driver.find_element(By.NAME, "answer") - captcha_element.clear() - captcha_element.send_keys(captcha_text) - return True - except Exception as e: - self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR) - return False - - def login( - self, - username: str, - password: str, - max_attempts: int = 5, - auto_captcha: bool = True - ) -> bool: - - if self.__driver is None: - self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING) - return False - # begin login process - for attempt in range(max_attempts): - self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......", no_log=True) - if not self.__fillLogInElements( - username, - password, - ): - continue - captcha_text = self.__solveCaptcha(auto_captcha) - if not captcha_text: - continue - if not self.__fillCaptchaElement(captcha_text): - continue - self._showTrace("尝试登录...", no_log=True) - try: - self.__driver.find_element( - By.XPATH, - "//input[@type='button' and @value='登录']" - ).click() - except Exception as e: - self._showTrace(f"尝试登录失败 ! : {e}") - continue - if self._waitResponseLoad(): - self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !") - return True - else: - self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !",self.TraceLevel.WARNING) - return False diff --git a/src/operators/LibLogout.py b/src/operators/LibLogout.py deleted file mode 100644 index 14bd02d..0000000 --- a/src/operators/LibLogout.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 queue - -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver - -from base.LibOperator import LibOperator - - -class LibLogout(LibOperator): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - - def _waitResponseLoad( - self - ) -> bool: - - return True - - def logout( - self, - username: str - ) -> bool: - - if self.__driver is None: - self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING) - return False - try: - self.__driver.find_element( - By.XPATH, "//a[@href='/logout']" - ).click() - self._showTrace(f"用户 {username} 注销成功 !") - return True - except Exception as e: - self._showTrace(f"用户 {username} 注销失败 ! : {e}", self.TraceLevel.ERROR) - return False diff --git a/src/operators/LibRenew.py b/src/operators/LibRenew.py deleted file mode 100644 index 2d9eec6..0000000 --- a/src/operators/LibRenew.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 queue - -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from operators.abs.LibTimeSelector import LibTimeSelector - - -class LibRenew(LibTimeSelector): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - - def _waitResponseLoad( - self - ) -> bool: - - self.__driver.refresh() - return True - - def __waitRenewDialog( - self - ) -> bool: - - try: - WebDriverWait(self.__driver, 2).until( - EC.visibility_of_element_located((By.ID, "extendDiv")) - ) - head_message = WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv p.messageHead")) - ) - result_message = WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage")) - ) - except: - self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR) - return False - head_message = head_message.text.strip() - if "警告" in head_message: - result_message = result_message.text.strip() - self._showTrace(f"\n"\ - f" 续约失败 !\n"\ - f" {result_message}", no_log=True) - return False - try: - WebDriverWait(self.__driver, 2).until( - EC.presence_of_all_elements_located( - (By.CSS_SELECTOR, "#extendDiv .renewal_List li") - ) - ) - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK")) - ) - except: - self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR) - return False - return True - - def __selectNearestTime( - self, - record: dict, - reserve_info: dict - ) -> bool: - - """ - Select the nearest available renewal time. - """ - end_time = record["time"]["end"] - renew_info = reserve_info["renew_time"] - max_diff = renew_info["max_diff"] - prefer_earlier = renew_info["prefer_early"] - 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): - return False - 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.TraceLevel.WARNING) - return False - - # Find best renewal time option - best_opt, best_text, actual_diff, free_times = self._findBestTimeOption( - renew_time_opts, target_renew_mins, max_diff, prefer_earlier, is_reserve=False - ) - if best_opt is not None: - return self.__confirmRenewal(best_opt, best_text, actual_diff, record, renew_ok_btn) - self._showTrace( - "无法选择最近的可用续约时间 ! " - f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !", - self.TraceLevel.WARNING - ) - self._showTrace(f"当前可供续约的时间有: {free_times}") - return False - - def __validateAndAdjustRenewTime( - self, - end_time: str, - target_renew_mins: int - ) -> bool: - - """ - Validate and adjust renewal time to library closing time if needed. - """ - LIBRARY_CLOSE_TIME = 1410 # 23:30 in minutes - 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) - return False - self._showTrace( - f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)}," - f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟" - ) - return True - return True - - def __confirmRenewal( - self, - best_opt, - best_text: str, - actual_diff: int, - record: dict, - ok_btn - ) -> bool: - - """ - Confirm the selected renewal time. - """ - try: - best_opt.click() - abs_diff = abs(actual_diff) - time_relation = self._formatTimeRelation(abs_diff, actual_diff, "续约时间") - self._showTrace( - f"选择距离期望续约时间最近的 {best_text}, " - f"与期望续约时间相比 {time_relation}" - ) - record["time"]["end"] = best_text.strip() - ok_btn.click() - return True - except: - self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR) - return False - - def renew( - self, - username: str, - record: dict, - reserve_info: dict - ) -> bool: - - if self.__driver is None: - 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.TraceLevel.ERROR) - return False - if "disabled" in renew_btn.get_attribute("class"): - self._showLog(f"用户 {username} 续约按钮不可用, 可能不在场馆内") - self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试", no_log=True) - return False - renew_btn.click() - if not self.__waitRenewDialog(): - 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.TraceLevel.ERROR) - self.__driver.refresh() - return False - if self._waitResponseLoad(): - return True diff --git a/src/operators/LibReserve.py b/src/operators/LibReserve.py deleted file mode 100644 index 930db0a..0000000 --- a/src/operators/LibReserve.py +++ /dev/null @@ -1,674 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright (c) 2025 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 time -import queue - -from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.webdriver import WebDriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from operators.abs.LibTimeSelector import LibTimeSelector - - -class LibReserve(LibTimeSelector): - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue, - driver: WebDriver - ): - - super().__init__(input_queue, output_queue) - - self.__driver = driver - # library floor and room mapping in website - self.__floor_map = { - "2": "二层", - "3": "三层", - "4": "四层", - "5": "五层" - } - self.__room_map = { - "1": "二层内环", - "2": "二层西区", - "3": "三层内环", - "4": "三层外环", - "5": "四层内环", - "6": "四层外环", - "7": "四层期刊", - "8": "五层考研" - } - - def _waitResponseLoad( - self, - ) -> bool: - - try: - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CLASS_NAME, "layoutSeat")) - ) - title_elements = [] - # reserve failed without title elements, so we need to try - try: - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".layoutSeat dt")) - ) - title_elements = self.__driver.find_elements( - By.CSS_SELECTOR, ".layoutSeat dt" - ) - except: - pass - content_elements = self.__driver.find_elements( - By.CSS_SELECTOR, ".layoutSeat dd" - ) - if not content_elements: - 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.TraceLevel.ERROR) - raise - if "预定好了" in title or "预约成功" in title or "操作成功" in title: - if len(contents) >= 6: - self._showTrace(f"\n"\ - f" 预约成功 !\n"\ - f" {contents[1]}\n"\ - f" {contents[2]}\n"\ - f" {contents[3]}\n"\ - f" 签到时间 :{contents[5]}" - ) - else: - self._showTrace("\n"\ - " 预约成功 !\n"\ - " 未找获取到详细信息" - ) - return True - except: - self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR) - return False - - def __containRequiredInfo( - self, - reserve_info: dict - ) -> bool: - - 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 ? - raise ValueError(f"该楼层 '{reserve_info['floor']}' 不存在") - if reserve_info.get("room") is None: - raise ValueError("未指定房间") - if reserve_info["room"] not in self.__room_map: - raise ValueError(f"该房间 '{reserve_info['room']}' 不存在") - if reserve_info.get("seat_id") is None: - raise ValueError("未指定座位") - if reserve_info["seat_id"] == "": - raise ValueError("未指定座位号") - return True - except ValueError as e: - self._showTrace( - f"预约信息错误 ! : {e}, "\ - f"由于缺少必要的预约信息, 无法开始预约流程", - self.TraceLevel.ERROR - ) - self._showTrace( - f"预约信息错误 ! : {e}, "\ - f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整", - no_log=True - ) - return False - - def __isValidDate( - self, - reserve_info: dict - ) -> bool: - - 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_str - self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date_str}") - else: - 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_str}, 自动设置为当前日期", - self.TraceLevel.WARNING - ) - reserve_info["date"] = cur_date_str - return True - - def __isValidBeginTime( - self, - reserve_info: dict - ) -> bool: - - cur_time = time.strftime("%H:%M", time.localtime()) - if reserve_info.get("begin_time") is None: - reserve_info["begin_time"] = {} - if "time" not in reserve_info["begin_time"]: - reserve_info["begin_time"]["time"] = cur_time - self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}") - if "max_diff" not in reserve_info["begin_time"]: - reserve_info["begin_time"]["max_diff"] = 30 - self._showTrace(f"开始时间最大时间差未指定, 自动设置为 30 分钟") - if "prefer_early" not in reserve_info["begin_time"]: - reserve_info["begin_time"]["prefer_early"] = True - self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True") - return True - - def __isValidExpectDuration( - self, - reserve_info: dict - ) -> bool: - - if reserve_info.get("satisfy_duration") is None: - reserve_info["satisfy_duration"] = True - self._showTrace("预约满足时长要求未指定, 默认满足") - if reserve_info["satisfy_duration"]: - if reserve_info.get("expect_duration") is None: - reserve_info["expect_duration"] = 4 - self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时") - return True - - def __isValidEndTime( - self, - reserve_info: dict - ) -> bool: - - 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"] = { - "time": self._minsToTimeStr(end_mins), - "max_diff": 30, - "prefer_early": False - } - self._showTrace( - f"结束时间未指定, 自动设置为开始时间加上期望时长: {reserve_info['end_time']['time']}" - ) - if "max_diff" not in reserve_info["end_time"]: - reserve_info["end_time"]["max_diff"] = 30 - self._showTrace(f"结束时间最大时间差未指定, 自动设置为 30 分钟") - if "prefer_early" not in reserve_info["end_time"]: - reserve_info["end_time"]["prefer_early"] = False - self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True") - return True - - def __finalCheck( - self, - reserve_info: dict - ): - - 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 - # 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']}, 尝试交换时间", - self.TraceLevel.WARNING - ) - 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 - max_end_mins = self._timeStrToMins("23:30") - if end_mins > max_end_mins: - self._showTrace( - f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30", - self.TraceLevel.WARNING - ) - reserve_info["end_time"]["time"] = "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: - self._showTrace( - f"该用户设置了优先满足时长要求, 但是预约期望持续时间 " - f"{reserve_info['expect_duration']} 小时 " - f"超出最大时长 8 小时, 自动设置为 8 小时", - self.TraceLevel.WARNING - ) - reserve_info["expect_duration"] = 8 - else: - if end_mins - begin_mins > 8*60: - self._showTrace( - f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 " - f"{float((end_mins - begin_mins)/60)} 小时 " - f"超出最大时长 8 小时, 自动设置为 8 小时", - self.TraceLevel.WARNING - ) - reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60) - return True - - def __checkReserveInfo( - self, - reserve_info: dict - ) -> bool: - - if not self.__containRequiredInfo(reserve_info): - return False - if not self.__isValidDate(reserve_info): - return False - if not self.__isValidBeginTime(reserve_info): - return False - if not self.__isValidExpectDuration(reserve_info): - return False - if not self.__isValidEndTime(reserve_info): - return False - if not self.__finalCheck(reserve_info): - return False - self._showTrace( - f"预约信息检查完成, 准备预约 " - f"{reserve_info['date']} " - 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']]} " - f"的座位 {reserve_info['seat_id']}" - ) - return True - - def __clickElement( - self, - trigger_locator: tuple, - fail_msg: str, - success_msg: str, - option_locator: tuple = None - ) -> bool: - - try: - # click the trigger element - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable(trigger_locator) - ).click() - if option_locator: - # select the option element if specified - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable(option_locator) - ).click() - self._showTrace(success_msg) - return True - except: - self._showTrace(fail_msg) - return False - - def __clickElementByJS( - self, - trigger_locator_id: str, - option_query_selector: str, - fail_msg: str, - success_msg: str, - ) -> bool: - - script = f""" - try {{ - var trigger = document.getElementById('{trigger_locator_id}'); - if (trigger) {{ - trigger.click(); - var option = document.querySelector("{option_query_selector}"); - if (option) {{ - option.click(); - return true; - }} - return false; - }} - return false; - }} catch (e) {{ - return false; - }} - """ - result = self.__driver.execute_script(script) - time.sleep(0.1) - if result: - self._showTrace(success_msg) - else: - self._showTrace(fail_msg) - return result - - def __selectDate( - self, - date_str: str - ) -> bool: - - if self.__clickElementByJS( - trigger_locator_id="onDate_select", - option_query_selector=f"p#options_onDate a[value='{date_str}']", - success_msg=f"日期 {date_str} 选择成功 !", - fail_msg=f"选择日期失败 ! : {date_str} 不可用" - ): - return True - return self.__clickElement( - trigger_locator=(By.ID, "onDate_select"), - option_locator=(By.XPATH, f"//p[@id='options_onDate']/a[@value='{date_str}']"), - success_msg=f"日期 {date_str} 选择成功 !", - fail_msg=f"选择日期失败 ! : {date_str} 不可用" - ) - - def __selectPlace( - self, - place: str - ) -> bool: - - place = "1" # the library only have this place :) - display_place = "图书馆" - if self.__clickElementByJS( - trigger_locator_id="display_building", - option_query_selector=f"p#options_building a[value='{place}']", - success_msg=f"预约场所 {display_place} 选择成功 !", - fail_msg=f"选择预约场所失败 ! : {display_place} 不可用" - ): - return True - return self.__clickElement( - trigger_locator=(By.ID, "display_building"), - option_locator=(By.XPATH, f"//p[@id='options_building']/a[@value='{place}']"), - success_msg=f"预约场所 {display_place} 选择成功 !", - fail_msg=f"选择预约场所失败 ! : {display_place} 不可用" - ) - - def __selectFloor( - self, - floor: str - ) -> bool: - - display_floor = self.__floor_map.get(floor) - if self.__clickElementByJS( - trigger_locator_id="floor_select", - option_query_selector=f"p#options_floor a[value='{floor}']", - success_msg=f"楼层 {display_floor} 选择成功 !", - fail_msg=f"选择楼层失败 ! : {display_floor} 不可用" - ): - return True - return self.__clickElement( - trigger_locator=(By.ID, "floor_select"), - option_locator=(By.XPATH, f"//p[@id='options_floor']/a[@value='{floor}']"), - success_msg=f"楼层 {display_floor} 选择成功 !", - fail_msg=f"选择楼层失败 ! : {display_floor} 不可用" - ) - - def __selectRoom( - self, - room: str - ) -> bool: - - display_room = self.__room_map.get(room) - # find room - try: - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable((By.ID, "findRoom")) - ).click() - except: - self._showTrace("加载房间/区域失败 !", self.TraceLevel.ERROR) - return False - # select room - try: - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable((By.ID, f"room_{room}")) - ).click() - self._showTrace(f"房间 {display_room} 选择成功 !") - return True - except: - self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) - return False - - def __selectSeat( - self, - seat_id: str - ) -> bool: - - try: - # wait fot seat layout element to load - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.ID, "seatLayout")) - ) - WebDriverWait(self.__driver, 2).until( - EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']")) - ) - except: - self._showTrace(f"座位加载失败 !", self.TraceLevel.ERROR) - return False - try: - all_seats = self.__driver.find_elements( - By.CSS_SELECTOR, "li[id^='seat_']" - ) - seat_id_upper = seat_id.lstrip('0').upper() - for seat in all_seats: - if not seat_id_upper == seat.text.lstrip('0'): - continue - seat_link = seat.find_element(By.TAG_NAME, "a") - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable(seat_link) - ) - seat_link.click() - seat_status = seat_link.get_attribute("title") - self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'") - return True - 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 - - def __selectNearestTime( - self, - time_id: str, - time_type: str, - target_time: int, - max_time_diff: int = 30, - prefer_earlier: bool = True - ) -> int: - - """ - Select the nearest available time option. - - Returns: - int: The actual selected time value in minutes. - """ - # Wait for time options to load - try: - WebDriverWait(self.__driver, 2).until( - EC.presence_of_all_elements_located( - (By.CSS_SELECTOR, f"#{time_id} ul li a") - ) - ) - except: - self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR) - return -1 - - # Find best time option - all_time_opts = self.__driver.find_elements( - By.CSS_SELECTOR, - f"#{time_id} ul li a" - ) - if not all_time_opts: - 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 - ) - if best_opt is not None: - best_opt.click() - abs_diff = abs(actual_diff) - time_relation = self._formatTimeRelation(abs_diff, actual_diff, time_type) - target_time += actual_diff - self._showTrace( - f"选择距离期望 {time_type} 最近的 {best_text}, " - f"与期望 {time_type} 相比 {time_relation}" - ) - return target_time - self._showTrace( - f"无法选择最近的 {time_type} {self._minsToTimeStr(target_time)}, " - f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", self.TraceLevel.WARNING - ) - self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") - return -1 - - def __selectSeatTime( - self, - begin_time: dict, - end_time: dict, - expect_duration: int = 4, - satisfy_duration: bool = True - ) -> bool: - - """ - Select seat begin and end 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) - act_end_mins = exp_end_mins - - # Select begin time - 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"] - ) - if act_beg_mins == -1: - return False - 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: - exp_end_mins = int(self.__validateAndAdjustEndTime(act_beg_mins, expect_duration)) - exp_end_tm_str = self._minsToTimeStr(exp_end_mins) - self._showTrace( - f"需要满足期望预约持续时间: {expect_duration} 小时, " - f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}" - ) - - # Select end time - 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"] - ) - if act_end_mins == -1: - return False - act_end_tm_str = self._minsToTimeStr(act_end_mins) - self._showTrace( - f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, " - f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}" - ) - return True - - def __validateAndAdjustEndTime( - self, - begin_mins: int, - duration: int - ) -> int: - - """ - Validate and adjust reserve end time to library closing time if needed. - """ - 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( - f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30", - self.TraceLevel.WARNING - ) - return expect_end_mins - - def reserve( - self, - username: str, - reserve_info: dict - ) -> bool: - - submit_reserve = False - reserve_success = False - have_hover_on_page = False - - # reserve info - if not self.__checkReserveInfo(reserve_info): - return False - # map page - try: - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']")) - ).click() - WebDriverWait(self.__driver, 2).until( - EC.presence_of_element_located((By.ID, "seatLayout")) - ) - except: - self._showTrace(f"加载预约选座页面失败 !", self.TraceLevel.ERROR) - return False - # date, place, floor, room - if not self.__selectDate(reserve_info["date"]): - return False - if not self.__selectPlace(reserve_info["place"]): - return False - if not self.__selectFloor(reserve_info["floor"]): - return False - if not self.__selectRoom(reserve_info["room"]): - return False - else: - have_hover_on_page = True - # seat selections - if not self.__selectSeat(reserve_info["seat_id"]): - pass - elif not self.__selectSeatTime( - begin_time=reserve_info["begin_time"], - end_time=reserve_info["end_time"], - expect_duration=reserve_info["expect_duration"], - satisfy_duration=reserve_info["satisfy_duration"] - ): - pass - else: - try: - WebDriverWait(self.__driver, 2).until( - EC.element_to_be_clickable((By.ID, "reserveBtn")) - ).click() - submit_reserve = True - if not self._waitResponseLoad(): - raise - reserve_success = True - except: - 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.TraceLevel.ERROR) - return reserve_success \ No newline at end of file diff --git a/src/operators/__init__.py b/src/operators/__init__.py deleted file mode 100644 index 27a9a50..0000000 --- a/src/operators/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" - Operators module for the AutoLibrary project. - - Here are the classes and modules in this package: - - AutoLib: AutoLibrary operator. - - LibLogin: Library operator for logging in. - - LibLogout: Library operator for logging out. - - LibReserve: Library operator for reserving seat. - - LibCheckin: Library operator for checking in seat. - - LibCheckout: Library operator for checking out seat. - - LibChecker: Library operator for checking record status. - - LibRenew: Library operator for renewing seat. -""" \ No newline at end of file diff --git a/src/operators/abs/LibTimeSelector.py b/src/operators/abs/LibTimeSelector.py deleted file mode 100644 index 472d6e4..0000000 --- a/src/operators/abs/LibTimeSelector.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- 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 queue - -from datetime import datetime - -from base.LibOperator import LibOperator - - -class LibTimeSelector(LibOperator): - """ - Abstract base class for time selection operations. - - This class provides common time selection logic for reservation and renewal - operations, including time conversion utilities and best time option finding. - """ - - def __init__( - self, - input_queue: queue.Queue, - output_queue: queue.Queue - ): - - super().__init__(input_queue, output_queue) - - @staticmethod - 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 _minsToTimeStr( - mins: int - ) -> str: - - """ - Convert minutes since midnight to time string "HH:MM". - - Example: - 600 -> "10:00" - 810 -> "13:30" - """ - hour, minute = divmod(int(mins), 60) - return f"{hour:02d}:{minute:02d}" - - def _formatTimeRelation( - self, - abs_diff: int, - actual_diff: int, - time_type: str - ) -> str: - - """ - Format time difference relation string. - """ - if actual_diff < 0: - return f"早了 {abs_diff} 分钟" - elif actual_diff > 0: - return f"晚了 {abs_diff} 分钟" - else: - return f"正好等于 {time_type}" - - def _findBestTimeOption( - self, - time_options: list, - target_time: int, - max_time_diff: int, - prefer_earlier: bool, - is_reserve: bool = True - ) -> tuple: - """ - Find the best time option from available times. - - Args: - time_options: List of WebElement time options - target_time: Target time in minutes - max_time_diff: Maximum acceptable time difference in minutes - prefer_earlier: If True, prefer earlier times when diffs are equal - is_reserve: If True, parse 'time' attribute; if False, parse 'id' attribute - - Returns: - Tuple of (best_time_element, best_time_text, actual_diff, free_times_list) - or (None, None, None, []) if no suitable option found - """ - free_times = [] - best_time_diff = max_time_diff - best_actual_diff = None - best_time_opt = None - - 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": - now = datetime.now() - time_val = now.hour*60 + now.minute - elif time_attr and time_attr.isdigit(): - time_val = int(time_attr) - else: - continue - else: - # Renewal context: parse 'id' attribute - time_attr = time_opt.get_attribute("id") - 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._minsToTimeStr(time_val)) - actual_diff = time_val - target_time - abs_diff = abs(actual_diff) - - # Update best option if current is better - if (abs_diff < best_time_diff or - (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/abs/__init__.py b/src/operators/abs/__init__.py deleted file mode 100644 index e6f47db..0000000 --- a/src/operators/abs/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" - Abstract layer class of the LibOperator - - Here are the classes and modules in this package: - - LibTimeSelector: Abstract base class for time selection operations. -""" \ No newline at end of file From f7167c13f459757d2b730351972a5ce017432f00 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Wed, 27 May 2026 20:25:19 +0800 Subject: [PATCH 36/49] =?UTF-8?q?fix(ALAutoScript*Dialog):=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E7=BC=96=E6=8E=92=E7=AA=97=E5=8F=A3=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=9A=84=20Lua=20=E5=87=BD=E6=95=B0=E5=90=8D=E4=B8=8E=20ASEngi?= =?UTF-8?q?ne=20=E8=BF=90=E8=A1=8C=E6=97=B6=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - date_add → dateadd, time_add → timeadd - CURRENT_DATE() → datenow(), CURRENT_TIME() → timenow() - 编辑窗口 Date/Time 字面量按钮模板同步更新为 date()/time() 格式 Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAutoScriptEditDialog.py | 8 +++---- src/gui/ALAutoScriptOrchDialog/_blocks.py | 1 - src/gui/ALAutoScriptOrchDialog/_helpers.py | 26 +++++++++++----------- src/gui/ALAutoScriptOrchDialog/_widgets.py | 22 ++++++++++-------- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 82b7fe9..245e18a 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -329,8 +329,8 @@ class ALAutoScriptEditDialog(QDialog): ] self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3) dateTimeButtons = [ - ("日期", '"2099-01-01"'), - ("时间", '"00:00"'), + ("日期", 'date(2026, 1, 1)'), + ("时间", 'time(0, 0)'), ] self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3) hintButtons = [ @@ -358,8 +358,8 @@ class ALAutoScriptEditDialog(QDialog): funcButtons = [ ("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"), ("timenow()", "timenow()", "返回当前时间在一天中的分钟数"), - ("dateadd(d, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"), - ("timeadd(t, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), + ("dateadd(day, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"), + ("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), ] for i, (text, template, tooltip) in enumerate(funcButtons): btn = QPushButton(text) diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py index de4b133..60a164f 100644 --- a/src/gui/ALAutoScriptOrchDialog/_blocks.py +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -203,7 +203,6 @@ class ConditionalBlock(QGroupBox): ] if not condTexts: condTexts = ["true"] - if len(condTexts) == 1: combined = condTexts[0] else: diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index 16a53c5..241361a 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -90,7 +90,7 @@ DATE_OFFSET_OPTIONS = [ ("天", "days"), ("周", "weeks"), # NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years, - # because date_add() works with second-level offsets (n * 86400). + # because dateadd() works with second-level offsets (n * 86400). ("月", "months"), ("年", "years"), ] @@ -423,7 +423,7 @@ def encodeValueStr( Encode a raw widget value as a Lua expression. Arithmetic expressions (A + B) are passed through for numeric types; - Date/Time arithmetic is translated to ``date_add()`` / ``time_add()`` calls. + Date/Time arithmetic is translated to ``dateadd()`` / ``timeadd()`` calls. """ if var_type in ("Date", "Time"): @@ -464,24 +464,24 @@ def encodeDateOrTime( right = m_arith.group(3).strip() operand = right if sign == "+" else f"-{right}" if left == "CURRENT_DATE": - return f"date_add(CURRENT_DATE(), {operand})" + return f"dateadd(datenow(), {operand})" if left == "CURRENT_TIME": - return f"time_add(CURRENT_TIME(), {operand})" + return f"timeadd(timenow(), {operand})" if var_type == "Date": - return f"date_add({left}, {operand})" + return f"dateadd({left}, {operand})" if var_type == "Time": - return f"time_add({left}, {operand})" + return f"timeadd({left}, {operand})" return f"{left} {sign} {right}" if up == "CURRENT_DATE": - return "CURRENT_DATE()" + return "datenow()" if up == "CURRENT_TIME": - return "CURRENT_TIME()" + return "timenow()" _REL_MAP = { - "前天": "date_add(CURRENT_DATE(), -2)", - "昨天": "date_add(CURRENT_DATE(), -1)", - "今天": "CURRENT_DATE()", - "明天": "date_add(CURRENT_DATE(), 1)", - "后天": "date_add(CURRENT_DATE(), 2)", + "前天": "dateadd(datenow(), -2)", + "昨天": "dateadd(datenow(), -1)", + "今天": "datenow()", + "明天": "dateadd(datenow(), 1)", + "后天": "dateadd(datenow(), 2)", } if s in _REL_MAP: return _REL_MAP[s] diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index f29f82f..949ab6d 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -178,9 +178,11 @@ class ConditionRowFrame(QFrame): if not data: return "" name, vartype = data - # CURRENT_DATE / CURRENT_TIME are Lua functions — call them, not reference them - if name in ("CURRENT_DATE", "CURRENT_TIME"): - name = f"{name}()" + # CURRENT_DATE / CURRENT_TIME map to datenow() / timenow() + if name == "CURRENT_DATE": + name = "datenow()" + elif name == "CURRENT_TIME": + name = "timenow()" opSym = self.opCombo.currentData() if self._rawRhsExpr: return f"{name} {opSym} {self._rawRhsExpr}" @@ -189,8 +191,10 @@ class ConditionRowFrame(QFrame): rd = self.rhsVarCombo.currentData() if rd: rhsName = rd[0] - if rhsName in ("CURRENT_DATE", "CURRENT_TIME"): - rhsName = f"{rhsName}()" + if rhsName == "CURRENT_DATE": + rhsName = "datenow()" + elif rhsName == "CURRENT_TIME": + rhsName = "timenow()" return f"{name} {opSym} {rhsName}" rhsText = self.rhsVarCombo.currentText().strip() if rhsText: @@ -384,18 +388,18 @@ class ActionStepFrame(QFrame): elif op == "add": if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): days = self.valueStack.currentWidget().getOffsetDays() - return f" {target} = date_add({target}, {days})" + return f" {target} = dateadd({target}, {days})" if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): hours = self.valueStack.currentWidget().getOffsetHours() - return f" {target} = time_add({target}, {hours})" + return f" {target} = timeadd({target}, {hours})" return f" {target} = {target} + {rawVal}" elif op == "sub": if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): days = self.valueStack.currentWidget().getOffsetDays() - return f" {target} = date_add({target}, -{days})" + return f" {target} = dateadd({target}, -{days})" if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): hours = self.valueStack.currentWidget().getOffsetHours() - return f" {target} = time_add({target}, -{hours})" + return f" {target} = timeadd({target}, -{hours})" return f" {target} = {target} - {rawVal}" return "" From 910e3e322477fcdd0139c5c92ddf6e1a44f3f00d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 01:35:55 +0800 Subject: [PATCH 37/49] =?UTF-8?q?chore:=20=E7=BB=9F=E4=B8=80=20=5F=5Finit?= =?UTF-8?q?=5F=5F.py=20=E8=AE=B8=E5=8F=AF=E5=A4=B4=E4=B8=BA=E7=89=88?= =?UTF-8?q?=E6=9D=83=E5=A3=B0=E6=98=8E=E5=B9=B6=E6=94=B9=E7=94=A8=E7=9B=B8?= =?UTF-8?q?=E5=AF=B9=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/autoscript/__init__.py | 11 +------- src/base/__init__.py | 11 +++++--- src/boot/__init__.py | 11 +++++--- src/gui/ALAutoScriptOrchDialog/__init__.py | 11 ++++++-- src/gui/__init__.py | 24 +++++----------- src/gui/resources/__init__.py | 10 +++++-- src/interfaces/__init__.py | 14 ++++----- src/managers/__init__.py | 13 +++++---- src/managers/config/__init__.py | 11 +++++--- src/managers/driver/__init__.py | 13 +++++---- src/managers/log/__init__.py | 11 +++++--- src/pages/__init__.py | 33 +++++++--------------- src/pages/components/__init__.py | 18 ++++-------- src/pages/flows/__init__.py | 12 ++------ src/pages/flows/_helpers.py | 8 +----- src/pages/services/__init__.py | 12 ++------ src/pages/strategies/__init__.py | 13 +-------- src/utils/__init__.py | 11 ++++---- 18 files changed, 102 insertions(+), 145 deletions(-) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index a3397b9..cc78a3d 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -7,16 +7,7 @@ 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. """ -from autoscript.ASEngine import ASEngine - - -__all__ = [ - "ASEngine", - "createEngine", - "createMockTargetData", - "createAllVariablesTable", - "createTargetVarDefs", -] +from .ASEngine import ASEngine _TARGET_VAR_DEFS = [ diff --git a/src/base/__init__.py b/src/base/__init__.py index 7becebc..5cb1dac 100644 --- a/src/base/__init__.py +++ b/src/base/__init__.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- """ - Base module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - Here are the classes and modules in this package: - - MsgBase: Base class for messages. -""" \ No newline at end of file +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. +""" diff --git a/src/boot/__init__.py b/src/boot/__init__.py index 393e4ca..5cb1dac 100644 --- a/src/boot/__init__.py +++ b/src/boot/__init__.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- """ - Boot module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - Here are the classes and modules in this package: - - AppInitializer: Application initializer class. -""" \ No newline at end of file +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. +""" diff --git a/src/gui/ALAutoScriptOrchDialog/__init__.py b/src/gui/ALAutoScriptOrchDialog/__init__.py index 38ca871..ead444a 100644 --- a/src/gui/ALAutoScriptOrchDialog/__init__.py +++ b/src/gui/ALAutoScriptOrchDialog/__init__.py @@ -1,3 +1,10 @@ -from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. -__all__ = ["ALAutoScriptOrchDialog"] +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. +""" +from ._dialog import ALAutoScriptOrchDialog diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 6e687ba..5cb1dac 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,19 +1,9 @@ +# -*- coding: utf-8 -*- """ - GUI module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - 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. - - ALAutoScriptOrchDialog: AutoScript orchestration 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 +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. +""" diff --git a/src/gui/resources/__init__.py b/src/gui/resources/__init__.py index 482953b..5cb1dac 100644 --- a/src/gui/resources/__init__.py +++ b/src/gui/resources/__init__.py @@ -1,3 +1,9 @@ +# -*- 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. """ - GUI resources module for the AutoLibrary project. -""" \ No newline at end of file diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py index 2df5c1b..5cb1dac 100644 --- a/src/interfaces/__init__.py +++ b/src/interfaces/__init__.py @@ -1,11 +1,9 @@ +# -*- coding: utf-8 -*- """ - Interfaces module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - Defines abstract interfaces (Protocols) and shared type definitions - used across layers to decouple consumers from concrete implementations. - - Key components: - - ConfigProvider: Abstract interface for configuration access. - - ConfigType: Enumeration of configuration file types. - - ConfigKey: Type-safe hierarchical key constants for config lookups. +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. """ diff --git a/src/managers/__init__.py b/src/managers/__init__.py index 6665757..5cb1dac 100644 --- a/src/managers/__init__.py +++ b/src/managers/__init__.py @@ -1,8 +1,9 @@ +# -*- coding: utf-8 -*- """ - Managers module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - 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 +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. +""" diff --git a/src/managers/config/__init__.py b/src/managers/config/__init__.py index 1c1c60e..5cb1dac 100644 --- a/src/managers/config/__init__.py +++ b/src/managers/config/__init__.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- """ - Config managers module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - Here are the classes and modules in this package: - - ConfigManager: Config manager for managing configuration files. -""" \ No newline at end of file +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. +""" diff --git a/src/managers/driver/__init__.py b/src/managers/driver/__init__.py index 7e0b908..5cb1dac 100644 --- a/src/managers/driver/__init__.py +++ b/src/managers/driver/__init__.py @@ -1,8 +1,9 @@ +# -*- coding: utf-8 -*- """ - Driver managers module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - 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 +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. +""" diff --git a/src/managers/log/__init__.py b/src/managers/log/__init__.py index bdf450a..5cb1dac 100644 --- a/src/managers/log/__init__.py +++ b/src/managers/log/__init__.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- """ - Log managers module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - Here are the classes and modules in this package: - - LogManager: Log manager for logging. -""" \ No newline at end of file +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. +""" diff --git a/src/pages/__init__.py b/src/pages/__init__.py index 5b0d495..e18a881 100644 --- a/src/pages/__init__.py +++ b/src/pages/__init__.py @@ -7,26 +7,13 @@ 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. """ -from pages.AutoLib import AutoLib -from pages.LoginPage import LoginPage -from pages.MainShell import MainShell -from pages.ReserveView import ReserveView -from pages.RecordsView import RecordsView -from pages.components.SeatMapDialog import SeatMapDialog -from pages.components.TimeSelectDialog import TimeSelectDialog -from pages.components.ReserveResultDialog import ReserveResultDialog -from pages.components.CheckinResultDialog import CheckinResultDialog -from pages.components.RenewDialog import RenewDialog - -__all__ = [ - "AutoLib", - "LoginPage", - "MainShell", - "ReserveView", - "RecordsView", - "SeatMapDialog", - "TimeSelectDialog", - "ReserveResultDialog", - "CheckinResultDialog", - "RenewDialog", -] +from .AutoLib import AutoLib +from .LoginPage import LoginPage +from .MainShell import MainShell +from .ReserveView import ReserveView +from .RecordsView import RecordsView +from .components.SeatMapDialog import SeatMapDialog +from .components.TimeSelectDialog import TimeSelectDialog +from .components.ReserveResultDialog import ReserveResultDialog +from .components.CheckinResultDialog import CheckinResultDialog +from .components.RenewDialog import RenewDialog diff --git a/src/pages/components/__init__.py b/src/pages/components/__init__.py index 3d61a36..f5b8f98 100644 --- a/src/pages/components/__init__.py +++ b/src/pages/components/__init__.py @@ -7,16 +7,8 @@ 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. """ -from pages.components.SeatMapDialog import SeatMapDialog -from pages.components.TimeSelectDialog import TimeSelectDialog -from pages.components.ReserveResultDialog import ReserveResultDialog -from pages.components.CheckinResultDialog import CheckinResultDialog -from pages.components.RenewDialog import RenewDialog - -__all__ = [ - "SeatMapDialog", - "TimeSelectDialog", - "ReserveResultDialog", - "CheckinResultDialog", - "RenewDialog", -] +from .SeatMapDialog import SeatMapDialog +from .TimeSelectDialog import TimeSelectDialog +from .ReserveResultDialog import ReserveResultDialog +from .CheckinResultDialog import CheckinResultDialog +from .RenewDialog import RenewDialog diff --git a/src/pages/flows/__init__.py b/src/pages/flows/__init__.py index c8f1d9f..764e56f 100644 --- a/src/pages/flows/__init__.py +++ b/src/pages/flows/__init__.py @@ -7,12 +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. """ -from pages.flows.ReserveFlow import ReserveFlow -from pages.flows.CheckinFlow import CheckinFlow -from pages.flows.RenewFlow import RenewFlow - -__all__ = [ - "ReserveFlow", - "CheckinFlow", - "RenewFlow", -] +from .ReserveFlow import ReserveFlow +from .CheckinFlow import CheckinFlow +from .RenewFlow import RenewFlow diff --git a/src/pages/flows/_helpers.py b/src/pages/flows/_helpers.py index 886d17c..ac16c01 100644 --- a/src/pages/flows/_helpers.py +++ b/src/pages/flows/_helpers.py @@ -9,11 +9,5 @@ See the LICENSE file for details. """ from pages.strategies.TimeSelectMaker import ( minsToTimeStr, - timeStrToMins + timeStrToMins, ) - - -__all__ = [ - "minsToTimeStr", - "timeStrToMins", -] \ No newline at end of file diff --git a/src/pages/services/__init__.py b/src/pages/services/__init__.py index cee361c..eff9ebe 100644 --- a/src/pages/services/__init__.py +++ b/src/pages/services/__init__.py @@ -7,12 +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. """ -from pages.services.CaptchaSolver import CaptchaSolver -from pages.services.ReserveChecker import ReserveChecker -from pages.services.RecordChecker import RecordChecker - -__all__ = [ - "CaptchaSolver", - "ReserveChecker", - "RecordChecker", -] +from .CaptchaSolver import CaptchaSolver +from .ReserveChecker import ReserveChecker +from .RecordChecker import RecordChecker diff --git a/src/pages/strategies/__init__.py b/src/pages/strategies/__init__.py index 23597e4..e28c45e 100644 --- a/src/pages/strategies/__init__.py +++ b/src/pages/strategies/__init__.py @@ -7,7 +7,7 @@ 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. """ -from pages.strategies.TimeSelectMaker import ( +from .TimeSelectMaker import ( TimeSelectMaker, TimeDecisionMaker, TimeOptionReader, @@ -17,14 +17,3 @@ from pages.strategies.TimeSelectMaker import ( TimeSelectionResult, TimeRangeResult, ) - -__all__ = [ - "TimeSelectMaker", - "TimeDecisionMaker", - "TimeOptionReader", - "ReserveTimeReader", - "RenewTimeReader", - "TimeOption", - "TimeSelectionResult", - "TimeRangeResult", -] diff --git a/src/utils/__init__.py b/src/utils/__init__.py index b84955c..5cb1dac 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,8 +1,9 @@ +# -*- coding: utf-8 -*- """ - Utils module for the AutoLibrary project. +Copyright (c) 2026 KenanZhu. +All rights reserved. - Here are the classes and modules in this package: - - TimerUtils: Timer utils class for the AutoLibrary project. - - JSONReader: JSON reader class for the AutoLibrary project. - - JSONWriter: JSON writer class for the AutoLibrary project. +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. """ From df7ad92f7f78a3a8e2581aff0a790a30799f7bda Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 01:36:18 +0800 Subject: [PATCH 38/49] =?UTF-8?q?fix(pages):=20=E7=A7=BB=E9=99=A4=E8=A3=B8?= =?UTF-8?q?=20except=20Exception=20=E6=94=B9=E7=94=A8=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E7=B1=BB=E5=9E=8B=E5=B9=B6=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E6=93=8D=E4=BD=9C=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/pages/AutoLib.py | 3 - src/pages/LoginPage.py | 38 +++----- src/pages/MainShell.py | 69 +++++++------ src/pages/RecordsView.py | 6 -- src/pages/ReserveView.py | 103 +++++++++----------- src/pages/components/CheckinResultDialog.py | 11 +-- src/pages/components/RenewDialog.py | 29 +++--- src/pages/components/ReserveResultDialog.py | 4 - src/pages/components/SeatMapDialog.py | 8 +- src/pages/components/TimeSelectDialog.py | 12 ++- src/pages/flows/CheckinFlow.py | 6 +- src/pages/flows/RenewFlow.py | 54 +++++----- src/pages/flows/ReserveFlow.py | 8 +- 13 files changed, 156 insertions(+), 195 deletions(-) diff --git a/src/pages/AutoLib.py b/src/pages/AutoLib.py index fab83e4..6b608e0 100644 --- a/src/pages/AutoLib.py +++ b/src/pages/AutoLib.py @@ -149,9 +149,6 @@ class AutoLib(MsgBase): except WebDriverException as e: self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR) return False - except Exception as e: - self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR) - return False self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") return True diff --git a/src/pages/LoginPage.py b/src/pages/LoginPage.py index db34756..3194276 100644 --- a/src/pages/LoginPage.py +++ b/src/pages/LoginPage.py @@ -85,9 +85,7 @@ class LoginPage: EC.presence_of_element_located(self.CAPTCHA_IMG) ) return True - except (NoSuchElementException, TimeoutException): - return False - except Exception: + except TimeoutException: return False def fillCredentials( @@ -104,17 +102,21 @@ class LoginPage: el.clear() el.send_keys(password) return True - except (NoSuchElementException, TimeoutException): - return False - except Exception: + except (NoSuchElementException, ElementNotInteractableException): return False def getCaptchaImageSrc( self, - ) -> str: + ) -> str | None: - captcha_el = self._driver.find_element(*self.CAPTCHA_IMG) - return captcha_el.get_attribute("src") + # return 'None' if captcha image element is not found. + # But the 'get_attribute("src")' also return 'None' if there's no attribute with + # that name, which is not what we want. + try: + captcha_el = self._driver.find_element(*self.CAPTCHA_IMG) + return captcha_el.get_attribute("src") + except NoSuchElementException: + return None def refreshCaptcha( self, @@ -123,10 +125,7 @@ class LoginPage: try: self._driver.find_element(*self.CAPTCHA_IMG).click() return True - except (NoSuchElementException, TimeoutException, - ElementNotInteractableException): - return False - except Exception: + except (NoSuchElementException, ElementNotInteractableException): return False def fillCaptcha( @@ -139,9 +138,7 @@ class LoginPage: el.clear() el.send_keys(captcha_text) return True - except (NoSuchElementException, TimeoutException): - return False - except Exception: + except (NoSuchElementException, ElementNotInteractableException): return False def clickLogin( @@ -151,10 +148,7 @@ class LoginPage: try: self._driver.find_element(*self.LOGIN_BUTTON).click() return True - except (NoSuchElementException, TimeoutException, - ElementNotInteractableException): - return False - except Exception: + except (NoSuchElementException, ElementNotInteractableException): return False def waitLoginSuccess( @@ -172,9 +166,7 @@ class LoginPage: EC.presence_of_element_located(self.SUCCESS_INDICATOR_CONTENT) ) return True - except (NoSuchElementException, TimeoutException): - return False - except Exception: + except TimeoutException: return False def stopPageLoad( diff --git a/src/pages/MainShell.py b/src/pages/MainShell.py index a9b6247..9e3bf18 100644 --- a/src/pages/MainShell.py +++ b/src/pages/MainShell.py @@ -14,8 +14,10 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.remote.webdriver import WebDriver from selenium.common.exceptions import ( + ElementNotInteractableException, NoSuchElementException, TimeoutException, + WebDriverException, ) from pages.ReserveView import ReserveView @@ -38,6 +40,15 @@ class MainShell: self._driver = driver + def _clickTab( + self, + locator: tuple, + ) -> None: + + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(locator) + ).click() + def gotoReserveView( self, ) -> ReserveView: @@ -65,9 +76,7 @@ class MainShell: try: self._driver.find_element(*self.TAB_LOGOUT).click() return True - except NoSuchElementException: - return False - except Exception: + except (NoSuchElementException, ElementNotInteractableException): return False def waitCheckinButton( @@ -81,8 +90,6 @@ class MainShell: return True except TimeoutException: return False - except Exception: - return False def waitExtendButton( self, @@ -95,40 +102,50 @@ class MainShell: return True except TimeoutException: return False - except Exception: - return False def isCheckinButtonDisabled( self, ) -> bool: - btn = self._driver.find_element(*self.BTN_CHECKIN) - return "disabled" in btn.get_attribute("class") + try: + btn = self._driver.find_element(*self.BTN_CHECKIN) + return "disabled" in btn.get_attribute("class") + except NoSuchElementException: + return True def isExtendButtonDisabled( self, ) -> bool: - btn = self._driver.find_element(*self.BTN_EXTEND) - return "disabled" in btn.get_attribute("class") + try: + btn = self._driver.find_element(*self.BTN_EXTEND) + return "disabled" in btn.get_attribute("class") + except NoSuchElementException: + return True def clickCheckinButton( self, ) -> None: - btn = WebDriverWait(self._driver, 2).until( - EC.element_to_be_clickable(self.BTN_CHECKIN) - ) - btn.click() + try: + btn = WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.BTN_CHECKIN) + ) + btn.click() + except (TimeoutException, ElementNotInteractableException): + return def clickExtendButton( self, ) -> None: - btn = WebDriverWait(self._driver, 2).until( - EC.element_to_be_clickable(self.BTN_EXTEND) - ) - btn.click() + try: + btn = WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(self.BTN_EXTEND) + ) + btn.click() + except (TimeoutException, ElementNotInteractableException): + return def enableCheckinButtonByJS( self, @@ -154,13 +171,7 @@ class MainShell: self, ) -> None: - self._driver.refresh() - - def _clickTab( - self, - locator: tuple, - ) -> None: - - WebDriverWait(self._driver, 2).until( - EC.element_to_be_clickable(locator) - ).click() + try: + self._driver.refresh() + except (TimeoutException, WebDriverException): + return diff --git a/src/pages/RecordsView.py b/src/pages/RecordsView.py index dc42faa..2c6d5d6 100644 --- a/src/pages/RecordsView.py +++ b/src/pages/RecordsView.py @@ -44,8 +44,6 @@ class RecordsView: return self._driver.find_elements(*self.RECORDS_LIST) except TimeoutException: return None - except Exception: - return None def getRecordTimeElement( self, @@ -71,8 +69,6 @@ class RecordsView: ) except TimeoutException: return False - except Exception: - return False try: more_btn = self._driver.find_element(*self.MORE_BTN) if more_btn.is_displayed() and more_btn.is_enabled(): @@ -82,8 +78,6 @@ class RecordsView: return False except (NoSuchElementException, StaleElementReferenceException): return False - except Exception: - return False def getRecordText( self, diff --git a/src/pages/ReserveView.py b/src/pages/ReserveView.py index 1f7e459..c8f1366 100644 --- a/src/pages/ReserveView.py +++ b/src/pages/ReserveView.py @@ -53,6 +53,50 @@ class ReserveView: self._driver = driver + def _clickOptionByJS( + self, + trigger_id: str, + option_css: str, + ) -> bool: + + script = f""" + try {{ + var trigger = document.getElementById('{trigger_id}'); + if (trigger) {{ + trigger.click(); + var option = document.querySelector("{option_css}"); + if (option) {{ + option.click(); + return true; + }} + return false; + }} + return false; + }} catch (e) {{ + return false; + }} + """ + result = self._driver.execute_script(script) + time.sleep(0.1) + return result + + def _clickOption( + self, + trigger: tuple, + option: tuple, + ) -> bool: + + try: + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(trigger) + ).click() + WebDriverWait(self._driver, 2).until( + EC.element_to_be_clickable(option) + ).click() + return True + except (TimeoutException, ElementNotInteractableException): + return False + def selectDate( self, date_str: str, @@ -109,21 +153,15 @@ class ReserveView: ).click() except (TimeoutException, ElementNotInteractableException): return None - except Exception: - return None try: WebDriverWait(self._driver, 2).until( EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room))) ).click() except (TimeoutException, ElementNotInteractableException): return None - except Exception: - return None try: return SeatMapDialog(self._driver) - except (TimeoutException): - return None - except Exception: + except TimeoutException: return None def submitReserve( @@ -137,57 +175,12 @@ class ReserveView: return True except (TimeoutException, ElementNotInteractableException): return False - except Exception: - return False def refresh( self, ) -> None: - self._driver.refresh() - - def _clickOptionByJS( - self, - trigger_id: str, - option_css: str, - ) -> bool: - - script = f""" - try {{ - var trigger = document.getElementById('{trigger_id}'); - if (trigger) {{ - trigger.click(); - var option = document.querySelector("{option_css}"); - if (option) {{ - option.click(); - return true; - }} - return false; - }} - return false; - }} catch (e) {{ - return false; - }} - """ - result = self._driver.execute_script(script) - time.sleep(0.1) - return result - - def _clickOption( - self, - trigger: tuple, - option: tuple, - ) -> bool: - try: - WebDriverWait(self._driver, 2).until( - EC.element_to_be_clickable(trigger) - ).click() - WebDriverWait(self._driver, 2).until( - EC.element_to_be_clickable(option) - ).click() - return True - except (TimeoutException, ElementNotInteractableException): - return False - except Exception: - return False + self._driver.refresh() + except TimeoutException: + return diff --git a/src/pages/components/CheckinResultDialog.py b/src/pages/components/CheckinResultDialog.py index 7843f41..b8252b1 100644 --- a/src/pages/components/CheckinResultDialog.py +++ b/src/pages/components/CheckinResultDialog.py @@ -45,9 +45,8 @@ class CheckinResultDialog(Dialog): self._waitPresence(self.RESULT_MSG) el = self._find(*self.RESULT_MSG) return el.text - except (TimeoutException, NoSuchElementException, StaleElementReferenceException): - return "" - except Exception: + except (TimeoutException, NoSuchElementException, + StaleElementReferenceException): return "" def getDetails( @@ -59,8 +58,6 @@ class CheckinResultDialog(Dialog): return [el.text for el in elements if el.text.strip()] except (NoSuchElementException, StaleElementReferenceException): return [] - except Exception: - return [] def clickOk( self, @@ -69,7 +66,5 @@ class CheckinResultDialog(Dialog): try: self._waitClickable(self.OK_BTN).click() return True - except (NoSuchElementException, TimeoutException, ElementNotInteractableException): - return False - except Exception: + except (TimeoutException, ElementNotInteractableException): return False diff --git a/src/pages/components/RenewDialog.py b/src/pages/components/RenewDialog.py index 150eca1..3628f1a 100644 --- a/src/pages/components/RenewDialog.py +++ b/src/pages/components/RenewDialog.py @@ -10,6 +10,7 @@ See the LICENSE file for details. from selenium.common.exceptions import ( ElementNotInteractableException, NoSuchElementException, + StaleElementReferenceException, TimeoutException, ) from selenium.webdriver.common.by import By @@ -50,9 +51,7 @@ class RenewDialog(Dialog): self._waitVisible(self.ROOT) self._waitPresence(self.MESSAGE_HEAD) self._waitPresence(self.RESULT_MSG) - except (NoSuchElementException, TimeoutException): - return False - except Exception: + except TimeoutException: return False head_msg = self._find(*self.MESSAGE_HEAD).text.strip() if "警告" in head_msg: @@ -60,9 +59,7 @@ class RenewDialog(Dialog): try: self._waitAllPresence(self.TIME_OPTS) self._waitPresence(self.OK_BTN) - except (NoSuchElementException, TimeoutException): - return False - except Exception: + except TimeoutException: return False return True @@ -70,13 +67,19 @@ class RenewDialog(Dialog): self, ) -> str: - return self._find(*self.MESSAGE_HEAD).text.strip() + try: + return self._find(*self.MESSAGE_HEAD).text.strip() + except (NoSuchElementException, StaleElementReferenceException): + return "" def getResultMessage( self, ) -> str: - return self._find(*self.RESULT_MSG).text.strip() + try: + return self._find(*self.RESULT_MSG).text.strip() + except (NoSuchElementException, StaleElementReferenceException): + return "" def getTimeOptions( self, @@ -101,7 +104,10 @@ class RenewDialog(Dialog): prefer_earlier, ) if result.selected_index >= 0: - all_time_opts[result.selected_index].click() + try: + all_time_opts[result.selected_index].click() + except (ElementNotInteractableException, StaleElementReferenceException): + return TimeSelectionResult(free_times=result.free_times) return result def getOkButton( @@ -117,8 +123,5 @@ class RenewDialog(Dialog): try: self._find(*self.OK_BTN).click() return True - except (NoSuchElementException, TimeoutException, - ElementNotInteractableException): - return False - except Exception: + except (NoSuchElementException, ElementNotInteractableException): return False diff --git a/src/pages/components/ReserveResultDialog.py b/src/pages/components/ReserveResultDialog.py index 8ddb858..dbe8338 100644 --- a/src/pages/components/ReserveResultDialog.py +++ b/src/pages/components/ReserveResultDialog.py @@ -45,8 +45,6 @@ class ReserveResultDialog(Dialog): return self._find(*self._titleLocator()).text except (NoSuchElementException, StaleElementReferenceException): return "" - except Exception: - return "" def isSuccess( self, @@ -77,5 +75,3 @@ class ReserveResultDialog(Dialog): return [el.text for el in elements if el.text.strip()] except (NoSuchElementException, StaleElementReferenceException): return [] - except Exception: - return [] diff --git a/src/pages/components/SeatMapDialog.py b/src/pages/components/SeatMapDialog.py index 9f9a5d5..23f82ed 100644 --- a/src/pages/components/SeatMapDialog.py +++ b/src/pages/components/SeatMapDialog.py @@ -43,9 +43,7 @@ class SeatMapDialog(Dialog): try: self._waitAllPresence(self.SEAT_ITEMS) - except (NoSuchElementException, TimeoutException): - return None - except Exception: + except TimeoutException: return None try: seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}") @@ -58,8 +56,6 @@ class SeatMapDialog(Dialog): except (NoSuchElementException, ValueError, TimeoutException, ElementNotInteractableException, StaleElementReferenceException): pass - except Exception: - pass try: all_seats = self._findAll(*self.SEAT_ITEMS) seat_id_upper = seat_id.lstrip('0').upper() @@ -76,5 +72,3 @@ class SeatMapDialog(Dialog): except (NoSuchElementException, TimeoutException, ElementNotInteractableException, StaleElementReferenceException): return None - except Exception: - return None diff --git a/src/pages/components/TimeSelectDialog.py b/src/pages/components/TimeSelectDialog.py index 1a843a3..f911591 100644 --- a/src/pages/components/TimeSelectDialog.py +++ b/src/pages/components/TimeSelectDialog.py @@ -13,7 +13,8 @@ import logging from typing import TYPE_CHECKING, Callable, Optional from selenium.common.exceptions import ( - NoSuchElementException, + ElementNotInteractableException, + StaleElementReferenceException, TimeoutException, ) from selenium.webdriver.common.by import By @@ -106,9 +107,7 @@ class TimeSelectDialog(Dialog): self._waitAllPresence( (By.CSS_SELECTOR, f"#{time_id} ul li a") ) - except (NoSuchElementException, TimeoutException): - return [] - except Exception: + except TimeoutException: return [] return self._findAll( By.CSS_SELECTOR, @@ -133,7 +132,10 @@ class TimeSelectDialog(Dialog): prefer_earlier, ) if result.selected_index >= 0: - all_time_opts[result.selected_index].click() + try: + all_time_opts[result.selected_index].click() + except (ElementNotInteractableException, StaleElementReferenceException): + return TimeSelectionResult(free_times=result.free_times) return result def selectTimeRange( diff --git a/src/pages/flows/CheckinFlow.py b/src/pages/flows/CheckinFlow.py index bb0689e..73ee581 100644 --- a/src/pages/flows/CheckinFlow.py +++ b/src/pages/flows/CheckinFlow.py @@ -10,6 +10,7 @@ See the LICENSE file for details. import queue from selenium.common.exceptions import ( + ElementNotInteractableException, NoSuchElementException, TimeoutException, ) @@ -83,9 +84,6 @@ class CheckinFlow(MsgBase): dialog.clickOk() self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR) return False - except (NoSuchElementException, TimeoutException): - self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) - return False - except Exception: + except (TimeoutException, NoSuchElementException, ElementNotInteractableException): self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR) return False diff --git a/src/pages/flows/RenewFlow.py b/src/pages/flows/RenewFlow.py index b8f6421..e52dd52 100644 --- a/src/pages/flows/RenewFlow.py +++ b/src/pages/flows/RenewFlow.py @@ -39,6 +39,28 @@ class RenewFlow(MsgBase): self._driver: WebDriver = driver self._shell: MainShell = shell + def _validateRenewTime( + self, + end_time: str, + target_renew_mins: int, + ) -> bool: + + if target_renew_mins > self.LIBRARY_CLOSE_MINS: + actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time) + if actual_renew_duration <= 0: + self._showTrace( + f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR + ) + return False + self._showTrace( + f"续约时间已调整至闭馆时间 " + f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)}," + f"实际续约时长为 " + f"{actual_renew_duration // 60} 小时 " + f"{actual_renew_duration % 60} 分钟" + ) + return True + def execute( self, username: str, @@ -106,37 +128,7 @@ class RenewFlow(MsgBase): self._showTrace(f"当前可供续约的时间有: {result.free_times}") self._shell.refresh() return False - except (NoSuchElementException, TimeoutException) as e: + except (NoSuchElementException, TimeoutException, ElementNotInteractableException) as e: self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) self._shell.refresh() return False - except (ElementNotInteractableException) as e: - self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) - self._shell.refresh() - return False - except Exception as e: - self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR) - self._shell.refresh() - return False - - def _validateRenewTime( - self, - end_time: str, - target_renew_mins: int, - ) -> bool: - - if target_renew_mins > self.LIBRARY_CLOSE_MINS: - actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time) - if actual_renew_duration <= 0: - self._showTrace( - f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR - ) - return False - self._showTrace( - f"续约时间已调整至闭馆时间 " - f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)}," - f"实际续约时长为 " - f"{actual_renew_duration // 60} 小时 " - f"{actual_renew_duration % 60} 分钟" - ) - return True diff --git a/src/pages/flows/ReserveFlow.py b/src/pages/flows/ReserveFlow.py index 1722903..88a2864 100644 --- a/src/pages/flows/ReserveFlow.py +++ b/src/pages/flows/ReserveFlow.py @@ -12,7 +12,6 @@ from dataclasses import dataclass from selenium.common.exceptions import ( ElementNotInteractableException, - NoSuchElementException, TimeoutException, ) from selenium.webdriver.remote.webdriver import WebDriver @@ -70,10 +69,7 @@ class ReserveFlow(MsgBase): try: view = self._shell.gotoReserveView() - except (NoSuchElementException, TimeoutException) as e: - self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR) - return False - except Exception as e: + except (TimeoutException, ElementNotInteractableException) as e: self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR) return False if not view.selectDate(ctx.date): @@ -140,8 +136,6 @@ class ReserveFlow(MsgBase): self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR) except (TimeoutException, ElementNotInteractableException): self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) - except Exception: - self._showTrace("预约提交失败 !", self.TraceLevel.ERROR) if not submit_reserve and have_hover_on_page: view.refresh() if reserve_success: From 2aace40a2607ab987331f7521b84867f10a40b7e Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 01:36:28 +0800 Subject: [PATCH 39/49] =?UTF-8?q?fix(services):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E8=AF=86=E5=88=AB=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E3=80=81=E9=A2=84=E7=BA=A6=E6=97=B6=E9=97=B4=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E4=B8=8E=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/pages/services/CaptchaSolver.py | 37 +++++++++------------------- src/pages/services/RecordChecker.py | 14 +++++++---- src/pages/services/ReserveChecker.py | 11 +++++++++ 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/pages/services/CaptchaSolver.py b/src/pages/services/CaptchaSolver.py index 1544bee..2a184ff 100644 --- a/src/pages/services/CaptchaSolver.py +++ b/src/pages/services/CaptchaSolver.py @@ -11,11 +11,6 @@ import base64 import queue import ddddocr -from selenium.common.exceptions import ( - NoSuchElementException, - TimeoutException, -) - from base.MsgBase import MsgBase from pages.LoginPage import LoginPage @@ -38,6 +33,9 @@ class CaptchaSolver(MsgBase): try: img_src = login_page.getCaptchaImageSrc() + if img_src is None: + self._showTrace("验证码图片元素定位时发生错误 !", self.TraceLevel.ERROR) + return "" base64_str = img_src.split(',', 1)[1] captcha_img = base64.b64decode(base64_str) captcha_text = self._ocr.classification(captcha_img) @@ -45,15 +43,9 @@ class CaptchaSolver(MsgBase): self._showTrace(f"识别到验证码为 : '{captcha_text}'", 20, no_log=True) if len(captcha_text) != 4: self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) - raise Exception("识别到的验证码长度不等于 4 个字符 !") + return "" return captcha_text - except (NoSuchElementException, TimeoutException) as e: - self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) - return "" - except (ValueError, OSError) as e: - self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) - return "" - except Exception as e: + except ValueError as e: self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) return "" @@ -61,20 +53,13 @@ class CaptchaSolver(MsgBase): self, ) -> str: - try: - self._showMsg("请输入验证码:") - captcha_text = self._waitMsg(timeout=15) - self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True) - if len(captcha_text) != 4: - self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) - raise Exception("输入的验证码长度不等于 4 个字符 !") - return captcha_text - except ValueError as e: - self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) - return "" - except Exception as e: - self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) + self._showMsg("请输入验证码:") + captcha_text = self._waitMsg(timeout=15) + self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True) + if len(captcha_text) != 4: + self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING) return "" + return captcha_text def solveCaptcha( self, diff --git a/src/pages/services/RecordChecker.py b/src/pages/services/RecordChecker.py index c7cef7f..7151f0b 100644 --- a/src/pages/services/RecordChecker.py +++ b/src/pages/services/RecordChecker.py @@ -112,20 +112,21 @@ class RecordChecker(MsgBase): try: time_element = records_view.getRecordTimeElement(reservation) info_elements = records_view.getRecordInfoElements(reservation) - except (NoSuchElementException, TimeoutException, StaleElementReferenceException): + except (NoSuchElementException, StaleElementReferenceException): return { "date": "", "time": {"begin": "", "end": ""}, "info": {"location": "", "status": ""}, } - except Exception: + try: + time_data = self._decodeReserveTime(time_element) + info_data = self._decodeReserveInfo(info_elements) + except StaleElementReferenceException: return { "date": "", "time": {"begin": "", "end": ""}, "info": {"location": "", "status": ""}, } - time_data = self._decodeReserveTime(time_element) - info_data = self._decodeReserveInfo(info_elements) return { "date": time_data["date"], "time": time_data["time"], @@ -152,7 +153,10 @@ class RecordChecker(MsgBase): records_view = shell.gotoRecordsView() for _ in range(max_check_times): - reservations = records_view.loadRecords() + try: + reservations = records_view.loadRecords() + except TimeoutException: + reservations = None if reservations is None: return None for reservation in reservations[checked_count:]: diff --git a/src/pages/services/ReserveChecker.py b/src/pages/services/ReserveChecker.py index 9be86e2..3d06acd 100644 --- a/src/pages/services/ReserveChecker.py +++ b/src/pages/services/ReserveChecker.py @@ -88,11 +88,22 @@ class ReserveChecker(MsgBase): ) -> bool: cur_time = time.strftime("%H:%M", time.localtime()) + cur_date = time.strftime("%Y-%m-%d", time.localtime()) if reserve_info.get("begin_time") is None: reserve_info["begin_time"] = {} if "time" not in reserve_info["begin_time"]: reserve_info["begin_time"]["time"] = cur_time self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}") + elif reserve_info.get("date") == cur_date: + begin_mins = timeStrToMins(reserve_info["begin_time"]["time"]) + cur_mins = timeStrToMins(cur_time) + if begin_mins < cur_mins: + self._showTrace( + f"开始时间 {reserve_info['begin_time']['time']} 已过当前时间 {cur_time}, " + f"自动调整为当前时间", + self.TraceLevel.WARNING, + ) + reserve_info["begin_time"]["time"] = cur_time if "max_diff" not in reserve_info["begin_time"]: reserve_info["begin_time"]["max_diff"] = 30 self._showTrace("开始时间最大时间差未指定, 自动设置为 30 分钟") From b78fd2d1e4525602e75358f94499d3d482019740 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 01:55:12 +0800 Subject: [PATCH 40/49] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20AutoLibrary?= =?UTF-8?q?=20=E5=BA=94=E7=94=A8=E5=9B=BE=E6=A0=87=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .../resources/icons/AutoLibrary_Logo_128.ico | Bin 0 -> 176704 bytes .../resources/icons/AutoLibrary_Logo_64.ico | Bin 0 -> 181912 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/gui/resources/icons/AutoLibrary_Logo_128.ico create mode 100644 src/gui/resources/icons/AutoLibrary_Logo_64.ico diff --git a/src/gui/resources/icons/AutoLibrary_Logo_128.ico b/src/gui/resources/icons/AutoLibrary_Logo_128.ico new file mode 100644 index 0000000000000000000000000000000000000000..123e14bd5d56689615bd1713073251c893f21ef4 GIT binary patch literal 176704 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%0A};2TG8NOoBngYYBwWA~mWCo{>1j7V9wtps;T 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 Date: Thu, 28 May 2026 02:49:46 +0800 Subject: [PATCH 41/49] =?UTF-8?q?fix(workflows):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=BC=95=E7=94=A8=E3=80=81=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E6=AD=BB=E4=BB=A3=E7=A0=81=E5=B9=B6=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=BE=93=E5=87=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build-test.yml | 24 ++++++++++++------------ .github/workflows/build.yml | 23 ++++++++++++++--------- .github/workflows/commit-release.yml | 10 +++++++++- .github/workflows/release.yml | 21 ++++++++++++++------- .github/workflows/update-version.yml | 16 +++++++++++++--- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 31da669..dc6c2de 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -4,10 +4,6 @@ name: Build Test # It is triggered when a pull request is opened, synchronized, or reopened against the main branch. on: - push: - branches: - - main - pull_request: branches: - main @@ -15,7 +11,6 @@ on: - opened - synchronize - reopened - # Allow manual trigger for testing workflow_dispatch: # @@ -49,6 +44,8 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.13' + cache: 'pip' + cache-dependency-path: requirement.txt - name: Install dependencies run: | @@ -125,7 +122,7 @@ jobs: " binaries=[]," " datas=[" " ('models\\common.onnx', 'ddddocr')," - " ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons')," + " ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons')," " ]," " hiddenimports=[]," " hookspath=[]," @@ -153,7 +150,7 @@ jobs: " target_arch=None," " codesign_identity=None," " entitlements_file=None," - " icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico']," + " icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico']," ")" "" "coll = COLLECT(" @@ -169,9 +166,11 @@ jobs: $specLines | Out-File -FilePath "Main.spec" -Encoding UTF8 Write-Host "✓ Main.spec (non-single file) generated successfully" - Write-Host "`nGenerated Main.spec ============" + Write-Host "`n========================================" + Write-Host "Generated Main.spec" + Write-Host "========================================" Get-Content "Main.spec" | Write-Host - Write-Host "==================================`n" + Write-Host "========================================`n" shell: pwsh - name: Build with PyInstaller @@ -186,7 +185,7 @@ jobs: $distDir = "dist/AutoLibrary-$version" $zipName = "AutoLibrary.$tagName-windows-x86_64.zip" - echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT + "ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append Write-Host "Looking for distribution directory: $distDir" if (Test-Path $distDir) { @@ -212,10 +211,11 @@ jobs: run: | Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + Write-Host "========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append - Write-Host "- Pull Request #${{ github.event.pull_request.number }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append - Write-Host "- Branch: ${{ github.event.pull_request.head.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + Write-Host "- Pull Request #${{ github.event.pull_request.number || 'N/A' }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + Write-Host "- Branch: ${{ github.event.pull_request.head.ref || github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append shell: pwsh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cea2c05..71e572d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,15 +76,17 @@ jobs: run: | $versionInfoFile = "src/gui/ALVersionInfo.py" Write-Host "Verifying $versionInfoFile content:" - Write-Host "==================================" + Write-Host "========================================" Get-Content $versionInfoFile | Write-Host - Write-Host "==================================" + Write-Host "========================================" shell: pwsh - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.13' + cache: 'pip' + cache-dependency-path: requirement.txt - name: Install dependencies run: | @@ -161,7 +163,7 @@ jobs: " binaries=[]," " datas=[" " ('models\\common.onnx', 'ddddocr')," - " ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons')," + " ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons')," " ]," " hiddenimports=[]," " hookspath=[]," @@ -189,7 +191,7 @@ jobs: " target_arch=None," " codesign_identity=None," " entitlements_file=None," - " icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico']," + " icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico']," ")" "" "coll = COLLECT(" @@ -205,9 +207,11 @@ jobs: $specLines | Out-File -FilePath "Main.spec" -Encoding UTF8 Write-Host "✓ Main.spec (non-single file) generated successfully" - Write-Host "`nGenerated Main.spec ============" + Write-Host "`n========================================" + Write-Host "Generated Main.spec" + Write-Host "========================================" Get-Content "Main.spec" | Write-Host - Write-Host "==================================`n" + Write-Host "========================================`n" shell: pwsh - name: Build with PyInstaller @@ -222,7 +226,7 @@ jobs: $distDir = "dist/AutoLibrary-$version" $zipName = "AutoLibrary.$tagName-windows-x86_64.zip" - echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT + "ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append Write-Host "Looking for distribution directory: $distDir" if (Test-Path $distDir) { @@ -242,13 +246,14 @@ jobs: name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64 path: | ${{ steps.zip_release.outputs.ZIP_NAME }} - retention-days: ${{ github.event_name != 'workflow_call' && 7 || 90 }} + retention-days: ${{ inputs.is_test == 'true' && 7 || 90 }} - name: Upload build summary - if: ${{ github.event_name != 'workflow_call' }} + if: ${{ inputs.is_test == 'true' }} run: | Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append + Write-Host "========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append diff --git a/.github/workflows/commit-release.yml b/.github/workflows/commit-release.yml index 00335d3..76d174a 100644 --- a/.github/workflows/commit-release.yml +++ b/.github/workflows/commit-release.yml @@ -83,7 +83,9 @@ jobs: echo "✓ File replaced: $FILE_PATH" echo "" - echo "Updated file content ===================" + echo "========================================" + echo "Updated file content" + echo "========================================" cat "$FILE_PATH" echo "========================================" @@ -151,3 +153,9 @@ jobs: COMMIT_SHA=$(git rev-parse --short HEAD) echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT echo "✓ New commit SHA: $COMMIT_SHA" + echo "## Commit Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "========================================" >> $GITHUB_STEP_SUMMARY + echo "- Version: ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- Tag: ${{ inputs.tag_name }}" >> $GITHUB_STEP_SUMMARY + echo "- Commit SHA: $COMMIT_SHA" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e87fab..4a49f86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ on: jobs: # # Start : - # virtual job that indacates the start of the release process + # virtual job that indicates the start of the release process # start: @@ -158,7 +158,7 @@ jobs: needs: - update-version - commit-release - if: always() && needs.update-version.result == 'success' && needs.commit-release.result == 'success' + if: always() && needs.update-version.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped') uses: ./.github/workflows/build.yml permissions: contents: write @@ -205,7 +205,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # End : - # virtual job that indacates the end of the release process + # virtual job that indicates the end of the release process # end: @@ -227,7 +227,7 @@ jobs: - release - extract-version - commit-release - if: ${{ needs.release.result == 'success' && needs.commit-release.result == 'success' }} + if: ${{ needs.release.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped') }} runs-on: ubuntu-latest permissions: contents: write @@ -267,9 +267,13 @@ jobs: git checkout ${MAIN_BRANCH} # Show branch status before merge - echo "=== Branch status before merge ===" + echo "========================================" + echo "Branch status before merge" + echo "========================================" git log --oneline --graph --all -5 - echo "=== Diff between ${MAIN_BRANCH} and origin/${BRANCH_NAME} ===" + echo "========================================" + echo "Diff: ${MAIN_BRANCH} vs origin/${BRANCH_NAME}" + echo "========================================" git diff ${MAIN_BRANCH} origin/${BRANCH_NAME} --stat || echo "No differences found" # Force create a merge commit even if there are no changes @@ -279,7 +283,9 @@ jobs: -m "chore(release): merge ${BRANCH_NAME} to ${MAIN_BRANCH} [auto release commit]" # Show merge result - echo "=== Merge result ===" + echo "========================================" + echo "Merge result" + echo "========================================" git log --oneline --graph -3 # Push to main @@ -310,6 +316,7 @@ jobs: echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "========================================" >> $GITHUB_STEP_SUMMARY echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Actions Performed:" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update-version.yml b/.github/workflows/update-version.yml index 112329f..512f6f3 100644 --- a/.github/workflows/update-version.yml +++ b/.github/workflows/update-version.yml @@ -128,10 +128,15 @@ jobs: echo "Build version file location: $VER_INFO_BUILDFILE" echo "Commit version file location: $VER_INFO_COMMITFILE" echo "" - echo "Build version ALVersionInfo.py content =" + echo "========================================" + echo "Build version ALVersionInfo.py" + echo "========================================" cat "$VER_INFO_BUILDFILE" + echo "========================================" echo "" - echo "Commit version ALVersionInfo.py content " + echo "========================================" + echo "Commit version ALVersionInfo.py" + echo "========================================" cat "$VER_INFO_COMMITFILE" echo "========================================" @@ -140,11 +145,16 @@ jobs: run: | if git diff --quiet src/gui/ALVersionInfo.py; then echo "has_changes=false" >> $GITHUB_OUTPUT - echo "! No changes detected in ALVersionInfo.py" + echo "⚠ No changes detected in ALVersionInfo.py" else echo "has_changes=true" >> $GITHUB_OUTPUT echo "✓ ALVersionInfo.py has been modified" fi + echo "## Update Version Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "========================================" >> $GITHUB_STEP_SUMMARY + echo "- Version: ${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" >> $GITHUB_STEP_SUMMARY - name: Upload modified ALVersionInfo.py ready for build if: steps.check_changes.outputs.has_changes == 'true' From d7e19dcd52321a8cc24324ea5a254a14bbfc9814 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 11:33:17 +0800 Subject: [PATCH 42/49] =?UTF-8?q?refactor(pages):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E9=A2=84=E7=BA=A6=E6=A3=80=E6=9F=A5=E6=B5=81=E7=A8=8B=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=EF=BC=8C=E6=95=B0=E6=8D=AE=E6=A0=A1=E9=AA=8C=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=E4=BB=A5=E9=81=BF=E5=85=8D=E6=97=A0=E6=95=88=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 ReserveChecker.check(纯数据校验)移至 RecordChecker.canReserve(浏览器查询)之前, 解决 canReserve 在校验前使用未规范化日期的隐式缺陷,并避免无效配置触发昂贵的页面导航操作。 Co-Authored-By: Claude Opus 4.7 --- src/pages/AutoLib.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pages/AutoLib.py b/src/pages/AutoLib.py index 6b608e0..18f5b00 100644 --- a/src/pages/AutoLib.py +++ b/src/pages/AutoLib.py @@ -233,6 +233,7 @@ class AutoLib(MsgBase): # result : -1 - terminate, 0 - success, 1 - failed, 2 - passed result: int = 2 + # login auto_captcha: bool = login_config.get("auto_captcha", True) if not self.__login_page.login( @@ -249,10 +250,11 @@ class AutoLib(MsgBase): "auto_checkin": run_mode_raw & 0x2, "auto_renewal": run_mode_raw & 0x4, } + # reserve if run_mode["auto_reserve"]: - if self.__record_checker.canReserve(self.__shell, reserve_info["date"]): - if self.__reserve_checker.check(reserve_info): + if self.__reserve_checker.check(reserve_info): + if self.__record_checker.canReserve(self.__shell, reserve_info["date"]): ctx = ReserveContext( username=username, date=reserve_info["date"], @@ -273,10 +275,10 @@ class AutoLib(MsgBase): else: result = 1 else: - result = 1 + self._showTrace(f"用户 {username} 无法预约, 已跳过") + result = 2 else: - self._showTrace(f"用户 {username} 无法预约, 已跳过") - result = 2 + result = 1 # checkin last_result: int = result @@ -317,8 +319,11 @@ class AutoLib(MsgBase): # logout if not self.__shell.logout(): + self._showTrace(f"用户 {username} 退出登录失败, 尝试直接重载页面") if not self.__initDriverUrl(): + self._showTrace(f"用户 {username} 重载页面失败, 无法继续操作, 该任务已终止 !") return -1 + self._showTrace(f"用户 {username} 已退出登录") return result def run( From 02b3a62868d701a9c92de95183232b103c47dd4d Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 16:54:14 +0800 Subject: [PATCH 43/49] =?UTF-8?q?chore(autoscript):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=89=88=E6=9C=AC=E5=8F=B7=20v1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/autoscript/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index cc78a3d..784d512 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -9,6 +9,7 @@ See the LICENSE file for details. """ from .ASEngine import ASEngine +__version__ = "1.0.0" # autoscript version _TARGET_VAR_DEFS = [ ("USERNAME", "String", ["username"], "用户名"), From 3ebebe015f20ff47fe6a97d82f23461f5594e7dd Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 19:34:36 +0800 Subject: [PATCH 44/49] =?UTF-8?q?refactor(gui):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=E5=AF=B9=E8=AF=9D=E6=A1=86=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20QTabWidget=20=E5=88=86=E9=A1=B5=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E4=B8=8E=E8=AE=B8=E5=8F=AF=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原本的单页文本浏览器替换为 TabWidget,分"关于"和"许可证"两个标签页。 同时优化了信息排版和样式,新增 Selenium 版本展示,移除了 UI 文件中的旧控件。 Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAboutDialog.py | 112 +++++++++++++++++++------- src/gui/resources/ui/ALAboutDialog.ui | 49 +---------- 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 94c9c7d..7b0524d 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -16,11 +16,16 @@ from PySide6.QtCore import ( from PySide6.QtGui import QIcon from PySide6.QtWidgets import ( QApplication, - QDialog + QDialog, + QTabWidget, + QTextBrowser ) from gui.ALVersionInfo import ( - AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE + AL_VERSION, + AL_COMMIT_SHA, + AL_COMMIT_DATE, + AL_BUILD_DATE ) from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog from gui.resources import ALResource @@ -43,12 +48,23 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): ): self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48)) - info_text = self.generateAboutText() - self.AboutInfoBrowser.setHtml(info_text) - browser_font = self.AboutInfoBrowser.font() - browser_font.setFamily("Courier New") - self.AboutInfoBrowser.setFont(browser_font) - self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) + self.TabWidget = QTabWidget() + self.TabWidget.setDocumentMode(True) + AboutBrowser = QTextBrowser() + AboutBrowser.setHtml(self.generateAboutText()) + AboutBrowser.setOpenExternalLinks(True) + AboutBrowser.setLineWrapMode(QTextBrowser.LineWrapMode.NoWrap) + AboutBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) + BrowserFont = AboutBrowser.font() + BrowserFont.setFamilies(["Courier New", "Consolas", "Menlo", "DejaVu Sans Mono", "monospace"]) + AboutBrowser.setFont(BrowserFont) + self.TabWidget.addTab(AboutBrowser, "关于") + LicenseBrowser = QTextBrowser() + LicenseBrowser.setHtml(self.generateLicenseText()) + LicenseBrowser.setOpenExternalLinks(True) + LicenseBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) + self.TabWidget.addTab(LicenseBrowser, "许可证") + self.AboutInfoLayout.addWidget(self.TabWidget) def connectSignals( self @@ -61,33 +77,57 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog): ) -> str: os_info = self.getOSInfo() + run_on = f"{os_info['system']} {os_info['version']} {os_info['architecture']}" + selenium_ver = self.getSeleniumVersion() about_text = f""" -

Version Information:

-Version: {AL_VERSION}
+VERSION: {AL_VERSION}
Commit SHA: {AL_COMMIT_SHA}
Commit date: {AL_COMMIT_DATE}
Build date: {AL_BUILD_DATE}
-Python version: {platform.python_version()}
-Qt version: {self.getQtVersion()}
- -

System Information:

+
+SYSTEM INFORMATION
+Running on: {run_on}
Processor: {platform.processor()}
-Operating system: {os_info['system']}
-System version: {os_info['version']}
-System architecture: {os_info['architecture']}
- -

Project Information:

-License: MIT License
-Project repository:
https://www.github.com/KenanZhu/AutoLibrary
-Project website: https://www.autolibrary.kenanzhu.com
- -

Author Information:

-Developer: KenanZhu
-Contact: nanoki_zh@163.com
-GitHub: https://www.github.com/KenanZhu
+
+DEPENDENCIES
+Python: {platform.python_version()}
+Qt(PySide6): {self.getQtVersion()}
+Selenium: {selenium_ver}
+
+PROJECT INFORMATION
+Website: https://www.autolibrary.kenanzhu.com
+Repository: https://www.github.com/KenanZhu/AutoLibrary
+
+AUTHOR
+Developer/Maintainer: KenanZhu
+Contact: nanoki_zh@163.com
+GitHub: https://www.github.com/KenanZhu
""" return about_text + def generateLicenseText( + self + ) -> str: + + return """ +MIT License +

Copyright © 2025 - 2026 KenanZhu

+

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+

The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software.

+

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.

""" + def getOSInfo( self ): @@ -129,13 +169,23 @@ GitHub: 800 - 400 + 600 @@ -103,53 +103,6 @@ 0 - - - - - 56 - 0 - - - - - 56 - 16777215 - - - - QFrame::Shape::NoFrame - - - QFrame::Shadow::Plain - - - 0 - - - - - - - QFrame::Shadow::Plain - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff - - - QTextEdit::LineWrapMode::NoWrap - - - true - - - true - - - From bb63ee6f0306e02f1e243692da93228a7a690108 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 28 May 2026 19:35:03 +0800 Subject: [PATCH 45/49] =?UTF-8?q?refactor(gui):=20=E7=BB=9F=E4=B8=80=20Qt?= =?UTF-8?q?=20=E6=8E=A7=E4=BB=B6=E5=8F=98=E9=87=8F=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E4=B8=BA=20PascalCase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将所有 self.xxx 形式的 Qt 控件属性名以及 Qt 对象局部变量由 snake_case 重命名为 PascalCase,提升代码可读性和一致性。涉及 14 个文件,涵盖: - AutoScript 编排/编辑对话框子模块 - 配置/主窗口/用户树/座位图等核心界面组件 - 定时任务管理相关界面 - 状态标签/浏览器驱动下载对话框 Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAutoScriptEditDialog.py | 474 ++++++++++----------- src/gui/ALAutoScriptOrchDialog/_blocks.py | 128 +++--- src/gui/ALAutoScriptOrchDialog/_dialog.py | 70 +-- src/gui/ALAutoScriptOrchDialog/_helpers.py | 136 +++--- src/gui/ALAutoScriptOrchDialog/_widgets.py | 248 +++++------ src/gui/ALConfigWidget.py | 200 ++++----- src/gui/ALMainWindow.py | 14 +- src/gui/ALSeatMapView.py | 16 +- src/gui/ALStatusLabel.py | 135 +++--- src/gui/ALTimerTaskAddDialog.py | 24 +- src/gui/ALTimerTaskHistoryDialog.py | 3 - src/gui/ALTimerTaskManageWidget.py | 88 ++-- src/gui/ALUserTreeWidget.py | 30 +- src/gui/ALWebDriverDownloadDialog.py | 6 - 14 files changed, 780 insertions(+), 792 deletions(-) diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 245e18a..750686b 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -74,54 +74,54 @@ class ALScriptHighlighter(QSyntaxHighlighter): super().__init__(parent) self._rules = [] - keywordFmt = QTextCharFormat() - keywordFmt.setForeground(QColor("#569CD6")) - keywordFmt.setFontWeight(QFont.Weight.Bold) + KeywordFmt = QTextCharFormat() + KeywordFmt.setForeground(QColor("#569CD6")) + KeywordFmt.setFontWeight(QFont.Weight.Bold) for kw in [ "if", "elseif", "else", "end", "then", "and", "or", "not", "local", "function", "return", "nil", ]: - self._rules.append((r"\b" + kw + r"\b", keywordFmt)) - boolFmt = QTextCharFormat() - boolFmt.setForeground(QColor("#4FC1FF")) - boolFmt.setFontWeight(QFont.Weight.Bold) - self._rules.append((r"\btrue\b", boolFmt)) - self._rules.append((r"\bfalse\b", boolFmt)) - cmpFmt = QTextCharFormat() - cmpFmt.setForeground(QColor("#C586C0")) - cmpFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b" + kw + r"\b", KeywordFmt)) + BoolFmt = QTextCharFormat() + BoolFmt.setForeground(QColor("#4FC1FF")) + BoolFmt.setFontWeight(QFont.Weight.Bold) + self._rules.append((r"\btrue\b", BoolFmt)) + self._rules.append((r"\bfalse\b", BoolFmt)) + CmpFmt = QTextCharFormat() + CmpFmt.setForeground(QColor("#C586C0")) + CmpFmt.setFontWeight(QFont.Weight.Normal) for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]: - self._rules.append((op, cmpFmt)) - arithFmt = QTextCharFormat() - arithFmt.setForeground(QColor("#C586C0")) - arithFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((op, CmpFmt)) + ArithFmt = QTextCharFormat() + ArithFmt.setForeground(QColor("#C586C0")) + ArithFmt.setFontWeight(QFont.Weight.Normal) for op in [r"\+", r"-", r"\*", r"/", r"\.\."]: - self._rules.append((op, arithFmt)) - funcFmt = QTextCharFormat() - funcFmt.setForeground(QColor("#DCDCAA")) - funcFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((op, ArithFmt)) + FuncFmt = QTextCharFormat() + FuncFmt.setForeground(QColor("#DCDCAA")) + FuncFmt.setFontWeight(QFont.Weight.Normal) for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]: - self._rules.append((r"\b" + fn + r"\b", funcFmt)) - varFmt = QTextCharFormat() - varFmt.setForeground(QColor("#9CDCFE")) - varFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b" + fn + r"\b", FuncFmt)) + VarFmt = QTextCharFormat() + VarFmt.setForeground(QColor("#9CDCFE")) + VarFmt.setFontWeight(QFont.Weight.Normal) var_names = [name for _, (name, _) in createAllVariablesTable().items()] for var in var_names: - self._rules.append((r"\b" + var + r"\b", varFmt)) - strFmt = QTextCharFormat() - strFmt.setForeground(QColor("#CE9178")) - strFmt.setFontWeight(QFont.Weight.Normal) - self._rules.append((r'"[^"]*"', strFmt)) - self._rules.append((r"'[^']*'", strFmt)) - numFmt = QTextCharFormat() - numFmt.setForeground(QColor("#B5CEA8")) - numFmt.setFontWeight(QFont.Weight.Normal) - self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt)) - commentFmt = QTextCharFormat() - commentFmt.setForeground(QColor("#6A9955")) - commentFmt.setFontItalic(True) - self._rules.append((r"--[^\n]*", commentFmt)) + self._rules.append((r"\b" + var + r"\b", VarFmt)) + StrFmt = QTextCharFormat() + StrFmt.setForeground(QColor("#CE9178")) + StrFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r'"[^"]*"', StrFmt)) + self._rules.append((r"'[^']*'", StrFmt)) + NumFmt = QTextCharFormat() + NumFmt.setForeground(QColor("#B5CEA8")) + NumFmt.setFontWeight(QFont.Weight.Normal) + self._rules.append((r"\b\d+(?:\.\d+)?\b", NumFmt)) + CommentFmt = QTextCharFormat() + CommentFmt.setForeground(QColor("#6A9955")) + CommentFmt.setFontItalic(True) + self._rules.append((r"--[^\n]*", CommentFmt)) def highlightBlock( self, @@ -147,22 +147,22 @@ class _DebugResultDialog(QDialog): super().__init__(parent) self.setWindowTitle("调试运行结果 - AutoLibrary") self.setMinimumSize(600, 200) - layout = QVBoxLayout(self) - table = QTableWidget(len(changes), 3) - table.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"]) - table.horizontalHeader().setStretchLastSection(True) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + DbgLayout = QVBoxLayout(self) + DbgTable = QTableWidget(len(changes), 3) + DbgTable.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"]) + DbgTable.horizontalHeader().setStretchLastSection(True) + DbgTable.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + DbgTable.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes): label = f"{display_name}: {name}({var_type})" - table.setItem(row, 0, QTableWidgetItem(label)) - table.setItem(row, 1, QTableWidgetItem(str(before_val))) - table.setItem(row, 2, QTableWidgetItem(str(after_val))) - layout.addWidget(table) - btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) - btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") - btnBox.accepted.connect(self.accept) - layout.addWidget(btnBox) + DbgTable.setItem(row, 0, QTableWidgetItem(label)) + DbgTable.setItem(row, 1, QTableWidgetItem(str(before_val))) + DbgTable.setItem(row, 2, QTableWidgetItem(str(after_val))) + DbgLayout.addWidget(DbgTable) + DbgBtnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + DbgBtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") + DbgBtnBox.accepted.connect(self.accept) + DbgLayout.addWidget(DbgBtnBox) class _TabToSpacesEditor(QPlainTextEdit): @@ -193,9 +193,9 @@ class ALAutoScriptEditDialog(QDialog): self.setupUi() self.connectSignals() - self.textEdit.setPlainText(script) - self._highlighter = ALScriptHighlighter( - self.textEdit.document() + self.TextEdit.setPlainText(script) + self._Highlighter = ALScriptHighlighter( + self.TextEdit.document() ) if mockData: self.setMockData(mockData) @@ -206,80 +206,80 @@ class ALAutoScriptEditDialog(QDialog): self.setWindowTitle("AutoScript 编辑 - AutoLibrary") self.setMinimumSize(660, 600) - layout = QVBoxLayout(self) - layout.setSpacing(3) - layout.setContentsMargins(3, 3, 3, 3) - toolbarLayout = QHBoxLayout() - self.zoomInBtn = QPushButton("+") - self.zoomInBtn.setFixedSize(25, 25) - self.zoomOutBtn = QPushButton("-") - self.zoomOutBtn.setFixedSize(25, 25) - self.zoomResetBtn = QPushButton("") - self.zoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg")) - self.zoomResetBtn.setIconSize(QSize(20, 20)) - self.zoomResetBtn.setFixedSize(25, 25) - self.zoomResetBtn.setToolTip("重置缩放") - self.zoomLabel = QLabel(f"{self._fontSize}px") - self.zoomLabel.setFixedHeight(25) - self.orchBtn = QPushButton("编排") - self.orchBtn.setFixedHeight(25) - self.orchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置") - toolbarLayout.addWidget(self.orchBtn) - self.debugBtn = QPushButton("▶ 调试运行") - self.debugBtn.setFixedHeight(25) - self.debugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化") - toolbarLayout.addWidget(self.debugBtn) - sep = QFrame() - sep.setFrameShape(QFrame.Shape.VLine) - sep.setFrameShadow(QFrame.Shadow.Sunken) - sep.setFixedWidth(1) - toolbarLayout.addWidget(sep) - toolbarLayout.addWidget(self.zoomInBtn) - toolbarLayout.addWidget(self.zoomOutBtn) - toolbarLayout.addWidget(self.zoomResetBtn) - toolbarLayout.addWidget(self.zoomLabel) - toolbarLayout.addStretch() - self.copyBtn = QPushButton("") - self.copyBtn.setIcon(QIcon(":/res/icons/Copy.svg")) - self.copyBtn.setIconSize(QSize(20, 20)) - self.copyBtn.setFixedSize(25, 25) - self.copyBtn.setToolTip("复制脚本") - toolbarLayout.addWidget(self.copyBtn) - layout.addLayout(toolbarLayout) - self.textEdit = _TabToSpacesEditor(self) - self.textEdit.setTabStopDistance(40) - self.textEdit.setLineWrapMode( + Layout = QVBoxLayout(self) + Layout.setSpacing(3) + Layout.setContentsMargins(3, 3, 3, 3) + ToolbarLayout = QHBoxLayout() + self.ZoomInBtn = QPushButton("+") + self.ZoomInBtn.setFixedSize(25, 25) + self.ZoomOutBtn = QPushButton("-") + self.ZoomOutBtn.setFixedSize(25, 25) + self.ZoomResetBtn = QPushButton("") + self.ZoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg")) + self.ZoomResetBtn.setIconSize(QSize(20, 20)) + self.ZoomResetBtn.setFixedSize(25, 25) + self.ZoomResetBtn.setToolTip("重置缩放") + self.ZoomLabel = QLabel(f"{self._fontSize}px") + self.ZoomLabel.setFixedHeight(25) + self.OrchBtn = QPushButton("编排") + self.OrchBtn.setFixedHeight(25) + self.OrchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置") + ToolbarLayout.addWidget(self.OrchBtn) + self.DebugBtn = QPushButton("▶ 调试运行") + self.DebugBtn.setFixedHeight(25) + self.DebugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化") + ToolbarLayout.addWidget(self.DebugBtn) + Sep = QFrame() + Sep.setFrameShape(QFrame.Shape.VLine) + Sep.setFrameShadow(QFrame.Shadow.Sunken) + Sep.setFixedWidth(1) + ToolbarLayout.addWidget(Sep) + ToolbarLayout.addWidget(self.ZoomInBtn) + ToolbarLayout.addWidget(self.ZoomOutBtn) + ToolbarLayout.addWidget(self.ZoomResetBtn) + ToolbarLayout.addWidget(self.ZoomLabel) + ToolbarLayout.addStretch() + self.CopyBtn = QPushButton("") + self.CopyBtn.setIcon(QIcon(":/res/icons/Copy.svg")) + self.CopyBtn.setIconSize(QSize(20, 20)) + self.CopyBtn.setFixedSize(25, 25) + self.CopyBtn.setToolTip("复制脚本") + ToolbarLayout.addWidget(self.CopyBtn) + Layout.addLayout(ToolbarLayout) + self.TextEdit = _TabToSpacesEditor(self) + self.TextEdit.setTabStopDistance(40) + self.TextEdit.setLineWrapMode( QPlainTextEdit.LineWrapMode.NoWrap ) - self.textEdit.setStyleSheet( + self.TextEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" f" font-size: {self._fontSize}px;" "}" ) - layout.addWidget(self.textEdit) - self.createButtonPanel(layout) - self.btnBox = QDialogButtonBox( + Layout.addWidget(self.TextEdit) + self.createButtonPanel(Layout) + self.BtnBox = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) - self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") - self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") - layout.addWidget(self.btnBox) + self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") + self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") + Layout.addWidget(self.BtnBox) def createButtonPanel( self, - parent_layout + ParentLayout ): - splitter = QSplitter(Qt.Orientation.Horizontal) - tabWidget = QTabWidget() - tabWidget.setMaximumHeight(150) - basicWidget = QWidget() - basicLayout = QGridLayout(basicWidget) - basicLayout.setSpacing(4) - basicLayout.setContentsMargins(4, 4, 4, 4) - basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + Splitter = QSplitter(Qt.Orientation.Horizontal) + TabWidget = QTabWidget() + TabWidget.setMaximumHeight(150) + BasicWidget = QWidget() + BasicLayout = QGridLayout(BasicWidget) + BasicLayout.setSpacing(4) + BasicLayout.setContentsMargins(4, 4, 4, 4) + BasicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) controlButtons = [ ("如果 (if...)", "if then\n \nend"), ("再如果 (elseif...)", "elseif then\n "), @@ -287,22 +287,22 @@ class ALAutoScriptEditDialog(QDialog): ("结束 (end)", "end"), ("跳过 (pass)", "-- pass"), ] - self.addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3) + self.addButtonsToGrid(BasicLayout, controlButtons, 0, 0, 3) assignButtons = [ ("赋值 (=)", " = "), ] - self.addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3) - tabWidget.addTab(basicWidget, "基本语法") - operatorWidget = QWidget() - operatorLayout = QGridLayout(operatorWidget) - operatorLayout.setSpacing(4) - operatorLayout.setContentsMargins(4, 4, 4, 4) - operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.addButtonsToGrid(BasicLayout, assignButtons, 1, 2, 3) + TabWidget.addTab(BasicWidget, "基本语法") + OperatorWidget = QWidget() + OperatorLayout = QGridLayout(OperatorWidget) + OperatorLayout.setSpacing(4) + OperatorLayout.setContentsMargins(4, 4, 4, 4) + OperatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) arithmeticButtons = [ ("加 (+)", " + "), ("减 (-)", " - "), ] - self.addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3) + self.addButtonsToGrid(OperatorLayout, arithmeticButtons, 0, 0, 3) compareButtons = [ ("等于 (==)", " == "), ("不等于 (~=)", " ~= "), @@ -311,50 +311,50 @@ class ALAutoScriptEditDialog(QDialog): ("大于等于 (>=)", " >= "), ("小于等于 (<=)", " <= "), ] - self.addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3) + self.addButtonsToGrid(OperatorLayout, compareButtons, 1, 0, 3) logic_buttons = [ ("且 (and)", " and "), ("或 (or)", " or "), ] - self.addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3) - tabWidget.addTab(operatorWidget, "运算符") - literalWidget = QWidget() - literalLayout = QGridLayout(literalWidget) - literalLayout.setSpacing(4) - literalLayout.setContentsMargins(4, 4, 4, 4) - literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.addButtonsToGrid(OperatorLayout, logic_buttons, 2, 0, 3) + TabWidget.addTab(OperatorWidget, "运算符") + LiteralWidget = QWidget() + LiteralLayout = QGridLayout(LiteralWidget) + LiteralLayout.setSpacing(4) + LiteralLayout.setContentsMargins(4, 4, 4, 4) + LiteralLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) bool_buttons = [ ("真 (true)", "true"), ("假 (false)", "false"), ] - self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3) + self.addButtonsToGrid(LiteralLayout, bool_buttons, 0, 0, 3) dateTimeButtons = [ ("日期", 'date(2026, 1, 1)'), ("时间", 'time(0, 0)'), ] - self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3) + self.addButtonsToGrid(LiteralLayout, dateTimeButtons, 1, 0, 3) hintButtons = [ ("字符串", '"请输入文本"'), ("数字", "123"), ("注释", "-- 请输入注释"), ] - self.addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) - tabWidget.addTab(literalWidget, "字面量") - varWidget = QWidget() - varLayout = QGridLayout(varWidget) - varLayout.setSpacing(4) - varLayout.setContentsMargins(4, 4, 4, 4) - varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.addButtonsToGrid(LiteralLayout, hintButtons, 2, 0, 3) + TabWidget.addTab(LiteralWidget, "字面量") + VarWidget = QWidget() + VarLayout = QGridLayout(VarWidget) + VarLayout.setSpacing(4) + VarLayout.setContentsMargins(4, 4, 4, 4) + VarLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) varButtons = [ (display_name, name) for display_name, (name, _) in createAllVariablesTable().items() ] - self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3) - tabWidget.addTab(varWidget, "变量") - funcWidget = QWidget() - funcLayout = QGridLayout(funcWidget) - funcLayout.setSpacing(4) - funcLayout.setContentsMargins(4, 4, 4, 4) - funcLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.addButtonsToGrid(VarLayout, varButtons, 0, 0, 3) + TabWidget.addTab(VarWidget, "变量") + FuncWidget = QWidget() + FuncLayout = QGridLayout(FuncWidget) + FuncLayout.setSpacing(4) + FuncLayout.setContentsMargins(4, 4, 4, 4) + FuncLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) funcButtons = [ ("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"), ("timenow()", "timenow()", "返回当前时间在一天中的分钟数"), @@ -362,22 +362,22 @@ class ALAutoScriptEditDialog(QDialog): ("timeadd(time, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"), ] for i, (text, template, tooltip) in enumerate(funcButtons): - btn = QPushButton(text) - btn.setProperty("template", template) - btn.clicked.connect(self.insertTemplate) - btn.setFixedWidth(100) - btn.setFixedHeight(25) - btn.setToolTip(tooltip) - funcLayout.addWidget(btn, i // 2, i % 2) - tabWidget.addTab(funcWidget, "工具函数") - mockPanel = self.createMockPanel() - mockPanel.setMinimumWidth(260) - splitter.addWidget(tabWidget) - splitter.addWidget(mockPanel) - splitter.setStretchFactor(0, 1) - splitter.setStretchFactor(1, 1) - splitter.setSizes([530, 530]) - parent_layout.addWidget(splitter) + Btn = QPushButton(text) + Btn.setProperty("template", template) + Btn.clicked.connect(self.insertTemplate) + Btn.setFixedWidth(100) + Btn.setFixedHeight(25) + Btn.setToolTip(tooltip) + FuncLayout.addWidget(Btn, i // 2, i % 2) + TabWidget.addTab(FuncWidget, "工具函数") + MockPanel = self.createMockPanel() + MockPanel.setMinimumWidth(260) + Splitter.addWidget(TabWidget) + Splitter.addWidget(MockPanel) + Splitter.setStretchFactor(0, 1) + Splitter.setStretchFactor(1, 1) + Splitter.setSizes([530, 530]) + ParentLayout.addWidget(Splitter) def addButtonsToGrid( self, @@ -392,13 +392,13 @@ class ALAutoScriptEditDialog(QDialog): row = start_row for btn_text, template in buttons: - btn = QPushButton(btn_text) - btn.setProperty("template", template) - btn.clicked.connect(self.insertTemplate) - btn.setFixedWidth(100) - btn.setFixedHeight(25) - btn.setToolTip(f"插入: {template}") - grid_layout.addWidget(btn, row, col) + Btn = QPushButton(btn_text) + Btn.setProperty("template", template) + Btn.clicked.connect(self.insertTemplate) + Btn.setFixedWidth(100) + Btn.setFixedHeight(25) + Btn.setToolTip(f"插入: {template}") + grid_layout.addWidget(Btn, row, col) col += 1 if col >= start_col + max_columns: col = start_col @@ -408,10 +408,10 @@ class ALAutoScriptEditDialog(QDialog): self ) -> QGroupBox: - group = QGroupBox("模拟目标数据") - form = QFormLayout(group) - form.setSpacing(4) - form.setContentsMargins(5, 10, 5, 5) + Group = QGroupBox("模拟目标数据") + Form = QFormLayout(Group) + Form.setSpacing(4) + Form.setContentsMargins(5, 10, 5, 5) self._mockWidgets = {} mockData = createMockTargetData() for name, var_type, key_path, display_name in createTargetVarDefs(): @@ -419,11 +419,11 @@ class ALAutoScriptEditDialog(QDialog): for key in key_path: d = d[key] default = d - widget = self.makeMockInput(var_type, default) - label = QLabel(f"{display_name}: {name}({var_type})") - form.addRow(label, widget) - self._mockWidgets[name] = (widget, var_type, key_path) - return group + Widget = self.makeMockInput(var_type, default) + Label = QLabel(f"{display_name}: {name}({var_type})") + Form.addRow(Label, Widget) + self._mockWidgets[name] = (Widget, var_type, key_path) + return Group def makeMockInput( self, @@ -432,41 +432,41 @@ class ALAutoScriptEditDialog(QDialog): ) -> QWidget: if var_type == "String": - w = QLineEdit() - w.setText(str(default)) - return w + W = QLineEdit() + W.setText(str(default)) + return W if var_type == "Boolean": - w = QComboBox() - w.addItems(["是", "否"]) - w.setCurrentIndex(0 if default else 1) - return w + W = QComboBox() + W.addItems(["是", "否"]) + W.setCurrentIndex(0 if default else 1) + return W if var_type == "Date": - w = QDateEdit() - w.setCalendarPopup(True) - w.setDisplayFormat("yyyy-MM-dd") - w.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) - return w + W = QDateEdit() + W.setCalendarPopup(True) + W.setDisplayFormat("yyyy-MM-dd") + W.setDate(QDate.fromString(str(default), "yyyy-MM-dd")) + return W if var_type == "Time": - w = QTimeEdit() - w.setDisplayFormat("HH:mm") - w.setTime(QTime.fromString(str(default), "HH:mm")) - return w + W = QTimeEdit() + W.setDisplayFormat("HH:mm") + W.setTime(QTime.fromString(str(default), "HH:mm")) + return W if var_type == "Int": - w = QSpinBox() - w.setMinimum(-999999) - w.setMaximum(999999) - w.setValue(int(default) if default else 0) - return w + W = QSpinBox() + W.setMinimum(-999999) + W.setMaximum(999999) + W.setValue(int(default) if default else 0) + return W if var_type == "Float": - w = QDoubleSpinBox() - w.setMinimum(-999999.0) - w.setMaximum(999999.0) - w.setDecimals(2) - w.setValue(float(default) if default else 0.0) - return w - w = QLineEdit() - w.setText(str(default)) - return w + W = QDoubleSpinBox() + W.setMinimum(-999999.0) + W.setMaximum(999999.0) + W.setDecimals(2) + W.setValue(float(default) if default else 0.0) + return W + W = QLineEdit() + W.setText(str(default)) + return W def getMockData( self @@ -541,45 +541,45 @@ class ALAutoScriptEditDialog(QDialog): self ): - self.btnBox.accepted.connect(self.accept) - self.btnBox.rejected.connect(self.reject) - self.orchBtn.clicked.connect(self.onOpenOrchDialog) - self.debugBtn.clicked.connect(self.onDebugRun) - self.zoomInBtn.clicked.connect(self.onZoomIn) - self.zoomOutBtn.clicked.connect(self.onZoomOut) - self.zoomResetBtn.clicked.connect(self.onZoomReset) - self.copyBtn.clicked.connect(self.onCopy) + self.BtnBox.accepted.connect(self.accept) + self.BtnBox.rejected.connect(self.reject) + self.OrchBtn.clicked.connect(self.onOpenOrchDialog) + self.DebugBtn.clicked.connect(self.onDebugRun) + self.ZoomInBtn.clicked.connect(self.onZoomIn) + self.ZoomOutBtn.clicked.connect(self.onZoomOut) + self.ZoomResetBtn.clicked.connect(self.onZoomReset) + self.CopyBtn.clicked.connect(self.onCopy) def getScript( self ) -> str: - return self.textEdit.toPlainText() + return self.TextEdit.toPlainText() def updateFontSize( self ): - self.textEdit.setStyleSheet( + self.TextEdit.setStyleSheet( "QPlainTextEdit {" " font-family: 'Courier New', 'Consolas', monospace;" f" font-size: {self._fontSize}px;" "}" ) - self.zoomLabel.setText(f"{self._fontSize}px") + self.ZoomLabel.setText(f"{self._fontSize}px") @Slot() def insertTemplate( self ): - btn = self.sender() - if not isinstance(btn, QPushButton): + Btn = self.sender() + if not isinstance(Btn, QPushButton): return - template = btn.property("template") + template = Btn.property("template") if not template: return - cursor = self.textEdit.textCursor() + cursor = self.TextEdit.textCursor() cursor.insertText(template) @Slot() @@ -611,11 +611,11 @@ class ALAutoScriptEditDialog(QDialog): self ): - clipboard = QApplication.clipboard() - clipboard.setText(self.textEdit.toPlainText()) - self.copyBtn.setEnabled(False) + Clipboard = QApplication.clipboard() + Clipboard.setText(self.TextEdit.toPlainText()) + self.CopyBtn.setEnabled(False) QTimer.singleShot(2000, lambda: ( - self.copyBtn.setEnabled(True) + self.CopyBtn.setEnabled(True) )) @Slot() @@ -624,20 +624,20 @@ class ALAutoScriptEditDialog(QDialog): ): from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog - dlg = ALAutoScriptOrchDialog(self) - if dlg.exec() == QDialog.DialogCode.Accepted: - script = dlg.getScript() + Dlg = ALAutoScriptOrchDialog(self) + if Dlg.exec() == QDialog.DialogCode.Accepted: + script = Dlg.getScript() if script: - cursor = self.textEdit.textCursor() + cursor = self.TextEdit.textCursor() cursor.insertText(script) - dlg.deleteLater() + Dlg.deleteLater() @Slot() def onDebugRun( self ): - script = self.textEdit.toPlainText().strip() + script = self.TextEdit.toPlainText().strip() if not script: QMessageBox.warning(self, "提示", "脚本内容为空。") return @@ -664,6 +664,6 @@ class ALAutoScriptEditDialog(QDialog): if not changes: QMessageBox.information(self, "调试运行", "目标变量未发生变化。") return - dlg = _DebugResultDialog(changes, self) - dlg.exec() - dlg.deleteLater() + Dlg = _DebugResultDialog(changes, self) + Dlg.exec() + Dlg.deleteLater() diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py index 60a164f..ac82f26 100644 --- a/src/gui/ALAutoScriptOrchDialog/_blocks.py +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -57,81 +57,81 @@ class ConditionalBlock(QGroupBox): "margin-top: 5px; padding-top: 5px; }" ) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) - mainLayout = QVBoxLayout(self) - mainLayout.setSpacing(6) - mainLayout.setContentsMargins(8, 8, 8, 8) - headerLayout = QHBoxLayout() - headerLayout.setSpacing(8) - self.typeCombo = QComboBox(self) - self.typeCombo.addItem("IF", "IF") - self.typeCombo.addItem("ELSE IF", "ELSE IF") - self.typeCombo.addItem("ELSE", "ELSE") - self.typeCombo.setFixedHeight(25) + MainLayout = QVBoxLayout(self) + MainLayout.setSpacing(6) + MainLayout.setContentsMargins(8, 8, 8, 8) + HeaderLayout = QHBoxLayout() + HeaderLayout.setSpacing(8) + self.TypeCombo = QComboBox(self) + self.TypeCombo.addItem("IF", "IF") + self.TypeCombo.addItem("ELSE IF", "ELSE IF") + self.TypeCombo.addItem("ELSE", "ELSE") + self.TypeCombo.setFixedHeight(25) if self.blockIndex == 0: - self.typeCombo.setEnabled(False) - headerLayout.addWidget(QLabel("类型:", self)) - headerLayout.addWidget(self.typeCombo) - headerLayout.addStretch() - self.deleteBlockBtn = QPushButton("删除此块", self) - self.deleteBlockBtn.setStyleSheet("color: red;") - self.deleteBlockBtn.setFixedHeight(25) - headerLayout.addWidget(self.deleteBlockBtn) - mainLayout.addLayout(headerLayout) - self.conditionWidget = QWidget(self) - self.conditionWidget.setSizePolicy( + self.TypeCombo.setEnabled(False) + HeaderLayout.addWidget(QLabel("类型:", self)) + HeaderLayout.addWidget(self.TypeCombo) + HeaderLayout.addStretch() + self.DeleteBlockBtn = QPushButton("删除此块", self) + self.DeleteBlockBtn.setStyleSheet("color: red;") + self.DeleteBlockBtn.setFixedHeight(25) + HeaderLayout.addWidget(self.DeleteBlockBtn) + MainLayout.addLayout(HeaderLayout) + self.ConditionWidget = QWidget(self) + self.ConditionWidget.setSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) - condLayout = QVBoxLayout(self.conditionWidget) - condLayout.setContentsMargins(4, 4, 4, 4) - condLayout.setSpacing(6) - self.condRowsLayout = QVBoxLayout() - self.condRowsLayout.setSpacing(4) - condLayout.addLayout(self.condRowsLayout) - self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget) - self.addCondBtn.setFixedHeight(25) - condLayout.addWidget(self.addCondBtn) - mainLayout.addWidget(self.conditionWidget) - self.actionLabel = QLabel("执行步骤:", self) - self.actionLabel.setFixedHeight(25) - mainLayout.addWidget(self.actionLabel) - self.actionsLayout = QVBoxLayout() - self.actionsLayout.setSpacing(4) - mainLayout.addLayout(self.actionsLayout) - self.addActionBtn = QPushButton("+ 添加执行步骤", self) - self.addActionBtn.setFixedHeight(25) - mainLayout.addWidget(self.addActionBtn) + CondLayout = QVBoxLayout(self.ConditionWidget) + CondLayout.setContentsMargins(4, 4, 4, 4) + CondLayout.setSpacing(6) + self.CondRowsLayout = QVBoxLayout() + self.CondRowsLayout.setSpacing(4) + CondLayout.addLayout(self.CondRowsLayout) + self.AddCondBtn = QPushButton("+ 添加条件", self.ConditionWidget) + self.AddCondBtn.setFixedHeight(25) + CondLayout.addWidget(self.AddCondBtn) + MainLayout.addWidget(self.ConditionWidget) + self.ActionLabel = QLabel("执行步骤:", self) + self.ActionLabel.setFixedHeight(25) + MainLayout.addWidget(self.ActionLabel) + self.ActionsLayout = QVBoxLayout() + self.ActionsLayout.setSpacing(4) + MainLayout.addLayout(self.ActionsLayout) + self.AddActionBtn = QPushButton("+ 添加执行步骤", self) + self.AddActionBtn.setFixedHeight(25) + MainLayout.addWidget(self.AddActionBtn) self.setUpdatesEnabled(True) def connectSignals( self ): - self.typeCombo.currentIndexChanged.connect(self.onTypeChanged) - self.addCondBtn.clicked.connect(self.addConditionRow) - self.addActionBtn.clicked.connect(self.addActionStep) + self.TypeCombo.currentIndexChanged.connect(self.onTypeChanged) + self.AddCondBtn.clicked.connect(self.addConditionRow) + self.AddActionBtn.clicked.connect(self.addActionStep) def addInitialConditionRow( self ): - row = ConditionRowFrame( + Row = ConditionRowFrame( self._varMgr, self.blockIndex, isFirst=True, parent=self ) - self._conditionRows.append(row) - self.condRowsLayout.addWidget(row) + self._conditionRows.append(Row) + self.CondRowsLayout.addWidget(Row) def addConditionRow( self ): - row = ConditionRowFrame( + Row = ConditionRowFrame( self._varMgr, self.blockIndex, isFirst=False, parent=self ) - row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row)) - self._conditionRows.append(row) - self.condRowsLayout.addWidget(row) + Row.DeleteBtn.clicked.connect(lambda: self.removeConditionRow(Row)) + self._conditionRows.append(Row) + self.CondRowsLayout.addWidget(Row) def removeConditionRow( self, @@ -140,7 +140,7 @@ class ConditionalBlock(QGroupBox): if row in self._conditionRows and len(self._conditionRows) > 1: self._conditionRows.remove(row) - self.condRowsLayout.removeWidget(row) + self.CondRowsLayout.removeWidget(row) row.hide() row.deleteLater() @@ -148,10 +148,10 @@ class ConditionalBlock(QGroupBox): self ): - step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self) - step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step)) - self._actionWidgets.append(step) - self.actionsLayout.addWidget(step) + Step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self) + Step.DeleteBtn.clicked.connect(lambda: self.removeActionStep(Step)) + self._actionWidgets.append(Step) + self.ActionsLayout.addWidget(Step) def removeActionStep( self, @@ -160,7 +160,7 @@ class ConditionalBlock(QGroupBox): if step in self._actionWidgets: self._actionWidgets.remove(step) - self.actionsLayout.removeWidget(step) + self.ActionsLayout.removeWidget(step) step.hide() step.deleteLater() @@ -168,7 +168,7 @@ class ConditionalBlock(QGroupBox): self ) -> str: - return self.typeCombo.currentData() + return self.TypeCombo.currentData() def getConditionRows( self @@ -239,18 +239,18 @@ class ConditionalBlock(QGroupBox): prevType: str | None ): - model = self.typeCombo.model() + model = self.TypeCombo.model() if model is None: return for data in ("ELSE IF", "ELSE"): - idx = self.typeCombo.findData(data) + idx = self.TypeCombo.findData(data) if idx < 0: continue item = model.item(idx) shouldEnable = prevType != "ELSE" item.setEnabled(shouldEnable) - if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"): - self.typeCombo.setCurrentIndex(0) + if prevType == "ELSE" and self.TypeCombo.currentData() in ("ELSE IF", "ELSE"): + self.TypeCombo.setCurrentIndex(0) @Slot(int) def onTypeChanged( @@ -258,8 +258,8 @@ class ConditionalBlock(QGroupBox): _idx ): - isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") - self.conditionWidget.setVisible(isCond) - self.actionLabel.setText( + isCond = self.TypeCombo.currentData() in ("IF", "ELSE IF") + self.ConditionWidget.setVisible(isCond) + self.ActionLabel.setText( "执行步骤:" if isCond else "ELSE 执行步骤:" ) diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 839a14b..55d8213 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -40,7 +40,7 @@ class ALAutoScriptOrchDialog(QDialog): self.setupUi() self.connectSignals() self.addBlock() - self.scrollLayout.addStretch() + self.ScrollLayout.addStretch() def setupUi( self @@ -49,33 +49,33 @@ class ALAutoScriptOrchDialog(QDialog): self.setWindowTitle("AutoScript 指令编排 - AutoLibrary") self.setMinimumSize(640, 600) self.setModal(True) - mainLayout = QVBoxLayout(self) - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.NoFrame) - scrollContent = QWidget() - self.scrollLayout = QVBoxLayout(scrollContent) - self.scrollLayout.setSpacing(5) - scroll.setWidget(scrollContent) - mainLayout.addWidget(scroll) - self.addBlockBtn = QPushButton("+ 添加判断块") - self.addBlockBtn.setFixedHeight(25) - mainLayout.addWidget(self.addBlockBtn) - self.btnBox = QDialogButtonBox( + MainLayout = QVBoxLayout(self) + Scroll = QScrollArea() + Scroll.setWidgetResizable(True) + Scroll.setFrameShape(QFrame.NoFrame) + ScrollContent = QWidget() + self.ScrollLayout = QVBoxLayout(ScrollContent) + self.ScrollLayout.setSpacing(5) + Scroll.setWidget(ScrollContent) + MainLayout.addWidget(Scroll) + self.AddBlockBtn = QPushButton("+ 添加判断块") + self.AddBlockBtn.setFixedHeight(25) + MainLayout.addWidget(self.AddBlockBtn) + self.BtnBox = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) - self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") - self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") - mainLayout.addWidget(self.btnBox) + self.BtnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定") + self.BtnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消") + MainLayout.addWidget(self.BtnBox) def connectSignals( self ): - self.btnBox.accepted.connect(self.onAccept) - self.btnBox.rejected.connect(self.reject) - self.addBlockBtn.clicked.connect(self.addBlock) + self.BtnBox.accepted.connect(self.onAccept) + self.BtnBox.rejected.connect(self.reject) + self.AddBlockBtn.clicked.connect(self.addBlock) def updateBlockTypeRestrictions( self @@ -90,24 +90,24 @@ class ALAutoScriptOrchDialog(QDialog): self ): - block = ConditionalBlock( + Block = ConditionalBlock( len(self._blocks), self._varMgr, parent=self ) - block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block)) - block.typeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions) - block.addActionStep() - self._blocks.append(block) + Block.DeleteBlockBtn.clicked.connect(lambda: self.removeBlock(Block)) + Block.TypeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions) + Block.addActionStep() + self._blocks.append(Block) self.updateBlockTypeRestrictions() - if self.scrollLayout.count() > 0: - lastItem = self.scrollLayout.itemAt( - self.scrollLayout.count() - 1 + if self.ScrollLayout.count() > 0: + lastItem = self.ScrollLayout.itemAt( + self.ScrollLayout.count() - 1 ) if lastItem and lastItem.spacerItem(): - self.scrollLayout.insertWidget( - self.scrollLayout.count() - 1, block + self.ScrollLayout.insertWidget( + self.ScrollLayout.count() - 1, Block ) return - self.scrollLayout.addWidget(block) + self.ScrollLayout.addWidget(Block) def removeBlock( self, @@ -119,16 +119,16 @@ class ALAutoScriptOrchDialog(QDialog): return if block in self._blocks: self._blocks.remove(block) - self.scrollLayout.removeWidget(block) + self.ScrollLayout.removeWidget(block) block.hide() block.deleteLater() for i, blk in enumerate(self._blocks): blk.blockIndex = i if i == 0: - blk.typeCombo.setEnabled(False) - blk.typeCombo.setCurrentIndex(0) + blk.TypeCombo.setEnabled(False) + blk.TypeCombo.setCurrentIndex(0) else: - blk.typeCombo.setEnabled(True) + blk.TypeCombo.setEnabled(True) blk.refreshVarCombos() self.updateBlockTypeRestrictions() diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index 241361a..d019d93 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -110,39 +110,39 @@ class _DateInputContainer(QWidget): self ): - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) - self._modeCombo = QComboBox(self) - self._modeCombo.addItem("相对日期", "relative") - self._modeCombo.addItem("绝对日期", "absolute") - self._modeCombo.setFixedHeight(25) - self._stack = QStackedWidget(self) - self._relCombo = QComboBox(self) + Layout = QHBoxLayout(self) + Layout.setContentsMargins(0, 0, 0, 0) + Layout.setSpacing(4) + self._ModeCombo = QComboBox(self) + self._ModeCombo.addItem("相对日期", "relative") + self._ModeCombo.addItem("绝对日期", "absolute") + self._ModeCombo.setFixedHeight(25) + self._Stack = QStackedWidget(self) + self._RelCombo = QComboBox(self) for display, data in DATE_OPTIONS: - self._relCombo.addItem(display, data) - self._relCombo.setFixedHeight(25) - self._stack.addWidget(self._relCombo) - self._dateEdit = QDateEdit(self) - self._dateEdit.setDisplayFormat("yyyy-MM-dd") - self._dateEdit.setCalendarPopup(True) - self._dateEdit.setFixedHeight(25) - self._stack.addWidget(self._dateEdit) - self._modeCombo.currentIndexChanged.connect( - lambda i: self._stack.setCurrentIndex(i) + self._RelCombo.addItem(display, data) + self._RelCombo.setFixedHeight(25) + self._Stack.addWidget(self._RelCombo) + self._DateEdit = QDateEdit(self) + self._DateEdit.setDisplayFormat("yyyy-MM-dd") + self._DateEdit.setCalendarPopup(True) + self._DateEdit.setFixedHeight(25) + self._Stack.addWidget(self._DateEdit) + self._ModeCombo.currentIndexChanged.connect( + lambda i: self._Stack.setCurrentIndex(i) ) - layout.addWidget(self._modeCombo) - layout.addWidget(self._stack) - layout.addStretch() + Layout.addWidget(self._ModeCombo) + Layout.addWidget(self._Stack) + Layout.addStretch() def getValue( self ) -> str: - mode = self._modeCombo.currentData() + mode = self._ModeCombo.currentData() if mode == "relative": - return self._relCombo.currentText() - return self._dateEdit.date().toString("yyyy-MM-dd") + return self._RelCombo.currentText() + return self._DateEdit.date().toString("yyyy-MM-dd") class _TimeInputContainer(QWidget): @@ -153,19 +153,19 @@ class _TimeInputContainer(QWidget): ): super().__init__(parent) - self._timeEdit = QTimeEdit(self) - self._timeEdit.setDisplayFormat("HH:mm") - self._timeEdit.setFixedHeight(25) + self._TimeEdit = QTimeEdit(self) + self._TimeEdit.setDisplayFormat("HH:mm") + self._TimeEdit.setFixedHeight(25) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._timeEdit) + Layout = QHBoxLayout(self) + Layout.setContentsMargins(0, 0, 0, 0) + Layout.addWidget(self._TimeEdit) def getValue( self ) -> str: - return self._timeEdit.time().toString("HH:mm") + return self._TimeEdit.time().toString("HH:mm") class _DateOffsetContainer(QWidget): @@ -176,20 +176,20 @@ class _DateOffsetContainer(QWidget): ): super().__init__(parent) - self._spinBox = QSpinBox(self) - self._spinBox.setRange(0, 99999) - self._spinBox.setFixedHeight(25) - self._unitCombo = QComboBox(self) + self._SpinBox = QSpinBox(self) + self._SpinBox.setRange(0, 99999) + self._SpinBox.setFixedHeight(25) + self._UnitCombo = QComboBox(self) for display, data in DATE_OFFSET_OPTIONS: - self._unitCombo.addItem(display, data) - self._unitCombo.setFixedHeight(25) + self._UnitCombo.addItem(display, data) + self._UnitCombo.setFixedHeight(25) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) - layout.addWidget(self._spinBox) - layout.addWidget(self._unitCombo) - layout.addStretch() + Layout = QHBoxLayout(self) + Layout.setContentsMargins(0, 0, 0, 0) + Layout.setSpacing(4) + Layout.addWidget(self._SpinBox) + Layout.addWidget(self._UnitCombo) + Layout.addStretch() def getValue( self @@ -201,8 +201,8 @@ class _DateOffsetContainer(QWidget): self ) -> int: - val = self._spinBox.value() - unit = self._unitCombo.currentData() + val = self._SpinBox.value() + unit = self._UnitCombo.currentData() if unit == "weeks": return val*7 if unit == "months": @@ -220,14 +220,14 @@ class _TimeOffsetContainer(QWidget): ): super().__init__(parent) - self._spinBox = QSpinBox(self) - self._spinBox.setRange(0, 99999) - self._spinBox.setSuffix(" 小时") - self._spinBox.setFixedHeight(25) + self._SpinBox = QSpinBox(self) + self._SpinBox.setRange(0, 99999) + self._SpinBox.setSuffix(" 小时") + self._SpinBox.setFixedHeight(25) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._spinBox) + Layout = QHBoxLayout(self) + Layout.setContentsMargins(0, 0, 0, 0) + Layout.addWidget(self._SpinBox) def getValue( self @@ -239,7 +239,7 @@ class _TimeOffsetContainer(QWidget): self ) -> int: - return self._spinBox.value() + return self._SpinBox.value() class VariableManager(QObject): @@ -364,11 +364,11 @@ def makeVarRefCombo( parent: QWidget = None ) -> QComboBox: - cb = QComboBox(parent) - cb.setFixedHeight(25) - cb.setMinimumWidth(120) - cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - return cb + Cb = QComboBox(parent) + Cb.setFixedHeight(25) + Cb.setMinimumWidth(120) + Cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + return Cb def makeComboWidget( items, @@ -376,12 +376,12 @@ def makeComboWidget( parent: QWidget = None ) -> QComboBox: - cb = QComboBox(parent) + Cb = QComboBox(parent) for display, data in items: - cb.addItem(display, data) - cb.setFixedHeight(25) - cb.setMinimumWidth(min_width) - return cb + Cb.addItem(display, data) + Cb.setFixedHeight(25) + Cb.setMinimumWidth(min_width) + return Cb def makeLabel( text: str, @@ -389,11 +389,11 @@ def makeLabel( width: int = None ) -> QLabel: - lbl = QLabel(text, parent) - lbl.setFixedHeight(25) + Lbl = QLabel(text, parent) + Lbl.setFixedHeight(25) if width: - lbl.setFixedWidth(width) - return lbl + Lbl.setFixedWidth(width) + return Lbl def getValueFromWidget( w: QWidget diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py index 949ab6d..5b7b7bd 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -66,42 +66,42 @@ class ConditionRowFrame(QFrame): self.setFrameShape(QFrame.StyledPanel) self.setFrameShadow(QFrame.Raised) self.setFixedHeight(32) - layout = QHBoxLayout(self) - layout.setContentsMargins(2, 2, 2, 2) - layout.setSpacing(4) + Layout = QHBoxLayout(self) + Layout.setContentsMargins(2, 2, 2, 2) + Layout.setSpacing(4) if self._isFirst: - self.logicCombo = None + self.LogicCombo = None else: - self.logicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self) - layout.addWidget(self.logicCombo) - self.leftVarCombo = QComboBox(self) - self.leftVarCombo.setFixedHeight(25) - self.leftVarCombo.setMinimumWidth(120) - self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.LogicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self) + Layout.addWidget(self.LogicCombo) + self.LeftVarCombo = QComboBox(self) + self.LeftVarCombo.setFixedHeight(25) + self.LeftVarCombo.setMinimumWidth(120) + self.LeftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.populateLeftVarCombo() - layout.addWidget(self.leftVarCombo) - self.opCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self) - layout.addWidget(self.opCombo) - self._compTypeCombo = makeComboWidget([ + Layout.addWidget(self.LeftVarCombo) + self.OpCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self) + Layout.addWidget(self.OpCombo) + self._CompTypeCombo = makeComboWidget([ ("特定值", "literal"), ("变量", "variable"), ], min_width=70, parent=self) - layout.addWidget(self._compTypeCombo) - self.rhsStack = QStackedWidget(self) - self.rhsStack.setFixedHeight(25) + Layout.addWidget(self._CompTypeCombo) + self.RhsStack = QStackedWidget(self) + self.RhsStack.setFixedHeight(25) self.initLiteralStack() - self.rhsVarCombo = makeVarRefCombo(self) - self.rhsStack.addWidget(self.rhsVarCombo) - self.rhsStack.setCurrentIndex(0) - layout.addWidget(self.rhsStack) + self.RhsVarCombo = makeVarRefCombo(self) + self.RhsStack.addWidget(self.RhsVarCombo) + self.RhsStack.setCurrentIndex(0) + Layout.addWidget(self.RhsStack) if not self._isFirst: - self.deleteBtn = QPushButton("×", self) - self.deleteBtn.setFixedSize(25, 25) - self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.deleteBtn) + self.DeleteBtn = QPushButton("×", self) + self.DeleteBtn.setFixedSize(25, 25) + self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;") + Layout.addWidget(self.DeleteBtn) else: - self.deleteBtn = None - layout.addStretch() + self.DeleteBtn = None + Layout.addStretch() self.setUpdatesEnabled(True) def populateLeftVarCombo( @@ -111,53 +111,53 @@ class ConditionRowFrame(QFrame): wasBool = self._isBoolMode boolName = None if wasBool: - data = self.leftVarCombo.currentData() + data = self.LeftVarCombo.currentData() if data: boolName = data[0] - self._varMgr.populateCombo(self.leftVarCombo) + self._varMgr.populateCombo(self.LeftVarCombo) # Append boolean literal sentinels at the end - self.leftVarCombo.insertSeparator(self.leftVarCombo.count()) - self.leftVarCombo.addItem("true", ("true", "Boolean")) - self.leftVarCombo.addItem("false", ("false", "Boolean")) + self.LeftVarCombo.insertSeparator(self.LeftVarCombo.count()) + self.LeftVarCombo.addItem("true", ("true", "Boolean")) + self.LeftVarCombo.addItem("false", ("false", "Boolean")) if wasBool and boolName: - for ci in range(self.leftVarCombo.count()): - d = self.leftVarCombo.itemData(ci) + for ci in range(self.LeftVarCombo.count()): + d = self.LeftVarCombo.itemData(ci) if d and d[0] == boolName: - self.leftVarCombo.setCurrentIndex(ci) + self.LeftVarCombo.setCurrentIndex(ci) break def populateRHSVarCombo( self ): - self._varMgr.populateCombo(self.rhsVarCombo) + self._varMgr.populateCombo(self.RhsVarCombo) def initLiteralStack( self ): - self.literalStack = QStackedWidget(self) - self.literalStack.setFixedHeight(25) + self.LiteralStack = QStackedWidget(self) + self.LiteralStack.setFixedHeight(25) self._literalWidgets = {} for vt in getTypeOrder(): - w = makeValueWidget(vt, self.literalStack) - self._literalWidgets[vt] = w - self.literalStack.addWidget(w) - self.literalStack.setCurrentWidget(self._literalWidgets.get("String")) - self.rhsStack.addWidget(self.literalStack) + W = makeValueWidget(vt, self.LiteralStack) + self._literalWidgets[vt] = W + self.LiteralStack.addWidget(W) + self.LiteralStack.setCurrentWidget(self._literalWidgets.get("String")) + self.RhsStack.addWidget(self.LiteralStack) def connectSignals( self ): - self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) - self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) + self.LeftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged) + self._CompTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged) def getLogic( self ) -> str: - return self.logicCombo.currentData() if self.logicCombo else "" + return self.LogicCombo.currentData() if self.LogicCombo else "" def updateRHSLiteralWidget( self, @@ -166,13 +166,13 @@ class ConditionRowFrame(QFrame): if vartype not in self._literalWidgets: vartype = "String" - self.literalStack.setCurrentWidget(self._literalWidgets[vartype]) + self.LiteralStack.setCurrentWidget(self._literalWidgets[vartype]) def toScript( self ) -> str: - data = self.leftVarCombo.currentData() + data = self.LeftVarCombo.currentData() if self._isBoolMode and data: return data[0] if not data: @@ -183,12 +183,12 @@ class ConditionRowFrame(QFrame): name = "datenow()" elif name == "CURRENT_TIME": name = "timenow()" - opSym = self.opCombo.currentData() + opSym = self.OpCombo.currentData() if self._rawRhsExpr: return f"{name} {opSym} {self._rawRhsExpr}" - isVarRef = (self._compTypeCombo.currentData() == "variable") + isVarRef = (self._CompTypeCombo.currentData() == "variable") if isVarRef: - rd = self.rhsVarCombo.currentData() + rd = self.RhsVarCombo.currentData() if rd: rhsName = rd[0] if rhsName == "CURRENT_DATE": @@ -196,7 +196,7 @@ class ConditionRowFrame(QFrame): elif rhsName == "CURRENT_TIME": rhsName = "timenow()" return f"{name} {opSym} {rhsName}" - rhsText = self.rhsVarCombo.currentText().strip() + rhsText = self.RhsVarCombo.currentText().strip() if rhsText: return f"{name} {opSym} {rhsText}" return "" @@ -223,15 +223,15 @@ class ConditionRowFrame(QFrame): self._rawRhsExpr = "" if idx < 0: return - data = self.leftVarCombo.itemData(idx) + data = self.LeftVarCombo.itemData(idx) if not data: return name, vartype = data isBool = name in ("true", "false") self._isBoolMode = isBool - self.opCombo.setVisible(not isBool) - self._compTypeCombo.setVisible(not isBool) - self.rhsStack.setVisible(not isBool) + self.OpCombo.setVisible(not isBool) + self._CompTypeCombo.setVisible(not isBool) + self.RhsStack.setVisible(not isBool) if not isBool: self.updateRHSLiteralWidget(vartype) @@ -242,8 +242,8 @@ class ConditionRowFrame(QFrame): ): self._rawRhsExpr = "" - isVar = (self._compTypeCombo.currentData() == "variable") - self.rhsStack.setCurrentIndex(1 if isVar else 0) + isVar = (self._CompTypeCombo.currentData() == "variable") + self.RhsStack.setCurrentIndex(1 if isVar else 0) if isVar: self.populateRHSVarCombo() @@ -273,52 +273,52 @@ class ActionStepFrame(QFrame): self.setFrameShape(QFrame.StyledPanel) self.setFrameShadow(QFrame.Raised) self.setFixedHeight(35) - layout = QHBoxLayout(self) - layout.setContentsMargins(2, 2, 2, 2) - layout.setSpacing(4) - self.opTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self) - layout.addWidget(self.opTypeCombo) - layout.addWidget(makeLabel("设置", self)) - self.targetCombo = QComboBox(self) - self.targetCombo.setFixedHeight(25) - self.targetCombo.setMinimumWidth(120) + Layout = QHBoxLayout(self) + Layout.setContentsMargins(2, 2, 2, 2) + Layout.setSpacing(4) + self.OpTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self) + Layout.addWidget(self.OpTypeCombo) + Layout.addWidget(makeLabel("设置", self)) + self.TargetCombo = QComboBox(self) + self.TargetCombo.setFixedHeight(25) + self.TargetCombo.setMinimumWidth(120) self.populateTargetCombo() - layout.addWidget(self.targetCombo) - layout.addWidget(makeLabel("为", self)) - self.valueSrcCombo = makeComboWidget([ + Layout.addWidget(self.TargetCombo) + Layout.addWidget(makeLabel("为", self)) + self.ValueSrcCombo = makeComboWidget([ ("特定值", "literal"), ("变量", "variable"), ], min_width=70, parent=self) - layout.addWidget(self.valueSrcCombo) - self.valueStack = QStackedWidget(self) - self.valueStack.setFixedHeight(25) + Layout.addWidget(self.ValueSrcCombo) + self.ValueStack = QStackedWidget(self) + self.ValueStack.setFixedHeight(25) self.initValueStacks() - layout.addWidget(self.valueStack) - self.existingVarCombo = makeVarRefCombo(self) - self.existingVarCombo.setVisible(False) - layout.addWidget(self.existingVarCombo) - self.deleteBtn = QPushButton("×", self) - self.deleteBtn.setFixedSize(25, 25) - self.deleteBtn.setStyleSheet("color: red; font-weight: bold;") - layout.addWidget(self.deleteBtn) + Layout.addWidget(self.ValueStack) + self.ExistingVarCombo = makeVarRefCombo(self) + self.ExistingVarCombo.setVisible(False) + Layout.addWidget(self.ExistingVarCombo) + self.DeleteBtn = QPushButton("×", self) + self.DeleteBtn.setFixedSize(25, 25) + self.DeleteBtn.setStyleSheet("color: red; font-weight: bold;") + Layout.addWidget(self.DeleteBtn) self.setUpdatesEnabled(True) def populateTargetCombo( self ): - self.targetCombo.blockSignals(True) - self.targetCombo.clear() + self.TargetCombo.blockSignals(True) + self.TargetCombo.clear() for p in getPresetVars(): if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): continue info = self._varMgr.getInfoByName(p["name"]) if info: - self.targetCombo.addItem( + self.TargetCombo.addItem( info["display"], (info["name"], info["type"]) ) - self.targetCombo.blockSignals(False) + self.TargetCombo.blockSignals(False) def initValueStacks( self @@ -327,45 +327,45 @@ class ActionStepFrame(QFrame): self._literalWidgets = {} self._offsetWidgets = {} for vt in getTypeOrder(): - self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack) - self.valueStack.addWidget(self._literalWidgets[vt]) + self._literalWidgets[vt] = makeValueWidget(vt, self.ValueStack) + self.ValueStack.addWidget(self._literalWidgets[vt]) if getArithType(vt): - self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack) - self.valueStack.addWidget(self._offsetWidgets[vt]) + self._offsetWidgets[vt] = makeOffsetWidget(vt, self.ValueStack) + self.ValueStack.addWidget(self._offsetWidgets[vt]) else: - lbl = QLabel("(不支持该操作)", self.valueStack) - lbl.setFixedHeight(25) - self._offsetWidgets[vt] = lbl - self.valueStack.addWidget(lbl) + Lbl = QLabel("(不支持该操作)", self.ValueStack) + Lbl.setFixedHeight(25) + self._offsetWidgets[vt] = Lbl + self.ValueStack.addWidget(Lbl) def connectSignals( self ): - self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged) - self.targetCombo.currentIndexChanged.connect(self.onTargetChanged) - self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) + self.OpTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged) + self.TargetCombo.currentIndexChanged.connect(self.onTargetChanged) + self.ValueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged) def getTargetName( self ) -> str: - data = self.targetCombo.currentData() + data = self.TargetCombo.currentData() return data[0] if data else "" def updateValueWidget( self ): - op = self.opTypeCombo.currentData() + op = self.OpTypeCombo.currentData() isArith = (op in ("add", "sub")) actualType = self._currentTargetType if isArith and actualType in self._offsetWidgets: - self.valueStack.setCurrentWidget(self._offsetWidgets[actualType]) + self.ValueStack.setCurrentWidget(self._offsetWidgets[actualType]) elif actualType in self._literalWidgets: - self.valueStack.setCurrentWidget(self._literalWidgets[actualType]) + self.ValueStack.setCurrentWidget(self._literalWidgets[actualType]) else: - self.valueStack.setCurrentWidget(self._literalWidgets.get("String")) + self.ValueStack.setCurrentWidget(self._literalWidgets.get("String")) def toScript( self @@ -375,7 +375,7 @@ class ActionStepFrame(QFrame): """ target = self.getTargetName() - op = self.opTypeCombo.currentData() + op = self.OpTypeCombo.currentData() if op == "pass": return " -- pass" if not target: @@ -386,19 +386,19 @@ class ActionStepFrame(QFrame): encoded = encodeValueStr(rawVal, vartype) return f" {target} = {encoded}" elif op == "add": - if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): - days = self.valueStack.currentWidget().getOffsetDays() + if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"): + days = self.ValueStack.currentWidget().getOffsetDays() return f" {target} = dateadd({target}, {days})" - if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): - hours = self.valueStack.currentWidget().getOffsetHours() + if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"): + hours = self.ValueStack.currentWidget().getOffsetHours() return f" {target} = timeadd({target}, {hours})" return f" {target} = {target} + {rawVal}" elif op == "sub": - if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): - days = self.valueStack.currentWidget().getOffsetDays() + if vartype == "Date" and hasattr(self.ValueStack.currentWidget(), "getOffsetDays"): + days = self.ValueStack.currentWidget().getOffsetDays() return f" {target} = dateadd({target}, -{days})" - if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): - hours = self.valueStack.currentWidget().getOffsetHours() + if vartype == "Time" and hasattr(self.ValueStack.currentWidget(), "getOffsetHours"): + hours = self.ValueStack.currentWidget().getOffsetHours() return f" {target} = timeadd({target}, -{hours})" return f" {target} = {target} - {rawVal}" return "" @@ -407,10 +407,10 @@ class ActionStepFrame(QFrame): self ) -> str: - if self.valueSrcCombo.currentData() == "variable": - data = self.existingVarCombo.currentData() + if self.ValueSrcCombo.currentData() == "variable": + data = self.ExistingVarCombo.currentData() return data[0] if data else "" - w = self.valueStack.currentWidget() + w = self.ValueStack.currentWidget() if w: return getValueFromWidget(w) return "" @@ -419,15 +419,15 @@ class ActionStepFrame(QFrame): self ): - currentData = self.targetCombo.currentData() + currentData = self.TargetCombo.currentData() self.populateTargetCombo() if currentData: - for i in range(self.targetCombo.count()): - d = self.targetCombo.itemData(i) + for i in range(self.TargetCombo.count()): + d = self.TargetCombo.itemData(i) if d and d[0] == currentData[0]: - self.targetCombo.setCurrentIndex(i) + self.TargetCombo.setCurrentIndex(i) break - self._varMgr.populateCombo(self.existingVarCombo) + self._varMgr.populateCombo(self.ExistingVarCombo) @Slot(int) def onTargetChanged( @@ -437,13 +437,13 @@ class ActionStepFrame(QFrame): if idx < 0: return - data = self.targetCombo.itemData(idx) + data = self.TargetCombo.itemData(idx) if not data: return _, vartype = data self._currentTargetType = vartype self.updateValueWidget() - self.onValueSrcChanged(self.valueSrcCombo.currentIndex()) + self.onValueSrcChanged(self.ValueSrcCombo.currentIndex()) @Slot(int) def onOpTypeChanged( @@ -459,10 +459,10 @@ class ActionStepFrame(QFrame): idx ): - isVar = (self.valueSrcCombo.currentData() == "variable") - self.valueStack.setVisible(not isVar) - self.existingVarCombo.setVisible(isVar) + isVar = (self.ValueSrcCombo.currentData() == "variable") + self.ValueStack.setVisible(not isVar) + self.ExistingVarCombo.setVisible(isVar) if isVar: - self._varMgr.populateCombo(self.existingVarCombo) + self._varMgr.populateCombo(self.ExistingVarCombo) else: self.updateValueWidget() diff --git a/src/gui/ALConfigWidget.py b/src/gui/ALConfigWidget.py index 9c2ddfb..eea0bb3 100644 --- a/src/gui/ALConfigWidget.py +++ b/src/gui/ALConfigWidget.py @@ -386,18 +386,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): user_config = self.defaultUserConfig() for i in range(self.UserTreeWidget.topLevelItemCount()): - group_item = self.UserTreeWidget.topLevelItem(i) + GroupItem = self.UserTreeWidget.topLevelItem(i) group_config = { - "name": group_item.text(0), - "enabled": group_item.checkState(1) == Qt.CheckState.Checked, + "name": GroupItem.text(0), + "enabled": GroupItem.checkState(1) == Qt.CheckState.Checked, "users": [] } - for j in range(group_item.childCount()): - user_item = group_item.child(j) - user = user_item.data(0, Qt.UserRole) + for j in range(GroupItem.childCount()): + UserItem = GroupItem.child(j) + user = UserItem.data(0, Qt.UserRole) if not user: continue - user["enabled"] = user_item.checkState(1) == Qt.CheckState.Checked + user["enabled"] = UserItem.checkState(1) == Qt.CheckState.Checked group_config["users"].append(user) user_config["groups"].append(group_config) return user_config @@ -453,18 +453,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): try: if "groups" in users: for group_config in users["groups"]: - group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) - group_item.setText(0, group_config["name"]) - group_item.setFlags(group_item.flags() | Qt.ItemIsEditable) - group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked) + GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) + GroupItem.setText(0, group_config["name"]) + GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable) + GroupItem.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked) for user_config in group_config["users"]: - user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value) - user_item.setText(0, user_config["username"]) - user_item.setText(1, "" if user_config.get("enabled", True) else "跳过") - user_item.setData(0, Qt.UserRole, user_config) - user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked) - user_item.setDisabled(not group_config.get("enabled", True)) - group_item.setExpanded(True) + UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value) + UserItem.setText(0, user_config["username"]) + UserItem.setText(1, "" if user_config.get("enabled", True) else "跳过") + UserItem.setData(0, Qt.UserRole, user_config) + UserItem.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked) + UserItem.setDisabled(not group_config.get("enabled", True)) + GroupItem.setExpanded(True) except KeyError as e: QMessageBox.warning( self, @@ -638,43 +638,43 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): ) -> QTreeWidgetItem: self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) - group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) + GroupItem = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value) if not group_name: group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}" - group_item.setText(0, group_name) - group_item.setFlags(group_item.flags() | Qt.ItemIsEditable) - group_item.setCheckState(1, Qt.Checked) - self.UserTreeWidget.setCurrentItem(group_item) + GroupItem.setText(0, group_name) + GroupItem.setFlags(GroupItem.flags() | Qt.ItemIsEditable) + GroupItem.setCheckState(1, Qt.Checked) + self.UserTreeWidget.setCurrentItem(GroupItem) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) - return group_item + return GroupItem def delGroup( self, - group_item: QTreeWidgetItem = None + GroupItem: QTreeWidgetItem = None ): - if group_item is None: + if GroupItem is None: return - if group_item.type() != ALUserTreeItemType.GROUP.value: + if GroupItem.type() != ALUserTreeItemType.GROUP.value: return - index = self.UserTreeWidget.indexOfTopLevelItem(group_item) + index = self.UserTreeWidget.indexOfTopLevelItem(GroupItem) self.UserTreeWidget.takeTopLevelItem(index) def addUser( self, - group_item: QTreeWidgetItem = None + GroupItem: QTreeWidgetItem = None ) -> QTreeWidgetItem: - if group_item is None: - current_item = self.UserTreeWidget.currentItem() - if current_item is None: - group_item = self.addGroup() - if group_item.type() == ALUserTreeItemType.USER.value: - group_item = group_item.parent() - if group_item.checkState(1) == Qt.CheckState.Unchecked: + if GroupItem is None: + CurrentItem = self.UserTreeWidget.currentItem() + if CurrentItem is None: + GroupItem = self.addGroup() + if GroupItem.type() == ALUserTreeItemType.USER.value: + GroupItem = GroupItem.parent() + if GroupItem.checkState(1) == Qt.CheckState.Unchecked: return None new_user = { - "username": f"新用户-{group_item.childCount()}", + "username": f"新用户-{GroupItem.childCount()}", "password": "000000", "enabled": True, "reserve_info": { @@ -703,30 +703,30 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): } } self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) - user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value) - user_item.setText(0, new_user["username"]) - user_item.setText(1, "") - user_item.setData(0, Qt.UserRole, new_user) - user_item.setCheckState(1, Qt.CheckState.Checked) - group_item.setExpanded(True) - self.UserTreeWidget.setCurrentItem(user_item) + UserItem = QTreeWidgetItem(GroupItem, ALUserTreeItemType.USER.value) + UserItem.setText(0, new_user["username"]) + UserItem.setText(1, "") + UserItem.setData(0, Qt.UserRole, new_user) + UserItem.setCheckState(1, Qt.CheckState.Checked) + GroupItem.setExpanded(True) + self.UserTreeWidget.setCurrentItem(UserItem) self.setUserToWidget(new_user) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) - return user_item + return UserItem def delUser( self, - user_item: QTreeWidgetItem = None + UserItem: QTreeWidgetItem = None ): - if user_item is None: + if UserItem is None: return - if user_item.type() != ALUserTreeItemType.USER.value: + if UserItem.type() != ALUserTreeItemType.USER.value: return - parent_item = user_item.parent() - index = parent_item.indexOfChild(user_item) - parent_item.takeChild(index) - if parent_item.childCount() == 0: + ParentItem = UserItem.parent() + index = ParentItem.indexOfChild(UserItem) + ParentItem.takeChild(index) + if ParentItem.childCount() == 0: self.UserTreeWidget.setCurrentItem(None) def renameItem( @@ -787,19 +787,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): room = self.RoomComboBox.currentText() floor_idx = self.__floor_rmap[floor] room_idx = self.__room_rmap[room] - dialog = ALSeatMapSelectDialog( + Dialog = ALSeatMapSelectDialog( self, floor, room, ALSeatMapTable[floor_idx][room_idx] ) - dialog.selectSeats(self.SeatIDEdit.text().split(",")) - if dialog.exec() == QDialog.DialogCode.Accepted: - selected_seats = dialog.getSelectedSeats() + Dialog.selectSeats(self.SeatIDEdit.text().split(",")) + if Dialog.exec() == QDialog.DialogCode.Accepted: + selected_seats = Dialog.getSelectedSeats() if len(selected_seats) == 0: self.SeatIDEdit.clear() return - self.SeatIDEdit.setText(",".join(dialog.getSelectedSeats())) + self.SeatIDEdit.setText(",".join(Dialog.getSelectedSeats())) @Slot() def onUserTreeWidgetCurrentItemChanged( @@ -844,10 +844,10 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): if item.type() == ALUserTreeItemType.GROUP.value: is_checked = item.checkState(1) == Qt.CheckState.Checked for i in range(item.childCount()): - child = item.child(i) - if self.UserTreeWidget.currentItem() == child: + Child = item.child(i) + if self.UserTreeWidget.currentItem() == Child: self.UserTreeWidget.setCurrentItem(item) - child.setDisabled(not is_checked) + Child.setDisabled(not is_checked) else: is_checked = item.checkState(1) == Qt.CheckState.Checked item.setText(1, "" if is_checked else "跳过") @@ -857,41 +857,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): menu: QMenu ): - add_group_action = QAction("添加分组", menu) - add_group_action.triggered.connect(self.addGroup) - menu.addAction(add_group_action) + AddGroupAction = QAction("添加分组", menu) + AddGroupAction.triggered.connect(self.addGroup) + menu.addAction(AddGroupAction) def showGroupMenu( self, menu: QMenu, - group_item: QTreeWidgetItem = None + GroupItem: QTreeWidgetItem = None ): - add_user_action = QAction("添加用户", menu) - rename_group_action = QAction("重命名分组", menu) - del_group_action = QAction("删除分组", menu) - add_user_action.triggered.connect(lambda: self.addUser(group_item)) - rename_group_action.triggered.connect(lambda: self.renameItem(group_item)) - del_group_action.triggered.connect(lambda: self.delGroup(group_item)) - menu.addAction(add_user_action) + AddUserAction = QAction("添加用户", menu) + RenameGroupAction = QAction("重命名分组", menu) + DelGroupAction = QAction("删除分组", menu) + AddUserAction.triggered.connect(lambda: self.addUser(GroupItem)) + RenameGroupAction.triggered.connect(lambda: self.renameItem(GroupItem)) + DelGroupAction.triggered.connect(lambda: self.delGroup(GroupItem)) + menu.addAction(AddUserAction) menu.addSeparator() - menu.addAction(rename_group_action) - menu.addAction(del_group_action) - if group_item.checkState(1) == Qt.CheckState.Unchecked: - add_user_action.setEnabled(False) + menu.addAction(RenameGroupAction) + menu.addAction(DelGroupAction) + if GroupItem.checkState(1) == Qt.CheckState.Unchecked: + AddUserAction.setEnabled(False) def showUserMenu( self, menu: QMenu, - user_item: QTreeWidgetItem = None + UserItem: QTreeWidgetItem = None ): - rename_user_action = QAction("重命名用户", menu) - del_user_action = QAction("删除用户", menu) - rename_user_action.triggered.connect(lambda: self.renameItem(user_item)) - del_user_action.triggered.connect(lambda: self.delUser(user_item)) - menu.addAction(rename_user_action) - menu.addAction(del_user_action) + RenameUserAction = QAction("重命名用户", menu) + DelUserAction = QAction("删除用户", menu) + RenameUserAction.triggered.connect(lambda: self.renameItem(UserItem)) + DelUserAction.triggered.connect(lambda: self.delUser(UserItem)) + menu.addAction(RenameUserAction) + menu.addAction(DelUserAction) @Slot() def onUserTreeWidgetContextMenu( @@ -899,31 +899,31 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): pos ): - current_item = self.UserTreeWidget.itemAt(pos) - menu = QMenu(self.UserTreeWidget) - if current_item is None: - self.showTreeMenu(menu) - elif current_item.type() == ALUserTreeItemType.GROUP.value: - self.showGroupMenu(menu, current_item) + CurrentItem = self.UserTreeWidget.itemAt(pos) + Menu = QMenu(self.UserTreeWidget) + if CurrentItem is None: + self.showTreeMenu(Menu) + elif CurrentItem.type() == ALUserTreeItemType.GROUP.value: + self.showGroupMenu(Menu, CurrentItem) else: - self.showUserMenu(menu, current_item) - menu.exec_(self.UserTreeWidget.mapToGlobal(pos)) + self.showUserMenu(Menu, CurrentItem) + Menu.exec_(self.UserTreeWidget.mapToGlobal(pos)) @Slot() def onAddUserButtonClicked( self ): - current_item = self.UserTreeWidget.currentItem() - self.addUser(current_item) + CurrentItem = self.UserTreeWidget.currentItem() + self.addUser(CurrentItem) @Slot() def onDelUserButtonClicked( self ): - current_item = self.UserTreeWidget.currentItem() - self.delUser(current_item) + CurrentItem = self.UserTreeWidget.currentItem() + self.delUser(CurrentItem) @Slot() def onBrowseBrowserDriverButtonClicked( @@ -944,10 +944,10 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self ): - dialog = ALWebDriverDownloadDialog(self) - dialog.show() - dialog.exec_() - selected_driver_info = dialog.getSelectedDriverInfo() + 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))) @@ -1133,8 +1133,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget): self ): - current_item = self.UserTreeWidget.currentItem() - if current_item and current_item.type() == ALUserTreeItemType.USER.value: + CurrentItem = self.UserTreeWidget.currentItem() + if CurrentItem and CurrentItem.type() == ALUserTreeItemType.USER.value: self.UserTreeWidget.setCurrentItem(None) if self.saveConfigs( self.__config_paths["run"], diff --git a/src/gui/ALMainWindow.py b/src/gui/ALMainWindow.py index 20dd3f8..82a944f 100644 --- a/src/gui/ALMainWindow.py +++ b/src/gui/ALMainWindow.py @@ -76,8 +76,8 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - self.icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg") - self.setWindowIcon(self.icon) + self.Icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg") + self.setWindowIcon(self.Icon) self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) self.ManualAction.triggered.connect(self.onManualActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered) @@ -106,15 +106,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): self ): - about_dialog = ALAboutDialog(self) - about_dialog.exec() + AboutDialog = ALAboutDialog(self) + AboutDialog.exec() def onManualActionTriggered( self ): - url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") - QDesktopServices.openUrl(url) + Url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") + QDesktopServices.openUrl(Url) def setupTray( self @@ -123,7 +123,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): if not QSystemTrayIcon.isSystemTrayAvailable(): self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING) return - self.TrayIcon = QSystemTrayIcon(self.icon, self) + self.TrayIcon = QSystemTrayIcon(self.Icon, self) self.TrayIcon.setToolTip("AutoLibrary") self.TrayMenu = QMenu() diff --git a/src/gui/ALSeatMapView.py b/src/gui/ALSeatMapView.py index 2fd1e2c..0379bb0 100644 --- a/src/gui/ALSeatMapView.py +++ b/src/gui/ALSeatMapView.py @@ -103,15 +103,15 @@ class ALSeatMapView(QGraphicsView): seats_number = [seat.strip() for seat in row.split(",")] for seat_number in seats_number: if seat_number: - seat_widget = ALSeatFrame(seat_number) - seat_widget.clicked.connect(self.onSeatClicked) - self.SeatsContainerLayout.addWidget(seat_widget, row_idx, col_idx) - self.__seat_frames[seat_number] = seat_widget + SeatWidget = ALSeatFrame(seat_number) + SeatWidget.clicked.connect(self.onSeatClicked) + self.SeatsContainerLayout.addWidget(SeatWidget, row_idx, col_idx) + self.__seat_frames[seat_number] = SeatWidget else: - spacer = QFrame() - spacer.setFixedSize(20, 30) - spacer.setStyleSheet("background-color: transparent; border: none;") - self.SeatsContainerLayout.addWidget(spacer, row_idx, col_idx) + Spacer = QFrame() + Spacer.setFixedSize(20, 30) + Spacer.setStyleSheet("background-color: transparent; border: none;") + self.SeatsContainerLayout.addWidget(Spacer, row_idx, col_idx) col_idx += 1 self.SeatsContainerLayout.setSpacing(20) self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20) diff --git a/src/gui/ALStatusLabel.py b/src/gui/ALStatusLabel.py index aae5809..5a08e59 100644 --- a/src/gui/ALStatusLabel.py +++ b/src/gui/ALStatusLabel.py @@ -56,7 +56,6 @@ class ALStatusLabel(QLabel): self.setFixedSize(36, 36) self.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.RunningAnimation = QPropertyAnimation(self, b"iconAngle") self.RunningAnimation.setDuration(1000) self.RunningAnimation.setStartValue(0) @@ -119,35 +118,35 @@ class ALStatusLabel(QLabel): event ): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) + 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( + 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( + 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), @@ -155,102 +154,102 @@ class ALStatusLabel(QLabel): ) 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( + 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) + 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) + 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( + Painter.drawLine( int(mark_path[0][0]),int(mark_path[0][1]), int(mark_path[1][0]),int(mark_path[1][1]) ) - painter.drawLine( + 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( + 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) + 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( + 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( + 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( + 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) + 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) + Pen.setColor(self.getMarkColor()) + Painter.setPen(Pen) mark_size = radius/3 - painter.drawLine( + Painter.drawLine( int(center_x - mark_size), int(center_y - mark_size), int(center_x + mark_size), int(center_y + mark_size) ) - painter.drawLine( + Painter.drawLine( int(center_x + mark_size), int(center_y - mark_size), int(center_x - mark_size), int(center_y + mark_size) ) - painter.end() + Painter.end() super().paintEvent(event) diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 10b4d65..1807898 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -80,7 +80,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.SpecificDateTimeEdit.setDateTime(QDateTime.currentDateTime().addSecs(60)) self.SpecificTimerLayout.addWidget(self.SpecificDateTimeEdit) self.TimerConfigLayout.addWidget(self.SpecificTimerWidget) - self.RelativeTimerWidget = QWidget() self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget) self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0) @@ -108,17 +107,16 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox) self.TimerConfigLayout.addWidget(self.RelativeTimerWidget) self.RelativeTimerWidget.setVisible(False) - self.AutoScriptGroupBox = QGroupBox("AutoScript 指令") self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox) self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3) self.AutoScriptLayout.setSpacing(3) - autoScriptBtnLayout = QHBoxLayout() + AutoScriptBtnLayout = QHBoxLayout() self.AutoScriptEditButton = QPushButton("编辑") self.AutoScriptEditButton.setMinimumHeight(25) self.AutoScriptEditButton.setFixedWidth(80) - autoScriptBtnLayout.addWidget(self.AutoScriptEditButton) - autoScriptBtnLayout.addStretch() + AutoScriptBtnLayout.addWidget(self.AutoScriptEditButton) + AutoScriptBtnLayout.addStretch() self.AutoScriptHelpButton = QPushButton("?") self.AutoScriptHelpButton.setFixedSize(20, 20) self.AutoScriptHelpButton.setToolTip( @@ -132,12 +130,12 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): "font-weight: bold; color: #555; }" "QPushButton:hover { background-color: #E0E0E0; }" ) - autoScriptBtnLayout.addWidget(self.AutoScriptHelpButton) + AutoScriptBtnLayout.addWidget(self.AutoScriptHelpButton) self.AutoScriptStatusLabel = QLabel("未设置") self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") self.AutoScriptStatusLabel.setFixedHeight(25) - autoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel) - self.AutoScriptLayout.addLayout(autoScriptBtnLayout) + AutoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel) + self.AutoScriptLayout.addLayout(AutoScriptBtnLayout) self.ALAddTimerTaskLayout.insertWidget( self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1, self.AutoScriptGroupBox @@ -305,18 +303,18 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): @Slot() def onPreviewAutoScript(self): from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog - dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data) - if dlg.exec() == QDialog.DialogCode.Accepted: - script = dlg.getScript() + Dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data) + if Dlg.exec() == QDialog.DialogCode.Accepted: + script = Dlg.getScript() self.__auto_script = script - self.__mock_target_data = dlg.getMockData() + self.__mock_target_data = Dlg.getMockData() if script: self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") else: self.AutoScriptStatusLabel.setText("未设置") self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") - dlg.deleteLater() + Dlg.deleteLater() @Slot() def onAutoScriptHelp( diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py index 03bba0d..40eec21 100644 --- a/src/gui/ALTimerTaskHistoryDialog.py +++ b/src/gui/ALTimerTaskHistoryDialog.py @@ -41,7 +41,6 @@ class ALTimerTaskHistoryDialog(QDialog): self.setWindowTitle("定时任务执行历史 - AutoLibrary") self.setMinimumSize(300, 300) self.setMaximumSize(500, 400) - MainLayout = QVBoxLayout(self) InfoLayout = QGridLayout() TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}") @@ -51,7 +50,6 @@ class ALTimerTaskHistoryDialog(QDialog): 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-size: 12px;") @@ -68,7 +66,6 @@ class ALTimerTaskHistoryDialog(QDialog): self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.loadHistory() MainLayout.addWidget(self.HistoryTableWidget) - ButtonLayout = QHBoxLayout() ButtonLayout.addStretch() self.CloseButton = QPushButton("关闭") diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py index f2df449..c6e191f 100644 --- a/src/gui/ALTimerTaskManageWidget.py +++ b/src/gui/ALTimerTaskManageWidget.py @@ -173,20 +173,20 @@ class ALTimerTaskItemWidget(QWidget): pos ): - menu = QMenu(self) - edit_action = QAction("编辑", self) - edit_action.triggered.connect( + Menu = QMenu(self) + EditAction = QAction("编辑", self) + EditAction.triggered.connect( lambda: self.editRequested.emit(self.__timer_task) ) - menu.addAction(edit_action) + Menu.addAction(EditAction) if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\ and self.__timer_task["status"] != ALTimerTaskStatus.READY: - delete_action = QAction("删除", self) - delete_action.triggered.connect( + DeleteAction = QAction("删除", self) + DeleteAction.triggered.connect( lambda: self.__manage_widget.deleteTask(self.__timer_task) ) - menu.addAction(delete_action) - menu.exec(self.mapToGlobal(pos)) + Menu.addAction(DeleteAction) + Menu.exec(self.mapToGlobal(pos)) class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): @@ -209,7 +209,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): super().__init__(parent) self.__cfg_mgr: ConfigProvider = ConfigManager.instance() self.__timer_tasks = [] - self.__check_timer = None + self.__CheckTimer = None self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME self.__sort_order = Qt.SortOrder.AscendingOrder @@ -233,9 +233,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self ): - self.__check_timer = QTimer(self) - self.__check_timer.timeout.connect(self.checkTasks) - self.__check_timer.start(500) + self.__CheckTimer = QTimer(self) + self.__CheckTimer.timeout.connect(self.checkTasks) + self.__CheckTimer.start(500) def initializeTimerTasks( self @@ -386,28 +386,28 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): self.TimerTasksListWidget.clear() self.sortTimerTasks(self.__sort_policy, self.__sort_order) for timer_task in self.__timer_tasks: - item = QListWidgetItem() - item.setData(Qt.UserRole, timer_task) - widget = ALTimerTaskItemWidget(self, timer_task) - widget.DeleteButton.clicked.connect( + Item = QListWidgetItem() + Item.setData(Qt.UserRole, timer_task) + Widget = ALTimerTaskItemWidget(self, timer_task) + Widget.DeleteButton.clicked.connect( lambda _, task = timer_task: self.deleteTask(task) ) - if timer_task.get("repeat", False) and hasattr(widget, "HistoryButton"): - widget.HistoryButton.clicked.connect( + if timer_task.get("repeat", False) and hasattr(Widget, "HistoryButton"): + Widget.HistoryButton.clicked.connect( lambda _, task = timer_task: self.showTaskHistory(task) ) - widget.editRequested.connect(self.editTask) - item.setSizeHint(widget.size()) - self.TimerTasksListWidget.addItem(item) - self.TimerTasksListWidget.setItemWidget(item, widget) + Widget.editRequested.connect(self.editTask) + Item.setSizeHint(Widget.size()) + self.TimerTasksListWidget.addItem(Item) + self.TimerTasksListWidget.setItemWidget(Item, Widget) def addTask( self ): - dialog = ALTimerTaskAddDialog(self) - if dialog.exec() == QDialog.DialogCode.Accepted: - timer_task = dialog.getTimerTask() + Dialog = ALTimerTaskAddDialog(self) + if Dialog.exec() == QDialog.DialogCode.Accepted: + timer_task = Dialog.getTimerTask() self.__timer_tasks.append(timer_task) self.timerTasksChanged.emit() @@ -416,9 +416,9 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): timer_task: dict ): - dialog = ALTimerTaskAddDialog(self, timer_task) - if dialog.exec() == QDialog.DialogCode.Accepted: - updated = dialog.getTimerTask() + Dialog = ALTimerTaskAddDialog(self, timer_task) + if Dialog.exec() == QDialog.DialogCode.Accepted: + updated = Dialog.getTimerTask() for i, task in enumerate(self.__timer_tasks): if task["uuid"] == updated["uuid"]: self.__timer_tasks[i] = updated @@ -449,19 +449,19 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ): if timer_task["repeat"]: # when delete a repeat task - msgbox = QMessageBox(self) - msgbox.setIcon(QMessageBox.Icon.Question) - msgbox.setWindowTitle("警告 - AutoLibrary") - msgbox.setStandardButtons( + MsgBox = QMessageBox(self) + MsgBox.setIcon(QMessageBox.Icon.Question) + MsgBox.setWindowTitle("警告 - AutoLibrary") + MsgBox.setStandardButtons( QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - msgbox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?") - msgbox.setDetailedText( + MsgBox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?") + MsgBox.setDetailedText( "以下可重复性任务将被删除:\n"\ "\n" f"{self.getTimerTaskDetailMessage(timer_task)}" ) - result = msgbox.exec() + result = MsgBox.exec() if result != QMessageBox.StandardButton.Yes: return task_uuid = timer_task["uuid"] @@ -506,13 +506,13 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): ] repeat_tasks_count = len(repeat_tasks) if repeat_tasks_count > 0: - msgbox = QMessageBox(self) - msgbox.setIcon(QMessageBox.Icon.Question) - msgbox.setWindowTitle("警告 - AutoLibrary") - msgbox.setStandardButtons( + MsgBox = QMessageBox(self) + MsgBox.setIcon(QMessageBox.Icon.Question) + MsgBox.setWindowTitle("警告 - AutoLibrary") + MsgBox.setStandardButtons( QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) - msgbox.setText( + MsgBox.setText( f"存在 {repeat_tasks_count} 个可重复性任务,\n" "删除可重复性任务将同时删除所有已执行的记录 !\n" "是否继续 ?" @@ -520,12 +520,12 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): delete_msgs = [ self.getTimerTaskDetailMessage(x) for x in repeat_tasks ] - msgbox.setDetailedText( + MsgBox.setDetailedText( "以下可重复性任务将被删除:\n"\ "\n" f"{"\n\n".join(delete_msgs)}" ) - result = msgbox.exec() + result = MsgBox.exec() if result != QMessageBox.StandardButton.Yes: return # clear all tasks @@ -537,8 +537,8 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget): task: dict ): - dialog = ALTimerTaskHistoryDialog(self, task) - if dialog.exec() == QDialog.DialogCode.Accepted: + Dialog = ALTimerTaskHistoryDialog(self, task) + if Dialog.exec() == QDialog.DialogCode.Accepted: self.timerTasksChanged.emit() def checkTasks( diff --git a/src/gui/ALUserTreeWidget.py b/src/gui/ALUserTreeWidget.py index fdb39e6..17c9ae4 100644 --- a/src/gui/ALUserTreeWidget.py +++ b/src/gui/ALUserTreeWidget.py @@ -51,9 +51,9 @@ class ALUserTreeWidget(QTreeWidget): self ): - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(0, u"\u5206\u7ec4/\u7528\u6237"); - self.setHeaderItem(__qtreewidgetitem) + __QTreeWidgetItem = QTreeWidgetItem() + __QTreeWidgetItem.setText(0, u"\u5206\u7ec4/\u7528\u6237"); + self.setHeaderItem(__QTreeWidgetItem) self.setObjectName(u"UserTreeWidget") self.setMinimumSize(QSize(230, 0)) self.setMaximumSize(QSize(250, 16777215)) @@ -81,8 +81,8 @@ class ALUserTreeWidget(QTreeWidget): self ): - ___qtreewidgetitem = self.headerItem() - ___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None)); + ___QTreeWidgetItem = self.headerItem() + ___QTreeWidgetItem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None)); @staticmethod def isDragPositionValid( @@ -109,27 +109,27 @@ class ALUserTreeWidget(QTreeWidget): super().dragMoveEvent(event) - source_item = self.currentItem() - target_item = self.itemAt(event.position().toPoint()) - if source_item is None: + SourceItem = self.currentItem() + TargetItem = self.itemAt(event.position().toPoint()) + if SourceItem is None: event.ignore() return - if source_item.type() == ALUserTreeItemType.GROUP.value: - if target_item is not None: + if SourceItem.type() == ALUserTreeItemType.GROUP.value: + if TargetItem is not None: event.ignore() return - elif source_item.type() == ALUserTreeItemType.USER.value: - if target_item is None: + elif SourceItem.type() == ALUserTreeItemType.USER.value: + if TargetItem is None: event.ignore() return - if target_item.type() != ALUserTreeItemType.GROUP.value: + if TargetItem.type() != ALUserTreeItemType.GROUP.value: event.ignore() return - if target_item.checkState(1) == Qt.CheckState.Unchecked: + if TargetItem.checkState(1) == Qt.CheckState.Unchecked: event.ignore() return if not self.isDragPositionValid( - self.visualItemRect(target_item), + self.visualItemRect(TargetItem), event.position().toPoint() ): event.ignore() diff --git a/src/gui/ALWebDriverDownloadDialog.py b/src/gui/ALWebDriverDownloadDialog.py index 43a40d8..42ac710 100644 --- a/src/gui/ALWebDriverDownloadDialog.py +++ b/src/gui/ALWebDriverDownloadDialog.py @@ -182,14 +182,11 @@ class ALWebDriverDownloadDialog(QDialog): 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() @@ -198,7 +195,6 @@ class ALWebDriverDownloadDialog(QDialog): 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) @@ -211,7 +207,6 @@ class ALWebDriverDownloadDialog(QDialog): 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) @@ -237,7 +232,6 @@ class ALWebDriverDownloadDialog(QDialog): 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) From b24f39456e07720271c5a5300dbe703b81d5ae0a Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 29 May 2026 14:05:20 +0800 Subject: [PATCH 46/49] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Git=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E5=A4=A7=E5=B0=8F=E5=86=99=E4=B8=8E?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F=E4=B8=8D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows 下 git core.ignorecase=true 导致文件重命名时 Git 无法 检测到大小写变化,推送后服务器上仍为旧命名。通过两步 git mv 强制更新索引,统一所有文件名为规范大小写。 Co-Authored-By: Claude Opus 4.7 --- license => LICENSE | 0 readme.md => README.md | 0 batchs/{readme.md => README.md} | 0 drivers/{readme.md => README.md} | 0 manuals/{readme.md => README.md} | 0 models/{readme.md => README.md} | 0 src/pages/strategies/{timeSelectMaker.py => TimeSelectMaker.py} | 0 templates/{readme.md => README.md} | 0 templates/configs/{readme.md => README.md} | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename license => LICENSE (100%) rename readme.md => README.md (100%) rename batchs/{readme.md => README.md} (100%) rename drivers/{readme.md => README.md} (100%) rename manuals/{readme.md => README.md} (100%) rename models/{readme.md => README.md} (100%) rename src/pages/strategies/{timeSelectMaker.py => TimeSelectMaker.py} (100%) rename templates/{readme.md => README.md} (100%) rename templates/configs/{readme.md => README.md} (100%) diff --git a/license b/LICENSE similarity index 100% rename from license rename to LICENSE diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md diff --git a/batchs/readme.md b/batchs/README.md similarity index 100% rename from batchs/readme.md rename to batchs/README.md diff --git a/drivers/readme.md b/drivers/README.md similarity index 100% rename from drivers/readme.md rename to drivers/README.md diff --git a/manuals/readme.md b/manuals/README.md similarity index 100% rename from manuals/readme.md rename to manuals/README.md diff --git a/models/readme.md b/models/README.md similarity index 100% rename from models/readme.md rename to models/README.md diff --git a/src/pages/strategies/timeSelectMaker.py b/src/pages/strategies/TimeSelectMaker.py similarity index 100% rename from src/pages/strategies/timeSelectMaker.py rename to src/pages/strategies/TimeSelectMaker.py diff --git a/templates/readme.md b/templates/README.md similarity index 100% rename from templates/readme.md rename to templates/README.md diff --git a/templates/configs/readme.md b/templates/configs/README.md similarity index 100% rename from templates/configs/readme.md rename to templates/configs/README.md From bea12d5f0c4d9bcc992ef593b0f6ae3d354c2ac4 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 29 May 2026 14:12:06 +0800 Subject: [PATCH 47/49] =?UTF-8?q?fix(docs):=20=E6=9B=B4=E6=96=B0=E6=89=8B?= =?UTF-8?q?=E5=86=8C=E5=9F=9F=E5=90=8D=E5=B9=B6=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9A=84=20requirements.txt=20=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 手册 URL 已从 www.autolibrary.kenanzhu.com/manuals 迁移至 manuals.autolibrary.kenanzhu.com - 构建步骤中不再引用已不存在的 requirements.txt Co-Authored-By: Claude Opus 4.7 --- README.md | 4 ++-- manuals/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6c7145..16c852b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ 6. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行,支持设置重复任务 7. 驱动管理 - 内置浏览器驱动自动管理,支持自动检测浏览器版本并下载对应驱动,无需手动下载 -*具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)* +*具体操作方法和注意事项请访问我们的 [帮助手册](https://manuals.autolibrary.kenanzhu.com)* ### 如何使用 @@ -42,7 +42,7 @@ 本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤: 1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。 -2. 安装所有依赖库,命令为 `pip install -r requirements.txt` (建议在虚拟环境下操作)。 +2. 安装所需依赖库,包括 PySide6、ddddocr、selenium、pillow 等(建议在虚拟环境下操作)。 3. 在 `batchs` 目录下运行 `compile_ui.bat` (linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。 4. 在上一步相同目录内运行 `compile_rc.bat` (linux 和 macOS 系统使用 `compile_rc.sh`) 文件来编译 Qt 的资源文件。 5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。 diff --git a/manuals/README.md b/manuals/README.md index ffcb6ba..712e7f9 100644 --- a/manuals/README.md +++ b/manuals/README.md @@ -1,3 +1,3 @@ This folder is used to store the manuals. -Our manuals are available at https://www.autolibrary.kenanzhu.com/manuals +Our manuals are available at https://manuals.autolibrary.kenanzhu.com From f3360423e58e390c9c65781d02f9c5577f536942 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 29 May 2026 14:16:35 +0800 Subject: [PATCH 48/49] =?UTF-8?q?fix(build):=20=E9=87=8D=E5=91=BD=E5=90=8D?= =?UTF-8?q?=20requirement.txt=20=E5=B9=B6=E7=BB=9F=E4=B8=80=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重命名 requirement.txt → requirements.txt - 更新 build.yml 和 build-test.yml 中的 pip cache 和 install 路径引用 - README.md 恢复 pip install -r requirements.txt 构建步骤 Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/build.yml | 4 ++-- README.md | 2 +- requirement.txt => requirements.txt | Bin 4 files changed, 5 insertions(+), 5 deletions(-) rename requirement.txt => requirements.txt (100%) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index dc6c2de..6157eda 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -45,12 +45,12 @@ jobs: with: python-version: '3.13' cache: 'pip' - cache-dependency-path: requirement.txt + cache-dependency-path: requirements.txt - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirement.txt + pip install -r requirements.txt - name: Solve ddddocr compatibility and copy model files run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71e572d..ce2ad8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,12 +86,12 @@ jobs: with: python-version: '3.13' cache: 'pip' - cache-dependency-path: requirement.txt + cache-dependency-path: requirements.txt - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirement.txt + pip install -r requirements.txt - name: Solve ddddocr compatibility and copy model files run: | diff --git a/README.md b/README.md index 16c852b..9f9525c 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ 本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤: 1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。 -2. 安装所需依赖库,包括 PySide6、ddddocr、selenium、pillow 等(建议在虚拟环境下操作)。 +2. 安装所有依赖库,命令为 `pip install -r requirements.txt` (建议在虚拟环境下操作)。 3. 在 `batchs` 目录下运行 `compile_ui.bat` (linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。 4. 在上一步相同目录内运行 `compile_rc.bat` (linux 和 macOS 系统使用 `compile_rc.sh`) 文件来编译 Qt 的资源文件。 5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。 diff --git a/requirement.txt b/requirements.txt similarity index 100% rename from requirement.txt rename to requirements.txt From 779aad13b803891bd2de07a9a619ba3ed25c4010 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Fri, 29 May 2026 14:17:53 +0800 Subject: [PATCH 49/49] =?UTF-8?q?refactor(gui):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=E5=AF=B9=E8=AF=9D=E6=A1=86=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SYSTEM INFORMATION → SYSTEM, PROJECT INFORMATION → PROJECT Co-Authored-By: Claude Opus 4.7 --- src/gui/ALAboutDialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/ALAboutDialog.py b/src/gui/ALAboutDialog.py index 7b0524d..34a5490 100644 --- a/src/gui/ALAboutDialog.py +++ b/src/gui/ALAboutDialog.py @@ -85,7 +85,7 @@ Commit SHA: {AL_COMMIT_SHA}
Commit date: {AL_COMMIT_DATE}
Build date: {AL_BUILD_DATE}

-SYSTEM INFORMATION
+SYSTEM
Running on: {run_on}
Processor: {platform.processor()}

@@ -94,7 +94,7 @@ Python: {platform.python_version()}
Qt(PySide6): {self.getQtVersion()}
Selenium: {selenium_ver}

-PROJECT INFORMATION
+PROJECT
Website:
https://www.autolibrary.kenanzhu.com
Repository: https://www.github.com/KenanZhu/AutoLibrary