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:
+129
-30
@@ -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}")
|
||||
|
||||
@@ -31,11 +31,11 @@ __all__ = [
|
||||
# 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"], "预约结束时间"),
|
||||
("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"], "预约结束时间"),
|
||||
]
|
||||
|
||||
# All variables (display_name -> (name, type)), derived from target vars + meta vars.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user