From 4d0d7a952cc417978e847eec81f83dd69880d494 Mon Sep 17 00:00:00 2001 From: Gogs Date: Fri, 8 May 2026 15:23:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(preproc):=20=E6=96=B0=E5=A2=9E=E9=80=82?= =?UTF-8?q?=E7=94=A8=E4=BA=8E=E9=87=8D=E5=A4=8D=E6=80=A7=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=9A=84=E9=A2=84=E5=A4=84=E7=90=86=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E4=BB=A5=E5=8F=8A=E5=8F=AF=E8=A7=86=E5=8C=96=E7=BC=96?= =?UTF-8?q?=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/ALMainWorkers.py | 89 +- src/gui/ALPreProcPrevDialog.py | 226 +++++ src/gui/ALPreprocOrchDialog.py | 864 +++++++++++++++++++ src/gui/ALTimerTaskAddDialog.py | 60 +- src/gui/__init__.py | 1 + src/gui/resources/ui/ALTimerTaskAddDialog.ui | 2 +- src/utils/PreprocEngine.py | 328 +++++++ 7 files changed, 1560 insertions(+), 10 deletions(-) create mode 100644 src/gui/ALPreProcPrevDialog.py create mode 100644 src/gui/ALPreprocOrchDialog.py create mode 100644 src/utils/PreprocEngine.py diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index c1503d1..3c9033b 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -18,6 +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 class AutoLibWorker(MsgBase, QThread): @@ -76,25 +77,28 @@ class AutoLibWorker(MsgBase, QThread): f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}", no_log=True ) - self.__run_config = JSONReader(self.__config_paths["run"]).data() + self._run_config = JSONReader(self.__config_paths["run"]).data() self._showTrace( f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}", no_log=True ) - self.__user_config = JSONReader(self.__config_paths["user"]).data() - if self.__run_config is None or self.__user_config is None: + self._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 ) return False - if not self.__user_config.get("groups"): + if not self._user_config.get("groups"): self._showTrace( "用户配置文件中无有效任务组, 请检查用户配置文件是否正确", self.TraceLevel.WARNING ) return False - self._showLog(f"配置文件加载成功, 任务组数量: {len(self.__user_config.get('groups', []))}", self.TraceLevel.INFO) + self._showLog( + f"配置文件加载成功, 任务组数量: {len(self._user_config.get('groups', []))}", + self.TraceLevel.INFO + ) return True @@ -115,9 +119,9 @@ class AutoLibWorker(MsgBase, QThread): auto_lib = AutoLib( self._input_queue, self._output_queue, - self.__run_config + self._run_config ) - groups = self.__user_config.get("groups") + groups = self._user_config.get("groups") for group in groups: if not group["enabled"]: self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True) @@ -162,7 +166,76 @@ class TimerTaskWorker(AutoLibWorker): ): self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行") - super().run() + 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._applyRepeatPreproc() + 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 _applyRepeatPreproc( + self + ): + + preproc_script = self.__timer_task.get("repeat_preproc", "") + if not preproc_script or not preproc_script.strip(): + return + self._showTrace( + f"检测到重复定时任务预处理脚本, 开始执行...", + no_log=True + ) + groups = self._user_config.get("groups", []) + affected_count = 0 + for group in groups: + if not group.get("enabled", False): + continue + for user in group.get("users", []): + try: + PreprocEngine.execute(preproc_script, user) + affected_count += 1 + except ValueError as e: + self._showTrace( + f"预处理脚本执行错误 (用户 {user['username']}): {e}", + self.TraceLevel.ERROR + ) + self._showLog( + f"预处理脚本执行完毕, " + f"影响 {affected_count} 个用户", + self.TraceLevel.INFO + ) @Slot() def onTimerTaskFinishedWithError( diff --git a/src/gui/ALPreProcPrevDialog.py b/src/gui/ALPreProcPrevDialog.py new file mode 100644 index 0000000..8ed6f63 --- /dev/null +++ b/src/gui/ALPreProcPrevDialog.py @@ -0,0 +1,226 @@ +# -*- 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.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", ".TRUE.", ".FALSE."]: + pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b" + self._rules.append((pattern, keywordFmt)) + + 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 ALScriptPreviewDialog(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("预处理脚本预览 - 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 _onZoomIn( + self + ): + + self.__fontSize = min(self.__fontSize + 2, 40) + self._updateFontSize() + + + def _onZoomOut( + self + ): + + self.__fontSize = max(self.__fontSize - 2, 8) + self._updateFontSize() + + + def _onZoomReset( + self + ): + + self.__fontSize = 13 + self._updateFontSize() + + + 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) + )) + + + 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") diff --git a/src/gui/ALPreprocOrchDialog.py b/src/gui/ALPreprocOrchDialog.py new file mode 100644 index 0000000..20509c4 --- /dev/null +++ b/src/gui/ALPreprocOrchDialog.py @@ -0,0 +1,864 @@ +# -*- 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 +from PySide6.QtWidgets import ( + QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QComboBox, QPushButton, QScrollArea, QTimeEdit, + QDateEdit, QLineEdit, QSpinBox, QDoubleSpinBox, + QStackedWidget, QFrame, QDialogButtonBox, + QGroupBox, QSizePolicy +) + +from utils.PreprocEngine import PreprocEngine + + +VARIABLE_META = PreprocEngine.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 _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) + + 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) + + +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: 8px; padding-top: 8px; }" + ) + 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 _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) + + def _onTypeChanged( + self, + idx + ): + isCond = self.typeCombo.currentData() in ("IF", "ELSE IF") + self.conditionWidget.setVisible(isCond) + self.actionLabel.setText("执行步骤:" if isCond else "ELSE 执行步骤:") + + def _addActionStep( + self + ): + + step = ActionStepFrame(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() + + 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) + + +class ALPreprocOrchDialog(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("预处理指令编排 - 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 _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) + + 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 diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index 9eb2f25..5f0b281 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -13,9 +13,10 @@ from enum import Enum from datetime import datetime, timedelta from PySide6.QtCore import Slot, QDateTime -from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QGridLayout, QDateTimeEdit +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 utils.TimerUtils import TimerUtils @@ -86,6 +87,33 @@ 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.ALAddTimerTaskLayout.insertWidget( + self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1, + self.PreprocGroupBox + ) + self.__repeat_preproc_script = "" + def connectSignals( self @@ -95,6 +123,35 @@ 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) + + + @Slot() + def _onSetPreproc(self): + dlg = ALPreprocOrchDialog(self, existingScript=self.__repeat_preproc_script) + if dlg.exec() == QDialog.DialogCode.Accepted: + script = dlg.getScript() + self.__repeat_preproc_script = script + if script: + self.PreprocStatusLabel.setText("已设置") + self.PreprocStatusLabel.setStyleSheet("color: #4CAF50;") + self.PreprocPreviewButton.setEnabled(True) + else: + self.PreprocStatusLabel.setText("未设置") + self.PreprocStatusLabel.setStyleSheet("color: #969696;") + self.PreprocPreviewButton.setEnabled(False) + 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 getTimerTask( @@ -129,6 +186,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog): "status": ALTimerTaskStatus.PENDING, "executed": False, "repeat": self.RepeatCheckBox.isChecked(), + "repeat_preproc": self.__repeat_preproc_script, } if task_data["repeat"]: task_data["history"] = [] # repeat history diff --git a/src/gui/__init__.py b/src/gui/__init__.py index c0cf4b0..d5ddc5f 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -10,6 +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. - ALTimerTaskHistoryDialog: Timer task history dialog class. - ALTimerTaskManageWidget: Timer task manage widget class. - ALUserTreeWidget: User tree widget class. diff --git a/src/gui/resources/ui/ALTimerTaskAddDialog.ui b/src/gui/resources/ui/ALTimerTaskAddDialog.ui index 1df3808..e2fc6c4 100644 --- a/src/gui/resources/ui/ALTimerTaskAddDialog.ui +++ b/src/gui/resources/ui/ALTimerTaskAddDialog.ui @@ -13,7 +13,7 @@ 350 - 400 + 460 diff --git a/src/utils/PreprocEngine.py b/src/utils/PreprocEngine.py new file mode 100644 index 0000000..4475105 --- /dev/null +++ b/src/utils/PreprocEngine.py @@ -0,0 +1,328 @@ +# -*- 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 PreprocEngine: + + COMPARE_OPS = { + ".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 = { + "预约开始时间": ("RESERVE_BEGIN_TIME", "Time"), + "预约结束时间": ("RESERVE_END_TIME", "Time"), + "预约日期": ("RESERVE_DATE", "Date"), + "用户名": ("USERNAME", "String"), + "用户启用": ("USER_ENABLE", "Boolean"), + "当前时间": ("CURRENT_TIME", "Time"), + "当前日期": ("CURRENT_DATE", "Date"), + } + + @staticmethod + def execute( + script_text: str, + user_data: dict + ): + + 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("语法错误: IF 缺少右括号") + condition_str = line[3:cond_end].strip() + matched = PreprocEngine._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") + cond_end = _findConditionEnd(upper_line) + if cond_end < 0: + raise ValueError("语法错误: ELSE IF 缺少右括号") + condition_str = line[8:cond_end].strip() + _, has_matched = if_stack[-1] + if not has_matched: + matched = PreprocEngine._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("语法错误: 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("语法错误: 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) + elif upper_line == "PASS": + continue + else: + should_execute = ( + all(ctx[0] for ctx in if_stack) if if_stack else True + ) + if should_execute: + PreprocEngine._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 + + @staticmethod + def _resolveField( + field_name: str, + user_data: dict + ) -> str: + + 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 str(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 _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": + user_data["enabled"] = value.upper() == "TRUE" + + @staticmethod + def _evaluateCondition( + condition_str: str, + user_data: dict + ) -> bool: + + for op, cmp_func in PreprocEngine.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 = PreprocEngine._resolveField(field_name, user_data) + right_val = PreprocEngine._resolveValue(value_str, user_data) + return cmp_func(left_val, right_val) + 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 = PreprocEngine._resolveValue(value_str, user_data) + PreprocEngine._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() + 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", "") + 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: + 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", "") + ) + 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("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", "") + ) + 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: + + 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