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
@@ -11,39 +11,78 @@ 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"),
"用户名": ("USERNAME", "String"),
"用户名": ("USERNAME", "String"),
"用户启用": ("USER_ENABLE", "Boolean"),
"当前时间": ("CURRENT_TIME", "Time"),
"当前日期": ("CURRENT_DATE", "Date"),
}
_FIELD_TYPE_MAP = {meta[0]: meta[1] for meta in VARIABLE_META.values()}
@staticmethod
def execute(
script_text: str,
user_data: dict
):
"""
Execute an AutoScript against the given user data.
The script is parsed line-by-line. All modifications are
applied directly to ``user_data`` in-place.
Args:
script_text (str): Raw AutoScript source code.
user_data (dict): User data dictionary to read from and
write to. Must conform to the standard user profile
structure (username, enabled, reserve_info, etc.).
Raises:
ValueError: On any syntax or type error encountered
during parsing or execution.
"""
if not script_text or not script_text.strip():
return
lines = [l.strip() for l in script_text.split("\n") if l.strip()]
if not lines:
return
if_stack = []
for line in lines:
@@ -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)
return cmp_func(left_val, right_val)
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}' 类型字段不支持操作运算"
)
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(
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.
"""