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