diff --git a/src/gui/ALAutoScriptOrchDialog.py b/src/gui/ALAutoScriptOrchDialog.py
new file mode 100644
index 0000000..d4b7e78
--- /dev/null
+++ b/src/gui/ALAutoScriptOrchDialog.py
@@ -0,0 +1,884 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+
+from PySide6.QtCore import QTime, QDate, Slot
+from PySide6.QtWidgets import (
+ QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QComboBox, QPushButton, QScrollArea, QTimeEdit,
+ QDateEdit, QLineEdit, QSpinBox, QDoubleSpinBox,
+ QStackedWidget, QFrame, QDialogButtonBox,
+ QGroupBox, QSizePolicy
+)
+
+from utils.AutoScriptEngine import AutoScriptEngine
+
+
+VARIABLE_META = AutoScriptEngine.VARIABLE_META
+_VAR_COMBO_ITEMS = [
+ (display, varname, vartype)
+ for display, (varname, vartype) in VARIABLE_META.items()
+]
+_VAR_COMBO_ITEMS_SET = [
+ (display, varname, vartype)
+ for display, (varname, vartype) in VARIABLE_META.items()
+ if not varname.startswith("CURRENT_")
+]
+OP_ITEMS = [
+ ("等于", ".EQ."),
+ ("不等于", ".NEQ."),
+ ("大于", ".BGT."),
+ ("小于", ".BLT."),
+ ("大于等于", ".BGE."),
+ ("小于等于", ".BLE."),
+]
+
+
+def _makeVarCombo(
+) -> QComboBox:
+
+ cb = QComboBox()
+ for display, varname, vartype in _VAR_COMBO_ITEMS:
+ cb.addItem(display, (varname, vartype))
+ cb.setMinimumWidth(120)
+ cb.setFixedHeight(25)
+ return cb
+
+
+def _makeSetVarCombo(
+) -> QComboBox:
+
+ cb = QComboBox()
+ for display, varname, vartype in _VAR_COMBO_ITEMS_SET:
+ cb.addItem(display, (varname, vartype))
+ cb.setMinimumWidth(120)
+ cb.setFixedHeight(25)
+ return cb
+
+
+def _makeOpCombo(
+) -> QComboBox:
+
+ cb = QComboBox()
+ for display, op in OP_ITEMS:
+ cb.addItem(display, op)
+ cb.setMinimumWidth(80)
+ cb.setFixedHeight(25)
+ return cb
+
+
+def _makeValueWidget(
+ data_type: str
+) -> QWidget:
+
+ if data_type == "Time":
+ w = QTimeEdit()
+ w.setDisplayFormat("HH:mm")
+ w.setMinimumWidth(100)
+ w.setFixedHeight(25)
+ elif data_type == "Date":
+ w = QDateEdit()
+ w.setDisplayFormat("yyyy-MM-dd")
+ w.setCalendarPopup(True)
+ w.setMinimumWidth(130)
+ w.setFixedHeight(25)
+ elif data_type == "Integer":
+ w = QSpinBox()
+ w.setMinimum(-999999)
+ w.setMaximum(999999)
+ w.setMinimumWidth(100)
+ w.setFixedHeight(25)
+ elif data_type == "Float":
+ w = QDoubleSpinBox()
+ w.setMinimum(-999999)
+ w.setMaximum(999999)
+ w.setDecimals(2)
+ w.setMinimumWidth(100)
+ w.setFixedHeight(25)
+ elif data_type == "Boolean":
+ w = QComboBox()
+ w.addItem(".TRUE.", ".TRUE.")
+ w.addItem(".FALSE.", ".FALSE.")
+ w.setMinimumWidth(100)
+ w.setFixedHeight(25)
+ else:
+ w = QLineEdit()
+ w.setPlaceholderText("输入值")
+ w.setMinimumWidth(120)
+ w.setFixedHeight(25)
+ return w
+
+
+def _makeActionValueWidget(
+ data_type: str
+) -> QWidget:
+
+ if data_type == "Date":
+ w = QComboBox()
+ w.addItem("今天", "today")
+ w.addItem("明天", "tomorrow")
+ w.setFixedHeight(25)
+ w.setMinimumWidth(100)
+ w._is_date_action = True
+ return w
+
+ if data_type == "Time":
+ container = QWidget()
+ layout = QHBoxLayout(container)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(2)
+ modeCombo = QComboBox()
+ modeCombo.addItem("固定时间", "fixed")
+ modeCombo.addItem("相对当前", "relative")
+ modeCombo.setFixedHeight(25)
+ stack = QStackedWidget()
+ timeEdit = QTimeEdit()
+ timeEdit.setDisplayFormat("HH:mm")
+ timeEdit.setFixedHeight(25)
+ spinBox = QSpinBox()
+ spinBox.setRange(0, 23)
+ spinBox.setSuffix("小时")
+ spinBox.setFixedHeight(25)
+ stack.addWidget(timeEdit)
+ stack.addWidget(spinBox)
+ modeCombo.currentIndexChanged.connect(
+ lambda i: stack.setCurrentIndex(i)
+ )
+ layout.addWidget(modeCombo)
+ layout.addWidget(stack)
+ container._modeCombo = modeCombo
+ container._timeEdit = timeEdit
+ container._spinBox = spinBox
+ container._isActionTime = True
+ return container
+
+ return _makeValueWidget(data_type)
+
+
+def _getValueFromWidget(
+ w: QWidget
+) -> str:
+
+ if getattr(w, '_isActionTime', False):
+ if w._modeCombo.currentData() == "fixed":
+ return w._timeEdit.time().toString("HH:mm")
+ else:
+ return f"+{w._spinBox.value()}"
+ if isinstance(w, QTimeEdit):
+ return w.time().toString("HH:mm")
+ if isinstance(w, QDateEdit):
+ return w.date().toString("yyyy-MM-dd")
+ if isinstance(w, QComboBox):
+ return w.currentText()
+ if isinstance(w, QSpinBox):
+ return str(w.value())
+ if isinstance(w, QDoubleSpinBox):
+ return str(w.value())
+ if isinstance(w, QLineEdit):
+ return w.text()
+ return ""
+
+
+def _encodeValueStr(
+ raw_value: str,
+ data_type: str
+) -> str:
+
+ if data_type == "Time":
+ if raw_value.startswith("+"):
+ return raw_value
+ return f"TIME({raw_value})"
+ elif data_type == "Date":
+ if raw_value == "今天":
+ return "CURRENT_DATE"
+ elif raw_value == "明天":
+ return "CURRENT_DATE + 1"
+ return f"DATE({raw_value})"
+ elif data_type == "Boolean":
+ return raw_value
+ elif data_type == "String":
+ escaped = raw_value.replace("'", "''")
+ return f"'{escaped}'"
+ else:
+ return raw_value
+
+
+def _setWidgetValue(
+ w: QWidget,
+ vartype: str,
+ expr: str
+):
+
+ import re
+ s = expr.strip()
+
+ if getattr(w, '_isActionTime', False):
+ timeMatch = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
+ if timeMatch:
+ w._modeCombo.setCurrentIndex(0)
+ parts = timeMatch.group(1).split(":")
+ w._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
+ return
+ relMatch = re.match(r"^\+(\d+)$", s)
+ if relMatch:
+ w._modeCombo.setCurrentIndex(1)
+ w._spinBox.setValue(int(relMatch.group(1)))
+ return
+ return
+ if getattr(w, '_is_date_action', False) and isinstance(w, QComboBox):
+ if s.upper() in ("CURRENT_DATE", "TODAY"):
+ w.setCurrentIndex(0)
+ elif s.upper() in ("CURRENT_DATE + 1", "TOMORROW"):
+ w.setCurrentIndex(1)
+ else:
+ dateMatch = re.match(
+ r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE
+ )
+ if dateMatch:
+ from datetime import datetime, timedelta
+ dateStr = dateMatch.group(1)
+ today = datetime.now().strftime("%Y-%m-%d")
+ tomorrow = (
+ datetime.now() + timedelta(days=1)
+ ).strftime("%Y-%m-%d")
+ if dateStr == today:
+ w.setCurrentIndex(0)
+ elif dateStr == tomorrow:
+ w.setCurrentIndex(1)
+ return
+ if vartype == "Time":
+ m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
+ if m and isinstance(w, QTimeEdit):
+ parts = m.group(1).split(":")
+ w.setTime(QTime(int(parts[0]), int(parts[1])))
+ elif vartype == "Date":
+ m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE)
+ if m and isinstance(w, QDateEdit):
+ parts = m.group(1).split("-")
+ w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
+ elif vartype == "Boolean" and isinstance(w, QComboBox):
+ for i in range(w.count()):
+ if w.itemData(i) == s.upper():
+ w.setCurrentIndex(i)
+ break
+ elif vartype == "Integer" and isinstance(w, QSpinBox):
+ try:
+ w.setValue(int(s))
+ except ValueError:
+ pass
+ elif vartype == "Float" and isinstance(w, QDoubleSpinBox):
+ try:
+ w.setValue(float(s))
+ except ValueError:
+ pass
+ elif isinstance(w, QLineEdit):
+ inner = s
+ if (inner.startswith("'") and inner.endswith("'")) or \
+ (inner.startswith('"') and inner.endswith('"')):
+ inner = inner[1:-1].replace("''", "'")
+ w.setText(inner)
+
+
+class ActionStepFrame(QFrame):
+
+ def __init__(
+ self,
+ parent=None
+ ):
+ super().__init__(parent)
+
+ self.setupUi()
+ self.connectSignals()
+ self.onTargetChanged(0)
+
+
+ def setupUi(
+ self
+ ):
+
+ self.setFrameShape(QFrame.Shape.StyledPanel)
+ self.setFrameShadow(QFrame.Shadow.Raised)
+ self.setFixedHeight(35)
+
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(2, 2, 2, 2)
+ layout.setSpacing(4)
+
+ self.targetCombo = _makeSetVarCombo()
+ self.valueWidgetStack = QStackedWidget()
+ self.valueWidgetStack.setFixedHeight(25)
+ self.initValueStack()
+
+ setLabel = QLabel("设置")
+ setLabel.setFixedHeight(25)
+ layout.addWidget(setLabel)
+ layout.addWidget(self.targetCombo)
+ toLabel = QLabel("为")
+ toLabel.setFixedHeight(25)
+ layout.addWidget(toLabel)
+ layout.addWidget(self.valueWidgetStack)
+
+ self.deleteBtn = QPushButton("×")
+ self.deleteBtn.setFixedSize(24, 25)
+ self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
+ layout.addWidget(self.deleteBtn)
+
+
+ def connectSignals(
+ self
+ ):
+
+ self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
+
+
+ def initValueStack(
+ self
+ ):
+
+ self._valueWidgets = {}
+ for _, _, vartype in _VAR_COMBO_ITEMS:
+ if vartype not in self._valueWidgets:
+ w = _makeActionValueWidget(vartype)
+ self._valueWidgets[vartype] = w
+ self.valueWidgetStack.addWidget(w)
+ self.valueWidgetStack.setCurrentWidget(
+ self._valueWidgets.get("String", self.valueWidgetStack.widget(0))
+ )
+
+
+ def getTarget(
+ self
+ ) -> str:
+
+ data = self.targetCombo.currentData()
+ return data[0] if data else ""
+
+
+ def getTargetType(
+ self
+ ) -> str:
+
+ data = self.targetCombo.currentData()
+ return data[1] if data else "String"
+
+
+ def getValueRaw(
+ self
+ ) -> str:
+
+ currentType = self.getTargetType()
+ w = self._valueWidgets.get(currentType)
+ if w:
+ return _getValueFromWidget(w)
+ return ""
+
+
+ def toScriptLine(
+ self
+ ) -> str:
+
+ target = self.getTarget()
+ if not target:
+ return ""
+ rawVal = self.getValueRaw()
+ targetType = self.getTargetType()
+ encoded = _encodeValueStr(rawVal, targetType)
+ if targetType == "Time" and rawVal.startswith("+"):
+ hours = rawVal[1:]
+ return f" {target} .ADD. {hours}"
+ return f" SET {target} = {encoded}"
+
+
+ def loadFromScript(
+ self,
+ targetVar: str,
+ valueExpr: str
+ ):
+
+ for idx in range(self.targetCombo.count()):
+ data = self.targetCombo.itemData(idx)
+ if data and data[0] == targetVar:
+ self.targetCombo.setCurrentIndex(idx)
+ break
+ self.setValueFromExpr(valueExpr)
+
+
+ def setValueFromExpr(
+ self,
+ expr: str
+ ):
+
+ targetType = self.getTargetType()
+ w = self._valueWidgets.get(targetType)
+ if not w:
+ return
+ _setWidgetValue(w, targetType, expr)
+
+ @Slot(int)
+ def onTargetChanged(
+ self,
+ idx
+ ):
+ if idx < 0:
+ return
+ data = self.targetCombo.itemData(idx)
+ if data:
+ _, vartype = data
+ w = self._valueWidgets.get(vartype)
+ if w:
+ self.valueWidgetStack.setCurrentWidget(w)
+
+
+class ConditionalBlock(QGroupBox):
+
+ def __init__(
+ self,
+ blockIndex: int,
+ parent=None
+ ):
+ super().__init__(parent)
+
+ self.blockIndex = blockIndex
+ self._actionWidgets = []
+
+ self.setupUi()
+ self.connectSignals()
+ self.onOperandChanged(0)
+
+
+ def setupUi(
+ self
+ ):
+
+ self.setStyleSheet(
+ "QGroupBox { font-weight: bold; border: 1px solid #ccc; "
+ "margin-top: 5px; padding-top: 5px; }"
+ )
+ self.setSizePolicy(
+ QSizePolicy.Policy.Preferred,
+ QSizePolicy.Policy.Fixed
+ )
+
+ mainLayout = QVBoxLayout(self)
+ mainLayout.setSpacing(4)
+ mainLayout.setContentsMargins(5, 5, 5, 5)
+
+ headerLayout = QHBoxLayout()
+ self.typeCombo = QComboBox()
+ self.typeCombo.addItem("IF", "IF")
+ self.typeCombo.addItem("ELSE IF", "ELSE IF")
+ self.typeCombo.addItem("ELSE", "ELSE")
+ if self.blockIndex == 0:
+ self.typeCombo.setEnabled(False)
+ typeLabel = QLabel("类型:")
+ typeLabel.setFixedHeight(25)
+ headerLayout.addWidget(typeLabel)
+ headerLayout.addWidget(self.typeCombo)
+ headerLayout.addStretch()
+ self.deleteBlockBtn = QPushButton("删除此块")
+ self.deleteBlockBtn.setStyleSheet("color: red;")
+ self.deleteBlockBtn.setFixedHeight(25)
+ headerLayout.addWidget(self.deleteBlockBtn)
+ mainLayout.addLayout(headerLayout)
+
+ self.conditionWidget = QWidget()
+ self.conditionWidget.setFixedHeight(60)
+ condLayout = QHBoxLayout(self.conditionWidget)
+ condLayout.setContentsMargins(0, 0, 0, 0)
+ ifLabel = QLabel("如果")
+ ifLabel.setFixedHeight(25)
+ condLayout.addWidget(ifLabel)
+ self.operandCombo = _makeVarCombo()
+ condLayout.addWidget(self.operandCombo)
+ self.opCombo = _makeOpCombo()
+ condLayout.addWidget(self.opCombo)
+
+ self.condValueStack = QStackedWidget()
+ self.condValueStack.setFixedHeight(25)
+ self._condValueWidgets = {}
+ for vartype in ["Time", "Date", "String", "Integer", "Float", "Boolean"]:
+ w = _makeValueWidget(vartype)
+ self._condValueWidgets[vartype] = w
+ self.condValueStack.addWidget(w)
+ self.condValueStack.setCurrentWidget(self._condValueWidgets.get("String"))
+ condLayout.addWidget(self.condValueStack)
+ mainLayout.addWidget(self.conditionWidget)
+
+ self.actionLabel = QLabel("执行步骤:")
+ self.actionLabel.setFixedHeight(25)
+ mainLayout.addWidget(self.actionLabel)
+
+ self.actionsLayout = QVBoxLayout()
+ self.actionsLayout.setSpacing(2)
+ mainLayout.addLayout(self.actionsLayout)
+
+ self.addActionBtn = QPushButton("+ 添加执行步骤")
+ self.addActionBtn.setFixedHeight(25)
+ mainLayout.addWidget(self.addActionBtn)
+
+
+ def connectSignals(
+ self
+ ):
+
+ self.operandCombo.currentIndexChanged.connect(self.onOperandChanged)
+ self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
+ self.addActionBtn.clicked.connect(self.addActionStep)
+
+
+ def removeActionStep(
+ self,
+ step: ActionStepFrame
+ ):
+
+ if step in self._actionWidgets:
+ self._actionWidgets.remove(step)
+ self.actionsLayout.removeWidget(step)
+ step.hide()
+ step.deleteLater()
+
+
+ def getBlockType(
+ self
+ ) -> str:
+
+ return self.typeCombo.currentData()
+
+
+ def toScriptLines(
+ self
+ ) -> list:
+
+ blockType = self.getBlockType()
+ lines = []
+
+ if blockType in ("IF", "ELSE IF"):
+ operand = self.operandCombo.currentData()
+ operandName = operand[0] if operand else ""
+ operandType = operand[1] if operand else "String"
+ opSym = self.opCombo.currentData()
+ rawVal = _getValueFromWidget(
+ self._condValueWidgets.get(operandType, QLineEdit())
+ )
+ encodedVal = _encodeValueStr(rawVal, operandType)
+ if blockType == "IF":
+ lines.append(f"IF({operandName} {opSym} {encodedVal}) THEN")
+ else:
+ lines.append(f"ELSE IF({operandName} {opSym} {encodedVal}) THEN")
+ else:
+ lines.append("ELSE")
+ for step in self._actionWidgets:
+ scriptLine = step.toScriptLine()
+ if scriptLine:
+ lines.append(scriptLine)
+
+ return lines
+
+
+ def getConditionSummary(
+ self
+ ) -> str:
+
+ bt = self.getBlockType()
+ if bt == "ELSE":
+ return "ELSE"
+ operandData = self.operandCombo.currentData()
+ if not operandData:
+ return bt
+ operandDisplay = self.operandCombo.currentText()
+ opDisplay = self.opCombo.currentText()
+ rawVal = self.getConditionRawValuePreview()
+ return f"{bt} ({operandDisplay} {opDisplay} {rawVal})"
+
+
+ def getConditionRawValuePreview(
+ self
+ ) -> str:
+
+ data = self.operandCombo.currentData()
+ if not data:
+ return ""
+ _, vartype = data
+ w = self._condValueWidgets.get(vartype)
+ if w:
+ return _getValueFromWidget(w)
+ return ""
+
+
+ def countActionSteps(
+ self
+ ) -> int:
+
+ return len(self._actionWidgets)
+
+ @Slot(int)
+ def onOperandChanged(
+ self,
+ idx
+ ):
+ if idx < 0:
+ return
+ data = self.operandCombo.itemData(idx)
+ if data:
+ _, vartype = data
+ w = self._condValueWidgets.get(vartype)
+ if w:
+ self.condValueStack.setCurrentWidget(w)
+
+ @Slot(int)
+ def onTypeChanged(
+ self,
+ idx
+ ):
+ isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
+ self.conditionWidget.setVisible(isCond)
+ self.actionLabel.setText("执行步骤:" if isCond else "ELSE 执行步骤:")
+
+ @Slot()
+ def addActionStep(
+ self
+ ):
+
+ step = ActionStepFrame(self)
+ step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
+ self._actionWidgets.append(step)
+ self.actionsLayout.addWidget(step)
+
+
+class ALAutoScriptOrchDialog(QDialog):
+
+ def __init__(
+ self,
+ parent=None,
+ existingScript: str = ""
+ ):
+ super().__init__(parent)
+ self._blocks: list[ConditionalBlock] = []
+
+ self.modifyUi()
+ self.connectSignals()
+
+ if existingScript and existingScript.strip():
+ self.loadFromScript(existingScript)
+ else:
+ self.addBlock()
+ self._scrollLayout.addStretch()
+
+
+ def modifyUi(
+ self
+ ):
+
+ self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
+ self.setMinimumSize(420, 400)
+ self.setModal(True)
+ mainLayout = QVBoxLayout(self)
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setFrameShape(QFrame.Shape.NoFrame)
+ scrollContent = QWidget()
+ self._scrollLayout = QVBoxLayout(scrollContent)
+ self._scrollLayout.setSpacing(5)
+ scroll.setWidget(scrollContent)
+ mainLayout.addWidget(scroll)
+ addBlockLayout = QHBoxLayout()
+ self.addBlockBtn = QPushButton("+ 添加判断块")
+ self.addBlockBtn.setFixedHeight(25)
+ addBlockLayout.addStretch()
+ addBlockLayout.addWidget(self.addBlockBtn)
+ addBlockLayout.addStretch()
+ mainLayout.addLayout(addBlockLayout)
+ self.btnBox = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok |
+ QDialogButtonBox.StandardButton.Cancel
+ )
+ self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
+ self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
+ mainLayout.addWidget(self.btnBox)
+
+
+ def connectSignals(
+ self
+ ):
+
+ self.btnBox.accepted.connect(self.accept)
+ self.btnBox.rejected.connect(self.reject)
+ self.addBlockBtn.clicked.connect(self.addBlock)
+
+
+ def removeBlock(
+ self,
+ block: ConditionalBlock
+ ):
+
+ if block in self._blocks:
+ self._blocks.remove(block)
+ self._scrollLayout.removeWidget(block)
+ block.hide()
+ block.deleteLater()
+
+
+ def getScript(
+ self
+ ) -> str:
+
+ parts = []
+ for i, block in enumerate(self._blocks):
+ blockType = block.getBlockType()
+ if blockType == "IF" and i > 0:
+ parts.append("ENDIF")
+ lines = block.toScriptLines()
+ parts.extend(lines)
+ if self._blocks and self._blocks[0].getBlockType() == "IF":
+ parts.append("ENDIF")
+ return "\n".join(parts)
+
+
+ def getScriptPreview(
+ self
+ ) -> str:
+
+ s = self.getScript()
+ if len(s) > 10:
+ return s[:7] + "..."
+ return s
+
+
+ def loadFromScript(
+ self,
+ script: str
+ ):
+
+ import re
+ lines = [l.strip() for l in script.split("\n") if l.strip()]
+ if not lines:
+ self.addBlock()
+ return
+
+ currentBlock = None
+ currentBlockType = None
+ actionsBuffer = []
+
+ def flushBlock():
+ nonlocal currentBlock, currentBlockType, actionsBuffer
+ if currentBlock is None:
+ return
+ typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2}
+ idx = typeIdxMap.get(currentBlockType, 0)
+ currentBlock.typeCombo.setCurrentIndex(idx)
+ currentBlock.onTypeChanged(idx)
+ for oldStep in list(currentBlock._actionWidgets):
+ currentBlock.removeActionStep(oldStep)
+ for target, valueExpr in actionsBuffer:
+ currentBlock.addActionStep()
+ step = currentBlock._actionWidgets[-1]
+ step.loadFromScript(target, valueExpr)
+ self._blocks.clear()
+ while self._scrollLayout.count():
+ item = self._scrollLayout.takeAt(0)
+ if item.widget():
+ item.widget().deleteLater()
+ for line in lines:
+ upper = line.upper()
+ ifMatch = re.match(r"^IF\((.+)\)\s*THEN\s*$", upper)
+ if ifMatch:
+ flushBlock()
+ currentBlockType = "IF"
+ actionsBuffer = []
+ self.addBlock()
+ currentBlock = self._blocks[-1]
+ self.parseConditionToBlock(currentBlock, ifMatch.group(1))
+ continue
+ elifIfMatch = re.match(r"^ELSE\s+IF\((.+)\)\s*THEN\s*$", upper)
+ if elifIfMatch:
+ flushBlock()
+ currentBlockType = "ELSE IF"
+ actionsBuffer = []
+ self.addBlock()
+ currentBlock = self._blocks[-1]
+ self.parseConditionToBlock(currentBlock, elifIfMatch.group(1))
+ continue
+ if upper == "ELSE":
+ flushBlock()
+ currentBlockType = "ELSE"
+ actionsBuffer = []
+ self.addBlock()
+ currentBlock = self._blocks[-1]
+ currentBlock.conditionWidget.setVisible(False)
+ continue
+ setMatch = re.match(r"^SET\s+(\w+)\s*=\s*(.+)$", line, re.IGNORECASE)
+ if setMatch:
+ target = setMatch.group(1).strip()
+ valueExpr = setMatch.group(2).strip()
+ actionsBuffer.append((target, valueExpr))
+ continue
+ addMatch = re.match(r"^(\w+)\s+\.ADD\.\s+(\d+)$", line, re.IGNORECASE)
+ if addMatch:
+ target = addMatch.group(1).strip()
+ hours = addMatch.group(2).strip()
+ actionsBuffer.append((target, f"+{hours}"))
+ continue
+ if upper in ("ENDIF", "END IF"):
+ flushBlock()
+ currentBlock = None
+ currentBlockType = None
+ actionsBuffer = []
+ continue
+ flushBlock()
+ if not self._blocks:
+ self.addBlock()
+
+
+ def parseConditionToBlock(
+ self,
+ block: ConditionalBlock,
+ condStr: str
+ ):
+
+ condStr = condStr.strip()
+ for _, opSym in OP_ITEMS:
+ idx = condStr.upper().find(opSym)
+ if idx >= 0:
+ leftPart = condStr[:idx].strip()
+ rightPart = condStr[idx + len(opSym):].strip()
+ for ci in range(block.operandCombo.count()):
+ data = block.operandCombo.itemData(ci)
+ if data and data[0] == leftPart:
+ block.operandCombo.setCurrentIndex(ci)
+ break
+ for oi in range(block.opCombo.count()):
+ if block.opCombo.itemData(oi) == opSym:
+ block.opCombo.setCurrentIndex(oi)
+ break
+ opData = block.operandCombo.currentData()
+ vartype = opData[1] if opData else "String"
+ w = block._condValueWidgets.get(vartype)
+ if w:
+ _setWidgetValue(w, vartype, rightPart)
+ return
+
+ @Slot()
+ def addBlock(
+ self
+ ):
+
+ block = ConditionalBlock(len(self._blocks), self)
+ block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
+ self._blocks.append(block)
+ block.addActionStep()
+ if self._scrollLayout.count() > 0:
+ lastItem = self._scrollLayout.itemAt(
+ self._scrollLayout.count() - 1
+ )
+ if lastItem and lastItem.spacerItem():
+ self._scrollLayout.insertWidget(
+ self._scrollLayout.count() - 1, block
+ )
+ return
+ self._scrollLayout.addWidget(block)
\ No newline at end of file
diff --git a/src/gui/ALAutoScriptPrevDialog.py b/src/gui/ALAutoScriptPrevDialog.py
new file mode 100644
index 0000000..4becb08
--- /dev/null
+++ b/src/gui/ALAutoScriptPrevDialog.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.QtCore import Slot
+
+from PySide6.QtGui import (
+ QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QIcon
+)
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
+ QDialogButtonBox, QPushButton, QLabel, QApplication, QStyle
+)
+
+
+class ALScriptHighlighter(QSyntaxHighlighter):
+
+ def __init__(
+ self,
+ parent=None
+ ):
+
+ super().__init__(parent)
+ self._rules = []
+
+ keywordFmt = QTextCharFormat()
+ keywordFmt.setForeground(QColor("#316BFF"))
+ keywordFmt.setFontWeight(QFont.Weight.Bold)
+ for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
+ "SET", "PASS", "THEN"]:
+ pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
+ self._rules.append((pattern, keywordFmt))
+ literalFmt = QTextCharFormat()
+ literalFmt.setForeground(QColor("#C2185B"))
+ literalFmt.setFontWeight(QFont.Weight.Bold)
+ for lit in [".TRUE.", ".FALSE."]:
+ self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt))
+ opFmt = QTextCharFormat()
+ opFmt.setForeground(QColor("#9C27B0"))
+ for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
+ r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]:
+ self._rules.append((op, opFmt))
+ varFmt = QTextCharFormat()
+ varFmt.setForeground(QColor("#E65100"))
+ for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME",
+ "RESERVE_DATE", "USERNAME", "USER_ENABLE",
+ "PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]:
+ self._rules.append((r"\b" + var + r"\b", varFmt))
+ funcFmt = QTextCharFormat()
+ funcFmt.setForeground(QColor("#2E7D32"))
+ self._rules.append((r"\bTIME\([^)]+\)", funcFmt))
+ self._rules.append((r"\bDATE\([^)]+\)", funcFmt))
+ strFmt = QTextCharFormat()
+ strFmt.setForeground(QColor("#388E3C"))
+ self._rules.append((r"'[^']*'", strFmt))
+ numFmt = QTextCharFormat()
+ numFmt.setForeground(QColor("#D32F2F"))
+ self._rules.append((r"\b\d+\b", numFmt))
+ commentFmt = QTextCharFormat()
+ commentFmt.setForeground(QColor("#999999"))
+ commentFmt.setFontItalic(True)
+ self._rules.append((r"//[^\n]*", commentFmt))
+
+
+ def highlightBlock(
+ self,
+ text
+ ):
+
+ import re
+ for pattern, fmt in self._rules:
+ for match in re.finditer(pattern, text, re.IGNORECASE):
+ start = match.start()
+ length = match.end() - match.start()
+ self.setFormat(start, length, fmt)
+
+
+class ALAutoScriptPreviewDialog(QDialog):
+
+ def __init__(
+ self,
+ parent=None,
+ script: str = ""
+ ):
+
+ super().__init__(parent)
+ self.__fontSize = 13
+
+ self.modifyUi()
+ self.connectSignals()
+
+ self._textEdit.setPlainText(script)
+ self._highlighter = ALScriptHighlighter(
+ self._textEdit.document()
+ )
+
+
+ def modifyUi(
+ self
+ ):
+
+ self.setWindowTitle("AutoScript 预览 - AutoLibrary")
+ self.setMinimumSize(520, 360)
+
+ layout = QVBoxLayout(self)
+ toolbarLayout = QHBoxLayout()
+ self._zoomInBtn = QPushButton("+")
+ self._zoomInBtn.setFixedSize(30, 25)
+ self._zoomOutBtn = QPushButton("-")
+ self._zoomOutBtn.setFixedSize(30, 25)
+ self._zoomResetBtn = QPushButton(
+ QApplication.style().standardIcon(
+ QStyle.StandardPixmap.SP_BrowserReload
+ ), ""
+ )
+ self._zoomResetBtn.setFixedSize(30, 25)
+ self._zoomResetBtn.setToolTip("重置缩放")
+ self._zoomLabel = QLabel(f"{self.__fontSize}px")
+ self._zoomLabel.setFixedHeight(25)
+ toolbarLayout.addWidget(self._zoomInBtn)
+ toolbarLayout.addWidget(self._zoomOutBtn)
+ toolbarLayout.addWidget(self._zoomResetBtn)
+ toolbarLayout.addWidget(self._zoomLabel)
+ toolbarLayout.addStretch()
+ self._copyBtn = QPushButton(
+ QApplication.style().standardIcon(
+ QStyle.StandardPixmap.SP_FileDialogDetailedView
+ ), ""
+ )
+ self._copyBtn.setFixedSize(30, 25)
+ self._copyBtn.setToolTip("复制脚本")
+ toolbarLayout.addWidget(self._copyBtn)
+ layout.addLayout(toolbarLayout)
+ self._textEdit = QPlainTextEdit(self)
+ self._textEdit.setReadOnly(True)
+ self._textEdit.setLineWrapMode(
+ QPlainTextEdit.LineWrapMode.NoWrap
+ )
+ self._textEdit.setStyleSheet(
+ "QPlainTextEdit {"
+ " font-family: 'Courier New', 'Consolas', monospace;"
+ " font-size: 13px;"
+ "}"
+ )
+ layout.addWidget(self._textEdit)
+
+ self._btnBox = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Close
+ )
+ self._btnBox.button(
+ QDialogButtonBox.StandardButton.Close
+ ).setText("关闭")
+ layout.addWidget(self._btnBox)
+
+
+ def connectSignals(
+ self
+ ):
+
+ self._btnBox.rejected.connect(self.reject)
+ self._zoomInBtn.clicked.connect(self.onZoomIn)
+ self._zoomOutBtn.clicked.connect(self.onZoomOut)
+ self._zoomResetBtn.clicked.connect(self.onZoomReset)
+ self._copyBtn.clicked.connect(self.onCopy)
+
+
+ def updateFontSize(
+ self
+ ):
+
+ font = self._textEdit.font()
+ font.setPointSize(self.__fontSize)
+ self._textEdit.setFont(font)
+ self._textEdit.setStyleSheet(
+ "QPlainTextEdit {"
+ " font-family: 'Courier New', 'Consolas', monospace;"
+ f" font-size: {self.__fontSize}px;"
+ "}"
+ )
+ self._zoomLabel.setText(f"{self.__fontSize}px")
+
+ @Slot()
+ def onZoomIn(
+ self
+ ):
+
+ self.__fontSize = min(self.__fontSize + 2, 40)
+ self.updateFontSize()
+
+ @Slot()
+ def onZoomOut(
+ self
+ ):
+
+ self.__fontSize = max(self.__fontSize - 2, 8)
+ self.updateFontSize()
+
+ @Slot()
+ def onZoomReset(
+ self
+ ):
+
+ self.__fontSize = 13
+ self.updateFontSize()
+
+ @Slot()
+ def onCopy(
+ self
+ ):
+
+ clipboard = QApplication.clipboard()
+ clipboard.setText(self._textEdit.toPlainText())
+ original = self._copyBtn.text()
+ self._copyBtn.setText("已复制")
+ self._copyBtn.setEnabled(False)
+ from PySide6.QtCore import QTimer
+ QTimer.singleShot(2000, lambda: (
+ self._copyBtn.setText(original),
+ self._copyBtn.setEnabled(True)
+ ))
diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py
index c1503d1..9bc1c9f 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.AutoScriptEngine import AutoScriptEngine
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)
@@ -157,12 +161,90 @@ class TimerTaskWorker(AutoLibWorker):
self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
+
def run(
self
):
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.applyRepeatAutoScript()
+ auto_lib = AutoLib(
+ self._input_queue,
+ self._output_queue,
+ self._run_config
+ )
+ groups = self._user_config.get("groups")
+ for group in groups:
+ if not group["enabled"]:
+ self._showTrace(
+ f"任务组 {group['name']} 已跳过",
+ no_log=True
+ )
+ continue
+ self._showTrace(
+ f"正在运行任务组 {group['name']}",
+ no_log=True
+ )
+ auto_lib.run(
+ {"users": group.get("users", [])}
+ )
+ auto_lib.close()
+ except Exception as e:
+ self._showTrace(
+ f"定时任务 {self.__timer_task['name']} 运行时发生异常: {e}",
+ self.TraceLevel.ERROR
+ )
+ self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
+ return
+ self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
+ self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
+
+
+ def applyRepeatAutoScript(
+ self
+ ):
+
+ auto_script = self.__timer_task.get("repeat_auto_script", "")
+ if not auto_script or not auto_script.strip():
+ return
+ self._showTrace(
+ f"检测到重复定时任务 AutoScript, 开始执行...",
+ no_log=True
+ )
+ 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:
+ AutoScriptEngine.execute(auto_script, user)
+ affected_count += 1
+ except ValueError as e:
+ self._showTrace(
+ f"AutoScript 执行错误 (用户 {user['username']}): {e}",
+ self.TraceLevel.ERROR
+ )
+ self._showLog(
+ f"AutoScript 执行完毕, "
+ f"影响 {affected_count} 个用户",
+ self.TraceLevel.INFO
+ )
+
+ @Slot()
+ def onTimerTaskIsFinished(
+ self
+ ):
+
+ self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
+ self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
@Slot()
def onTimerTaskFinishedWithError(
@@ -174,11 +256,3 @@ class TimerTaskWorker(AutoLibWorker):
self.TraceLevel.ERROR
)
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
-
- @Slot()
- def onTimerTaskIsFinished(
- self
- ):
-
- self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
- self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py
index 9eb2f25..dab9139 100644
--- a/src/gui/ALTimerTaskAddDialog.py
+++ b/src/gui/ALTimerTaskAddDialog.py
@@ -12,10 +12,12 @@ import uuid
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.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.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
from utils.TimerUtils import TimerUtils
@@ -34,15 +36,20 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
def __init__(
self,
- parent = None
+ parent = None,
+ timer_task: dict = None
):
super().__init__(parent)
+ self.__edit_timer_task = timer_task
self.setupUi(self)
self.modifyUi()
self.connectSignals()
+ if self.__edit_timer_task:
+ self.loadTask(self.__edit_timer_task)
+
def modifyUi(
self
@@ -86,6 +93,86 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False)
+ self.AutoScriptGroupBox = QGroupBox("AutoScript 指令")
+ self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox)
+ self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
+ self.AutoScriptLayout.setSpacing(3)
+ autoScriptBtnLayout = QHBoxLayout()
+ 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.AutoScriptGroupBox
+ )
+ self.AutoScriptGroupBox.setVisible(False)
+ self.__auto_script = ""
+
+
+ def loadTask(
+ self,
+ task: dict
+ ):
+
+ self.TaskNameLineEdit.setText(task.get("name", ""))
+ time_type = task.get("time_type", "特定时间")
+ self.TimerTypeComboBox.setCurrentText(time_type)
+ self.SpecificDateTimeEdit.setDateTime(
+ QDateTime(task["execute_time"])
+ )
+ self.RelativeDaySpinBox.setValue(0)
+ self.RelativeHourSpinBox.setValue(0)
+ self.RelativeMinuteSpinBox.setValue(0)
+ self.RelativeSecondSpinBox.setValue(0)
+ if task.get("silent", False):
+ self.SilentlyRunRadioButton.setChecked(True)
+ else:
+ self.ShowBeforeRunRadioButton.setChecked(True)
+ repeat = task.get("repeat", False)
+ self.RepeatCheckBox.setChecked(repeat)
+ if repeat:
+ repeat_days = task.get("repeat_days", [])
+ self.MonCheckBox.setChecked(0 in repeat_days)
+ self.TueCheckBox.setChecked(1 in repeat_days)
+ self.WedCheckBox.setChecked(2 in repeat_days)
+ self.ThuCheckBox.setChecked(3 in repeat_days)
+ self.FriCheckBox.setChecked(4 in repeat_days)
+ self.SatCheckBox.setChecked(5 in repeat_days)
+ self.SunCheckBox.setChecked(6 in repeat_days)
+ auto_script = task.get("repeat_auto_script", "")
+ if auto_script:
+ self.__auto_script = auto_script
+ self.AutoScriptStatusLabel.setText("已设置")
+ self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
+ self.AutoScriptPreviewButton.setEnabled(True)
+ self.ConfirmButton.setText("保存")
+
def connectSignals(
self
@@ -95,6 +182,9 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
+ self.AutoScriptSetButton.clicked.connect(self.onSetAutoScript)
+ self.AutoScriptPreviewButton.clicked.connect(self.onPreviewAutoScript)
+ self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp)
def getTimerTask(
@@ -119,19 +209,34 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value()
)
- task_data = {
- "name": name,
- "uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
- "time_type": self.TimerTypeComboBox.currentText(),
- "execute_time": execute_time,
- "silent": silent,
- "added_time": added_time,
- "status": ALTimerTaskStatus.PENDING,
- "executed": False,
- "repeat": self.RepeatCheckBox.isChecked(),
- }
- if task_data["repeat"]:
- task_data["history"] = [] # repeat history
+
+ if self.__edit_timer_task:
+ task_data = dict(self.__edit_timer_task)
+ task_data["name"] = name
+ task_data["execute_time"] = execute_time
+ task_data["silent"] = silent
+ task_data["status"] = ALTimerTaskStatus.PENDING
+ task_data["executed"] = False
+ task_data["repeat_auto_script"] = self.__auto_script
+ else:
+ task_data = {
+ "name": name,
+ "uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
+ "time_type": self.TimerTypeComboBox.currentText(),
+ "execute_time": execute_time,
+ "silent": silent,
+ "added_time": added_time,
+ "status": ALTimerTaskStatus.PENDING,
+ "executed": False,
+ "repeat": self.RepeatCheckBox.isChecked(),
+ "repeat_auto_script": self.__auto_script,
+ }
+
+ repeat = self.RepeatCheckBox.isChecked()
+ task_data["repeat"] = repeat
+ if repeat:
+ if "repeat_history" not in task_data:
+ task_data["repeat_history"] = []
repeat_days = []
if self.MonCheckBox.isChecked():
repeat_days.append(0)
@@ -182,4 +287,41 @@ 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)
+
+ @Slot()
+ def onSetAutoScript(self):
+ dlg = ALAutoScriptOrchDialog(self, existingScript=self.__auto_script)
+ if dlg.exec() == QDialog.DialogCode.Accepted:
+ script = dlg.getScript()
+ self.__auto_script = script
+ if script:
+ self.AutoScriptStatusLabel.setText("已设置")
+ self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
+ self.AutoScriptPreviewButton.setEnabled(True)
+ else:
+ self.AutoScriptStatusLabel.setText("未设置")
+ self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
+ self.AutoScriptPreviewButton.setEnabled(False)
+ dlg.deleteLater()
+
+ @Slot()
+ def onPreviewAutoScript(self):
+ if not self.__auto_script:
+ return
+ from gui.ALAutoScriptPrevDialog import ALAutoScriptPreviewDialog
+ dlg = ALAutoScriptPreviewDialog(self, self.__auto_script)
+ dlg.exec()
+ dlg.deleteLater()
+
+ @Slot()
+ def onAutoScriptHelp(
+ self
+ ):
+
+ QDesktopServices.openUrl(
+ QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript")
+ )
+
+
diff --git a/src/gui/ALTimerTaskHistoryDialog.py b/src/gui/ALTimerTaskHistoryDialog.py
index f054b15..8500c96 100644
--- a/src/gui/ALTimerTaskHistoryDialog.py
+++ b/src/gui/ALTimerTaskHistoryDialog.py
@@ -30,7 +30,7 @@ class ALTimerTaskHistoryDialog(QDialog):
super().__init__(parent)
self.__task_data = task_data
- self.__history = task_data.get("history", [])
+ self.__history = task_data.get("repeat_history", [])
self.modifyUi()
self.connectSignals()
@@ -130,6 +130,13 @@ class ALTimerTaskHistoryDialog(QDialog):
self.HistoryTableWidget.setItem(row, 2, DurationItem)
self.HistoryTableWidget.setRowHeight(row, 25)
+
+ def getHistory(
+ self
+ ) -> list:
+
+ return self.__history
+
@Slot()
def onClearHistoryButtonClicked(
self
@@ -137,11 +144,4 @@ class ALTimerTaskHistoryDialog(QDialog):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
- self.__task_data["history"] = self.__history
-
-
- def getHistory(
- self
- ) -> list:
-
- return self.__history
+ self.__task_data["repeat_history"] = self.__history
diff --git a/src/gui/ALTimerTaskManageWidget.py b/src/gui/ALTimerTaskManageWidget.py
index 9dcf792..c603903 100644
--- a/src/gui/ALTimerTaskManageWidget.py
+++ b/src/gui/ALTimerTaskManageWidget.py
@@ -19,10 +19,10 @@ from PySide6.QtCore import (
)
from PySide6.QtWidgets import (
QDialog, QWidget, QListWidgetItem, QMessageBox,
- QHBoxLayout, QVBoxLayout, QLabel, QPushButton
+ QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QMenu
)
from PySide6.QtGui import (
- QCloseEvent
+ QCloseEvent, QAction
)
import managers.config.ConfigManager as ConfigManager
@@ -35,6 +35,8 @@ from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
class ALTimerTaskItemWidget(QWidget):
+ editRequested = Signal(dict)
+
def __init__(
self,
parent = None,
@@ -43,8 +45,11 @@ class ALTimerTaskItemWidget(QWidget):
super().__init__(parent)
self.__timer_task = timer_task
+ self.__manage_widget = parent
self.modifyUi()
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+ self.customContextMenuRequested.connect(self.showContextMenu)
def modifyUi(
@@ -145,6 +150,27 @@ class ALTimerTaskItemWidget(QWidget):
self.DeleteButton.setEnabled(False)
self.setFixedHeight(55)
+ @Slot(object)
+ def showContextMenu(
+ self,
+ pos
+ ):
+
+ menu = QMenu(self)
+ edit_action = QAction("编辑", self)
+ edit_action.triggered.connect(
+ lambda: self.editRequested.emit(self.__timer_task)
+ )
+ menu.addAction(edit_action)
+ if self.__timer_task["status"] != ALTimerTaskStatus.RUNNING\
+ and self.__timer_task["status"] != ALTimerTaskStatus.READY:
+ delete_action = QAction("删除", self)
+ delete_action.triggered.connect(
+ lambda: self.__manage_widget.deleteTask(self.__timer_task)
+ )
+ menu.addAction(delete_action)
+ menu.exec(self.mapToGlobal(pos))
+
class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
@@ -224,8 +250,8 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
task["status"] = ALTimerTaskStatus(task["status"])
- if "history" in task:
- for item in task["history"]:
+ if "repeat_history" in task:
+ for item in task["repeat_history"]:
item["result"] = ALTimerTaskStatus(item["result"])
return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误")
@@ -248,8 +274,8 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
task["added_time"] = task["added_time"].strftime("%Y-%m-%d %H:%M:%S")
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value
- if "history" in task:
- for item in task["history"]:
+ if "repeat_history" in task:
+ for item in task["repeat_history"]:
item["result"] = item["result"].value
self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks })
return True
@@ -363,6 +389,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task)
)
+ widget.editRequested.connect(self.editTask)
item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget)
@@ -378,15 +405,30 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit()
+
+ def editTask(
+ self,
+ timer_task: dict
+ ):
+
+ dialog = ALTimerTaskAddDialog(self, timer_task)
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ updated = dialog.getTimerTask()
+ for i, task in enumerate(self.__timer_tasks):
+ if task["uuid"] == updated["uuid"]:
+ self.__timer_tasks[i] = updated
+ break
+ self.timerTasksChanged.emit()
+
@staticmethod
def getTimerTaskDetailMessage(
timer_task: dict
):
- if "history" not in timer_task:
+ if "repeat_history" not in timer_task:
history = []
else:
- history = timer_task["history"]
+ history = timer_task["repeat_history"]
history_count = len(history)
return (
f"任务名称:{timer_task["name"]}\n"
@@ -395,7 +437,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n"
f"已记录次数:{history_count}"
)
-
+
def deleteTask(
self,
@@ -559,7 +601,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.updateTimerTaskList()
self.updateStat()
-
@Slot(dict)
def onTimerTaskIsRunning(
self,
@@ -584,12 +625,12 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
ALTimerTaskStatus.OUTDATED}
if status not in valid_statuses:
return timer_task
- if "history" not in timer_task:
- timer_task["history"] = []
+ if "repeat_history" not in timer_task:
+ timer_task["repeat_history"] = []
if status != ALTimerTaskStatus.OUTDATED:
executed_time = datetime.now()
duration = (executed_time - timer_task["execute_time"]).total_seconds()
- timer_task["history"].append({
+ timer_task["repeat_history"].append({
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
"result": status,
@@ -603,7 +644,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
delta_days = (current_time - execute_time).days
for i in range(delta_days + 1):
if (execute_weekday + i)%7 in timer_task["repeat_days"]:
- timer_task["history"].append({
+ timer_task["repeat_history"].append({
"execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
"result": status,
diff --git a/src/gui/__init__.py b/src/gui/__init__.py
index c0cf4b0..6e687ba 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.
+ - 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/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/AutoScriptEngine.py b/src/utils/AutoScriptEngine.py
new file mode 100644
index 0000000..7504ea6
--- /dev/null
+++ b/src/utils/AutoScriptEngine.py
@@ -0,0 +1,386 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (c) 2026 KenanZhu.
+All rights reserved.
+
+This software is provided "as is", without any warranty of any kind.
+You may use, modify, and distribute this file under the terms of the MIT License.
+See the LICENSE file for details.
+"""
+import re
+from datetime import datetime, timedelta
+
+
+class AutoScriptEngine:
+ """
+ AutoScript script engine.
+
+ Parses and executes AutoScript — a lightweight scripting DSL
+ used in repeatable timer tasks to preprocess user reservation
+ data before automation runs.
+
+ Supports IF/ELSE IF/ELSE/END IF control flow, SET assignments,
+ .ADD./.SUB. operations on Date/Time fields, and rich comparison
+ operators (.EQ. .NEQ. .BGT. .BLT. .BGE. .BLE.).
+
+ Examples:
+ >>> engine = AutoScriptEngine
+ >>> user = {
+ ... "username": "test",
+ ... "enabled": True,
+ ... "reserve_info": {"date": "2026-05-07"}
+ ... }
+ >>> engine.execute(
+ ... 'IF(CURRENT_TIME .BGT. TIME(19:00))\\n'
+ ... ' RESERVE_DATE .ADD. 1\\n'
+ ... 'END IF',
+ ... user
+ ... )
+ """
+ COMPARE_OPS = { # compare operators
+ ".EQ." : lambda a, b: a == b,
+ ".NEQ.": lambda a, b: a != b,
+ ".BGT.": lambda a, b: a > b,
+ ".BLT.": lambda a, b: a < b,
+ ".BGE.": lambda a, b: a >= b,
+ ".BLE.": lambda a, b: a <= b,
+ }
+ VARIABLE_META = { # variable metadata
+ "预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
+ "预约结束时间": ("RESERVE_END_TIME", "Time"),
+ "预约日期": ("RESERVE_DATE", "Date"),
+ "用户名": ("USERNAME", "String"),
+ "用户启用": ("USER_ENABLE", "Boolean"),
+ "当前时间": ("CURRENT_TIME", "Time"),
+ "当前日期": ("CURRENT_DATE", "Date"),
+ }
+ _FIELD_TYPE_MAP = {meta[0]: meta[1] for meta in VARIABLE_META.values()}
+
+ @staticmethod
+ def execute(
+ script_text: str,
+ user_data: dict
+ ):
+ """
+ Execute an AutoScript against the given user data.
+
+ The script is parsed line-by-line. All modifications are
+ applied directly to ``user_data`` in-place.
+
+ Args:
+ script_text (str): Raw AutoScript source code.
+ user_data (dict): User data dictionary to read from and
+ write to. Must conform to the standard user profile
+ structure (username, enabled, reserve_info, etc.).
+
+ Raises:
+ ValueError: On any syntax or type error encountered
+ during parsing or execution.
+ """
+
+ if not script_text or not script_text.strip():
+ return
+ lines = [l.strip() for l in script_text.split("\n") if l.strip()]
+ if not lines:
+ return
+ if_stack = []
+
+ for line in lines:
+ upper_line = line.upper().strip()
+ if upper_line.startswith("IF("):
+ cond_end = _findConditionEnd(upper_line)
+ if cond_end < 0:
+ raise ValueError("AutoScript 语法错误: IF 缺少右括号")
+ condition_str = line[3:cond_end].strip()
+ matched = AutoScriptEngine._evaluateCondition(
+ condition_str, user_data
+ )
+ if_stack.append([matched, matched])
+ elif upper_line.startswith("ELSE IF("):
+ if not if_stack:
+ raise ValueError("AutoScript 语法错误: ELSE IF 前缺少 IF")
+ cond_end = _findConditionEnd(upper_line)
+ if cond_end < 0:
+ raise ValueError("AutoScript 语法错误: ELSE IF 缺少右括号")
+ condition_str = line[8:cond_end].strip()
+ _, has_matched = if_stack[-1]
+ if not has_matched:
+ matched = AutoScriptEngine._evaluateCondition(
+ condition_str, user_data
+ )
+ if_stack[-1] = [matched, matched]
+ else:
+ if_stack[-1][0] = False
+ elif upper_line == "ELSE":
+ if not if_stack:
+ raise ValueError("AutoScript 语法错误: ELSE 前缺少 IF")
+ _, has_matched = if_stack[-1]
+ if not has_matched:
+ if_stack[-1] = [True, True]
+ else:
+ if_stack[-1][0] = False
+ elif upper_line in ("ENDIF", "END IF"):
+ if not if_stack:
+ raise ValueError("AutoScript 语法错误: ENDIF/END IF 前缺少 IF")
+ if_stack.pop()
+ elif upper_line.startswith("SET "):
+ should_execute = (
+ all(ctx[0] for ctx in if_stack) if if_stack else True
+ )
+ if should_execute:
+ AutoScriptEngine._executeSet(line, user_data)
+ elif upper_line == "PASS":
+ continue
+ else:
+ should_execute = (
+ all(ctx[0] for ctx in if_stack) if if_stack else True
+ )
+ if should_execute:
+ AutoScriptEngine._executeOperation(line, user_data)
+ if if_stack:
+ raise ValueError("AutoScript 语法错误: IF 与 ENDIF/END IF 不匹配")
+
+ @staticmethod
+ def _resolveField(
+ field_name: str,
+ user_data: dict
+ ):
+
+ upper_name = field_name.upper().strip()
+ if upper_name == "CURRENT_DATE":
+ return datetime.now().strftime("%Y-%m-%d")
+ elif upper_name == "CURRENT_TIME":
+ return datetime.now().strftime("%H:%M")
+ elif upper_name == "USERNAME":
+ return user_data.get("username", "")
+ elif upper_name == "USER_ENABLE":
+ return user_data.get("enabled", False)
+ elif upper_name == "RESERVE_DATE":
+ return user_data.get("reserve_info", {}).get("date", "")
+ elif upper_name == "RESERVE_BEGIN_TIME":
+ return (
+ user_data
+ .get("reserve_info", {})
+ .get("begin_time", {})
+ .get("time", "")
+ )
+ elif upper_name == "RESERVE_END_TIME":
+ return (
+ user_data
+ .get("reserve_info", {})
+ .get("end_time", {})
+ .get("time", "")
+ )
+ return ""
+
+ @staticmethod
+ def _resolveValue(
+ value_str: str,
+ user_data: dict
+ ):
+
+ s = value_str.strip()
+ time_match = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE)
+ if time_match:
+ h, m = time_match.group(1), time_match.group(2)
+ return f"{int(h):02d}:{int(m):02d}"
+ date_match = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE)
+ if date_match:
+ y, mo, d = date_match.group(1), date_match.group(2), date_match.group(3)
+ return f"{int(y):04d}-{int(mo):02d}-{int(d):02d}"
+ if s.upper() == ".TRUE.":
+ return True
+ if s.upper() == ".FALSE.":
+ return False
+ if s.startswith("'") and s.endswith("'"):
+ inner = s[1:-1].replace("''", "'")
+ return inner
+ if s.startswith('"') and s.endswith('"'):
+ return s[1:-1]
+ relDate = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE)
+ if relDate:
+ days = int(relDate.group(1))
+ return (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
+ relTime = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE)
+ if relTime:
+ hours = int(relTime.group(1))
+ return (datetime.now() + timedelta(hours=hours)).strftime("%H:%M")
+ try:
+ return int(s)
+ except ValueError:
+ pass
+ try:
+ return float(s)
+ except ValueError:
+ pass
+ resolved = AutoScriptEngine._resolveField(s, user_data)
+ return resolved
+
+ @staticmethod
+ def _setField(
+ field_name: str,
+ value: str,
+ user_data: dict
+ ):
+ upper_name = field_name.upper().strip()
+ if upper_name == "RESERVE_DATE":
+ user_data.setdefault("reserve_info", {})["date"] = value
+ elif upper_name == "RESERVE_BEGIN_TIME":
+ ri = user_data.setdefault("reserve_info", {})
+ ri.setdefault("begin_time", {})["time"] = value
+ elif upper_name == "RESERVE_END_TIME":
+ ri = user_data.setdefault("reserve_info", {})
+ ri.setdefault("end_time", {})["time"] = value
+ elif upper_name == "USERNAME":
+ user_data["username"] = value
+ elif upper_name == "USER_ENABLE":
+ if isinstance(value, bool):
+ user_data["enabled"] = value
+ else:
+ user_data["enabled"] = (str(value).upper() == "TRUE")
+
+ @staticmethod
+ def _evaluateCondition(
+ condition_str: str,
+ user_data: dict
+ ) -> bool:
+
+ for op, cmp_func in AutoScriptEngine.COMPARE_OPS.items():
+ if op not in condition_str.upper():
+ continue
+ idx = condition_str.upper().find(op)
+ parts = [condition_str[:idx], condition_str[idx + len(op):]]
+ if len(parts) != 2:
+ continue
+ field_name = parts[0].strip()
+ value_str = parts[1].strip()
+ left_val = AutoScriptEngine._resolveField(field_name, user_data)
+ right_val = AutoScriptEngine._resolveValue(value_str, user_data)
+ try:
+ return cmp_func(left_val, right_val)
+ except TypeError:
+ raise ValueError(
+ f"AutoScript 语法错误: 无法比较 "
+ f"'{field_name}' ({type(left_val).__name__}) "
+ f"与 '{value_str}' ({type(right_val).__name__})"
+ )
+ return False
+
+ @staticmethod
+ def _executeSet(
+ line: str,
+ user_data: dict
+ ):
+ rest = line[3:].strip()
+ eq_idx = rest.find("=")
+ if eq_idx < 0:
+ return
+ field_name = rest[:eq_idx].strip()
+ value_str = rest[eq_idx + 1:].strip()
+ if not field_name:
+ return
+ resolved = AutoScriptEngine._resolveValue(value_str, user_data)
+ AutoScriptEngine._setField(field_name, resolved, user_data)
+
+ @staticmethod
+ def _executeOperation(
+ line: str,
+ user_data: dict
+ ):
+
+ parts = line.split()
+ if len(parts) < 3:
+ return
+ field_name = parts[0].upper().strip()
+ op = parts[1].upper().strip()
+ raw_value = parts[2].strip()
+ field_type = AutoScriptEngine._FIELD_TYPE_MAP.get(field_name)
+ if not field_type:
+ raise ValueError(
+ f"AutoScript 语法错误: 未知字段 '{field_name}'"
+ )
+ try:
+ num_value = float(raw_value) if "." in raw_value else int(raw_value)
+ except (ValueError, TypeError):
+ raise ValueError(
+ f"AutoScript 语法错误: 无效操作数 '{raw_value}'"
+ )
+ if field_type == "Date":
+ date_str = AutoScriptEngine._resolveField(field_name, user_data)
+ if not date_str:
+ return
+ try:
+ date_obj = datetime.strptime(date_str, "%Y-%m-%d")
+ except (ValueError, TypeError):
+ return
+ if op == ".ADD.":
+ date_obj += timedelta(days=num_value)
+ elif op == ".SUB.":
+ date_obj -= timedelta(days=num_value)
+ else:
+ raise ValueError(
+ f"AutoScript 语法错误: Date 类型不支持操作 '{op}'"
+ )
+ AutoScriptEngine._setField(
+ field_name, date_obj.strftime("%Y-%m-%d"), user_data
+ )
+ elif field_type == "Time":
+ time_str = AutoScriptEngine._resolveField(field_name, user_data)
+ if not time_str:
+ return
+ try:
+ time_obj = datetime.strptime(time_str, "%H:%M")
+ except (ValueError, TypeError):
+ return
+ if op == ".ADD.":
+ time_obj += timedelta(hours=num_value)
+ elif op == ".SUB.":
+ time_obj -= timedelta(hours=num_value)
+ else:
+ raise ValueError(
+ f"AutoScript 语法错误: Time 类型不支持操作 '{op}'"
+ )
+ AutoScriptEngine._setField(
+ field_name, time_obj.strftime("%H:%M"), user_data
+ )
+ elif field_type in ("String", "Boolean"):
+ raise ValueError(
+ f"AutoScript 语法错误: '{field_type}' 类型字段不支持操作运算"
+ )
+ else:
+ raise ValueError(
+ f"AutoScript 语法错误: 未知字段类型 '{field_type}'"
+ )
+
+
+def _findConditionEnd(
+ upper_line: str
+) -> int:
+ """
+ Find the index of the closing parenthesis that matches the
+ opening parenthesis in a condition expression, handling nested
+ parentheses and optional ``THEN`` keyword.
+
+ Args:
+ upper_line (str): The uppercased line text containing the
+ condition, e.g. ``"IF(A .BGT. B) THEN"``.
+
+ Returns:
+ int: Index of the matching ``)``, or ``-1`` if no match
+ is found.
+ """
+
+ line = upper_line.rstrip()
+ if line.endswith(" THEN"):
+ line = line[:-5].rstrip()
+ paren_depth = 0
+ start_found = False
+ for i, ch in enumerate(line):
+ if ch == "(":
+ paren_depth += 1
+ start_found = True
+ elif ch == ")":
+ paren_depth -= 1
+ if start_found and paren_depth == 0:
+ return i
+ return -1
diff --git a/src/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