diff --git a/src/autoscript/ASEngine.py b/src/autoscript/ASEngine.py index 129e803..feee592 100644 --- a/src/autoscript/ASEngine.py +++ b/src/autoscript/ASEngine.py @@ -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 ""]:', "").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}") diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index 480d09e..3f85baf 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -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. diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py index 0884f82..23656ac 100644 --- a/src/gui/ALAutoScriptEditDialog.py +++ b/src/gui/ALAutoScriptEditDialog.py @@ -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;" diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py index 10ca230..b48ee37 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -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