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
)
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(
+10 -9
View File
@@ -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
)
+70 -45
View File
@@ -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
View File
@@ -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,18 +11,41 @@ import re
from datetime import datetime, timedelta
class PreprocEngine:
class AutoScriptEngine:
"""
AutoScript script engine.
COMPARE_OPS = {
".EQ.": lambda a, b: a == b,
Parses and executes AutoScript a lightweight scripting DSL
used in repeatable timer tasks to preprocess user reservation
data before automation runs.
Supports IF/ELSE IF/ELSE/END IF control flow, SET assignments,
.ADD./.SUB. operations on Date/Time fields, and rich comparison
operators (.EQ. .NEQ. .BGT. .BLT. .BGE. .BLE.).
Examples:
>>> engine = AutoScriptEngine
>>> user = {
... "username": "test",
... "enabled": True,
... "reserve_info": {"date": "2026-05-07"}
... }
>>> engine.execute(
... 'IF(CURRENT_TIME .BGT. TIME(19:00))\\n'
... ' RESERVE_DATE .ADD. 1\\n'
... 'END IF',
... user
... )
"""
COMPARE_OPS = { # compare operators
".EQ." : lambda a, b: a == b,
".NEQ.": lambda a, b: a != b,
".BGT.": lambda a, b: a > b,
".BLT.": lambda a, b: a < b,
".BGE.": lambda a, b: a >= b,
".BLE.": lambda a, b: a <= b,
}
VARIABLE_META = {
VARIABLE_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"):
+2
View File
@@ -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.
"""