1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-17 23:13:03 +08:00

refactor(autoscript): 完善 Lua 错误分类与 Date/Time 严格校验,清理死代码并补齐类型注解

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 01:02:17 +08:00
parent 4761cade26
commit a03ab38279
4 changed files with 167 additions and 69 deletions
+129 -30
View File
@@ -14,6 +14,15 @@ from datetime import (
from lupa import LuaRuntime as _LuaRuntime
try:
from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
except ImportError:
try:
from lupa.lua54 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
except ImportError:
_LuaError = Exception
_LuaSyntaxError = Exception
__all__ = ["execute", "addTargetVar", "resetEngine"]
@@ -23,11 +32,19 @@ _TARGET_VARS: dict[str, dict] = {}
_lua = None
# Built-in meta variable definitions (name / type / display-name)
META_VARS = {
META_VARS: dict[str, dict[str, str]] = {
"CURRENT_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"},
"CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "display": "当前时间"},
}
# Per-type fallback value when target_data entry is missing.
_DEFAULT_BY_TYPE: dict[str, str | int | float | bool] = {
"String": "",
"Int": 0,
"Float": 0.0,
"Boolean": False,
}
def _getLua(
):
@@ -170,6 +187,62 @@ def _assignPath(
d[key_path[-1]] = value
def _pyTypeToASType(
value
) -> str:
"""
Map a Python runtime value to its AutoScript type name.
"""
if isinstance(value, bool):
return "Boolean"
if isinstance(value, int):
return "Int"
if isinstance(value, float):
return "Float"
if isinstance(value, str):
return "String"
return "Unknown"
def _checkDateFormat(
date_str: str,
var_name: str = "",
) -> None:
"""
Validate that *date_str* is in YYYY-MM-DD format.
Raises ValueError with a descriptive message on failure.
"""
prefix = f"Date 类型变量 '{var_name}'" if var_name else ""
try:
date.fromisoformat(date_str)
except ValueError:
raise ValueError(
f"{prefix}'{date_str}' 不是合法的日期格式,"
f"应为 YYYY-MM-DD"
)
def _checkTimeFormat(
time_str: str,
var_name: str = "",
) -> None:
"""
Validate that *time_str* is in HH:MM format.
Raises ValueError with a descriptive message on failure.
"""
prefix = f"Time 类型变量 '{var_name}'" if var_name else ""
try:
datetime.strptime(time_str, "%H:%M")
except ValueError:
raise ValueError(
f"{prefix}'{time_str}' 不是合法的时间格式,"
f"应为 HH:MM"
)
def _checkType(
var_name: str,
var_type: str,
@@ -188,17 +261,17 @@ def _checkType(
if not isinstance(value, str):
raise ValueError(
f"Date 类型变量 '{var_name}' 只能接受日期字符串,"
f"不能接受 {type(value).__name__} 类型"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
date.fromisoformat(value)
_checkDateFormat(value, var_name)
return
if var_type == "Time":
if not isinstance(value, str):
raise ValueError(
f"Time 类型变量 '{var_name}' 只能接受时间字符串,"
f"不能接受 {type(value).__name__} 类型"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
datetime.strptime(value, "%H:%M")
_checkTimeFormat(value, var_name)
return
if var_type == "Int":
if isinstance(value, bool):
@@ -207,7 +280,7 @@ def _checkType(
)
if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)):
raise ValueError(
f"Int 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值"
f"Int 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if var_type == "Float":
@@ -217,19 +290,19 @@ def _checkType(
)
if not isinstance(value, (int, float)):
raise ValueError(
f"Float 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值"
f"Float 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if var_type == "Boolean":
if not isinstance(value, bool):
raise ValueError(
f"Boolean 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值"
f"Boolean 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if var_type == "String":
if not isinstance(value, str):
raise ValueError(
f"String 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值"
f"String 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
@@ -238,7 +311,7 @@ def addTargetVar(
name: str,
var_type: str,
key_path: list,
display_name: str = None,
_display_name: str = None,
) -> None:
"""
Register a new target variable bound to a path in the application data dict.
@@ -247,7 +320,6 @@ def addTargetVar(
name (str): The canonical variable name (e.g. "RESERVE_DATE").
var_type (str): "Int" | "Float" | "Boolean" | "Date" | "Time" | "String".
key_path (list): Nested path into target_data, e.g. ["reserve_info", "date"].
display_name (str): Optional Chinese alias (unused by the engine).
"""
upper_name = name.upper().strip()
@@ -263,6 +335,7 @@ def resetEngine(
Reset the engine to its initial state: clear all target variables
and release the Lua runtime.
"""
global _TARGET_VARS, _lua
_TARGET_VARS = {}
_lua = None
@@ -274,6 +347,9 @@ def _push(
"""
Push target_data values into Lua globals.
Date / Time strings are converted to native Lua types (timestamp / minutes).
Raises ValueError for missing / malformed Date or Time values so that
execute() can surface them as user-visible AutoScript execution errors.
"""
lua = _getLua()
@@ -285,28 +361,27 @@ def _push(
key_path = info["key_path"]
vt = info["type"]
raw = _navigatePath(target_data, key_path)
if vt == "Date":
if raw and isinstance(raw, str):
try:
date.fromisoformat(raw.strip())
except (ValueError, AttributeError):
raw = "2099-01-01"
else:
raw = "2099-01-01"
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Date 类型变量 '{var_name}' 对应的数据为空或不是字符串类型,"
f"请检查路径 {key_path} 的值是否为合法的日期字符串 (YYYY-MM-DD)"
)
raw = raw.strip()
_checkDateFormat(raw, var_name)
g[var_name] = _toDate(raw)
elif vt == "Time":
if raw and isinstance(raw, str):
try:
datetime.strptime(raw.strip(), "%H:%M")
except (ValueError, AttributeError):
raw = "00:00"
else:
raw = "00:00"
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Time 类型变量 '{var_name}' 对应的数据为空或不是字符串类型,"
f"请检查路径 {key_path} 的值是否为合法的时间字符串 (HH:MM)"
)
raw = raw.strip()
_checkTimeFormat(raw, var_name)
g[var_name] = _toTime(raw)
else:
if raw is None:
raw = "" if vt == "String" else 0 if vt == "Int" else 0.0 if vt == "Float" else False
raw = _DEFAULT_BY_TYPE.get(vt, False)
g[var_name] = raw
@@ -326,7 +401,7 @@ def _pull(
for var_name, info in _TARGET_VARS.items():
try:
lua_val = g[var_name]
except (KeyError, AttributeError):
except KeyError:
continue
vt = info["type"]
if vt == "Date":
@@ -339,6 +414,20 @@ def _pull(
_assignPath(target_data, info["key_path"], lua_val)
def _cleanLuaError(
raw_msg: str
) -> str:
"""
Strip internal source prefix and stack traceback from a Lua error message.
"""
msg = raw_msg.replace('[string "<python>"]:', "").strip()
stack_idx = msg.find("stack traceback:")
if stack_idx != -1:
msg = msg[:stack_idx].strip()
return msg
def execute(
script_text: str,
target_data: dict,
@@ -366,9 +455,19 @@ def execute(
if not script_text or not script_text.strip():
return
_push(target_data)
try:
_push(target_data)
_getLua().execute(script_text)
_pull(target_data)
except _LuaSyntaxError as e:
raise ValueError(
f"AutoScript 语法错误: {_cleanLuaError(str(e))}"
)
except _LuaError as e:
raise ValueError(
f"AutoScript 运行时错误: {_cleanLuaError(str(e))}"
)
except ValueError as e:
raise ValueError(f"AutoScript 数据错误: {e}")
except Exception as e:
raise ValueError(f"AutoScript 执行错误: {e}")
raise ValueError(f"AutoScript 未知错误: {e}")
+1 -1
View File
@@ -31,7 +31,7 @@ __all__ = [
# Key paths into target_data dict for each target variable.
# (name, type, key_path, display_name)
_TARGET_VAR_DEFS = [
("USERNAME", "String",["username"], "用户名"),
("USERNAME", "String", ["username"], "用户名"),
("USER_ENABLE", "Boolean",["enabled"], "用户启用"),
("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"),
("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"),
+29 -34
View File
@@ -169,7 +169,7 @@ class ALAutoScriptEditDialog(QDialog):
):
super().__init__(parent)
self._fontSize = 19
self._fontSize = 13
self._mockWidgets = {}
self.setupUi()
@@ -267,17 +267,17 @@ class ALAutoScriptEditDialog(QDialog):
basicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [
("if", "if then\n \nend"),
("elseif", "elseif then\n "),
("else", "else"),
("end", "end"),
("-- pass", "-- pass"),
("如果 (if...)", "if then\n \nend"),
("再如果 (elseif...)", "elseif then\n "),
("否则 (else)", "else"),
("结束 (end)", "end"),
("跳过 (pass)", "-- pass"),
]
self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5)
self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3)
assignButtons = [
("=", " = "),
("赋值 (=)", " = "),
]
self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1)
self._addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3)
tabWidget.addTab(basicWidget, "基本语法")
operatorWidget = QWidget()
operatorLayout = QGridLayout(operatorWidget)
@@ -285,24 +285,24 @@ class ALAutoScriptEditDialog(QDialog):
operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [
("+", " + "),
("-", " - "),
("加 (+)", " + "),
("减 (-)", " - "),
]
self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2)
self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3)
compareButtons = [
("==", " == "),
("~=", " ~= "),
(">", " > "),
("<", " < "),
(">=", " >= "),
("<=", " <= "),
("等于 (==)", " == "),
("不等于 (~=)", " ~= "),
("大于 (>)", " > "),
("小于 (<)", " < "),
("大于等于 (>=)", " >= "),
("小于等于 (<=)", " <= "),
]
self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6)
self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3)
logic_buttons = [
("and", " and "),
("or", " or "),
("且 (and)", " and "),
("或 (or)", " or "),
]
self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2)
self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3)
tabWidget.addTab(operatorWidget, "运算符")
literalWidget = QWidget()
literalLayout = QGridLayout(literalWidget)
@@ -310,15 +310,15 @@ class ALAutoScriptEditDialog(QDialog):
literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [
("true", "true"),
("false", "false"),
("真 (true)", "true"),
("假 (false)", "false"),
]
self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2)
self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [
("日期", '"2099-01-01"'),
("时间", '"00:00"'),
]
self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2)
self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3)
hintButtons = [
("字符串", '"请输入文本"'),
("数字", "123"),
@@ -334,16 +334,15 @@ class ALAutoScriptEditDialog(QDialog):
varButtons = [
(display_name, name) for display_name, (name, _) in ALL_VARIABLES.items()
]
self._addButtonsToGrid(varLayout, varButtons, 0, 0, 5)
self._addButtonsToGrid(varLayout, varButtons, 0, 0, 3)
tabWidget.addTab(varWidget, "变量")
mockPanel = self._createMockPanel()
mockPanel.setMinimumWidth(260)
splitter.addWidget(tabWidget)
splitter.addWidget(mockPanel)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 0)
splitter.setSizes([660, 400])
splitter.setStretchFactor(1, 1)
splitter.setSizes([530, 530])
parent_layout.addWidget(splitter)
@@ -367,7 +366,6 @@ class ALAutoScriptEditDialog(QDialog):
btn.setFixedHeight(25)
btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col)
col += 1
if col >= start_col + max_columns:
col = start_col
@@ -585,9 +583,6 @@ class ALAutoScriptEditDialog(QDialog):
self
):
font = self.textEdit.font()
font.setPointSize(self._fontSize)
self.textEdit.setFont(font)
self.textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
@@ -88,6 +88,8 @@ DATE_RELATIVE_OPTIONS = [
DATE_OFFSET_UNITS = [
("", "days"),
("", "weeks"),
# NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years,
# because date_add() works with second-level offsets (n * 86400).
("", "months"),
("", "years"),
]
@@ -657,6 +659,8 @@ def _encodeDateOrTime(
s = raw_value.strip()
up = s.upper()
# Input comes from widget values — single binary expressions only (e.g. "A + 3",
# "CURRENT_DATE + 5"). Multi-operator expressions are not produced by the UI.
m_arith_spaced = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s)
m_arith_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s)
m_arith = m_arith_spaced or m_arith_nospace