diff --git a/src/gui/ALPreprocOrchDialog.py b/src/gui/ALAutoScriptOrchDialog.py similarity index 99% rename from src/gui/ALPreprocOrchDialog.py rename to src/gui/ALAutoScriptOrchDialog.py index 20509c4..3ffa63d 100644 --- a/src/gui/ALPreprocOrchDialog.py +++ b/src/gui/ALAutoScriptOrchDialog.py @@ -17,10 +17,10 @@ from PySide6.QtWidgets import ( QGroupBox, QSizePolicy ) -from utils.PreprocEngine import PreprocEngine +from utils.AutoScriptEngine import AutoScriptEngine -VARIABLE_META = PreprocEngine.VARIABLE_META +VARIABLE_META = AutoScriptEngine.VARIABLE_META _VAR_COMBO_ITEMS = [ (display, varname, vartype) @@ -637,7 +637,7 @@ class ConditionalBlock(QGroupBox): return len(self._actionWidgets) -class ALPreprocOrchDialog(QDialog): +class ALAutoScriptOrchDialog(QDialog): def __init__( self, @@ -661,7 +661,7 @@ class ALPreprocOrchDialog(QDialog): self ): - self.setWindowTitle("预处理指令编排 - AutoLibrary") + self.setWindowTitle("AutoScript 指令编排 - AutoLibrary") self.setMinimumSize(420, 400) self.setModal(True) mainLayout = QVBoxLayout(self) diff --git a/src/gui/ALPreProcPrevDialog.py b/src/gui/ALAutoScriptPrevDialog.py similarity index 93% rename from src/gui/ALPreProcPrevDialog.py rename to src/gui/ALAutoScriptPrevDialog.py index 8ed6f63..78bd7ec 100644 --- a/src/gui/ALPreProcPrevDialog.py +++ b/src/gui/ALAutoScriptPrevDialog.py @@ -31,41 +31,41 @@ class ALScriptHighlighter(QSyntaxHighlighter): keywordFmt.setForeground(QColor("#316BFF")) keywordFmt.setFontWeight(QFont.Weight.Bold) for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF", - "SET", "PASS", "THEN", ".TRUE.", ".FALSE."]: + "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 @@ -79,7 +79,7 @@ class ALScriptHighlighter(QSyntaxHighlighter): self.setFormat(start, length, fmt) -class ALScriptPreviewDialog(QDialog): +class ALAutoScriptPreviewDialog(QDialog): def __init__( self, @@ -88,7 +88,6 @@ class ALScriptPreviewDialog(QDialog): ): super().__init__(parent) - self.__fontSize = 13 self.modifyUi() @@ -104,7 +103,7 @@ class ALScriptPreviewDialog(QDialog): self ): - self.setWindowTitle("预处理脚本预览 - AutoLibrary") + self.setWindowTitle("AutoScript 预览 - AutoLibrary") self.setMinimumSize(520, 360) layout = QVBoxLayout(self) @@ -136,7 +135,6 @@ class ALScriptPreviewDialog(QDialog): self._copyBtn.setToolTip("复制脚本") toolbarLayout.addWidget(self._copyBtn) layout.addLayout(toolbarLayout) - self._textEdit = QPlainTextEdit(self) self._textEdit.setReadOnly(True) self._textEdit.setLineWrapMode( diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index 3c9033b..4bb736f 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.PreprocEngine import PreprocEngine +from utils.AutoScriptEngine import AutoScriptEngine class AutoLibWorker(MsgBase, QThread): @@ -161,6 +161,7 @@ class TimerTaskWorker(AutoLibWorker): self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished) self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError) + def run( self ): @@ -173,7 +174,7 @@ class TimerTaskWorker(AutoLibWorker): try: if not self.loadConfigs(): raise Exception("配置文件加载失败") - self._applyRepeatPreproc() + self._applyRepeatAutoScript() auto_lib = AutoLib( self._input_queue, self._output_queue, @@ -206,15 +207,15 @@ class TimerTaskWorker(AutoLibWorker): self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) - def _applyRepeatPreproc( + def _applyRepeatAutoScript( self ): - preproc_script = self.__timer_task.get("repeat_preproc", "") - if not preproc_script or not preproc_script.strip(): + auto_script = self.__timer_task.get("repeat_auto_script", "") + if not auto_script or not auto_script.strip(): return self._showTrace( - f"检测到重复定时任务预处理脚本, 开始执行...", + f"检测到重复定时任务 AutoScript, 开始执行...", no_log=True ) groups = self._user_config.get("groups", []) @@ -224,15 +225,15 @@ class TimerTaskWorker(AutoLibWorker): continue for user in group.get("users", []): try: - PreprocEngine.execute(preproc_script, user) + AutoScriptEngine.execute(auto_script, user) affected_count += 1 except ValueError as e: self._showTrace( - f"预处理脚本执行错误 (用户 {user['username']}): {e}", + f"AutoScript 执行错误 (用户 {user['username']}): {e}", self.TraceLevel.ERROR ) self._showLog( - f"预处理脚本执行完毕, " + f"AutoScript 执行完毕, " f"影响 {affected_count} 个用户", self.TraceLevel.INFO ) diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 5f0b281..cd4e8ac 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -12,11 +12,12 @@ import uuid from enum import Enum from datetime import datetime, timedelta -from PySide6.QtCore import Slot, QDateTime +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 gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog -from gui.ALPreprocOrchDialog import ALPreprocOrchDialog +from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog from utils.TimerUtils import TimerUtils @@ -87,32 +88,46 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.TimerConfigLayout.addWidget(self.RelativeTimerWidget) self.RelativeTimerWidget.setVisible(False) - self.PreprocGroupBox = QGroupBox("预处理脚本") - self.PreprocLayout = QVBoxLayout(self.PreprocGroupBox) - self.PreprocLayout.setContentsMargins(3, 3, 3, 3) - self.PreprocLayout.setSpacing(3) - - preproc_btn_layout = QHBoxLayout() - self.PreprocSetButton = QPushButton("设置预处理指令") - self.PreprocSetButton.setMinimumHeight(25) - self.PreprocSetButton.setFixedWidth(130) - preproc_btn_layout.addWidget(self.PreprocSetButton) - self.PreprocPreviewButton = QPushButton("预览") - self.PreprocPreviewButton.setMinimumHeight(25) - self.PreprocPreviewButton.setFixedWidth(60) - self.PreprocPreviewButton.setEnabled(False) - preproc_btn_layout.addWidget(self.PreprocPreviewButton) - preproc_btn_layout.addStretch() - self.PreprocStatusLabel = QLabel("未设置") - self.PreprocStatusLabel.setStyleSheet("color: #969696;") - self.PreprocStatusLabel.setFixedHeight(25) - preproc_btn_layout.addWidget(self.PreprocStatusLabel) - self.PreprocLayout.addLayout(preproc_btn_layout) + self.AutoScriptGroupBox = QGroupBox("AutoScript 指令") + self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox) + 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) + self.AutoScriptPreviewButton.setEnabled(False) + autoScriptBtnLayout.addWidget(self.AutoScriptPreviewButton) + autoScriptBtnLayout.addStretch() + self.AutoScriptHelpButton = QPushButton("?") + self.AutoScriptHelpButton.setFixedSize(20, 20) + self.AutoScriptHelpButton.setToolTip( + "AutoScript 是一种轻量级 DSL\n" + "用于在重复定时任务执行前,对用户的预约数据进行预处理\n" + "\n" + "点击查看完整在线文档" + ) + self.AutoScriptHelpButton.setStyleSheet( + "QPushButton { border-radius: 10px; border: 1px solid #999; " + "font-weight: bold; color: #555; }" + "QPushButton:hover { background-color: #E0E0E0; }" + ) + autoScriptBtnLayout.addWidget(self.AutoScriptHelpButton) + self.AutoScriptStatusLabel = QLabel("未设置") + self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") + self.AutoScriptStatusLabel.setFixedHeight(25) + autoScriptBtnLayout.addWidget(self.AutoScriptStatusLabel) + self.AutoScriptLayout.addLayout(autoScriptBtnLayout) self.ALAddTimerTaskLayout.insertWidget( self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1, - self.PreprocGroupBox + self.AutoScriptGroupBox ) - self.__repeat_preproc_script = "" + self.AutoScriptGroupBox.setVisible(False) + self.__auto_script = "" def connectSignals( @@ -123,35 +138,44 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.ConfirmButton.clicked.connect(self.accept) self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged) self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled) - self.PreprocSetButton.clicked.connect(self._onSetPreproc) - self.PreprocPreviewButton.clicked.connect(self._onPreviewPreproc) - + self.AutoScriptSetButton.clicked.connect(self._onSetAutoScript) + self.AutoScriptPreviewButton.clicked.connect(self._onPreviewAutoScript) + self.AutoScriptHelpButton.clicked.connect(self._onAutoScriptHelp) @Slot() - def _onSetPreproc(self): - dlg = ALPreprocOrchDialog(self, existingScript=self.__repeat_preproc_script) + def _onSetAutoScript(self): + dlg = ALAutoScriptOrchDialog(self, existingScript=self.__auto_script) if dlg.exec() == QDialog.DialogCode.Accepted: script = dlg.getScript() - self.__repeat_preproc_script = script + self.__auto_script = script if script: - self.PreprocStatusLabel.setText("已设置") - self.PreprocStatusLabel.setStyleSheet("color: #4CAF50;") - self.PreprocPreviewButton.setEnabled(True) + self.AutoScriptStatusLabel.setText("已设置") + self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") + self.AutoScriptPreviewButton.setEnabled(True) else: - self.PreprocStatusLabel.setText("未设置") - self.PreprocStatusLabel.setStyleSheet("color: #969696;") - self.PreprocPreviewButton.setEnabled(False) + 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() dlg.deleteLater() @Slot() - def _onPreviewPreproc(self): - if not self.__repeat_preproc_script: - return - from gui.ALPreProcPrevDialog import ALScriptPreviewDialog - dlg = ALScriptPreviewDialog(self, self.__repeat_preproc_script) - dlg.exec() - dlg.deleteLater() + def _onAutoScriptHelp( + self + ): + + QDesktopServices.openUrl( + QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript") + ) def getTimerTask( @@ -186,7 +210,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): "status": ALTimerTaskStatus.PENDING, "executed": False, "repeat": self.RepeatCheckBox.isChecked(), - "repeat_preproc": self.__repeat_preproc_script, + "repeat_auto_script": self.__auto_script, } if task_data["repeat"]: task_data["history"] = [] # repeat history @@ -240,4 +264,5 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): self.ThuCheckBox.setEnabled(checked) self.FriCheckBox.setEnabled(checked) self.SatCheckBox.setEnabled(checked) - self.SunCheckBox.setEnabled(checked) \ No newline at end of file + self.SunCheckBox.setEnabled(checked) + self.AutoScriptGroupBox.setVisible(checked) \ No newline at end of file diff --git a/src/gui/__init__.py b/src/gui/__init__.py index d5ddc5f..6e687ba 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -10,7 +10,7 @@ - ALSeatMapTable: Seat map table class. - ALSeatMapSelectDialog: Seat map select dialog class. - ALTimerTaskAddDialog: Timer task add dialog class. - - ALPreprocOrchDialog: Preprocessing script orchestration dialog class. + - ALAutoScriptOrchDialog: AutoScript orchestration dialog class. - ALTimerTaskHistoryDialog: Timer task history dialog class. - ALTimerTaskManageWidget: Timer task manage widget class. - ALUserTreeWidget: User tree widget class. diff --git a/src/utils/PreprocEngine.py b/src/utils/AutoScriptEngine.py similarity index 60% rename from src/utils/PreprocEngine.py rename to src/utils/AutoScriptEngine.py index 4475105..7504ea6 100644 --- a/src/utils/PreprocEngine.py +++ b/src/utils/AutoScriptEngine.py @@ -11,39 +11,78 @@ import re from datetime import datetime, timedelta -class PreprocEngine: +class AutoScriptEngine: + """ + AutoScript script engine. - COMPARE_OPS = { - ".EQ.": lambda a, b: a == b, + 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_META = { # variable metadata "预约开始时间": ("RESERVE_BEGIN_TIME", "Time"), "预约结束时间": ("RESERVE_END_TIME", "Time"), "预约日期": ("RESERVE_DATE", "Date"), - "用户名": ("USERNAME", "String"), + "用户名": ("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: @@ -51,22 +90,22 @@ class PreprocEngine: if upper_line.startswith("IF("): cond_end = _findConditionEnd(upper_line) if cond_end < 0: - raise ValueError("语法错误: IF 缺少右括号") + raise ValueError("AutoScript 语法错误: IF 缺少右括号") condition_str = line[3:cond_end].strip() - matched = PreprocEngine._evaluateCondition( + matched = AutoScriptEngine._evaluateCondition( condition_str, user_data ) if_stack.append([matched, matched]) elif upper_line.startswith("ELSE IF("): if not if_stack: - raise ValueError("语法错误: ELSE IF 前缺少 IF") + raise ValueError("AutoScript 语法错误: ELSE IF 前缺少 IF") cond_end = _findConditionEnd(upper_line) if cond_end < 0: - raise ValueError("语法错误: ELSE IF 缺少右括号") + raise ValueError("AutoScript 语法错误: ELSE IF 缺少右括号") condition_str = line[8:cond_end].strip() _, has_matched = if_stack[-1] if not has_matched: - matched = PreprocEngine._evaluateCondition( + matched = AutoScriptEngine._evaluateCondition( condition_str, user_data ) if_stack[-1] = [matched, matched] @@ -74,7 +113,7 @@ class PreprocEngine: if_stack[-1][0] = False elif upper_line == "ELSE": if not if_stack: - raise ValueError("语法错误: ELSE 前缺少 IF") + raise ValueError("AutoScript 语法错误: ELSE 前缺少 IF") _, has_matched = if_stack[-1] if not has_matched: if_stack[-1] = [True, True] @@ -82,14 +121,14 @@ class PreprocEngine: if_stack[-1][0] = False elif upper_line in ("ENDIF", "END IF"): if not if_stack: - raise ValueError("语法错误: ENDIF/END IF 前缺少 IF") + 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: - PreprocEngine._executeSet(line, user_data) + AutoScriptEngine._executeSet(line, user_data) elif upper_line == "PASS": continue else: @@ -97,55 +136,15 @@ class PreprocEngine: all(ctx[0] for ctx in if_stack) if if_stack else True ) if should_execute: - PreprocEngine._executeOperation(line, user_data) + AutoScriptEngine._executeOperation(line, user_data) if if_stack: - raise ValueError("语法错误: IF 与 ENDIF/END IF 不匹配") - - @staticmethod - def _resolveValue( - value_str: str, - user_data: dict - ) -> str: - - 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: - float(s) - return s - except ValueError: - pass - resolved = PreprocEngine._resolveField(s, user_data) - return resolved + raise ValueError("AutoScript 语法错误: IF 与 ENDIF/END IF 不匹配") @staticmethod def _resolveField( field_name: str, user_data: dict - ) -> str: + ): upper_name = field_name.upper().strip() if upper_name == "CURRENT_DATE": @@ -155,7 +154,7 @@ class PreprocEngine: elif upper_name == "USERNAME": return user_data.get("username", "") elif upper_name == "USER_ENABLE": - return str(user_data.get("enabled", "False")) + 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": @@ -174,6 +173,49 @@ class PreprocEngine: ) 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, @@ -192,7 +234,10 @@ class PreprocEngine: elif upper_name == "USERNAME": user_data["username"] = value elif upper_name == "USER_ENABLE": - user_data["enabled"] = value.upper() == "TRUE" + if isinstance(value, bool): + user_data["enabled"] = value + else: + user_data["enabled"] = (str(value).upper() == "TRUE") @staticmethod def _evaluateCondition( @@ -200,7 +245,7 @@ class PreprocEngine: user_data: dict ) -> bool: - for op, cmp_func in PreprocEngine.COMPARE_OPS.items(): + for op, cmp_func in AutoScriptEngine.COMPARE_OPS.items(): if op not in condition_str.upper(): continue idx = condition_str.upper().find(op) @@ -209,9 +254,16 @@ class PreprocEngine: continue field_name = parts[0].strip() value_str = parts[1].strip() - left_val = PreprocEngine._resolveField(field_name, user_data) - right_val = PreprocEngine._resolveValue(value_str, user_data) - return cmp_func(left_val, right_val) + 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 @@ -227,8 +279,8 @@ class PreprocEngine: value_str = rest[eq_idx + 1:].strip() if not field_name: return - resolved = PreprocEngine._resolveValue(value_str, user_data) - PreprocEngine._setField(field_name, resolved, user_data) + resolved = AutoScriptEngine._resolveValue(value_str, user_data) + AutoScriptEngine._setField(field_name, resolved, user_data) @staticmethod def _executeOperation( @@ -242,12 +294,19 @@ class PreprocEngine: 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): - return - if field_name == "RESERVE_DATE": - date_str = user_data.get("reserve_info", {}).get("date", "") + raise ValueError( + f"AutoScript 语法错误: 无效操作数 '{raw_value}'" + ) + if field_type == "Date": + date_str = AutoScriptEngine._resolveField(field_name, user_data) if not date_str: return try: @@ -259,16 +318,14 @@ class PreprocEngine: elif op == ".SUB.": date_obj -= timedelta(days=num_value) else: - return - user_data.setdefault("reserve_info", {})["date"] = \ - date_obj.strftime("%Y-%m-%d") - elif field_name == "RESERVE_BEGIN_TIME": - time_str = ( - user_data - .get("reserve_info", {}) - .get("begin_time", {}) - .get("time", "") + 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: @@ -280,37 +337,38 @@ class PreprocEngine: elif op == ".SUB.": time_obj -= timedelta(hours=num_value) else: - return - ri = user_data.setdefault("reserve_info", {}) - ri.setdefault("begin_time", {})["time"] = \ - time_obj.strftime("%H:%M") - elif field_name == "RESERVE_END_TIME": - time_str = ( - user_data - .get("reserve_info", {}) - .get("end_time", {}) - .get("time", "") + 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}'" ) - 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: - return - ri = user_data.setdefault("reserve_info", {}) - ri.setdefault("end_time", {})["time"] = \ - time_obj.strftime("%H:%M") 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"): diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 4d0a056..8c4cdf0 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -5,4 +5,6 @@ - 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