mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 07:23:03 +08:00
feat(autoscript): 将预处理脚本重构为 AutoScript DSL,新增可视化编排与预览对话框
This commit is contained in:
@@ -17,10 +17,10 @@ from PySide6.QtWidgets import (
|
|||||||
QGroupBox, QSizePolicy
|
QGroupBox, QSizePolicy
|
||||||
)
|
)
|
||||||
|
|
||||||
from utils.PreprocEngine import PreprocEngine
|
from utils.AutoScriptEngine import AutoScriptEngine
|
||||||
|
|
||||||
|
|
||||||
VARIABLE_META = PreprocEngine.VARIABLE_META
|
VARIABLE_META = AutoScriptEngine.VARIABLE_META
|
||||||
|
|
||||||
_VAR_COMBO_ITEMS = [
|
_VAR_COMBO_ITEMS = [
|
||||||
(display, varname, vartype)
|
(display, varname, vartype)
|
||||||
@@ -637,7 +637,7 @@ class ConditionalBlock(QGroupBox):
|
|||||||
return len(self._actionWidgets)
|
return len(self._actionWidgets)
|
||||||
|
|
||||||
|
|
||||||
class ALPreprocOrchDialog(QDialog):
|
class ALAutoScriptOrchDialog(QDialog):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -661,7 +661,7 @@ class ALPreprocOrchDialog(QDialog):
|
|||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self.setWindowTitle("预处理指令编排 - AutoLibrary")
|
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
|
||||||
self.setMinimumSize(420, 400)
|
self.setMinimumSize(420, 400)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
mainLayout = QVBoxLayout(self)
|
mainLayout = QVBoxLayout(self)
|
||||||
@@ -31,41 +31,41 @@ class ALScriptHighlighter(QSyntaxHighlighter):
|
|||||||
keywordFmt.setForeground(QColor("#316BFF"))
|
keywordFmt.setForeground(QColor("#316BFF"))
|
||||||
keywordFmt.setFontWeight(QFont.Weight.Bold)
|
keywordFmt.setFontWeight(QFont.Weight.Bold)
|
||||||
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
|
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
|
||||||
"SET", "PASS", "THEN", ".TRUE.", ".FALSE."]:
|
"SET", "PASS", "THEN"]:
|
||||||
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
|
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
|
||||||
self._rules.append((pattern, keywordFmt))
|
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 = QTextCharFormat()
|
||||||
opFmt.setForeground(QColor("#9C27B0"))
|
opFmt.setForeground(QColor("#9C27B0"))
|
||||||
for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
|
for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
|
||||||
r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]:
|
r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]:
|
||||||
self._rules.append((op, opFmt))
|
self._rules.append((op, opFmt))
|
||||||
|
|
||||||
varFmt = QTextCharFormat()
|
varFmt = QTextCharFormat()
|
||||||
varFmt.setForeground(QColor("#E65100"))
|
varFmt.setForeground(QColor("#E65100"))
|
||||||
for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME",
|
for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME",
|
||||||
"RESERVE_DATE", "USERNAME", "USER_ENABLE",
|
"RESERVE_DATE", "USERNAME", "USER_ENABLE",
|
||||||
"PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]:
|
"PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]:
|
||||||
self._rules.append((r"\b" + var + r"\b", varFmt))
|
self._rules.append((r"\b" + var + r"\b", varFmt))
|
||||||
|
|
||||||
funcFmt = QTextCharFormat()
|
funcFmt = QTextCharFormat()
|
||||||
funcFmt.setForeground(QColor("#2E7D32"))
|
funcFmt.setForeground(QColor("#2E7D32"))
|
||||||
self._rules.append((r"\bTIME\([^)]+\)", funcFmt))
|
self._rules.append((r"\bTIME\([^)]+\)", funcFmt))
|
||||||
self._rules.append((r"\bDATE\([^)]+\)", funcFmt))
|
self._rules.append((r"\bDATE\([^)]+\)", funcFmt))
|
||||||
|
|
||||||
strFmt = QTextCharFormat()
|
strFmt = QTextCharFormat()
|
||||||
strFmt.setForeground(QColor("#388E3C"))
|
strFmt.setForeground(QColor("#388E3C"))
|
||||||
self._rules.append((r"'[^']*'", strFmt))
|
self._rules.append((r"'[^']*'", strFmt))
|
||||||
|
|
||||||
numFmt = QTextCharFormat()
|
numFmt = QTextCharFormat()
|
||||||
numFmt.setForeground(QColor("#D32F2F"))
|
numFmt.setForeground(QColor("#D32F2F"))
|
||||||
self._rules.append((r"\b\d+\b", numFmt))
|
self._rules.append((r"\b\d+\b", numFmt))
|
||||||
|
|
||||||
commentFmt = QTextCharFormat()
|
commentFmt = QTextCharFormat()
|
||||||
commentFmt.setForeground(QColor("#999999"))
|
commentFmt.setForeground(QColor("#999999"))
|
||||||
commentFmt.setFontItalic(True)
|
commentFmt.setFontItalic(True)
|
||||||
self._rules.append((r"//[^\n]*", commentFmt))
|
self._rules.append((r"//[^\n]*", commentFmt))
|
||||||
|
|
||||||
|
|
||||||
def highlightBlock(
|
def highlightBlock(
|
||||||
self,
|
self,
|
||||||
text
|
text
|
||||||
@@ -79,7 +79,7 @@ class ALScriptHighlighter(QSyntaxHighlighter):
|
|||||||
self.setFormat(start, length, fmt)
|
self.setFormat(start, length, fmt)
|
||||||
|
|
||||||
|
|
||||||
class ALScriptPreviewDialog(QDialog):
|
class ALAutoScriptPreviewDialog(QDialog):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -88,7 +88,6 @@ class ALScriptPreviewDialog(QDialog):
|
|||||||
):
|
):
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.__fontSize = 13
|
self.__fontSize = 13
|
||||||
|
|
||||||
self.modifyUi()
|
self.modifyUi()
|
||||||
@@ -104,7 +103,7 @@ class ALScriptPreviewDialog(QDialog):
|
|||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
self.setWindowTitle("预处理脚本预览 - AutoLibrary")
|
self.setWindowTitle("AutoScript 预览 - AutoLibrary")
|
||||||
self.setMinimumSize(520, 360)
|
self.setMinimumSize(520, 360)
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
@@ -136,7 +135,6 @@ class ALScriptPreviewDialog(QDialog):
|
|||||||
self._copyBtn.setToolTip("复制脚本")
|
self._copyBtn.setToolTip("复制脚本")
|
||||||
toolbarLayout.addWidget(self._copyBtn)
|
toolbarLayout.addWidget(self._copyBtn)
|
||||||
layout.addLayout(toolbarLayout)
|
layout.addLayout(toolbarLayout)
|
||||||
|
|
||||||
self._textEdit = QPlainTextEdit(self)
|
self._textEdit = QPlainTextEdit(self)
|
||||||
self._textEdit.setReadOnly(True)
|
self._textEdit.setReadOnly(True)
|
||||||
self._textEdit.setLineWrapMode(
|
self._textEdit.setLineWrapMode(
|
||||||
@@ -18,7 +18,7 @@ from PySide6.QtCore import (
|
|||||||
from base.MsgBase import MsgBase
|
from base.MsgBase import MsgBase
|
||||||
from operators.AutoLib import AutoLib
|
from operators.AutoLib import AutoLib
|
||||||
from utils.JSONReader import JSONReader
|
from utils.JSONReader import JSONReader
|
||||||
from utils.PreprocEngine import PreprocEngine
|
from utils.AutoScriptEngine import AutoScriptEngine
|
||||||
|
|
||||||
|
|
||||||
class AutoLibWorker(MsgBase, QThread):
|
class AutoLibWorker(MsgBase, QThread):
|
||||||
@@ -161,6 +161,7 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
|
self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
|
||||||
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
|
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
|
||||||
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
@@ -173,7 +174,7 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
try:
|
try:
|
||||||
if not self.loadConfigs():
|
if not self.loadConfigs():
|
||||||
raise Exception("配置文件加载失败")
|
raise Exception("配置文件加载失败")
|
||||||
self._applyRepeatPreproc()
|
self._applyRepeatAutoScript()
|
||||||
auto_lib = AutoLib(
|
auto_lib = AutoLib(
|
||||||
self._input_queue,
|
self._input_queue,
|
||||||
self._output_queue,
|
self._output_queue,
|
||||||
@@ -206,15 +207,15 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
||||||
|
|
||||||
|
|
||||||
def _applyRepeatPreproc(
|
def _applyRepeatAutoScript(
|
||||||
self
|
self
|
||||||
):
|
):
|
||||||
|
|
||||||
preproc_script = self.__timer_task.get("repeat_preproc", "")
|
auto_script = self.__timer_task.get("repeat_auto_script", "")
|
||||||
if not preproc_script or not preproc_script.strip():
|
if not auto_script or not auto_script.strip():
|
||||||
return
|
return
|
||||||
self._showTrace(
|
self._showTrace(
|
||||||
f"检测到重复定时任务预处理脚本, 开始执行...",
|
f"检测到重复定时任务 AutoScript, 开始执行...",
|
||||||
no_log=True
|
no_log=True
|
||||||
)
|
)
|
||||||
groups = self._user_config.get("groups", [])
|
groups = self._user_config.get("groups", [])
|
||||||
@@ -224,15 +225,15 @@ class TimerTaskWorker(AutoLibWorker):
|
|||||||
continue
|
continue
|
||||||
for user in group.get("users", []):
|
for user in group.get("users", []):
|
||||||
try:
|
try:
|
||||||
PreprocEngine.execute(preproc_script, user)
|
AutoScriptEngine.execute(auto_script, user)
|
||||||
affected_count += 1
|
affected_count += 1
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self._showTrace(
|
self._showTrace(
|
||||||
f"预处理脚本执行错误 (用户 {user['username']}): {e}",
|
f"AutoScript 执行错误 (用户 {user['username']}): {e}",
|
||||||
self.TraceLevel.ERROR
|
self.TraceLevel.ERROR
|
||||||
)
|
)
|
||||||
self._showLog(
|
self._showLog(
|
||||||
f"预处理脚本执行完毕, "
|
f"AutoScript 执行完毕, "
|
||||||
f"影响 {affected_count} 个用户",
|
f"影响 {affected_count} 个用户",
|
||||||
self.TraceLevel.INFO
|
self.TraceLevel.INFO
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import uuid
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from PySide6.QtCore import Slot, QDateTime
|
from PySide6.QtCore import Slot, QDateTime, QUrl
|
||||||
|
from PySide6.QtGui import QDesktopServices
|
||||||
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton
|
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton
|
||||||
|
|
||||||
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
|
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
|
||||||
from gui.ALPreprocOrchDialog import ALPreprocOrchDialog
|
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
|
||||||
from utils.TimerUtils import TimerUtils
|
from utils.TimerUtils import TimerUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -87,32 +88,46 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
|||||||
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
|
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
|
||||||
self.RelativeTimerWidget.setVisible(False)
|
self.RelativeTimerWidget.setVisible(False)
|
||||||
|
|
||||||
self.PreprocGroupBox = QGroupBox("预处理脚本")
|
self.AutoScriptGroupBox = QGroupBox("AutoScript 指令")
|
||||||
self.PreprocLayout = QVBoxLayout(self.PreprocGroupBox)
|
self.AutoScriptLayout = QVBoxLayout(self.AutoScriptGroupBox)
|
||||||
self.PreprocLayout.setContentsMargins(3, 3, 3, 3)
|
self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
|
||||||
self.PreprocLayout.setSpacing(3)
|
self.AutoScriptLayout.setSpacing(3)
|
||||||
|
autoScriptBtnLayout = QHBoxLayout()
|
||||||
preproc_btn_layout = QHBoxLayout()
|
self.AutoScriptSetButton = QPushButton("设置指令")
|
||||||
self.PreprocSetButton = QPushButton("设置预处理指令")
|
self.AutoScriptSetButton.setMinimumHeight(25)
|
||||||
self.PreprocSetButton.setMinimumHeight(25)
|
self.AutoScriptSetButton.setFixedWidth(130)
|
||||||
self.PreprocSetButton.setFixedWidth(130)
|
autoScriptBtnLayout.addWidget(self.AutoScriptSetButton)
|
||||||
preproc_btn_layout.addWidget(self.PreprocSetButton)
|
self.AutoScriptPreviewButton = QPushButton("预览")
|
||||||
self.PreprocPreviewButton = QPushButton("预览")
|
self.AutoScriptPreviewButton.setMinimumHeight(25)
|
||||||
self.PreprocPreviewButton.setMinimumHeight(25)
|
self.AutoScriptPreviewButton.setFixedWidth(60)
|
||||||
self.PreprocPreviewButton.setFixedWidth(60)
|
self.AutoScriptPreviewButton.setEnabled(False)
|
||||||
self.PreprocPreviewButton.setEnabled(False)
|
autoScriptBtnLayout.addWidget(self.AutoScriptPreviewButton)
|
||||||
preproc_btn_layout.addWidget(self.PreprocPreviewButton)
|
autoScriptBtnLayout.addStretch()
|
||||||
preproc_btn_layout.addStretch()
|
self.AutoScriptHelpButton = QPushButton("?")
|
||||||
self.PreprocStatusLabel = QLabel("未设置")
|
self.AutoScriptHelpButton.setFixedSize(20, 20)
|
||||||
self.PreprocStatusLabel.setStyleSheet("color: #969696;")
|
self.AutoScriptHelpButton.setToolTip(
|
||||||
self.PreprocStatusLabel.setFixedHeight(25)
|
"AutoScript 是一种轻量级 DSL\n"
|
||||||
preproc_btn_layout.addWidget(self.PreprocStatusLabel)
|
"用于在重复定时任务执行前,对用户的预约数据进行预处理\n"
|
||||||
self.PreprocLayout.addLayout(preproc_btn_layout)
|
"\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.insertWidget(
|
||||||
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
|
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
|
||||||
self.PreprocGroupBox
|
self.AutoScriptGroupBox
|
||||||
)
|
)
|
||||||
self.__repeat_preproc_script = ""
|
self.AutoScriptGroupBox.setVisible(False)
|
||||||
|
self.__auto_script = ""
|
||||||
|
|
||||||
|
|
||||||
def connectSignals(
|
def connectSignals(
|
||||||
@@ -123,35 +138,44 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
|||||||
self.ConfirmButton.clicked.connect(self.accept)
|
self.ConfirmButton.clicked.connect(self.accept)
|
||||||
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
|
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
|
||||||
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
|
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
|
||||||
self.PreprocSetButton.clicked.connect(self._onSetPreproc)
|
self.AutoScriptSetButton.clicked.connect(self._onSetAutoScript)
|
||||||
self.PreprocPreviewButton.clicked.connect(self._onPreviewPreproc)
|
self.AutoScriptPreviewButton.clicked.connect(self._onPreviewAutoScript)
|
||||||
|
self.AutoScriptHelpButton.clicked.connect(self._onAutoScriptHelp)
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def _onSetPreproc(self):
|
def _onSetAutoScript(self):
|
||||||
dlg = ALPreprocOrchDialog(self, existingScript=self.__repeat_preproc_script)
|
dlg = ALAutoScriptOrchDialog(self, existingScript=self.__auto_script)
|
||||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
script = dlg.getScript()
|
script = dlg.getScript()
|
||||||
self.__repeat_preproc_script = script
|
self.__auto_script = script
|
||||||
if script:
|
if script:
|
||||||
self.PreprocStatusLabel.setText("已设置")
|
self.AutoScriptStatusLabel.setText("已设置")
|
||||||
self.PreprocStatusLabel.setStyleSheet("color: #4CAF50;")
|
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
|
||||||
self.PreprocPreviewButton.setEnabled(True)
|
self.AutoScriptPreviewButton.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
self.PreprocStatusLabel.setText("未设置")
|
self.AutoScriptStatusLabel.setText("未设置")
|
||||||
self.PreprocStatusLabel.setStyleSheet("color: #969696;")
|
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
|
||||||
self.PreprocPreviewButton.setEnabled(False)
|
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()
|
dlg.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def _onPreviewPreproc(self):
|
def _onAutoScriptHelp(
|
||||||
if not self.__repeat_preproc_script:
|
self
|
||||||
return
|
):
|
||||||
from gui.ALPreProcPrevDialog import ALScriptPreviewDialog
|
|
||||||
dlg = ALScriptPreviewDialog(self, self.__repeat_preproc_script)
|
QDesktopServices.openUrl(
|
||||||
dlg.exec()
|
QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript")
|
||||||
dlg.deleteLater()
|
)
|
||||||
|
|
||||||
|
|
||||||
def getTimerTask(
|
def getTimerTask(
|
||||||
@@ -186,7 +210,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
|||||||
"status": ALTimerTaskStatus.PENDING,
|
"status": ALTimerTaskStatus.PENDING,
|
||||||
"executed": False,
|
"executed": False,
|
||||||
"repeat": self.RepeatCheckBox.isChecked(),
|
"repeat": self.RepeatCheckBox.isChecked(),
|
||||||
"repeat_preproc": self.__repeat_preproc_script,
|
"repeat_auto_script": self.__auto_script,
|
||||||
}
|
}
|
||||||
if task_data["repeat"]:
|
if task_data["repeat"]:
|
||||||
task_data["history"] = [] # repeat history
|
task_data["history"] = [] # repeat history
|
||||||
@@ -240,4 +264,5 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
|||||||
self.ThuCheckBox.setEnabled(checked)
|
self.ThuCheckBox.setEnabled(checked)
|
||||||
self.FriCheckBox.setEnabled(checked)
|
self.FriCheckBox.setEnabled(checked)
|
||||||
self.SatCheckBox.setEnabled(checked)
|
self.SatCheckBox.setEnabled(checked)
|
||||||
self.SunCheckBox.setEnabled(checked)
|
self.SunCheckBox.setEnabled(checked)
|
||||||
|
self.AutoScriptGroupBox.setVisible(checked)
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
- ALSeatMapTable: Seat map table class.
|
- ALSeatMapTable: Seat map table class.
|
||||||
- ALSeatMapSelectDialog: Seat map select dialog class.
|
- ALSeatMapSelectDialog: Seat map select dialog class.
|
||||||
- ALTimerTaskAddDialog: Timer task add dialog class.
|
- ALTimerTaskAddDialog: Timer task add dialog class.
|
||||||
- ALPreprocOrchDialog: Preprocessing script orchestration dialog class.
|
- ALAutoScriptOrchDialog: AutoScript orchestration dialog class.
|
||||||
- ALTimerTaskHistoryDialog: Timer task history dialog class.
|
- ALTimerTaskHistoryDialog: Timer task history dialog class.
|
||||||
- ALTimerTaskManageWidget: Timer task manage widget class.
|
- ALTimerTaskManageWidget: Timer task manage widget class.
|
||||||
- ALUserTreeWidget: User tree widget class.
|
- ALUserTreeWidget: User tree widget class.
|
||||||
|
|||||||
@@ -11,39 +11,78 @@ import re
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
class PreprocEngine:
|
class AutoScriptEngine:
|
||||||
|
"""
|
||||||
|
AutoScript script engine.
|
||||||
|
|
||||||
COMPARE_OPS = {
|
Parses and executes AutoScript — a lightweight scripting DSL
|
||||||
".EQ.": lambda a, b: a == b,
|
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,
|
".NEQ.": lambda a, b: a != b,
|
||||||
".BGT.": lambda a, b: a > b,
|
".BGT.": lambda a, b: a > b,
|
||||||
".BLT.": lambda a, b: a < b,
|
".BLT.": lambda a, b: a < b,
|
||||||
".BGE.": lambda a, b: a >= b,
|
".BGE.": lambda a, b: a >= b,
|
||||||
".BLE.": lambda a, b: a <= b,
|
".BLE.": lambda a, b: a <= b,
|
||||||
}
|
}
|
||||||
|
VARIABLE_META = { # variable metadata
|
||||||
VARIABLE_META = {
|
|
||||||
"预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
|
"预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
|
||||||
"预约结束时间": ("RESERVE_END_TIME", "Time"),
|
"预约结束时间": ("RESERVE_END_TIME", "Time"),
|
||||||
"预约日期": ("RESERVE_DATE", "Date"),
|
"预约日期": ("RESERVE_DATE", "Date"),
|
||||||
"用户名": ("USERNAME", "String"),
|
"用户名": ("USERNAME", "String"),
|
||||||
"用户启用": ("USER_ENABLE", "Boolean"),
|
"用户启用": ("USER_ENABLE", "Boolean"),
|
||||||
"当前时间": ("CURRENT_TIME", "Time"),
|
"当前时间": ("CURRENT_TIME", "Time"),
|
||||||
"当前日期": ("CURRENT_DATE", "Date"),
|
"当前日期": ("CURRENT_DATE", "Date"),
|
||||||
}
|
}
|
||||||
|
_FIELD_TYPE_MAP = {meta[0]: meta[1] for meta in VARIABLE_META.values()}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def execute(
|
def execute(
|
||||||
script_text: str,
|
script_text: str,
|
||||||
user_data: dict
|
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():
|
if not script_text or not script_text.strip():
|
||||||
return
|
return
|
||||||
lines = [l.strip() for l in script_text.split("\n") if l.strip()]
|
lines = [l.strip() for l in script_text.split("\n") if l.strip()]
|
||||||
if not lines:
|
if not lines:
|
||||||
return
|
return
|
||||||
|
|
||||||
if_stack = []
|
if_stack = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@@ -51,22 +90,22 @@ class PreprocEngine:
|
|||||||
if upper_line.startswith("IF("):
|
if upper_line.startswith("IF("):
|
||||||
cond_end = _findConditionEnd(upper_line)
|
cond_end = _findConditionEnd(upper_line)
|
||||||
if cond_end < 0:
|
if cond_end < 0:
|
||||||
raise ValueError("语法错误: IF 缺少右括号")
|
raise ValueError("AutoScript 语法错误: IF 缺少右括号")
|
||||||
condition_str = line[3:cond_end].strip()
|
condition_str = line[3:cond_end].strip()
|
||||||
matched = PreprocEngine._evaluateCondition(
|
matched = AutoScriptEngine._evaluateCondition(
|
||||||
condition_str, user_data
|
condition_str, user_data
|
||||||
)
|
)
|
||||||
if_stack.append([matched, matched])
|
if_stack.append([matched, matched])
|
||||||
elif upper_line.startswith("ELSE IF("):
|
elif upper_line.startswith("ELSE IF("):
|
||||||
if not if_stack:
|
if not if_stack:
|
||||||
raise ValueError("语法错误: ELSE IF 前缺少 IF")
|
raise ValueError("AutoScript 语法错误: ELSE IF 前缺少 IF")
|
||||||
cond_end = _findConditionEnd(upper_line)
|
cond_end = _findConditionEnd(upper_line)
|
||||||
if cond_end < 0:
|
if cond_end < 0:
|
||||||
raise ValueError("语法错误: ELSE IF 缺少右括号")
|
raise ValueError("AutoScript 语法错误: ELSE IF 缺少右括号")
|
||||||
condition_str = line[8:cond_end].strip()
|
condition_str = line[8:cond_end].strip()
|
||||||
_, has_matched = if_stack[-1]
|
_, has_matched = if_stack[-1]
|
||||||
if not has_matched:
|
if not has_matched:
|
||||||
matched = PreprocEngine._evaluateCondition(
|
matched = AutoScriptEngine._evaluateCondition(
|
||||||
condition_str, user_data
|
condition_str, user_data
|
||||||
)
|
)
|
||||||
if_stack[-1] = [matched, matched]
|
if_stack[-1] = [matched, matched]
|
||||||
@@ -74,7 +113,7 @@ class PreprocEngine:
|
|||||||
if_stack[-1][0] = False
|
if_stack[-1][0] = False
|
||||||
elif upper_line == "ELSE":
|
elif upper_line == "ELSE":
|
||||||
if not if_stack:
|
if not if_stack:
|
||||||
raise ValueError("语法错误: ELSE 前缺少 IF")
|
raise ValueError("AutoScript 语法错误: ELSE 前缺少 IF")
|
||||||
_, has_matched = if_stack[-1]
|
_, has_matched = if_stack[-1]
|
||||||
if not has_matched:
|
if not has_matched:
|
||||||
if_stack[-1] = [True, True]
|
if_stack[-1] = [True, True]
|
||||||
@@ -82,14 +121,14 @@ class PreprocEngine:
|
|||||||
if_stack[-1][0] = False
|
if_stack[-1][0] = False
|
||||||
elif upper_line in ("ENDIF", "END IF"):
|
elif upper_line in ("ENDIF", "END IF"):
|
||||||
if not if_stack:
|
if not if_stack:
|
||||||
raise ValueError("语法错误: ENDIF/END IF 前缺少 IF")
|
raise ValueError("AutoScript 语法错误: ENDIF/END IF 前缺少 IF")
|
||||||
if_stack.pop()
|
if_stack.pop()
|
||||||
elif upper_line.startswith("SET "):
|
elif upper_line.startswith("SET "):
|
||||||
should_execute = (
|
should_execute = (
|
||||||
all(ctx[0] for ctx in if_stack) if if_stack else True
|
all(ctx[0] for ctx in if_stack) if if_stack else True
|
||||||
)
|
)
|
||||||
if should_execute:
|
if should_execute:
|
||||||
PreprocEngine._executeSet(line, user_data)
|
AutoScriptEngine._executeSet(line, user_data)
|
||||||
elif upper_line == "PASS":
|
elif upper_line == "PASS":
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
@@ -97,55 +136,15 @@ class PreprocEngine:
|
|||||||
all(ctx[0] for ctx in if_stack) if if_stack else True
|
all(ctx[0] for ctx in if_stack) if if_stack else True
|
||||||
)
|
)
|
||||||
if should_execute:
|
if should_execute:
|
||||||
PreprocEngine._executeOperation(line, user_data)
|
AutoScriptEngine._executeOperation(line, user_data)
|
||||||
if if_stack:
|
if if_stack:
|
||||||
raise ValueError("语法错误: IF 与 ENDIF/END IF 不匹配")
|
raise ValueError("AutoScript 语法错误: 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
|
@staticmethod
|
||||||
def _resolveField(
|
def _resolveField(
|
||||||
field_name: str,
|
field_name: str,
|
||||||
user_data: dict
|
user_data: dict
|
||||||
) -> str:
|
):
|
||||||
|
|
||||||
upper_name = field_name.upper().strip()
|
upper_name = field_name.upper().strip()
|
||||||
if upper_name == "CURRENT_DATE":
|
if upper_name == "CURRENT_DATE":
|
||||||
@@ -155,7 +154,7 @@ class PreprocEngine:
|
|||||||
elif upper_name == "USERNAME":
|
elif upper_name == "USERNAME":
|
||||||
return user_data.get("username", "")
|
return user_data.get("username", "")
|
||||||
elif upper_name == "USER_ENABLE":
|
elif upper_name == "USER_ENABLE":
|
||||||
return str(user_data.get("enabled", "False"))
|
return user_data.get("enabled", False)
|
||||||
elif upper_name == "RESERVE_DATE":
|
elif upper_name == "RESERVE_DATE":
|
||||||
return user_data.get("reserve_info", {}).get("date", "")
|
return user_data.get("reserve_info", {}).get("date", "")
|
||||||
elif upper_name == "RESERVE_BEGIN_TIME":
|
elif upper_name == "RESERVE_BEGIN_TIME":
|
||||||
@@ -174,6 +173,49 @@ class PreprocEngine:
|
|||||||
)
|
)
|
||||||
return ""
|
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
|
@staticmethod
|
||||||
def _setField(
|
def _setField(
|
||||||
field_name: str,
|
field_name: str,
|
||||||
@@ -192,7 +234,10 @@ class PreprocEngine:
|
|||||||
elif upper_name == "USERNAME":
|
elif upper_name == "USERNAME":
|
||||||
user_data["username"] = value
|
user_data["username"] = value
|
||||||
elif upper_name == "USER_ENABLE":
|
elif upper_name == "USER_ENABLE":
|
||||||
user_data["enabled"] = value.upper() == "TRUE"
|
if isinstance(value, bool):
|
||||||
|
user_data["enabled"] = value
|
||||||
|
else:
|
||||||
|
user_data["enabled"] = (str(value).upper() == "TRUE")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _evaluateCondition(
|
def _evaluateCondition(
|
||||||
@@ -200,7 +245,7 @@ class PreprocEngine:
|
|||||||
user_data: dict
|
user_data: dict
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
||||||
for op, cmp_func in PreprocEngine.COMPARE_OPS.items():
|
for op, cmp_func in AutoScriptEngine.COMPARE_OPS.items():
|
||||||
if op not in condition_str.upper():
|
if op not in condition_str.upper():
|
||||||
continue
|
continue
|
||||||
idx = condition_str.upper().find(op)
|
idx = condition_str.upper().find(op)
|
||||||
@@ -209,9 +254,16 @@ class PreprocEngine:
|
|||||||
continue
|
continue
|
||||||
field_name = parts[0].strip()
|
field_name = parts[0].strip()
|
||||||
value_str = parts[1].strip()
|
value_str = parts[1].strip()
|
||||||
left_val = PreprocEngine._resolveField(field_name, user_data)
|
left_val = AutoScriptEngine._resolveField(field_name, user_data)
|
||||||
right_val = PreprocEngine._resolveValue(value_str, user_data)
|
right_val = AutoScriptEngine._resolveValue(value_str, user_data)
|
||||||
return cmp_func(left_val, right_val)
|
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
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -227,8 +279,8 @@ class PreprocEngine:
|
|||||||
value_str = rest[eq_idx + 1:].strip()
|
value_str = rest[eq_idx + 1:].strip()
|
||||||
if not field_name:
|
if not field_name:
|
||||||
return
|
return
|
||||||
resolved = PreprocEngine._resolveValue(value_str, user_data)
|
resolved = AutoScriptEngine._resolveValue(value_str, user_data)
|
||||||
PreprocEngine._setField(field_name, resolved, user_data)
|
AutoScriptEngine._setField(field_name, resolved, user_data)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _executeOperation(
|
def _executeOperation(
|
||||||
@@ -242,12 +294,19 @@ class PreprocEngine:
|
|||||||
field_name = parts[0].upper().strip()
|
field_name = parts[0].upper().strip()
|
||||||
op = parts[1].upper().strip()
|
op = parts[1].upper().strip()
|
||||||
raw_value = parts[2].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:
|
try:
|
||||||
num_value = float(raw_value) if "." in raw_value else int(raw_value)
|
num_value = float(raw_value) if "." in raw_value else int(raw_value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return
|
raise ValueError(
|
||||||
if field_name == "RESERVE_DATE":
|
f"AutoScript 语法错误: 无效操作数 '{raw_value}'"
|
||||||
date_str = user_data.get("reserve_info", {}).get("date", "")
|
)
|
||||||
|
if field_type == "Date":
|
||||||
|
date_str = AutoScriptEngine._resolveField(field_name, user_data)
|
||||||
if not date_str:
|
if not date_str:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -259,16 +318,14 @@ class PreprocEngine:
|
|||||||
elif op == ".SUB.":
|
elif op == ".SUB.":
|
||||||
date_obj -= timedelta(days=num_value)
|
date_obj -= timedelta(days=num_value)
|
||||||
else:
|
else:
|
||||||
return
|
raise ValueError(
|
||||||
user_data.setdefault("reserve_info", {})["date"] = \
|
f"AutoScript 语法错误: Date 类型不支持操作 '{op}'"
|
||||||
date_obj.strftime("%Y-%m-%d")
|
)
|
||||||
elif field_name == "RESERVE_BEGIN_TIME":
|
AutoScriptEngine._setField(
|
||||||
time_str = (
|
field_name, date_obj.strftime("%Y-%m-%d"), user_data
|
||||||
user_data
|
|
||||||
.get("reserve_info", {})
|
|
||||||
.get("begin_time", {})
|
|
||||||
.get("time", "")
|
|
||||||
)
|
)
|
||||||
|
elif field_type == "Time":
|
||||||
|
time_str = AutoScriptEngine._resolveField(field_name, user_data)
|
||||||
if not time_str:
|
if not time_str:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -280,37 +337,38 @@ class PreprocEngine:
|
|||||||
elif op == ".SUB.":
|
elif op == ".SUB.":
|
||||||
time_obj -= timedelta(hours=num_value)
|
time_obj -= timedelta(hours=num_value)
|
||||||
else:
|
else:
|
||||||
return
|
raise ValueError(
|
||||||
ri = user_data.setdefault("reserve_info", {})
|
f"AutoScript 语法错误: Time 类型不支持操作 '{op}'"
|
||||||
ri.setdefault("begin_time", {})["time"] = \
|
)
|
||||||
time_obj.strftime("%H:%M")
|
AutoScriptEngine._setField(
|
||||||
elif field_name == "RESERVE_END_TIME":
|
field_name, time_obj.strftime("%H:%M"), user_data
|
||||||
time_str = (
|
)
|
||||||
user_data
|
elif field_type in ("String", "Boolean"):
|
||||||
.get("reserve_info", {})
|
raise ValueError(
|
||||||
.get("end_time", {})
|
f"AutoScript 语法错误: '{field_type}' 类型字段不支持操作运算"
|
||||||
.get("time", "")
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"AutoScript 语法错误: 未知字段类型 '{field_type}'"
|
||||||
)
|
)
|
||||||
if not time_str:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
time_obj = datetime.strptime(time_str, "%H:%M")
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return
|
|
||||||
if op == ".ADD.":
|
|
||||||
time_obj += timedelta(hours=num_value)
|
|
||||||
elif op == ".SUB.":
|
|
||||||
time_obj -= timedelta(hours=num_value)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
ri = user_data.setdefault("reserve_info", {})
|
|
||||||
ri.setdefault("end_time", {})["time"] = \
|
|
||||||
time_obj.strftime("%H:%M")
|
|
||||||
|
|
||||||
|
|
||||||
def _findConditionEnd(
|
def _findConditionEnd(
|
||||||
upper_line: str
|
upper_line: str
|
||||||
) -> int:
|
) -> 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()
|
line = upper_line.rstrip()
|
||||||
if line.endswith(" THEN"):
|
if line.endswith(" THEN"):
|
||||||
@@ -5,4 +5,6 @@
|
|||||||
- TimerUtils: Timer utils class for the AutoLibrary project.
|
- TimerUtils: Timer utils class for the AutoLibrary project.
|
||||||
- JSONReader: JSON reader class for the AutoLibrary project.
|
- JSONReader: JSON reader class for the AutoLibrary project.
|
||||||
- JSONWriter: JSON writer 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.
|
||||||
"""
|
"""
|
||||||
Reference in New Issue
Block a user