1
1
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:
2026-05-08 20:46:54 +08:00
parent 4d0d7a952c
commit 46b3447d1e
7 changed files with 258 additions and 174 deletions
@@ -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(
+10 -9
View File
@@ -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
) )
+71 -46
View File
@@ -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
View File
@@ -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"):
+2
View File
@@ -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.
""" """