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