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 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"] __all__ = ["execute", "addTargetVar", "resetEngine"]
@@ -23,11 +32,19 @@ _TARGET_VARS: dict[str, dict] = {}
_lua = None _lua = None
# Built-in meta variable definitions (name / type / display-name) # 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_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"},
"CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "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( def _getLua(
): ):
@@ -170,6 +187,62 @@ def _assignPath(
d[key_path[-1]] = value 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( def _checkType(
var_name: str, var_name: str,
var_type: str, var_type: str,
@@ -188,17 +261,17 @@ def _checkType(
if not isinstance(value, str): if not isinstance(value, str):
raise ValueError( raise ValueError(
f"Date 类型变量 '{var_name}' 只能接受日期字符串," f"Date 类型变量 '{var_name}' 只能接受日期字符串,"
f"不能接受 {type(value).__name__} 类型" f"不能接受 {_pyTypeToASType(value)} 类型"
) )
date.fromisoformat(value) _checkDateFormat(value, var_name)
return return
if var_type == "Time": if var_type == "Time":
if not isinstance(value, str): if not isinstance(value, str):
raise ValueError( raise ValueError(
f"Time 类型变量 '{var_name}' 只能接受时间字符串," f"Time 类型变量 '{var_name}' 只能接受时间字符串,"
f"不能接受 {type(value).__name__} 类型" f"不能接受 {_pyTypeToASType(value)} 类型"
) )
datetime.strptime(value, "%H:%M") _checkTimeFormat(value, var_name)
return return
if var_type == "Int": if var_type == "Int":
if isinstance(value, bool): if isinstance(value, bool):
@@ -207,7 +280,7 @@ def _checkType(
) )
if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)): if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)):
raise ValueError( raise ValueError(
f"Int 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" f"Int 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
) )
return return
if var_type == "Float": if var_type == "Float":
@@ -217,19 +290,19 @@ def _checkType(
) )
if not isinstance(value, (int, float)): if not isinstance(value, (int, float)):
raise ValueError( raise ValueError(
f"Float 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" f"Float 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
) )
return return
if var_type == "Boolean": if var_type == "Boolean":
if not isinstance(value, bool): if not isinstance(value, bool):
raise ValueError( raise ValueError(
f"Boolean 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" f"Boolean 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
) )
return return
if var_type == "String": if var_type == "String":
if not isinstance(value, str): if not isinstance(value, str):
raise ValueError( raise ValueError(
f"String 类型变量 '{var_name}' 不能接受 {type(value).__name__} 类型的值" f"String 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
) )
return return
@@ -238,7 +311,7 @@ def addTargetVar(
name: str, name: str,
var_type: str, var_type: str,
key_path: list, key_path: list,
display_name: str = None, _display_name: str = None,
) -> None: ) -> None:
""" """
Register a new target variable bound to a path in the application data dict. 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"). name (str): The canonical variable name (e.g. "RESERVE_DATE").
var_type (str): "Int" | "Float" | "Boolean" | "Date" | "Time" | "String". var_type (str): "Int" | "Float" | "Boolean" | "Date" | "Time" | "String".
key_path (list): Nested path into target_data, e.g. ["reserve_info", "date"]. 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() upper_name = name.upper().strip()
@@ -263,6 +335,7 @@ def resetEngine(
Reset the engine to its initial state: clear all target variables Reset the engine to its initial state: clear all target variables
and release the Lua runtime. and release the Lua runtime.
""" """
global _TARGET_VARS, _lua global _TARGET_VARS, _lua
_TARGET_VARS = {} _TARGET_VARS = {}
_lua = None _lua = None
@@ -274,6 +347,9 @@ def _push(
""" """
Push target_data values into Lua globals. Push target_data values into Lua globals.
Date / Time strings are converted to native Lua types (timestamp / minutes). 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() lua = _getLua()
@@ -285,28 +361,27 @@ def _push(
key_path = info["key_path"] key_path = info["key_path"]
vt = info["type"] vt = info["type"]
raw = _navigatePath(target_data, key_path) raw = _navigatePath(target_data, key_path)
if vt == "Date": if vt == "Date":
if raw and isinstance(raw, str): if not isinstance(raw, str) or not raw.strip():
try: raise ValueError(
date.fromisoformat(raw.strip()) f"Date 类型变量 '{var_name}' 对应的数据为空或不是字符串类型,"
except (ValueError, AttributeError): f"请检查路径 {key_path} 的值是否为合法的日期字符串 (YYYY-MM-DD)"
raw = "2099-01-01" )
else: raw = raw.strip()
raw = "2099-01-01" _checkDateFormat(raw, var_name)
g[var_name] = _toDate(raw) g[var_name] = _toDate(raw)
elif vt == "Time": elif vt == "Time":
if raw and isinstance(raw, str): if not isinstance(raw, str) or not raw.strip():
try: raise ValueError(
datetime.strptime(raw.strip(), "%H:%M") f"Time 类型变量 '{var_name}' 对应的数据为空或不是字符串类型,"
except (ValueError, AttributeError): f"请检查路径 {key_path} 的值是否为合法的时间字符串 (HH:MM)"
raw = "00:00" )
else: raw = raw.strip()
raw = "00:00" _checkTimeFormat(raw, var_name)
g[var_name] = _toTime(raw) g[var_name] = _toTime(raw)
else: else:
if raw is None: 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 g[var_name] = raw
@@ -326,7 +401,7 @@ def _pull(
for var_name, info in _TARGET_VARS.items(): for var_name, info in _TARGET_VARS.items():
try: try:
lua_val = g[var_name] lua_val = g[var_name]
except (KeyError, AttributeError): except KeyError:
continue continue
vt = info["type"] vt = info["type"]
if vt == "Date": if vt == "Date":
@@ -339,6 +414,20 @@ def _pull(
_assignPath(target_data, info["key_path"], lua_val) _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( def execute(
script_text: str, script_text: str,
target_data: dict, target_data: dict,
@@ -366,9 +455,19 @@ def execute(
if not script_text or not script_text.strip(): if not script_text or not script_text.strip():
return return
_push(target_data)
try: try:
_push(target_data)
_getLua().execute(script_text) _getLua().execute(script_text)
_pull(target_data) _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: except Exception as e:
raise ValueError(f"AutoScript 执行错误: {e}") raise ValueError(f"AutoScript 未知错误: {e}")
+5 -5
View File
@@ -31,11 +31,11 @@ __all__ = [
# Key paths into target_data dict for each target variable. # Key paths into target_data dict for each target variable.
# (name, type, key_path, display_name) # (name, type, key_path, display_name)
_TARGET_VAR_DEFS = [ _TARGET_VAR_DEFS = [
("USERNAME", "String",["username"], "用户名"), ("USERNAME", "String", ["username"], "用户名"),
("USER_ENABLE", "Boolean",["enabled"], "用户启用"), ("USER_ENABLE", "Boolean",["enabled"], "用户启用"),
("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"), ("RESERVE_DATE", "Date", ["reserve_info", "date"], "预约日期"),
("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"), ("RESERVE_BEGIN_TIME", "Time", ["reserve_info", "begin_time", "time"], "预约开始时间"),
("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"), ("RESERVE_END_TIME", "Time", ["reserve_info", "end_time", "time"], "预约结束时间"),
] ]
# All variables (display_name -> (name, type)), derived from target vars + meta vars. # All variables (display_name -> (name, type)), derived from target vars + meta vars.
+29 -34
View File
@@ -169,7 +169,7 @@ class ALAutoScriptEditDialog(QDialog):
): ):
super().__init__(parent) super().__init__(parent)
self._fontSize = 19 self._fontSize = 13
self._mockWidgets = {} self._mockWidgets = {}
self.setupUi() self.setupUi()
@@ -267,17 +267,17 @@ class ALAutoScriptEditDialog(QDialog):
basicLayout.setContentsMargins(4, 4, 4, 4) basicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [ controlButtons = [
("if", "if then\n \nend"), ("如果 (if...)", "if then\n \nend"),
("elseif", "elseif then\n "), ("再如果 (elseif...)", "elseif then\n "),
("else", "else"), ("否则 (else)", "else"),
("end", "end"), ("结束 (end)", "end"),
("-- pass", "-- pass"), ("跳过 (pass)", "-- pass"),
] ]
self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5) self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3)
assignButtons = [ assignButtons = [
("=", " = "), ("赋值 (=)", " = "),
] ]
self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1) self._addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3)
tabWidget.addTab(basicWidget, "基本语法") tabWidget.addTab(basicWidget, "基本语法")
operatorWidget = QWidget() operatorWidget = QWidget()
operatorLayout = QGridLayout(operatorWidget) operatorLayout = QGridLayout(operatorWidget)
@@ -285,24 +285,24 @@ class ALAutoScriptEditDialog(QDialog):
operatorLayout.setContentsMargins(4, 4, 4, 4) operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [ arithmeticButtons = [
("+", " + "), ("加 (+)", " + "),
("-", " - "), ("减 (-)", " - "),
] ]
self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2) self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3)
compareButtons = [ compareButtons = [
("==", " == "), ("等于 (==)", " == "),
("~=", " ~= "), ("不等于 (~=)", " ~= "),
(">", " > "), ("大于 (>)", " > "),
("<", " < "), ("小于 (<)", " < "),
(">=", " >= "), ("大于等于 (>=)", " >= "),
("<=", " <= "), ("小于等于 (<=)", " <= "),
] ]
self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6) self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3)
logic_buttons = [ logic_buttons = [
("and", " and "), ("且 (and)", " and "),
("or", " or "), ("或 (or)", " or "),
] ]
self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2) self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3)
tabWidget.addTab(operatorWidget, "运算符") tabWidget.addTab(operatorWidget, "运算符")
literalWidget = QWidget() literalWidget = QWidget()
literalLayout = QGridLayout(literalWidget) literalLayout = QGridLayout(literalWidget)
@@ -310,15 +310,15 @@ class ALAutoScriptEditDialog(QDialog):
literalLayout.setContentsMargins(4, 4, 4, 4) literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [ bool_buttons = [
("true", "true"), ("真 (true)", "true"),
("false", "false"), ("假 (false)", "false"),
] ]
self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2) self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [ dateTimeButtons = [
("日期", '"2099-01-01"'), ("日期", '"2099-01-01"'),
("时间", '"00:00"'), ("时间", '"00:00"'),
] ]
self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2) self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3)
hintButtons = [ hintButtons = [
("字符串", '"请输入文本"'), ("字符串", '"请输入文本"'),
("数字", "123"), ("数字", "123"),
@@ -334,16 +334,15 @@ class ALAutoScriptEditDialog(QDialog):
varButtons = [ varButtons = [
(display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() (display_name, name) for display_name, (name, _) in ALL_VARIABLES.items()
] ]
self._addButtonsToGrid(varLayout, varButtons, 0, 0, 3)
self._addButtonsToGrid(varLayout, varButtons, 0, 0, 5)
tabWidget.addTab(varWidget, "变量") tabWidget.addTab(varWidget, "变量")
mockPanel = self._createMockPanel() mockPanel = self._createMockPanel()
mockPanel.setMinimumWidth(260) mockPanel.setMinimumWidth(260)
splitter.addWidget(tabWidget) splitter.addWidget(tabWidget)
splitter.addWidget(mockPanel) splitter.addWidget(mockPanel)
splitter.setStretchFactor(0, 1) splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 0) splitter.setStretchFactor(1, 1)
splitter.setSizes([660, 400]) splitter.setSizes([530, 530])
parent_layout.addWidget(splitter) parent_layout.addWidget(splitter)
@@ -367,7 +366,6 @@ class ALAutoScriptEditDialog(QDialog):
btn.setFixedHeight(25) btn.setFixedHeight(25)
btn.setToolTip(f"插入: {template}") btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col) grid_layout.addWidget(btn, row, col)
col += 1 col += 1
if col >= start_col + max_columns: if col >= start_col + max_columns:
col = start_col col = start_col
@@ -585,9 +583,6 @@ class ALAutoScriptEditDialog(QDialog):
self self
): ):
font = self.textEdit.font()
font.setPointSize(self._fontSize)
self.textEdit.setFont(font)
self.textEdit.setStyleSheet( self.textEdit.setStyleSheet(
"QPlainTextEdit {" "QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;" " font-family: 'Courier New', 'Consolas', monospace;"
@@ -88,6 +88,8 @@ DATE_RELATIVE_OPTIONS = [
DATE_OFFSET_UNITS = [ DATE_OFFSET_UNITS = [
("", "days"), ("", "days"),
("", "weeks"), ("", "weeks"),
# NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years,
# because date_add() works with second-level offsets (n * 86400).
("", "months"), ("", "months"),
("", "years"), ("", "years"),
] ]
@@ -657,6 +659,8 @@ def _encodeDateOrTime(
s = raw_value.strip() s = raw_value.strip()
up = s.upper() 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_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_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s)
m_arith = m_arith_spaced or m_arith_nospace m_arith = m_arith_spaced or m_arith_nospace