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

refactor(autoscript): 对象化 ASEngine、移除旧变量导出、清理编排窗口解析逻辑

- ASEngine 转为类,目标变量注册作为 __init__ 接口,配套函数提取到 _helpers.py
- Lua 函数重命名 CURRENT_DATE→datenow, CURRENT_TIME→timenow, date_add→dateadd 等
- __init__.py 移除 ALL_VARIABLES/_TARGET_VAR_DEFS/_MOCK_TYPE_VALUES 导出,替换为接口函数
- 编排窗口移除脚本→控件的反向解析逻辑,合并常量定义为查询接口
- 编辑窗口新增工具函数 Tab、Tab 键插入 4 空格、图标改用 setIcon 加载

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 19:10:07 +08:00
parent 5e898180c7
commit 106463b9e5
13 changed files with 536 additions and 801 deletions
+199 -395
View File
@@ -14,6 +14,16 @@ from datetime import (
from lupa import LuaRuntime as _LuaRuntime
from autoscript._helpers import (
_TYPE_DEFAULT_VAR,
_assignPath,
_checkDateFormat,
_checkTimeFormat,
_checkType,
_cleanLuaError,
_navigatePath,
)
try:
from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
except ImportError:
@@ -24,435 +34,229 @@ except ImportError:
_LuaSyntaxError = Exception
__all__ = ["execute", "addTargetVar", "resetEngine"]
__all__ = ["ASEngine"]
# Engine state
_TARGET_VARS: dict[str, dict] = {}
_lua = None
class ASEngine:
# Built-in meta variable definitions (name / type / display-name)
META_VARS: dict[str, dict[str, str]] = {
"CURRENT_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"},
"CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "display": "当前时间"},
}
@staticmethod
def getCurrentDate(
) -> str:
# 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,
}
return date.today().isoformat()
def _getLua(
):
"""
Return the sandboxed Lua runtime singleton.
"""
@staticmethod
def getCurrentTime(
) -> str:
global _lua
if _lua is None:
_lua = _LuaRuntime(unpack_returned_tuples = True)
_sandbox(_lua)
_registerHelpers(_lua)
return _lua
return datetime.now().strftime("%H:%M")
def _sandbox(
lua,
) -> None:
"""
Remove dangerous Lua globals while keeping os.date / os.time for date-time helpers.
"""
@staticmethod
def _sandbox(
lua,
) -> None:
lua.execute("""
io = nil
require = nil
dofile = nil
loadfile = nil
load = nil
package = nil
rawget = nil
rawset = nil
rawequal = nil
getfenv = nil
setfenv = nil
debug = nil
-- selectively disable dangerous os functions, keep date / time
if os then
os.execute = nil
os.exit = nil
os.getenv = nil
os.remove = nil
os.rename = nil
os.tmpname = nil
os.setlocale = nil
end
""")
lua.execute("""
io = nil
require = nil
dofile = nil
loadfile = nil
load = nil
package = nil
rawget = nil
rawset = nil
rawequal = nil
getfenv = nil
setfenv = nil
debug = nil
if os then
os.execute = nil
os.exit = nil
os.getenv = nil
os.remove = nil
os.rename = nil
os.tmpname = nil
os.setlocale = nil
end
""")
def _registerHelpers(
lua,
) -> None:
"""
Inject Date / Time helpers as pure Lua functions.
@staticmethod
def _registerHelpers(
lua,
) -> None:
Date values are os.time timestamps (seconds since epoch).
Time values are minutes since midnight (0-1439).
lua.execute("""
function date(y, m, d)
return os.time({year = y, month = m, day = d})
end
This keeps Date / Time as native Lua numbers during script execution,
enabling type-safe arithmetic (+, -) and comparisons (<, <=, ==, ~=).
"""
function time(h, m)
return h * 60 + m
end
lua.execute("""
function date(y, m, d)
return os.time({year = y, month = m, day = d})
end
function datenow()
local now = os.date("*t")
return os.time({year = now.year, month = now.month, day = now.day})
end
function time(h, m)
return h * 60 + m
end
function timenow()
local now = os.date("*t")
return now.hour * 60 + now.min
end
function CURRENT_DATE()
local now = os.date("*t")
return os.time({year = now.year, month = now.month, day = now.day})
end
function dateadd(date_val, n)
return date_val + n * 86400
end
function CURRENT_TIME()
local now = os.date("*t")
return now.hour * 60 + now.min
end
function timeadd(time_val, n)
return (time_val + n * 60) % 1440
end
function date_add(date_val, n)
return date_val + n * 86400
end
function strtodate(iso_str)
local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)")
return os.time({year = y, month = m, day = d})
end
function time_add(time_val, n)
return (time_val + n * 60) % 1440
end
function strtotime(hm_str)
local h, m = hm_str:match("(%d+):(%d+)")
return h * 60 + m
end
-- push helpers: string -> native type
function _to_date(iso_str)
local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)")
return os.time({year = y, month = m, day = d})
end
function datetostr(ts)
return os.date("%Y-%m-%d", ts)
end
function _to_time(hm_str)
local h, m = hm_str:match("(%d+):(%d+)")
return h * 60 + m
end
function timetostr(m)
return string.format("%02d:%02d", math.floor(m / 60), m % 60)
end
""")
-- pull helpers: native type -> string
function _from_date(ts)
return os.date("%Y-%m-%d", ts)
end
def __init__(
self,
targetVars: list[tuple] = None,
):
function _from_time(m)
return string.format("%02d:%02d", math.floor(m / 60), m % 60)
end
""")
self._targetVars: dict[str, dict] = {}
self._lua = None
def _navigatePath(
data: dict,
key_path: list,
default = None,
):
"""
Walk *key_path* into *data* and return the value at the leaf.
"""
if targetVars:
for item in targetVars:
name, varType, keyPath = item[0], item[1], item[2]
self.addTargetVar(name, varType, keyPath)
d = data
for key in key_path[:-1]:
d = d.get(key, {})
if not isinstance(d, dict):
return default
return d.get(key_path[-1], default)
def _getLua(
self,
):
def _assignPath(
data: dict,
key_path: list,
value,
) -> None:
"""
Walk *key_path* into *data* and set *value* at the leaf.
"""
if self._lua is None:
self._lua = _LuaRuntime(unpack_returned_tuples=True)
self._sandbox(self._lua)
self._registerHelpers(self._lua)
return self._lua
d = data
for key in key_path[:-1]:
d = d.setdefault(key, {})
d[key_path[-1]] = value
def _push(
self,
targetData: dict,
) -> None:
def _pyTypeToASType(
value
) -> str:
"""
Map a Python runtime value to its AutoScript type name.
"""
lua = self._getLua()
g = lua.globals()
strToDate = g["strtodate"]
strToTime = g["strtotime"]
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"
for varName, info in self._targetVars.items():
keyPath = info["keyPath"]
vt = info["type"]
raw = _navigatePath(targetData, keyPath)
if vt == "Date":
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Date 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
f"请检查路径 {keyPath} 的值是否为合法的日期字符串 (YYYY-MM-DD)"
)
raw = raw.strip()
_checkDateFormat(raw, varName)
g[varName] = strToDate(raw)
elif vt == "Time":
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Time 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
f"请检查路径 {keyPath} 的值是否为合法的时间字符串 (HH:MM)"
)
raw = raw.strip()
_checkTimeFormat(raw, varName)
g[varName] = strToTime(raw)
else:
if raw is None:
raw = _TYPE_DEFAULT_VAR.get(vt, False)
g[varName] = raw
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.
"""
def _pull(
self,
targetData: dict,
) -> None:
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"
)
lua = self._getLua()
g = lua.globals()
dateToStr = g["datetostr"]
timeToStr = g["timetostr"]
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.
"""
for varName, info in self._targetVars.items():
try:
luaVal = g[varName]
except KeyError:
continue
vt = info["type"]
if vt == "Date":
luaVal = dateToStr(luaVal)
elif vt == "Time":
luaVal = timeToStr(luaVal)
elif vt == "Float" and isinstance(luaVal, int) and not isinstance(luaVal, bool):
luaVal = float(luaVal)
_checkType(varName, vt, luaVal)
_assignPath(targetData, info["keyPath"], luaVal)
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 addTargetVar(
self,
name: str,
varType: str,
keyPath: list,
) -> None:
def _checkType(
var_name: str,
var_type: str,
value,
) -> None:
"""
Validate that *value* matches the declared variable type.
upperName = name.upper().strip()
self._targetVars[upperName] = {
"type": varType,
"keyPath": keyPath,
}
Date / Time values arrive as ISO / HH:MM strings (already converted
from Lua native types during the pull phase).
Int / Float / Boolean / String check Python type identity.
Int -> Float widening is allowed.
"""
def execute(
self,
scriptText: str,
targetData: dict,
) -> None:
if var_type == "Date":
if not isinstance(value, str):
raise ValueError(
f"Date 类型变量 '{var_name}' 只能接受日期字符串,"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
_checkDateFormat(value, var_name)
return
if var_type == "Time":
if not isinstance(value, str):
raise ValueError(
f"Time 类型变量 '{var_name}' 只能接受时间字符串,"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
_checkTimeFormat(value, var_name)
return
if var_type == "Int":
if isinstance(value, bool):
raise ValueError(
f"Int 类型变量 '{var_name}' 不能接受 Boolean 类型的值"
)
if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)):
raise ValueError(
f"Int 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if var_type == "Float":
if isinstance(value, bool):
raise ValueError(
f"Float 类型变量 '{var_name}' 不能接受 Boolean 类型的值"
)
if not isinstance(value, (int, float)):
raise ValueError(
f"Float 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if var_type == "Boolean":
if not isinstance(value, bool):
raise ValueError(
f"Boolean 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if var_type == "String":
if not isinstance(value, str):
raise ValueError(
f"String 类型变量 '{var_name}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
def addTargetVar(
name: str,
var_type: str,
key_path: list,
_display_name: str = None,
) -> None:
"""
Register a new target variable bound to a path in the application data dict.
Args:
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"].
"""
upper_name = name.upper().strip()
_TARGET_VARS[upper_name] = {
"type": var_type,
"key_path": key_path,
}
def resetEngine(
) -> None:
"""
Reset the engine to its initial state: clear all target variables
and release the Lua runtime.
"""
global _TARGET_VARS, _lua
_TARGET_VARS = {}
_lua = None
def _push(
target_data: dict,
) -> None:
"""
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()
g = lua.globals()
_toDate = g["_to_date"]
_toTime = g["_to_time"]
for var_name, info in _TARGET_VARS.items():
key_path = info["key_path"]
vt = info["type"]
raw = _navigatePath(target_data, key_path)
if vt == "Date":
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 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 = _DEFAULT_BY_TYPE.get(vt, False)
g[var_name] = raw
def _pull(
target_data: dict,
) -> None:
"""
Pull Lua global values back into target_data.
Date / Time native types are converted back to ISO / HH:MM strings.
"""
lua = _getLua()
g = lua.globals()
_fromDate = g["_from_date"]
_fromTime = g["_from_time"]
for var_name, info in _TARGET_VARS.items():
if not scriptText or not scriptText.strip():
return
try:
lua_val = g[var_name]
except KeyError:
continue
vt = info["type"]
if vt == "Date":
lua_val = _fromDate(lua_val)
elif vt == "Time":
lua_val = _fromTime(lua_val)
elif vt == "Float" and isinstance(lua_val, int) and not isinstance(lua_val, bool):
lua_val = float(lua_val)
_checkType(var_name, vt, lua_val)
_assignPath(target_data, info["key_path"], lua_val)
self._push(targetData)
self._getLua().execute(scriptText)
self._pull(targetData)
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}")
def _cleanLuaError(
raw_msg: str
) -> str:
"""
Strip internal source prefix and stack traceback from a Lua error message.
"""
def reset(
self,
) -> None:
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,
) -> None:
"""
Execute an AutoScript (Lua) on the given target data.
The script runs in a sandboxed Lua environment with target variables
exposed as globals. The following helpers are available as Lua functions:
date(y, m, d) -> timestamp (os.time seconds)
time(h, m) -> minutes since midnight (0-1439)
CURRENT_DATE() -> today's timestamp
CURRENT_TIME() -> current minutes since midnight
date_add(ts, n) -> ts + n * 86400
time_add(m, n) -> (m + n * 60) % 1440
Date and Time values are native Lua numbers during execution.
Arithmetic (+, -) and comparisons (<, <=, ==, ~=, >, >=) work
with strong type safety — no implicit string coercion.
Raises:
ValueError: On Lua compilation/runtime errors or type mismatches.
"""
if not script_text or not script_text.strip():
return
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}")
self._targetVars = {}
self._lua = None
+34 -48
View File
@@ -7,45 +7,25 @@ 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 autoscript.ASEngine import (
execute,
addTargetVar,
resetEngine,
META_VARS,
)
from autoscript.ASEngine import ASEngine
__all__ = [
"execute",
"addTargetVar",
"resetEngine",
"registerDefaultTargetVars",
"buildMockTargetData",
"META_VARS",
"ALL_VARIABLES",
"_TARGET_VAR_DEFS",
"_MOCK_TYPE_VALUES",
"ASEngine",
"createEngine",
"createMockTargetData",
"createAllVariablesTable",
"createTargetVarDefs",
]
# 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.
ALL_VARIABLES = {
display_name: (name, var_type)
for name, var_type, _, display_name in _TARGET_VAR_DEFS
} | {
v["display"]: (v["name"], v["type"])
for v in META_VARS.values()
}
_MOCK_TYPE_VALUES = {
"String": "__mock__",
"Boolean": True,
@@ -55,26 +35,32 @@ _MOCK_TYPE_VALUES = {
"Float": 0.0,
}
def buildMockTargetData(
def createAllVariablesTable(
) -> dict:
"""
Build a target_data dict filled with type-appropriate mock values
for all registered target variables.
"""
return {
displayName: (name, varType)
for name, varType, _, displayName in _TARGET_VAR_DEFS
}
def createTargetVarDefs(
) -> list:
return list(_TARGET_VAR_DEFS)
def createMockTargetData(
) -> dict:
data = {}
for _, var_type, key_path, _ in _TARGET_VAR_DEFS:
for _, varType, keyPath, _ in _TARGET_VAR_DEFS:
d = data
for key in key_path[:-1]:
for key in keyPath[:-1]:
d = d.setdefault(key, {})
d[key_path[-1]] = _MOCK_TYPE_VALUES.get(var_type, "")
d[keyPath[-1]] = _MOCK_TYPE_VALUES.get(varType, "")
return data
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)
def createEngine(
) -> ASEngine:
return ASEngine(_TARGET_VAR_DEFS)
+153
View File
@@ -0,0 +1,153 @@
# -*- 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 (
date,
datetime,
)
_TYPE_DEFAULT_VAR: dict[str, str | int | float | bool] = {
"String": "",
"Int": 0,
"Float": 0.0,
"Boolean": False,
}
def _navigatePath(
data: dict,
keyPath: list,
default=None,
):
d = data
for key in keyPath[:-1]:
d = d.get(key, {})
if not isinstance(d, dict):
return default
return d.get(keyPath[-1], default)
def _assignPath(
data: dict,
keyPath: list,
value,
) -> None:
d = data
for key in keyPath[:-1]:
d = d.setdefault(key, {})
d[keyPath[-1]] = value
def _checkDateFormat(
dateStr: str,
varName: str = "",
) -> None:
prefix = f"Date 类型变量 '{varName}'" if varName else ""
try:
date.fromisoformat(dateStr)
except ValueError:
raise ValueError(
f"{prefix}'{dateStr}' 不是合法的日期格式,"
f"应为 YYYY-MM-DD"
)
def _checkTimeFormat(
timeStr: str,
varName: str = "",
) -> None:
prefix = f"Time 类型变量 '{varName}'" if varName else ""
try:
datetime.strptime(timeStr, "%H:%M")
except ValueError:
raise ValueError(
f"{prefix}'{timeStr}' 不是合法的时间格式,"
f"应为 HH:MM"
)
def _checkType(
varName: str,
varType: str,
value,
) -> None:
if varType == "Date":
if not isinstance(value, str):
raise ValueError(
f"Date 类型变量 '{varName}' 只能接受日期字符串,"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
_checkDateFormat(value, varName)
return
if varType == "Time":
if not isinstance(value, str):
raise ValueError(
f"Time 类型变量 '{varName}' 只能接受时间字符串,"
f"不能接受 {_pyTypeToASType(value)} 类型"
)
_checkTimeFormat(value, varName)
return
if varType == "Int":
if isinstance(value, bool):
raise ValueError(
f"Int 类型变量 '{varName}' 不能接受 Boolean 类型的值"
)
if not isinstance(value, int) and not (isinstance(value, float) and value == int(value)):
raise ValueError(
f"Int 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if varType == "Float":
if isinstance(value, bool):
raise ValueError(
f"Float 类型变量 '{varName}' 不能接受 Boolean 类型的值"
)
if not isinstance(value, (int, float)):
raise ValueError(
f"Float 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if varType == "Boolean":
if not isinstance(value, bool):
raise ValueError(
f"Boolean 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
if varType == "String":
if not isinstance(value, str):
raise ValueError(
f"String 类型变量 '{varName}' 不能接受 {_pyTypeToASType(value)} 类型的值"
)
return
def _pyTypeToASType(
value,
) -> str:
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 _cleanLuaError(
rawMsg: str,
) -> str:
msg = rawMsg.replace('[string "<python>"]:', "").strip()
stackIdx = msg.find("stack traceback:")
if stackIdx != -1:
msg = msg[:stackIdx].strip()
return msg