mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 07:23:03 +08:00
refactor(autoscript): 替换 dsl 包为 autoscript 引擎模块
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime, timedelta, date, time
|
||||
|
||||
from .ASObject import ASObject, _META_VARS, _inferType
|
||||
from .ASOperator import ASOperator
|
||||
|
||||
|
||||
__all__ = ["execute", "addTargetVar"]
|
||||
|
||||
|
||||
# Engine state
|
||||
# User-registered target variables (bound to target_data paths)
|
||||
_TARGET_VARS = {}
|
||||
# Free-form script variables (not bound to target_data)
|
||||
_SCRIPT_VARS = {}
|
||||
# Name -> ASObject lookup map built from _META_VARS, _TARGET_VARS, and display names
|
||||
_FIELD_MAP = {}
|
||||
# Current line number for error reporting
|
||||
_CUR_LINE = 0
|
||||
|
||||
|
||||
def _errPos(
|
||||
message: str
|
||||
) -> str:
|
||||
"""
|
||||
Format an error message with the current script line number.
|
||||
|
||||
Args:
|
||||
message (str): The error description.
|
||||
|
||||
Returns:
|
||||
str: A formatted error string like "AutoScript syntax error(line X): message".
|
||||
"""
|
||||
return f"AutoScript 语法错误(第{_CUR_LINE}行): {message}"
|
||||
|
||||
|
||||
def _findConditionBegin(
|
||||
upper_line: str
|
||||
) -> int:
|
||||
"""
|
||||
Find the position of the opening parenthesis that starts a condition.
|
||||
|
||||
Args:
|
||||
upper_line (str): The uppercased IF / ELSE IF line.
|
||||
|
||||
Returns:
|
||||
int: Index of '(' or -1 if not found.
|
||||
"""
|
||||
return upper_line.find("(")
|
||||
|
||||
|
||||
def _findConditionEnd(
|
||||
upper_line: str,
|
||||
start_pos: int
|
||||
) -> int:
|
||||
"""
|
||||
Find the matching closing parenthesis for a condition expression.
|
||||
|
||||
Handles nested parentheses and optionally strips a trailing "THEN" keyword.
|
||||
|
||||
Args:
|
||||
upper_line (str): The uppercased IF / ELSE IF line.
|
||||
start_pos (int): Index of the opening '('.
|
||||
|
||||
Returns:
|
||||
int: Index of the matching ')' or -1 if unbalanced.
|
||||
"""
|
||||
|
||||
line = upper_line.rstrip()
|
||||
if line.endswith(" THEN"):
|
||||
line = line[:-5].rstrip()
|
||||
depth = 1
|
||||
for i in range(start_pos + 1, len(line)):
|
||||
ch = line[i]
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def _splitTopLevel(
|
||||
text: str,
|
||||
delimiter: str
|
||||
) -> list:
|
||||
"""
|
||||
Split a condition expression by a delimiter (.AND. / .OR.), respecting parentheses.
|
||||
|
||||
Only splits at the top nesting level; delimiters inside parentheses are ignored.
|
||||
|
||||
Args:
|
||||
text (str): The condition expression to split.
|
||||
delimiter (str): The delimiter string, e.g. ".OR." or ".AND.".
|
||||
|
||||
Returns:
|
||||
list: A list of sub-expression strings (stripped of leading/trailing whitespace).
|
||||
"""
|
||||
|
||||
parts = []
|
||||
depth = 0
|
||||
buf = ""
|
||||
i = 0
|
||||
text_upper = text.upper()
|
||||
delim_upper = delimiter.upper()
|
||||
dlen = len(delim_upper)
|
||||
while i < len(text):
|
||||
if text[i] == "(":
|
||||
depth += 1
|
||||
buf += text[i]
|
||||
elif text[i] == ")":
|
||||
depth -= 1
|
||||
buf += text[i]
|
||||
elif depth == 0 and text_upper[i:i + dlen] == delim_upper:
|
||||
parts.append(buf)
|
||||
buf = ""
|
||||
i += dlen
|
||||
continue
|
||||
else:
|
||||
buf += text[i]
|
||||
i += 1
|
||||
if buf.strip():
|
||||
parts.append(buf)
|
||||
return parts
|
||||
|
||||
|
||||
def _buildFieldMap():
|
||||
"""
|
||||
Rebuild the _FIELD_MAP lookup from _META_VARS and _TARGET_VARS.
|
||||
|
||||
Each variable is registered under both its canonical name (uppercased)
|
||||
and its display_name (if present), so that scripts can refer to either.
|
||||
"""
|
||||
|
||||
_FIELD_MAP.clear()
|
||||
for ch_name, obj in _META_VARS.items():
|
||||
_FIELD_MAP[obj.name.upper()] = obj
|
||||
_FIELD_MAP[ch_name.upper().strip()] = obj
|
||||
for obj in _TARGET_VARS.values():
|
||||
_FIELD_MAP[obj.name.upper()] = obj
|
||||
if obj.display_name:
|
||||
_FIELD_MAP[obj.display_name.upper().strip()] = obj
|
||||
|
||||
|
||||
def _resolveFieldObj(
|
||||
field_name: str
|
||||
):
|
||||
"""
|
||||
Resolve a field name to its ASObject by looking up _FIELD_MAP then _SCRIPT_VARS.
|
||||
|
||||
Unlike getting a raw value, this returns the ASObject instance itself,
|
||||
preserving type information for operations and comparisons.
|
||||
|
||||
Args:
|
||||
field_name (str): The field name (case-insensitive).
|
||||
|
||||
Returns:
|
||||
ASObject or None: The resolved ASObject, or None if not found.
|
||||
"""
|
||||
|
||||
upper_name = field_name.upper().strip()
|
||||
obj = _FIELD_MAP.get(upper_name)
|
||||
if obj:
|
||||
return obj
|
||||
obj = _SCRIPT_VARS.get(upper_name)
|
||||
if obj:
|
||||
return obj
|
||||
return None
|
||||
|
||||
|
||||
def _resolveValue(
|
||||
value_str: str,
|
||||
target_data: dict
|
||||
):
|
||||
"""
|
||||
Parse and resolve a value string from a script into a Python object.
|
||||
|
||||
Supports the following literal forms:
|
||||
- TIME(hh:mm)
|
||||
- DATE(yyyy-mm-dd)
|
||||
- .TRUE. / .FALSE.
|
||||
- Single/double quoted strings (with escaped single quotes)
|
||||
- CURRENT_DATE + N / CURRENT_TIME + N (relative offsets)
|
||||
- Numeric literals (int / float)
|
||||
- Field references (resolved via _resolveFieldObj)
|
||||
|
||||
Args:
|
||||
value_str (str): The raw value string from the script.
|
||||
target_data (dict): The application data dict.
|
||||
|
||||
Returns:
|
||||
The resolved Python value.
|
||||
"""
|
||||
|
||||
s = value_str.strip()
|
||||
m = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE)
|
||||
if m:
|
||||
return time(int(m.group(1)), int(m.group(2)))
|
||||
m = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE)
|
||||
if m:
|
||||
return date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
||||
up = s.upper()
|
||||
if up == ".TRUE.":
|
||||
return True
|
||||
if up == ".FALSE.":
|
||||
return False
|
||||
if s.startswith("'") and s.endswith("'"):
|
||||
return s[1:-1].replace("''", "'")
|
||||
if s.startswith('"') and s.endswith('"'):
|
||||
return s[1:-1]
|
||||
m = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE)
|
||||
if m:
|
||||
days = int(m.group(1))
|
||||
return datetime.now().date() + timedelta(days=days)
|
||||
m = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE)
|
||||
if m:
|
||||
hours = int(m.group(1))
|
||||
return (datetime.now() + timedelta(hours=hours)).time()
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
pass
|
||||
obj = _resolveFieldObj(s)
|
||||
if obj:
|
||||
return obj.getValue(target_data)
|
||||
return ""
|
||||
|
||||
|
||||
def _resolveAsObject(
|
||||
expr: str,
|
||||
target_data: dict
|
||||
) -> ASObject:
|
||||
"""
|
||||
Resolve a value expression to an ASObject.
|
||||
|
||||
- If the expression is a registered field name, returns its ASObject directly.
|
||||
- If the expression is a literal (number, string, DATE(), TIME(), bool),
|
||||
creates a temporary ASObject with the inferred type.
|
||||
|
||||
This is the key function that ensures all internal operations work
|
||||
with typed ASObject instances rather than raw Python values.
|
||||
|
||||
Args:
|
||||
expr (str): The raw expression string from the script.
|
||||
target_data (dict): The application data dict.
|
||||
|
||||
Returns:
|
||||
ASObject: A registered or temporary ASObject representing the expression value.
|
||||
"""
|
||||
|
||||
s = expr.strip()
|
||||
obj = _resolveFieldObj(s)
|
||||
if obj is not None:
|
||||
return obj
|
||||
value = _resolveValue(s, target_data)
|
||||
inferred = _inferType(value, s)
|
||||
return ASObject._makeTemp(value, inferred)
|
||||
|
||||
|
||||
def _evaluateCondition(
|
||||
condition_str: str,
|
||||
target_data: dict
|
||||
) -> bool:
|
||||
"""
|
||||
Evaluate a condition expression and return a boolean result.
|
||||
|
||||
Supports:
|
||||
- Boolean literals: .TRUE., .FALSE.
|
||||
- .AND. / .OR. operators (lowest precedence)
|
||||
- Parenthesised sub-expressions
|
||||
- Comparison operators: .EQ., .NEQ., .BGT., .BLT., .BGE., .BLE.
|
||||
|
||||
All operands are resolved as ASObject instances (via _resolveAsObject)
|
||||
and comparisons are delegated to ASOperator.compare().
|
||||
|
||||
Args:
|
||||
condition_str (str): The raw condition expression from the script.
|
||||
target_data (dict): The application data dict.
|
||||
|
||||
Returns:
|
||||
bool: The evaluation result.
|
||||
|
||||
Raises:
|
||||
ValueError: If the expression contains unrecognised tokens or type-mismatched comparisons.
|
||||
"""
|
||||
|
||||
s = condition_str.strip()
|
||||
if not s:
|
||||
return False
|
||||
or_parts = _splitTopLevel(s, ".OR.")
|
||||
if len(or_parts) > 1:
|
||||
return any(
|
||||
_evaluateCondition(p.strip(), target_data)
|
||||
for p in or_parts
|
||||
)
|
||||
and_parts = _splitTopLevel(s, ".AND.")
|
||||
if len(and_parts) > 1:
|
||||
return all(
|
||||
_evaluateCondition(p.strip(), target_data)
|
||||
for p in and_parts
|
||||
)
|
||||
s = s.strip()
|
||||
if s.startswith("(") and s.endswith(")"):
|
||||
return _evaluateCondition(s[1:-1], target_data)
|
||||
up = s.upper()
|
||||
if up == ".TRUE.":
|
||||
return True
|
||||
if up == ".FALSE.":
|
||||
return False
|
||||
for op in ASOperator._COMPARE:
|
||||
idx = up.find(op.upper())
|
||||
if idx < 0:
|
||||
continue
|
||||
left_raw = s[:idx].strip()
|
||||
right_raw = s[idx + len(op):].strip()
|
||||
left_obj = _resolveAsObject(left_raw, target_data)
|
||||
right_obj = _resolveAsObject(right_raw, target_data)
|
||||
return ASOperator.compare(left_obj, right_obj, op, target_data)
|
||||
raise ValueError(
|
||||
_errPos(f"无法识别的条件表达式 '{condition_str}'")
|
||||
)
|
||||
|
||||
|
||||
def _executeSet(
|
||||
line: str,
|
||||
target_data: dict
|
||||
):
|
||||
"""
|
||||
Execute a SET statement to assign a value to a field or script variable.
|
||||
|
||||
Parses the line as "SET field_name = value_expr", resolves the value,
|
||||
and assigns it. If the target field does not exist, a new script variable
|
||||
is created with an inferred type.
|
||||
|
||||
Args:
|
||||
line (str): The raw SET line from the script.
|
||||
target_data (dict): The application data dict.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value string contains unexpected extra tokens.
|
||||
"""
|
||||
|
||||
rest = line[3:].strip()
|
||||
eq_idx = rest.find("=")
|
||||
if eq_idx < 0:
|
||||
return
|
||||
field_name = rest[:eq_idx].strip()
|
||||
value_str = rest[eq_idx + 1:].strip()
|
||||
if not field_name:
|
||||
return
|
||||
resolved = _resolveValue(value_str, target_data)
|
||||
stripped = value_str.strip()
|
||||
if resolved == "" and stripped not in ("''", '""') and len(stripped.split()) > 1:
|
||||
raise ValueError(_errPos(f"SET 值中存在多余内容 '{stripped}'"))
|
||||
upper_name = field_name.upper().strip()
|
||||
obj = _FIELD_MAP.get(upper_name)
|
||||
if not obj:
|
||||
obj = _SCRIPT_VARS.get(upper_name)
|
||||
if obj:
|
||||
obj.setValue(resolved, target_data)
|
||||
return
|
||||
inferred_type = _inferType(resolved, stripped)
|
||||
new_var = ASObject(
|
||||
upper_name,
|
||||
inferred_type,
|
||||
read_only=False,
|
||||
is_config=False,
|
||||
default_value=resolved
|
||||
)
|
||||
_SCRIPT_VARS[upper_name] = new_var
|
||||
|
||||
|
||||
def _executeOperation(
|
||||
line: str,
|
||||
target_data: dict
|
||||
):
|
||||
"""
|
||||
Execute a field operation statement: "FIELD .ADD. N" or "FIELD .SUB. N".
|
||||
|
||||
Resolves the left side as a registered ASObject and the right side
|
||||
as a temporary numeric ASObject, then delegates to ASOperator.apply().
|
||||
|
||||
Args:
|
||||
line (str): The raw operation line from the script (e.g. "RESERVE_DATE .ADD. 1").
|
||||
target_data (dict): The application data dict.
|
||||
|
||||
Raises:
|
||||
ValueError: If the field is unknown, the operand is invalid,
|
||||
or the type does not support the operation.
|
||||
"""
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
return
|
||||
if len(parts) > 3:
|
||||
raise ValueError(
|
||||
_errPos(f"操作语句中存在多余内容 '{' '.join(parts[3:])}'")
|
||||
)
|
||||
field_name = parts[0].upper().strip()
|
||||
op = parts[1].upper().strip()
|
||||
raw_value = parts[2].strip()
|
||||
target = _resolveFieldObj(field_name)
|
||||
if target is None:
|
||||
raise ValueError(_errPos(f"未知字段 '{field_name}'"))
|
||||
operand = _resolveAsObject(raw_value, target_data)
|
||||
ASOperator.apply(target, operand, op, target_data)
|
||||
|
||||
|
||||
def _assertInIf(
|
||||
if_stack: list,
|
||||
line: str
|
||||
):
|
||||
"""
|
||||
Assert that an executable statement is inside an IF block.
|
||||
|
||||
Args:
|
||||
if_stack (list): The current IF nesting stack.
|
||||
line (str): The statement line (used for error message).
|
||||
|
||||
Raises:
|
||||
ValueError: If if_stack is empty (statement is outside any IF block).
|
||||
"""
|
||||
|
||||
if not if_stack:
|
||||
raise ValueError(_errPos(f"可执行语句必须位于 IF 块内: {line}"))
|
||||
|
||||
|
||||
def addTargetVar(
|
||||
name: str,
|
||||
var_type: str,
|
||||
key_path: list,
|
||||
display_name: str = None
|
||||
):
|
||||
"""
|
||||
Register a new target variable bound to a path in the application data dict.
|
||||
|
||||
Once registered, the variable can be read, written, and operated on in scripts
|
||||
using its canonical name or display_name.
|
||||
|
||||
Args:
|
||||
name (str): The canonical variable name (e.g. "RESERVE_DATE").
|
||||
var_type (str): The type ("Int", "Float", "Boolean", "Date", "Time", "String").
|
||||
key_path (list): The nested path into target_data, e.g. ["reserve_info", "date"].
|
||||
display_name (str): An optional Chinese alias for use in script conditions.
|
||||
|
||||
Example:
|
||||
>>> addTargetVar("MY_FIELD", "String", ["custom", "field"], display_name="自定义字段")
|
||||
"""
|
||||
|
||||
upper_name = name.upper().strip()
|
||||
obj = ASObject(
|
||||
upper_name,
|
||||
var_type,
|
||||
is_config=True,
|
||||
key_path=key_path,
|
||||
display_name=display_name
|
||||
)
|
||||
_TARGET_VARS[upper_name] = obj
|
||||
|
||||
|
||||
def execute(
|
||||
script_text: str,
|
||||
target_data: dict
|
||||
):
|
||||
"""
|
||||
Execute an AutoScript on the given target data.
|
||||
|
||||
Parses the script line by line, maintaining an IF nesting stack to control
|
||||
which blocks are active. Supports IF / ELSE IF / ELSE / END IF control flow,
|
||||
SET assignments, PASS no-ops, and .ADD. / .SUB. field operations.
|
||||
|
||||
Args:
|
||||
script_text (str): The AutoScript source code.
|
||||
target_data (dict): The application data dict to read from / write to.
|
||||
|
||||
Raises:
|
||||
ValueError: On syntax errors, unbalanced IF/END IF, unknown fields, etc.
|
||||
|
||||
Example:
|
||||
>>> data = {"reserve_info": {"date": "2026-05-01"}}
|
||||
>>> execute(
|
||||
... "IF(.TRUE.)\\n"
|
||||
... " RESERVE_DATE .ADD. 1\\n"
|
||||
... "END IF",
|
||||
... data
|
||||
... )
|
||||
>>> data["reserve_info"]["date"]
|
||||
'2026-05-02'
|
||||
"""
|
||||
|
||||
global _CUR_LINE
|
||||
|
||||
_buildFieldMap()
|
||||
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 _CUR_LINE, line in enumerate(lines, 1):
|
||||
upper_line = line.upper().strip()
|
||||
if upper_line.startswith("IF"):
|
||||
paren_open = _findConditionBegin(upper_line)
|
||||
if paren_open < 0:
|
||||
raise ValueError(_errPos("IF 缺少左括号"))
|
||||
cond_end = _findConditionEnd(upper_line, paren_open)
|
||||
if cond_end < 0:
|
||||
raise ValueError(_errPos("IF 缺少右括号"))
|
||||
remaining = upper_line[cond_end + 1:].strip()
|
||||
if remaining and remaining.upper() != "THEN":
|
||||
raise ValueError(_errPos(f"IF 条件后存在多余内容 '{remaining}'"))
|
||||
condition_str = line[paren_open + 1:cond_end].strip()
|
||||
matched = _evaluateCondition(condition_str, target_data)
|
||||
if_stack.append([matched, matched])
|
||||
elif upper_line.startswith("ELSE IF"):
|
||||
if not if_stack:
|
||||
raise ValueError(_errPos("ELSE IF 前缺少 IF"))
|
||||
paren_open = _findConditionBegin(upper_line)
|
||||
if paren_open < 0:
|
||||
raise ValueError(_errPos("ELSE IF 缺少左括号"))
|
||||
cond_end = _findConditionEnd(upper_line, paren_open)
|
||||
if cond_end < 0:
|
||||
raise ValueError(_errPos("ELSE IF 缺少右括号"))
|
||||
remaining = upper_line[cond_end + 1:].strip()
|
||||
if remaining and remaining.upper() != "THEN":
|
||||
raise ValueError(_errPos(f"ELSE IF 条件后存在多余内容 '{remaining}'"))
|
||||
_, branch_matched = if_stack[-1]
|
||||
if not branch_matched:
|
||||
condition_str = line[paren_open + 1:cond_end].strip()
|
||||
matched = _evaluateCondition(condition_str, target_data)
|
||||
if_stack[-1] = [matched, matched]
|
||||
else:
|
||||
if_stack[-1][0] = False
|
||||
elif upper_line == "ELSE":
|
||||
if not if_stack:
|
||||
raise ValueError(_errPos("ELSE 前缺少 IF"))
|
||||
_, branch_matched = if_stack[-1]
|
||||
if not branch_matched:
|
||||
if_stack[-1] = [True, True]
|
||||
else:
|
||||
if_stack[-1][0] = False
|
||||
elif upper_line in ("ENDIF", "END IF"):
|
||||
if not if_stack:
|
||||
raise ValueError(_errPos("ENDIF / END IF 前缺少 IF"))
|
||||
if_stack.pop()
|
||||
elif upper_line.startswith("SET "):
|
||||
_assertInIf(if_stack, line)
|
||||
if all(ctx[0] for ctx in if_stack):
|
||||
_executeSet(line, target_data)
|
||||
elif upper_line == "PASS":
|
||||
continue
|
||||
else:
|
||||
_assertInIf(if_stack, line)
|
||||
if all(ctx[0] for ctx in if_stack):
|
||||
_executeOperation(line, target_data)
|
||||
if if_stack:
|
||||
raise ValueError(
|
||||
"AutoScript 语法错误: IF 与 ENDIF / END IF 不匹配"
|
||||
)
|
||||
@@ -0,0 +1,251 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime, date, time
|
||||
|
||||
|
||||
__all__ = ["ASObject", "_META_VARS", "_inferType"]
|
||||
|
||||
|
||||
# Default values for each supported type when no value is present
|
||||
_TYPE_DEFAULTS = {
|
||||
"Int": 0,
|
||||
"Float": 0.0,
|
||||
"Boolean": False,
|
||||
"Date": None,
|
||||
"Time": None,
|
||||
"String": "",
|
||||
}
|
||||
|
||||
|
||||
class ASObject:
|
||||
"""
|
||||
Represents a variable object used throughout the AutoScript engine.
|
||||
|
||||
An ASObject can be a meta variable (read-only, e.g. CURRENT_DATE),
|
||||
a target variable (bound to a config data dict via key_path),
|
||||
or a script variable (free-form, stored internally).
|
||||
|
||||
Args:
|
||||
name (str): The canonical name of the variable (case-insensitive).
|
||||
var_type (str): One of "Int", "Float", "Boolean", "Date", "Time", "String".
|
||||
read_only (bool): Whether the variable is read-only (default: False).
|
||||
is_config (bool): Whether the variable maps to a target_data path (default: False).
|
||||
key_path (list): The nested key path into target_data, e.g. ["reserve_info", "date"].
|
||||
default_value: The fallback value when no target_data value is found.
|
||||
display_name (str): An alias for use in script conditions and assignments.
|
||||
|
||||
Example:
|
||||
>>> obj = ASObject("MY_DATE", "Date", is_config=True,
|
||||
... key_path=["reserve_info", "date"],
|
||||
... display_name="预约日期")
|
||||
>>> obj.getValue({"reserve_info": {"date": "2026-05-01"}})
|
||||
datetime.date(2026, 5, 1)
|
||||
"""
|
||||
|
||||
_TEMP_COUNTER = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
var_type: str, *,
|
||||
read_only: bool = False,
|
||||
is_config: bool = False,
|
||||
key_path: list = None,
|
||||
default_value=None,
|
||||
display_name: str = None
|
||||
):
|
||||
|
||||
self.name = name
|
||||
self.var_type = var_type
|
||||
self.read_only = read_only
|
||||
self.is_config = is_config
|
||||
self.key_path = key_path or []
|
||||
self._value = default_value
|
||||
self.display_name = display_name
|
||||
|
||||
@classmethod
|
||||
def _makeTemp(
|
||||
cls,
|
||||
value,
|
||||
inferred_type: str
|
||||
):
|
||||
"""
|
||||
Create a temporary unnamed ASObject from a literal value.
|
||||
|
||||
Temporary objects are used for inline script literals (e.g. 42,
|
||||
'hello', DATE(2026-01-01)) so they can participate in typed
|
||||
operations alongside registered variables.
|
||||
|
||||
Args:
|
||||
value: The resolved Python value.
|
||||
inferred_type (str): The AutoScript type name.
|
||||
|
||||
Returns:
|
||||
ASObject: A temporary, non-config, read-write ASObject.
|
||||
"""
|
||||
|
||||
cls._TEMP_COUNTER += 1
|
||||
return cls(
|
||||
f"__TMP_{cls._TEMP_COUNTER}",
|
||||
inferred_type,
|
||||
read_only=False,
|
||||
is_config=False,
|
||||
default_value=value
|
||||
)
|
||||
|
||||
|
||||
def _typeEmpty(
|
||||
self
|
||||
):
|
||||
"""
|
||||
Return the type-appropriate empty / default value.
|
||||
"""
|
||||
|
||||
return _TYPE_DEFAULTS.get(self.var_type, "")
|
||||
|
||||
|
||||
def getValue(
|
||||
self,
|
||||
target_data: dict = None
|
||||
):
|
||||
"""
|
||||
Retrieve the current value of this variable.
|
||||
|
||||
For read-only variables (CURRENT_DATE, CURRENT_TIME), returns the
|
||||
live datetime. For config variables, traverses the key_path into
|
||||
target_data and parses Date/Time strings. Otherwise returns the
|
||||
internal _value.
|
||||
|
||||
Args:
|
||||
target_data (dict): The application data dict (required for config vars).
|
||||
|
||||
Returns:
|
||||
The resolved value, or a type-appropriate default if missing.
|
||||
"""
|
||||
|
||||
if self.read_only:
|
||||
if self.name == "CURRENT_DATE":
|
||||
return datetime.now().date()
|
||||
if self.name == "CURRENT_TIME":
|
||||
return datetime.now().time()
|
||||
return self._value
|
||||
if self.is_config and target_data is not None and self.key_path:
|
||||
d = target_data
|
||||
for key in self.key_path[:-1]:
|
||||
d = d.get(key, {})
|
||||
if not isinstance(d, dict):
|
||||
return self._typeEmpty()
|
||||
raw = d.get(self.key_path[-1])
|
||||
if raw is None:
|
||||
return self._typeEmpty()
|
||||
if self.var_type == "Date" and isinstance(raw, str):
|
||||
try:
|
||||
return datetime.strptime(raw, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return self._typeEmpty()
|
||||
if self.var_type == "Time" and isinstance(raw, str):
|
||||
try:
|
||||
return datetime.strptime(raw, "%H:%M").time()
|
||||
except ValueError:
|
||||
return self._typeEmpty()
|
||||
return raw
|
||||
return self._value
|
||||
|
||||
|
||||
def setValue(
|
||||
self,
|
||||
value,
|
||||
target_data: dict = None
|
||||
):
|
||||
"""
|
||||
Assign a new value to this variable, with type coercion.
|
||||
|
||||
Performs coercion for Boolean (string -> bool), Int, and Float types.
|
||||
For config variables, dates/times are converted back to strings before
|
||||
writing into target_data.
|
||||
|
||||
Args:
|
||||
value: The value to assign.
|
||||
target_data (dict): The application data dict (required for config vars).
|
||||
|
||||
Raises:
|
||||
ValueError: If the variable is read-only or value cannot be coerced.
|
||||
"""
|
||||
|
||||
if self.read_only:
|
||||
raise ValueError(f"不能修改只读变量 '{self.name}'")
|
||||
if self.var_type == "Boolean" and not isinstance(value, bool):
|
||||
value = (str(value).upper() == "TRUE")
|
||||
if self.var_type == "Int" and not isinstance(value, int):
|
||||
try:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(f"无法将值 '{value}' 转换为 Int 类型")
|
||||
if self.var_type == "Float" and not isinstance(value, float):
|
||||
try:
|
||||
value = float(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(f"无法将值 '{value}' 转换为 Float 类型")
|
||||
if self.is_config:
|
||||
if self.var_type == "Date" and isinstance(value, date):
|
||||
value = value.strftime("%Y-%m-%d")
|
||||
if self.var_type == "Time" and isinstance(value, time):
|
||||
value = value.strftime("%H:%M")
|
||||
if self.is_config and target_data is not None and self.key_path:
|
||||
d = target_data
|
||||
for key in self.key_path[:-1]:
|
||||
d = d.setdefault(key, {})
|
||||
d[self.key_path[-1]] = value
|
||||
else:
|
||||
self._value = value
|
||||
|
||||
|
||||
# Built-in read-only meta variables available to all scripts
|
||||
_META_VARS = {
|
||||
"CURRENT_DATE": ASObject("CURRENT_DATE", "Date", read_only=True, display_name="当前日期"),
|
||||
"CURRENT_TIME": ASObject("CURRENT_TIME", "Time", read_only=True, display_name="当前时间"),
|
||||
}
|
||||
|
||||
|
||||
def _inferType(
|
||||
value,
|
||||
raw_expr: str = None
|
||||
) -> str:
|
||||
"""
|
||||
Infer the ASObject type string from a Python value or raw expression.
|
||||
|
||||
When the Python type is ambiguous (e.g. int can be Int or a component
|
||||
of Date), the raw_expr is used as a hint.
|
||||
|
||||
Args:
|
||||
value: The resolved Python value.
|
||||
raw_expr (str): The original expression string from the script (optional).
|
||||
|
||||
Returns:
|
||||
str: One of "Boolean", "Int", "Float", "Date", "Time", "String".
|
||||
"""
|
||||
|
||||
if isinstance(value, bool):
|
||||
return "Boolean"
|
||||
if isinstance(value, int):
|
||||
return "Int"
|
||||
if isinstance(value, float):
|
||||
return "Float"
|
||||
if isinstance(value, date):
|
||||
return "Date"
|
||||
if isinstance(value, time):
|
||||
return "Time"
|
||||
if raw_expr:
|
||||
if re.match(r"^DATE\(\d{4}-\d{2}-\d{2}\)$", raw_expr, re.IGNORECASE):
|
||||
return "Date"
|
||||
if re.match(r"^TIME\(\d{1,2}:\d{2}\)$", raw_expr, re.IGNORECASE):
|
||||
return "Time"
|
||||
return "String"
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
from datetime import datetime, timedelta, date, time
|
||||
|
||||
from .ASObject import ASObject
|
||||
|
||||
|
||||
__all__ = ["ASOperator"]
|
||||
|
||||
|
||||
class ASOperator:
|
||||
"""
|
||||
Centralised type-safe operations for AutoScript engine types.
|
||||
|
||||
All arithmetic (ADD / SUB) and comparison operators are routed through
|
||||
this class, which dispatches to the correct Python-level logic based on
|
||||
the ASObject's var_type. This keeps type-specific branching in one
|
||||
place instead of scattering it across the engine.
|
||||
|
||||
Args:
|
||||
op (str): One of ".ADD.", ".SUB.", ".EQ.", ".NEQ.", ".BGT.",
|
||||
".BLT.", ".BGE.", ".BLE.".
|
||||
|
||||
Example:
|
||||
>>> obj = ASObject("X", "Int", default_value=10)
|
||||
>>> ASOperator.apply(obj, ASObject._makeTemp(5, "Int"), ".ADD.", None)
|
||||
>>> obj.getValue()
|
||||
15
|
||||
>>> ASOperator.compare(
|
||||
... obj,
|
||||
... ASObject._makeTemp(15, "Int"),
|
||||
... ".EQ.", None
|
||||
... )
|
||||
True
|
||||
"""
|
||||
|
||||
_COMPARE = {
|
||||
".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,
|
||||
}
|
||||
_ARITH_TYPES = {"Date", "Time", "Int", "Float"}
|
||||
|
||||
@staticmethod
|
||||
def apply(
|
||||
target: ASObject,
|
||||
operand: ASObject,
|
||||
op: str,
|
||||
target_data: dict
|
||||
):
|
||||
"""
|
||||
Apply ADD or SUB to a target ASObject, modifying it in place.
|
||||
|
||||
Args:
|
||||
target (ASObject): The variable to modify.
|
||||
operand (ASObject): The operand (numeric value for Date/Time/Int/Float).
|
||||
op (str): ".ADD." or ".SUB.".
|
||||
target_data (dict): Application data dict (passed through to getValue/setValue).
|
||||
|
||||
Raises:
|
||||
ValueError: If the type does not support the operation or values are invalid.
|
||||
"""
|
||||
|
||||
tp = target.var_type
|
||||
op_tp = operand.var_type
|
||||
if tp not in ASOperator._ARITH_TYPES:
|
||||
raise ValueError(f"'{tp}' 类型字段不支持操作运算")
|
||||
if op_tp not in ("Int", "Float"):
|
||||
raise ValueError(f"操作数类型 '{op_tp}' 不能用于运算,需要数值类型 (Int / Float)")
|
||||
if tp in ("Date", "Time") and op_tp != "Int":
|
||||
raise ValueError(f"'{tp}' 类型的加减法操作数必须为 Int 类型,不允许 Float")
|
||||
target_val = target.getValue(target_data)
|
||||
if target_val is None:
|
||||
raise ValueError(f"'{target.name}' 的值为空,无法进行运算")
|
||||
if op == ".ADD.":
|
||||
ASOperator._arithAdd(target, target_val, operand, target_data)
|
||||
elif op == ".SUB.":
|
||||
ASOperator._arithSub(target, target_val, operand, target_data)
|
||||
else:
|
||||
raise ValueError(f"不支持的操作 '{op}'")
|
||||
|
||||
@staticmethod
|
||||
def _arithAdd(
|
||||
target: ASObject,
|
||||
target_val,
|
||||
operand: ASObject,
|
||||
target_data: dict
|
||||
):
|
||||
"""Dispatch ADD per type."""
|
||||
|
||||
tp = target.var_type
|
||||
raw_op = operand._value
|
||||
|
||||
if tp == "Date":
|
||||
if not isinstance(target_val, date):
|
||||
raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期")
|
||||
new_val = target_val + timedelta(days=int(raw_op))
|
||||
elif tp == "Time":
|
||||
if not isinstance(target_val, time):
|
||||
raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间")
|
||||
dt = datetime.combine(datetime.today(), target_val) + timedelta(hours=int(raw_op))
|
||||
new_val = dt.time()
|
||||
elif tp == "Int":
|
||||
new_val = int(target_val) + int(raw_op)
|
||||
elif tp == "Float":
|
||||
new_val = float(target_val) + float(raw_op)
|
||||
else:
|
||||
raise ValueError(f"'{tp}' 类型不支持 ADD 操作")
|
||||
target.setValue(new_val, target_data)
|
||||
|
||||
@staticmethod
|
||||
def _arithSub(
|
||||
target: ASObject,
|
||||
target_val,
|
||||
operand: ASObject,
|
||||
target_data: dict
|
||||
):
|
||||
|
||||
"""Dispatch SUB per type."""
|
||||
|
||||
tp = target.var_type
|
||||
raw_op = operand._value
|
||||
|
||||
if tp == "Date":
|
||||
if not isinstance(target_val, date):
|
||||
raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效日期")
|
||||
new_val = target_val - timedelta(days=int(raw_op))
|
||||
elif tp == "Time":
|
||||
if not isinstance(target_val, time):
|
||||
raise ValueError(f"'{target.name}' 的值 '{target_val}' 不是有效时间")
|
||||
dt = datetime.combine(datetime.today(), target_val) - timedelta(hours=int(raw_op))
|
||||
new_val = dt.time()
|
||||
elif tp == "Int":
|
||||
new_val = int(target_val) - int(raw_op)
|
||||
elif tp == "Float":
|
||||
new_val = float(target_val) - float(raw_op)
|
||||
else:
|
||||
raise ValueError(f"'{tp}' 类型不支持 SUB 操作")
|
||||
target.setValue(new_val, target_data)
|
||||
|
||||
@staticmethod
|
||||
def compare(
|
||||
left: ASObject,
|
||||
right: ASObject,
|
||||
op: str,
|
||||
target_data: dict
|
||||
) -> bool:
|
||||
"""
|
||||
Compare two ASObjects using the given comparison operator.
|
||||
|
||||
Args:
|
||||
left (ASObject): Left-hand side.
|
||||
right (ASObject): Right-hand side.
|
||||
op (str): One of ".EQ.", ".NEQ.", ".BGT.", ".BLT.", ".BGE.", ".BLE.".
|
||||
target_data (dict): Application data dict.
|
||||
|
||||
Returns:
|
||||
bool: The comparison result.
|
||||
|
||||
Raises:
|
||||
ValueError: If the types are incompatible for comparison.
|
||||
"""
|
||||
|
||||
cmp_func = ASOperator._COMPARE.get(op)
|
||||
if cmp_func is None:
|
||||
raise ValueError(f"未知的比较操作 '{op}'")
|
||||
left_val = left.getValue(target_data)
|
||||
right_val = right.getValue(target_data)
|
||||
try:
|
||||
return cmp_func(left_val, right_val)
|
||||
except TypeError:
|
||||
raise ValueError(
|
||||
f"无法比较 '{left.name}' ({left.var_type}) "
|
||||
f"与 '{right.name}' ({right.var_type})"
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
AutoScript module for the AutoLibrary project.
|
||||
|
||||
A lightweight scripting DSL for preprocessing user reservation data
|
||||
in repeatable timer tasks. Supports IF/ELSE IF/ELSE/END IF control
|
||||
flow, SET assignments, .ADD./.SUB. operations, and rich comparisons.
|
||||
|
||||
Public API:
|
||||
- execute(script_text, target_data): Execute an AutoScript.
|
||||
- addTargetVar(name, var_type, key_path, display_name): Register a variable.
|
||||
- registerDefaultTargetVars(): Register all built-in target variables.
|
||||
- META_VARS: dict of built-in read-only meta variables.
|
||||
- ALL_VARIABLES: dict of all available variables (display_name -> (name, type)).
|
||||
"""
|
||||
|
||||
from autoscript.ASEngine import execute, addTargetVar
|
||||
from autoscript.ASObject import _META_VARS as META_VARS
|
||||
|
||||
__all__ = [
|
||||
"execute", "addTargetVar", "registerDefaultTargetVars",
|
||||
"META_VARS", "ALL_VARIABLES",
|
||||
]
|
||||
|
||||
# All variables available to scripts (display_name -> (name, type)).
|
||||
# This mirrors the old AutoScriptEngine.VARIABLE_META for backward
|
||||
# compatibility in the UI orchestration dialog.
|
||||
ALL_VARIABLES: dict = {
|
||||
"用户名": ("USERNAME", "String"),
|
||||
"用户启用": ("USER_ENABLE", "Boolean"),
|
||||
"预约日期": ("RESERVE_DATE", "Date"),
|
||||
"预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
|
||||
"预约结束时间": ("RESERVE_END_TIME", "Time"),
|
||||
"当前时间": ("CURRENT_TIME", "Time"),
|
||||
"当前日期": ("CURRENT_DATE", "Date"),
|
||||
}
|
||||
|
||||
# Key paths into target_data dict for each target variable.
|
||||
# (name, type, key_path, display_name)
|
||||
_TARGET_VAR_DEFS = [
|
||||
("USERNAME", "String",["username"], "用户名"),
|
||||
("USER_ENABLE", "Boolean",["enabled"], "用户启用"),
|
||||
("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"),
|
||||
("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"),
|
||||
("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"),
|
||||
]
|
||||
|
||||
|
||||
def registerDefaultTargetVars() -> None:
|
||||
"""
|
||||
Register all built-in target variables with the engine.
|
||||
This must be called before any script execution.
|
||||
Calling multiple times is idempotent (re-registers same keys).
|
||||
"""
|
||||
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS:
|
||||
addTargetVar(name, var_type, key_path, display_name)
|
||||
@@ -1,386 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class AutoScriptEngine:
|
||||
"""
|
||||
AutoScript script engine.
|
||||
|
||||
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 metadata
|
||||
"预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
|
||||
"预约结束时间": ("RESERVE_END_TIME", "Time"),
|
||||
"预约日期": ("RESERVE_DATE", "Date"),
|
||||
"用户名": ("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:
|
||||
upper_line = line.upper().strip()
|
||||
if upper_line.startswith("IF("):
|
||||
cond_end = _findConditionEnd(upper_line)
|
||||
if cond_end < 0:
|
||||
raise ValueError("AutoScript 语法错误: IF 缺少右括号")
|
||||
condition_str = line[3:cond_end].strip()
|
||||
matched = AutoScriptEngine._evaluateCondition(
|
||||
condition_str, user_data
|
||||
)
|
||||
if_stack.append([matched, matched])
|
||||
elif upper_line.startswith("ELSE IF("):
|
||||
if not if_stack:
|
||||
raise ValueError("AutoScript 语法错误: ELSE IF 前缺少 IF")
|
||||
cond_end = _findConditionEnd(upper_line)
|
||||
if cond_end < 0:
|
||||
raise ValueError("AutoScript 语法错误: ELSE IF 缺少右括号")
|
||||
condition_str = line[8:cond_end].strip()
|
||||
_, has_matched = if_stack[-1]
|
||||
if not has_matched:
|
||||
matched = AutoScriptEngine._evaluateCondition(
|
||||
condition_str, user_data
|
||||
)
|
||||
if_stack[-1] = [matched, matched]
|
||||
else:
|
||||
if_stack[-1][0] = False
|
||||
elif upper_line == "ELSE":
|
||||
if not if_stack:
|
||||
raise ValueError("AutoScript 语法错误: ELSE 前缺少 IF")
|
||||
_, has_matched = if_stack[-1]
|
||||
if not has_matched:
|
||||
if_stack[-1] = [True, True]
|
||||
else:
|
||||
if_stack[-1][0] = False
|
||||
elif upper_line in ("ENDIF", "END IF"):
|
||||
if not if_stack:
|
||||
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:
|
||||
AutoScriptEngine._executeSet(line, user_data)
|
||||
elif upper_line == "PASS":
|
||||
continue
|
||||
else:
|
||||
should_execute = (
|
||||
all(ctx[0] for ctx in if_stack) if if_stack else True
|
||||
)
|
||||
if should_execute:
|
||||
AutoScriptEngine._executeOperation(line, user_data)
|
||||
if if_stack:
|
||||
raise ValueError("AutoScript 语法错误: IF 与 ENDIF/END IF 不匹配")
|
||||
|
||||
@staticmethod
|
||||
def _resolveField(
|
||||
field_name: str,
|
||||
user_data: dict
|
||||
):
|
||||
|
||||
upper_name = field_name.upper().strip()
|
||||
if upper_name == "CURRENT_DATE":
|
||||
return datetime.now().strftime("%Y-%m-%d")
|
||||
elif upper_name == "CURRENT_TIME":
|
||||
return datetime.now().strftime("%H:%M")
|
||||
elif upper_name == "USERNAME":
|
||||
return user_data.get("username", "")
|
||||
elif upper_name == "USER_ENABLE":
|
||||
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":
|
||||
return (
|
||||
user_data
|
||||
.get("reserve_info", {})
|
||||
.get("begin_time", {})
|
||||
.get("time", "")
|
||||
)
|
||||
elif upper_name == "RESERVE_END_TIME":
|
||||
return (
|
||||
user_data
|
||||
.get("reserve_info", {})
|
||||
.get("end_time", {})
|
||||
.get("time", "")
|
||||
)
|
||||
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,
|
||||
value: str,
|
||||
user_data: dict
|
||||
):
|
||||
upper_name = field_name.upper().strip()
|
||||
if upper_name == "RESERVE_DATE":
|
||||
user_data.setdefault("reserve_info", {})["date"] = value
|
||||
elif upper_name == "RESERVE_BEGIN_TIME":
|
||||
ri = user_data.setdefault("reserve_info", {})
|
||||
ri.setdefault("begin_time", {})["time"] = value
|
||||
elif upper_name == "RESERVE_END_TIME":
|
||||
ri = user_data.setdefault("reserve_info", {})
|
||||
ri.setdefault("end_time", {})["time"] = value
|
||||
elif upper_name == "USERNAME":
|
||||
user_data["username"] = value
|
||||
elif upper_name == "USER_ENABLE":
|
||||
if isinstance(value, bool):
|
||||
user_data["enabled"] = value
|
||||
else:
|
||||
user_data["enabled"] = (str(value).upper() == "TRUE")
|
||||
|
||||
@staticmethod
|
||||
def _evaluateCondition(
|
||||
condition_str: str,
|
||||
user_data: dict
|
||||
) -> bool:
|
||||
|
||||
for op, cmp_func in AutoScriptEngine.COMPARE_OPS.items():
|
||||
if op not in condition_str.upper():
|
||||
continue
|
||||
idx = condition_str.upper().find(op)
|
||||
parts = [condition_str[:idx], condition_str[idx + len(op):]]
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
field_name = parts[0].strip()
|
||||
value_str = parts[1].strip()
|
||||
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
|
||||
def _executeSet(
|
||||
line: str,
|
||||
user_data: dict
|
||||
):
|
||||
rest = line[3:].strip()
|
||||
eq_idx = rest.find("=")
|
||||
if eq_idx < 0:
|
||||
return
|
||||
field_name = rest[:eq_idx].strip()
|
||||
value_str = rest[eq_idx + 1:].strip()
|
||||
if not field_name:
|
||||
return
|
||||
resolved = AutoScriptEngine._resolveValue(value_str, user_data)
|
||||
AutoScriptEngine._setField(field_name, resolved, user_data)
|
||||
|
||||
@staticmethod
|
||||
def _executeOperation(
|
||||
line: str,
|
||||
user_data: dict
|
||||
):
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
return
|
||||
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):
|
||||
raise ValueError(
|
||||
f"AutoScript 语法错误: 无效操作数 '{raw_value}'"
|
||||
)
|
||||
if field_type == "Date":
|
||||
date_str = AutoScriptEngine._resolveField(field_name, user_data)
|
||||
if not date_str:
|
||||
return
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
if op == ".ADD.":
|
||||
date_obj += timedelta(days=num_value)
|
||||
elif op == ".SUB.":
|
||||
date_obj -= timedelta(days=num_value)
|
||||
else:
|
||||
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:
|
||||
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:
|
||||
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}'"
|
||||
)
|
||||
|
||||
|
||||
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"):
|
||||
line = line[:-5].rstrip()
|
||||
paren_depth = 0
|
||||
start_found = False
|
||||
for i, ch in enumerate(line):
|
||||
if ch == "(":
|
||||
paren_depth += 1
|
||||
start_found = True
|
||||
elif ch == ")":
|
||||
paren_depth -= 1
|
||||
if start_found and paren_depth == 0:
|
||||
return i
|
||||
return -1
|
||||
@@ -1,9 +0,0 @@
|
||||
"""
|
||||
DSL module for the AutoLibrary project.
|
||||
|
||||
Contains the AutoScript DSL engine and related components
|
||||
for preprocessing user reservation data in timer tasks.
|
||||
|
||||
Classes:
|
||||
- AutoScriptEngine: AutoScript script engine class.
|
||||
"""
|
||||
Reference in New Issue
Block a user