1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-17 23:13: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 lupa import LuaRuntime as _LuaRuntime
from autoscript._helpers import (
_TYPE_DEFAULT_VAR,
_assignPath,
_checkDateFormat,
_checkTimeFormat,
_checkType,
_cleanLuaError,
_navigatePath,
)
try: try:
from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError from lupa.lua55 import LuaError as _LuaError, LuaSyntaxError as _LuaSyntaxError
except ImportError: except ImportError:
@@ -24,435 +34,229 @@ except ImportError:
_LuaSyntaxError = Exception _LuaSyntaxError = Exception
__all__ = ["execute", "addTargetVar", "resetEngine"] __all__ = ["ASEngine"]
# Engine state class ASEngine:
_TARGET_VARS: dict[str, dict] = {}
_lua = None
# Built-in meta variable definitions (name / type / display-name) @staticmethod
META_VARS: dict[str, dict[str, str]] = { def getCurrentDate(
"CURRENT_DATE": {"name": "CURRENT_DATE", "type": "Date", "display": "当前日期"}, ) -> str:
"CURRENT_TIME": {"name": "CURRENT_TIME", "type": "Time", "display": "当前时间"},
}
# Per-type fallback value when target_data entry is missing. return date.today().isoformat()
_DEFAULT_BY_TYPE: dict[str, str | int | float | bool] = {
"String": "",
"Int": 0,
"Float": 0.0,
"Boolean": False,
}
def _getLua( @staticmethod
): def getCurrentTime(
""" ) -> str:
Return the sandboxed Lua runtime singleton.
"""
global _lua return datetime.now().strftime("%H:%M")
if _lua is None:
_lua = _LuaRuntime(unpack_returned_tuples = True)
_sandbox(_lua)
_registerHelpers(_lua)
return _lua
def _sandbox( @staticmethod
lua, def _sandbox(
) -> None: lua,
""" ) -> None:
Remove dangerous Lua globals while keeping os.date / os.time for date-time helpers.
"""
lua.execute(""" lua.execute("""
io = nil io = nil
require = nil require = nil
dofile = nil dofile = nil
loadfile = nil loadfile = nil
load = nil load = nil
package = nil package = nil
rawget = nil rawget = nil
rawset = nil rawset = nil
rawequal = nil rawequal = nil
getfenv = nil getfenv = nil
setfenv = nil setfenv = nil
debug = nil debug = nil
-- selectively disable dangerous os functions, keep date / time if os then
if os then os.execute = nil
os.execute = nil os.exit = nil
os.exit = nil os.getenv = nil
os.getenv = nil os.remove = nil
os.remove = nil os.rename = nil
os.rename = nil os.tmpname = nil
os.tmpname = nil os.setlocale = nil
os.setlocale = nil end
end """)
""")
def _registerHelpers( @staticmethod
lua, def _registerHelpers(
) -> None: lua,
""" ) -> None:
Inject Date / Time helpers as pure Lua functions.
Date values are os.time timestamps (seconds since epoch). lua.execute("""
Time values are minutes since midnight (0-1439). 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, function time(h, m)
enabling type-safe arithmetic (+, -) and comparisons (<, <=, ==, ~=). return h * 60 + m
""" end
lua.execute(""" function datenow()
function date(y, m, d) local now = os.date("*t")
return os.time({year = y, month = m, day = d}) return os.time({year = now.year, month = now.month, day = now.day})
end end
function time(h, m) function timenow()
return h * 60 + m local now = os.date("*t")
end return now.hour * 60 + now.min
end
function CURRENT_DATE() function dateadd(date_val, n)
local now = os.date("*t") return date_val + n * 86400
return os.time({year = now.year, month = now.month, day = now.day}) end
end
function CURRENT_TIME() function timeadd(time_val, n)
local now = os.date("*t") return (time_val + n * 60) % 1440
return now.hour * 60 + now.min end
end
function date_add(date_val, n) function strtodate(iso_str)
return date_val + n * 86400 local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)")
end return os.time({year = y, month = m, day = d})
end
function time_add(time_val, n) function strtotime(hm_str)
return (time_val + n * 60) % 1440 local h, m = hm_str:match("(%d+):(%d+)")
end return h * 60 + m
end
-- push helpers: string -> native type function datetostr(ts)
function _to_date(iso_str) return os.date("%Y-%m-%d", ts)
local y, m, d = iso_str:match("(%d+)-(%d+)-(%d+)") end
return os.time({year = y, month = m, day = d})
end
function _to_time(hm_str) function timetostr(m)
local h, m = hm_str:match("(%d+):(%d+)") return string.format("%02d:%02d", math.floor(m / 60), m % 60)
return h * 60 + m end
end """)
-- pull helpers: native type -> string def __init__(
function _from_date(ts) self,
return os.date("%Y-%m-%d", ts) targetVars: list[tuple] = None,
end ):
function _from_time(m) self._targetVars: dict[str, dict] = {}
return string.format("%02d:%02d", math.floor(m / 60), m % 60) self._lua = None
end
""")
def _navigatePath( if targetVars:
data: dict, for item in targetVars:
key_path: list, name, varType, keyPath = item[0], item[1], item[2]
default = None, self.addTargetVar(name, varType, keyPath)
):
"""
Walk *key_path* into *data* and return the value at the leaf.
"""
d = data def _getLua(
for key in key_path[:-1]: self,
d = d.get(key, {}) ):
if not isinstance(d, dict):
return default
return d.get(key_path[-1], default)
def _assignPath( if self._lua is None:
data: dict, self._lua = _LuaRuntime(unpack_returned_tuples=True)
key_path: list, self._sandbox(self._lua)
value, self._registerHelpers(self._lua)
) -> None: return self._lua
"""
Walk *key_path* into *data* and set *value* at the leaf.
"""
d = data def _push(
for key in key_path[:-1]: self,
d = d.setdefault(key, {}) targetData: dict,
d[key_path[-1]] = value ) -> None:
def _pyTypeToASType( lua = self._getLua()
value g = lua.globals()
) -> str: strToDate = g["strtodate"]
""" strToTime = g["strtotime"]
Map a Python runtime value to its AutoScript type name.
"""
if isinstance(value, bool): for varName, info in self._targetVars.items():
return "Boolean" keyPath = info["keyPath"]
if isinstance(value, int): vt = info["type"]
return "Int" raw = _navigatePath(targetData, keyPath)
if isinstance(value, float): if vt == "Date":
return "Float" if not isinstance(raw, str) or not raw.strip():
if isinstance(value, str): raise ValueError(
return "String" f"Date 类型变量 '{varName}' 对应的数据为空或不是字符串类型,"
return "Unknown" 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( def _pull(
date_str: str, self,
var_name: str = "", targetData: dict,
) -> None: ) -> 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 "" lua = self._getLua()
try: g = lua.globals()
date.fromisoformat(date_str) dateToStr = g["datetostr"]
except ValueError: timeToStr = g["timetostr"]
raise ValueError(
f"{prefix}'{date_str}' 不是合法的日期格式,"
f"应为 YYYY-MM-DD"
)
def _checkTimeFormat( for varName, info in self._targetVars.items():
time_str: str, try:
var_name: str = "", luaVal = g[varName]
) -> None: except KeyError:
""" continue
Validate that *time_str* is in HH:MM format. vt = info["type"]
Raises ValueError with a descriptive message on failure. 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 "" def addTargetVar(
try: self,
datetime.strptime(time_str, "%H:%M") name: str,
except ValueError: varType: str,
raise ValueError( keyPath: list,
f"{prefix}'{time_str}' 不是合法的时间格式," ) -> None:
f"应为 HH:MM"
)
def _checkType( upperName = name.upper().strip()
var_name: str, self._targetVars[upperName] = {
var_type: str, "type": varType,
value, "keyPath": keyPath,
) -> None: }
"""
Validate that *value* matches the declared variable type.
Date / Time values arrive as ISO / HH:MM strings (already converted def execute(
from Lua native types during the pull phase). self,
Int / Float / Boolean / String check Python type identity. scriptText: str,
Int -> Float widening is allowed. targetData: dict,
""" ) -> None:
if var_type == "Date": if not scriptText or not scriptText.strip():
if not isinstance(value, str): return
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():
try: try:
lua_val = g[var_name] self._push(targetData)
except KeyError: self._getLua().execute(scriptText)
continue self._pull(targetData)
vt = info["type"] except _LuaSyntaxError as e:
if vt == "Date": raise ValueError(
lua_val = _fromDate(lua_val) f"AutoScript 语法错误: {_cleanLuaError(str(e))}"
elif vt == "Time": )
lua_val = _fromTime(lua_val) except _LuaError as e:
elif vt == "Float" and isinstance(lua_val, int) and not isinstance(lua_val, bool): raise ValueError(
lua_val = float(lua_val) f"AutoScript 运行时错误: {_cleanLuaError(str(e))}"
_checkType(var_name, vt, lua_val) )
_assignPath(target_data, info["key_path"], lua_val) except ValueError as e:
raise ValueError(f"AutoScript 数据错误: {e}")
except Exception as e:
raise ValueError(f"AutoScript 未知错误: {e}")
def _cleanLuaError( def reset(
raw_msg: str self,
) -> str: ) -> None:
"""
Strip internal source prefix and stack traceback from a Lua error message.
"""
msg = raw_msg.replace('[string "<python>"]:', "").strip() self._targetVars = {}
stack_idx = msg.find("stack traceback:") self._lua = None
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}")
+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. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from autoscript.ASEngine import ( from autoscript.ASEngine import ASEngine
execute,
addTargetVar,
resetEngine,
META_VARS,
)
__all__ = [ __all__ = [
"execute", "ASEngine",
"addTargetVar", "createEngine",
"resetEngine", "createMockTargetData",
"registerDefaultTargetVars", "createAllVariablesTable",
"buildMockTargetData", "createTargetVarDefs",
"META_VARS",
"ALL_VARIABLES",
"_TARGET_VAR_DEFS",
"_MOCK_TYPE_VALUES",
] ]
# Key paths into target_data dict for each target variable.
# (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, 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 = { _MOCK_TYPE_VALUES = {
"String": "__mock__", "String": "__mock__",
"Boolean": True, "Boolean": True,
@@ -55,26 +35,32 @@ _MOCK_TYPE_VALUES = {
"Float": 0.0, "Float": 0.0,
} }
def buildMockTargetData(
def createAllVariablesTable(
) -> dict: ) -> dict:
"""
Build a target_data dict filled with type-appropriate mock values return {
for all registered target variables. displayName: (name, varType)
""" for name, varType, _, displayName in _TARGET_VAR_DEFS
}
def createTargetVarDefs(
) -> list:
return list(_TARGET_VAR_DEFS)
def createMockTargetData(
) -> dict:
data = {} data = {}
for _, var_type, key_path, _ in _TARGET_VAR_DEFS: for _, varType, keyPath, _ in _TARGET_VAR_DEFS:
d = data d = data
for key in key_path[:-1]: for key in keyPath[:-1]:
d = d.setdefault(key, {}) d = d.setdefault(key, {})
d[key_path[-1]] = _MOCK_TYPE_VALUES.get(var_type, "") d[keyPath[-1]] = _MOCK_TYPE_VALUES.get(varType, "")
return data return data
def registerDefaultTargetVars( def createEngine(
) -> None: ) -> ASEngine:
"""
Register all built-in target variables with the engine. return ASEngine(_TARGET_VAR_DEFS)
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)
+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
+1 -4
View File
@@ -13,10 +13,7 @@ from PySide6.QtCore import (
Qt, Qt,
QTimer QTimer
) )
from PySide6.QtGui import ( from PySide6.QtGui import QIcon
QFont,
QIcon
)
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
QDialog QDialog
+68 -30
View File
@@ -9,10 +9,18 @@ See the LICENSE file for details.
""" """
from copy import deepcopy from copy import deepcopy
from PySide6.QtCore import QDate, Qt, QTime, QTimer, Slot from PySide6.QtCore import (
QDate,
QSize,
Qt,
QTime,
QTimer,
Slot
)
from PySide6.QtGui import ( from PySide6.QtGui import (
QColor, QColor,
QFont, QFont,
QIcon,
QSyntaxHighlighter, QSyntaxHighlighter,
QTextCharFormat, QTextCharFormat,
) )
@@ -46,11 +54,10 @@ from PySide6.QtWidgets import (
) )
from autoscript import ( from autoscript import (
ALL_VARIABLES, createAllVariablesTable,
_MOCK_TYPE_VALUES, createMockTargetData,
_TARGET_VAR_DEFS, createTargetVarDefs,
execute, createEngine,
registerDefaultTargetVars,
) )
@@ -94,12 +101,12 @@ class ALScriptHighlighter(QSyntaxHighlighter):
funcFmt = QTextCharFormat() funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#DCDCAA")) funcFmt.setForeground(QColor("#DCDCAA"))
funcFmt.setFontWeight(QFont.Weight.Normal) funcFmt.setFontWeight(QFont.Weight.Normal)
for fn in ["CURRENT_DATE", "CURRENT_TIME", "date_add", "time_add"]: for fn in [ "time", "date", "datenow", "timenow", "dateadd", "timeadd"]:
self._rules.append((r"\b" + fn + r"\b", funcFmt)) self._rules.append((r"\b" + fn + r"\b", funcFmt))
varFmt = QTextCharFormat() varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#9CDCFE")) varFmt.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal) varFmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in ALL_VARIABLES.items()] var_names = [name for _, (name, _) in createAllVariablesTable().items()]
for var in var_names: for var in var_names:
self._rules.append((r"\b" + var + r"\b", varFmt)) self._rules.append((r"\b" + var + r"\b", varFmt))
strFmt = QTextCharFormat() strFmt = QTextCharFormat()
@@ -158,6 +165,19 @@ class _DebugResultDialog(QDialog):
layout.addWidget(btnBox) layout.addWidget(btnBox)
class _TabToSpacesEditor(QPlainTextEdit):
def keyPressEvent(
self,
event
):
if event.key() == Qt.Key.Key_Tab:
self.insertPlainText(" ")
return
super().keyPressEvent(event)
class ALAutoScriptEditDialog(QDialog): class ALAutoScriptEditDialog(QDialog):
def __init__( def __init__(
@@ -194,11 +214,9 @@ class ALAutoScriptEditDialog(QDialog):
self.zoomInBtn.setFixedSize(25, 25) self.zoomInBtn.setFixedSize(25, 25)
self.zoomOutBtn = QPushButton("") self.zoomOutBtn = QPushButton("")
self.zoomOutBtn.setFixedSize(25, 25) self.zoomOutBtn.setFixedSize(25, 25)
self.zoomResetBtn = QPushButton( self.zoomResetBtn = QPushButton("")
QApplication.style().standardIcon( self.zoomResetBtn.setIcon(QIcon(":/res/icons/Reset.svg"))
QStyle.StandardPixmap.SP_BrowserReload self.zoomResetBtn.setIconSize(QSize(20, 20))
), ""
)
self.zoomResetBtn.setFixedSize(25, 25) self.zoomResetBtn.setFixedSize(25, 25)
self.zoomResetBtn.setToolTip("重置缩放") self.zoomResetBtn.setToolTip("重置缩放")
self.zoomLabel = QLabel(f"{self._fontSize}px") self.zoomLabel = QLabel(f"{self._fontSize}px")
@@ -221,16 +239,15 @@ class ALAutoScriptEditDialog(QDialog):
toolbarLayout.addWidget(self.zoomResetBtn) toolbarLayout.addWidget(self.zoomResetBtn)
toolbarLayout.addWidget(self.zoomLabel) toolbarLayout.addWidget(self.zoomLabel)
toolbarLayout.addStretch() toolbarLayout.addStretch()
self.copyBtn = QPushButton( self.copyBtn = QPushButton("")
QApplication.style().standardIcon( self.copyBtn.setIcon(QIcon(":/res/icons/Copy.svg"))
QStyle.StandardPixmap.SP_FileDialogDetailedView self.copyBtn.setIconSize(QSize(20, 20))
), ""
)
self.copyBtn.setFixedSize(25, 25) self.copyBtn.setFixedSize(25, 25)
self.copyBtn.setToolTip("复制脚本") self.copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self.copyBtn) toolbarLayout.addWidget(self.copyBtn)
layout.addLayout(toolbarLayout) layout.addLayout(toolbarLayout)
self.textEdit = QPlainTextEdit(self) self.textEdit = _TabToSpacesEditor(self)
self.textEdit.setTabStopDistance(40)
self.textEdit.setLineWrapMode( self.textEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap QPlainTextEdit.LineWrapMode.NoWrap
) )
@@ -329,10 +346,30 @@ class ALAutoScriptEditDialog(QDialog):
varLayout.setContentsMargins(4, 4, 4, 4) varLayout.setContentsMargins(4, 4, 4, 4)
varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
varButtons = [ varButtons = [
(display_name, name) for display_name, (name, _) in ALL_VARIABLES.items() (display_name, name) for display_name, (name, _) in createAllVariablesTable().items()
] ]
self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3) self.addButtonsToGrid(varLayout, varButtons, 0, 0, 3)
tabWidget.addTab(varWidget, "变量") tabWidget.addTab(varWidget, "变量")
funcWidget = QWidget()
funcLayout = QGridLayout(funcWidget)
funcLayout.setSpacing(4)
funcLayout.setContentsMargins(4, 4, 4, 4)
funcLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
funcButtons = [
("datenow()", "datenow()", "返回当前日期的 Unix 时间戳"),
("timenow()", "timenow()", "返回当前时间在一天中的分钟数"),
("dateadd(d, n)", "dateadd(, )", "日期偏移: dateadd(日期时间戳, 天数)"),
("timeadd(t, n)", "timeadd(, )", "时间偏移: timeadd(分钟数, 分钟数)"),
]
for i, (text, template, tooltip) in enumerate(funcButtons):
btn = QPushButton(text)
btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(25)
btn.setToolTip(tooltip)
funcLayout.addWidget(btn, i // 2, i % 2)
tabWidget.addTab(funcWidget, "工具函数")
mockPanel = self.createMockPanel() mockPanel = self.createMockPanel()
mockPanel.setMinimumWidth(260) mockPanel.setMinimumWidth(260)
splitter.addWidget(tabWidget) splitter.addWidget(tabWidget)
@@ -376,8 +413,12 @@ class ALAutoScriptEditDialog(QDialog):
form.setSpacing(4) form.setSpacing(4)
form.setContentsMargins(5, 10, 5, 5) form.setContentsMargins(5, 10, 5, 5)
self._mockWidgets = {} self._mockWidgets = {}
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: mockData = createMockTargetData()
default = _MOCK_TYPE_VALUES.get(var_type, "") for name, var_type, key_path, display_name in createTargetVarDefs():
d = mockData
for key in key_path:
d = d[key]
default = d
widget = self.makeMockInput(var_type, default) widget = self.makeMockInput(var_type, default)
label = QLabel(f"{display_name}: {name}({var_type})") label = QLabel(f"{display_name}: {name}({var_type})")
form.addRow(label, widget) form.addRow(label, widget)
@@ -432,7 +473,7 @@ class ALAutoScriptEditDialog(QDialog):
) -> dict: ) -> dict:
data = {} data = {}
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: for name, var_type, key_path, display_name in createTargetVarDefs():
widget, _, _ = self._mockWidgets[name] widget, _, _ = self._mockWidgets[name]
value = self.getMockValue(widget, var_type) value = self.getMockValue(widget, var_type)
d = data d = data
@@ -448,7 +489,7 @@ class ALAutoScriptEditDialog(QDialog):
if not data: if not data:
return return
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: for name, var_type, key_path, display_name in createTargetVarDefs():
d = data d = data
try: try:
for key in key_path: for key in key_path:
@@ -572,11 +613,8 @@ class ALAutoScriptEditDialog(QDialog):
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
clipboard.setText(self.textEdit.toPlainText()) clipboard.setText(self.textEdit.toPlainText())
original = self.copyBtn.text()
self.copyBtn.setText("已复制")
self.copyBtn.setEnabled(False) self.copyBtn.setEnabled(False)
QTimer.singleShot(2000, lambda: ( QTimer.singleShot(2000, lambda: (
self.copyBtn.setText(original),
self.copyBtn.setEnabled(True) self.copyBtn.setEnabled(True)
)) ))
@@ -606,13 +644,13 @@ class ALAutoScriptEditDialog(QDialog):
target_data = self.getMockData() target_data = self.getMockData()
before = deepcopy(target_data) before = deepcopy(target_data)
try: try:
registerDefaultTargetVars() engine = createEngine()
execute(script, target_data) engine.execute(script, target_data)
except ValueError as e: except ValueError as e:
QMessageBox.warning(self, "运行错误", str(e)) QMessageBox.warning(self, "运行错误", str(e))
return return
changes = [] changes = []
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS: for name, var_type, key_path, display_name in createTargetVarDefs():
before_val = before before_val = before
after_val = target_data after_val = target_data
try: try:
+50 -305
View File
@@ -12,11 +12,7 @@ See the LICENSE file for details.
""" """
import re import re
from PySide6.QtCore import ( from PySide6.QtCore import QObject
QObject,
QDate,
QTime
)
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QComboBox, QComboBox,
QDateEdit, QDateEdit,
@@ -31,61 +27,66 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from autoscript import ( from autoscript import createAllVariablesTable
ALL_VARIABLES,
)
# Types that support arithmetic operations (add/sub) VARTYPE_INFOS = [
ARITH_TYPES = {"Date", "Time", "Int", "Float"} # varType, isArithType
VAR_TYPE_ORDER = [ ("String", False),
"String", ("Int", True),
"Int", ("Float", True),
"Float", ("Boolean", False),
"Boolean", ("Date", True),
"Date", ("Time", True),
"Time"
] ]
PRESET_VARIABLES = [
{
"name": name.upper(), def getTypeOrder(
"type": vtype, ) -> list:
"display": display
} return [t for t, _ in VARTYPE_INFOS]
for display, (name, vtype) in ALL_VARIABLES.items()
def getArithType(
varType: str
) -> bool:
for t, a in VARTYPE_INFOS:
if t == varType:
return a
def getPresetVars(
) -> list:
return [
{"name": name.upper(), "type": vtype, "display": display}
for display, (name, vtype) in createAllVariablesTable().items()
]
COMPARE_OPTIONS = [
("等于", "=="),
("不等于", "~="),
("大于", ">"),
("小于", "<"),
("大于等于", ">="),
("小于等于", "<="),
] ]
PRESET_NAMES = { LOGIC_OPTIONS = [
p["name"] for p in PRESET_VARIABLES
}
# Operator display names (UI-specific), using Lua operator symbols
_COMPARE_DISPLAY_MAP = {
"==": "等于",
"~=": "不等于",
">": "大于",
"<": "小于",
">=": "大于等于",
"<=": "小于等于",
}
COMPARE_OPERATORS = sorted(
[(name, op) for op, name in _COMPARE_DISPLAY_MAP.items()],
key=lambda x: len(x[1]), reverse=True
)
LOGIC_OPERATORS = [
("并且 (and)", "and"), ("并且 (and)", "and"),
("或者 (or)", "or"), ("或者 (or)", "or"),
] ]
ACTION_TYPES = [ ACTION_OPTIONS = [
("设置为", "set"), ("设置为", "set"),
("增加", "add"), ("增加", "add"),
("减少", "sub"), ("减少", "sub"),
] ]
DATE_RELATIVE_OPTIONS = [ DATE_OPTIONS = [
("前天", "day_before_yesterday"), ("前天", "day_before_yesterday"),
("昨天", "yesterday"), ("昨天", "yesterday"),
("今天", "today"), ("今天", "today"),
("明天", "tomorrow"), ("明天", "tomorrow"),
("后天", "day_after_tomorrow") ("后天", "day_after_tomorrow")
] ]
DATE_OFFSET_UNITS = [ DATE_OFFSET_OPTIONS = [
("", "days"), ("", "days"),
("", "weeks"), ("", "weeks"),
# NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years, # NOTE: "月" and "年" use fixed day counts (30 / 365), not calendar months/years,
@@ -103,7 +104,6 @@ class _DateInputContainer(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self._dynamicItems = {} # index -> raw expression, for one-way parsed items
self.setupUi() self.setupUi()
def setupUi( def setupUi(
@@ -119,7 +119,7 @@ class _DateInputContainer(QWidget):
self._modeCombo.setFixedHeight(25) self._modeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self) self._stack = QStackedWidget(self)
self._relCombo = QComboBox(self) self._relCombo = QComboBox(self)
for display, data in DATE_RELATIVE_OPTIONS: for display, data in DATE_OPTIONS:
self._relCombo.addItem(display, data) self._relCombo.addItem(display, data)
self._relCombo.setFixedHeight(25) self._relCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo) self._stack.addWidget(self._relCombo)
@@ -135,69 +135,15 @@ class _DateInputContainer(QWidget):
layout.addWidget(self._stack) layout.addWidget(self._stack)
layout.addStretch() layout.addStretch()
_RE_DATE_ADD_CURRENT = re.compile(
r'^date_add\(CURRENT_DATE\(\),\s*(-?\d+)\)$', re.IGNORECASE
)
def getValue( def getValue(
self self
) -> str: ) -> str:
mode = self._modeCombo.currentData() mode = self._modeCombo.currentData()
if mode == "relative": if mode == "relative":
idx = self._relCombo.currentIndex()
if idx in self._dynamicItems:
return self._dynamicItems[idx]
return self._relCombo.currentText() return self._relCombo.currentText()
return self._dateEdit.date().toString("yyyy-MM-dd") return self._dateEdit.date().toString("yyyy-MM-dd")
def setValue(
self,
expr: str
):
s = expr.strip()
up = s.upper()
if up == "CURRENT_DATE()":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(2)
return
m_add = self._RE_DATE_ADD_CURRENT.match(up)
if m_add:
n = int(m_add.group(1))
_OFFSET_IDX = {-2: 0, -1: 1, 0: 2, 1: 3, 2: 4}
idx = _OFFSET_IDX.get(n)
if idx is not None:
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(idx)
return
label = f"{n}天后" if n >= 0 else f"{-n}天前"
raw = f"CURRENT_DATE {'+' if n >= 0 else '-'} {abs(n)}"
self._modeCombo.setCurrentIndex(0)
for ci in range(self._relCombo.count()):
if ci in self._dynamicItems and self._dynamicItems[ci] == raw:
self._relCombo.setCurrentIndex(ci)
return
idx = self._relCombo.count()
self._relCombo.addItem(label)
self._dynamicItems[idx] = raw
self._relCombo.setCurrentIndex(idx)
return
m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up)
if m_date_ctor:
self._modeCombo.setCurrentIndex(1)
self._dateEdit.setDate(QDate(
int(m_date_ctor.group(1)),
int(m_date_ctor.group(2)),
int(m_date_ctor.group(3)),
))
return
m_date = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s)
if m_date:
self._modeCombo.setCurrentIndex(1)
parts = m_date.group(1).split("-")
self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
class _TimeInputContainer(QWidget): class _TimeInputContainer(QWidget):
@@ -221,25 +167,6 @@ class _TimeInputContainer(QWidget):
return self._timeEdit.time().toString("HH:mm") return self._timeEdit.time().toString("HH:mm")
def setValue(
self,
expr: str
):
s = expr.strip()
up = s.upper()
m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up)
if m_time_ctor:
self._timeEdit.setTime(QTime(
int(m_time_ctor.group(1)),
int(m_time_ctor.group(2)),
))
return
m = re.match(r'^"(\d{1,2}:\d{2})"$', s)
if m:
parts = m.group(1).split(":")
self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
class _DateOffsetContainer(QWidget): class _DateOffsetContainer(QWidget):
@@ -253,7 +180,7 @@ class _DateOffsetContainer(QWidget):
self._spinBox.setRange(0, 99999) self._spinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25) self._spinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self) self._unitCombo = QComboBox(self)
for display, data in DATE_OFFSET_UNITS: for display, data in DATE_OFFSET_OPTIONS:
self._unitCombo.addItem(display, data) self._unitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25) self._unitCombo.setFixedHeight(25)
@@ -270,17 +197,6 @@ class _DateOffsetContainer(QWidget):
return str(self.getOffsetDays()) return str(self.getOffsetDays())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetDays( def getOffsetDays(
self self
) -> int: ) -> int:
@@ -295,12 +211,6 @@ class _DateOffsetContainer(QWidget):
return val * 365 return val * 365
return val return val
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
class _TimeOffsetContainer(QWidget): class _TimeOffsetContainer(QWidget):
@@ -325,29 +235,12 @@ class _TimeOffsetContainer(QWidget):
return str(self.getOffsetHours()) return str(self.getOffsetHours())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetHours( def getOffsetHours(
self self
) -> int: ) -> int:
return self._spinBox.value() return self._spinBox.value()
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
class VariableManager(QObject): class VariableManager(QObject):
@@ -360,13 +253,13 @@ class VariableManager(QObject):
self._vars = [] self._vars = []
self._nameMap = {} self._nameMap = {}
self._initPresetVars() self.initPresetVars()
def _initPresetVars( def initPresetVars(
self self
): ):
for p in PRESET_VARIABLES: for p in getPresetVars():
entry = {"name": p["name"], "type": p["type"], "display": p["display"]} entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry) self._vars.append(entry)
self._nameMap[p["name"]] = entry self._nameMap[p["name"]] = entry
@@ -399,19 +292,6 @@ class VariableManager(QObject):
break break
combo.blockSignals(False) combo.blockSignals(False)
def findExactNameEntry(
self,
combo: QComboBox,
name: str
) -> int:
name = name.upper().strip()
for i in range(combo.count()):
d = combo.itemData(i)
if d and len(d) >= 1 and d[0].upper().strip() == name:
return i
return -1
def makeValueWidget( def makeValueWidget(
var_type: str, var_type: str,
@@ -535,68 +415,6 @@ def getValueFromWidget(
return w.text() return w.text()
return "" return ""
def setValueToWidget(
w: QWidget,
var_type: str,
expr: str
):
"""
Set a widget's value from a Lua script expression.
"""
if hasattr(w, "setValue"):
w.setValue(expr)
return
s = expr.strip()
up = s.upper()
if isinstance(w, QTimeEdit):
m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up)
if m_time_ctor:
w.setTime(QTime(int(m_time_ctor.group(1)), int(m_time_ctor.group(2))))
else:
m = re.match(r'^"(\d{1,2}:\d{2})"$', s)
if m:
parts = m.group(1).split(":")
w.setTime(QTime(int(parts[0]), int(parts[1])))
elif isinstance(w, QDateEdit):
m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up)
if m_date_ctor:
w.setDate(QDate(
int(m_date_ctor.group(1)),
int(m_date_ctor.group(2)),
int(m_date_ctor.group(3)),
))
else:
m = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s)
if m:
parts = m.group(1).split("-")
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
elif isinstance(w, QComboBox):
for i in range(w.count()):
d = w.itemData(i)
if d is not None:
if str(d).upper() == up:
w.setCurrentIndex(i)
return
if w.itemText(i).upper() == up:
w.setCurrentIndex(i)
return
elif isinstance(w, QSpinBox):
try:
w.setValue(int(expr))
except ValueError:
pass
elif isinstance(w, QDoubleSpinBox):
try:
w.setValue(float(expr))
except ValueError:
pass
elif isinstance(w, QLineEdit):
inner = expr.strip()
if inner.startswith('"') and inner.endswith('"'):
inner = inner[1:-1].replace('\\"', '"')
w.setText(inner)
def encodeValueStr( def encodeValueStr(
raw_value: str, raw_value: str,
var_type: str var_type: str
@@ -683,23 +501,6 @@ def encodeDateOrTime(
return s return s
return f'"{s}"' return f'"{s}"'
def stripOuterParens(
s: str
) -> str:
s = s.strip()
if s.startswith("(") and s.endswith(")"):
depth = 0
for i, ch in enumerate(s):
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0 and i < len(s) - 1:
return s
return s[1:-1].strip()
return s
# Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B) # Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B)
_RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$') _RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$')
_RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$') _RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$')
@@ -713,59 +514,3 @@ def isArithExpr(
s = expr.strip() s = expr.strip()
return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s)) return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s))
def isVarReference(
expr: str
) -> bool:
"""
Return True if *expr* looks like a variable name reference
(as opposed to a literal value or function call).
"""
s = expr.strip()
up = s.upper()
if up in ("TRUE", "FALSE"):
return False
if re.match(r"^DATE\(|^TIME\(|^DATE_ADD\(|^TIME_ADD\(|^CURRENT_DATE\(|^CURRENT_TIME\(|^CURRENT_", up):
return False
if up.startswith('"') or up.startswith("'"):
return False
if re.match(r"^[+-]?\d", s):
return False
if isArithExpr(s):
return False
return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up))
def isInsideLiteral(
text: str,
pos: int
) -> bool:
in_single = False
in_double = False
for i, ch in enumerate(text):
if i >= pos:
break
if ch == "'" and not in_double:
in_single = not in_single
elif ch == '"' and not in_single:
in_double = not in_double
return in_single or in_double
def findOperatorIn(
text: str,
operators: list
) -> tuple[int, str] | None:
for op in operators:
op_upper = op.upper()
start = 0
while True:
idx = text.upper().find(op_upper, start)
if idx < 0:
break
if isInsideLiteral(text, idx):
start = idx + 1
continue
return (idx, op)
return None
+13 -13
View File
@@ -22,14 +22,14 @@ from PySide6.QtWidgets import (
) )
from gui.ALAutoScriptOrchDialog._helpers import ( from gui.ALAutoScriptOrchDialog._helpers import (
ACTION_TYPES, ACTION_OPTIONS,
ARITH_TYPES, COMPARE_OPTIONS,
COMPARE_OPERATORS, LOGIC_OPTIONS,
LOGIC_OPERATORS,
PRESET_VARIABLES,
VAR_TYPE_ORDER,
encodeValueStr, encodeValueStr,
getPresetVars,
getTypeOrder,
getValueFromWidget, getValueFromWidget,
getArithType,
makeComboWidget, makeComboWidget,
makeLabel, makeLabel,
makeOffsetWidget, makeOffsetWidget,
@@ -72,7 +72,7 @@ class ConditionRowFrame(QFrame):
if self._isFirst: if self._isFirst:
self.logicCombo = None self.logicCombo = None
else: else:
self.logicCombo = makeComboWidget(LOGIC_OPERATORS, min_width=110, parent=self) self.logicCombo = makeComboWidget(LOGIC_OPTIONS, min_width=110, parent=self)
layout.addWidget(self.logicCombo) layout.addWidget(self.logicCombo)
self.leftVarCombo = QComboBox(self) self.leftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25) self.leftVarCombo.setFixedHeight(25)
@@ -80,7 +80,7 @@ class ConditionRowFrame(QFrame):
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo() self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo) layout.addWidget(self.leftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPERATORS, min_width=80, parent=self) self.opCombo = makeComboWidget(COMPARE_OPTIONS, min_width=80, parent=self)
layout.addWidget(self.opCombo) layout.addWidget(self.opCombo)
self._compTypeCombo = makeComboWidget([ self._compTypeCombo = makeComboWidget([
("特定值", "literal"), ("特定值", "literal"),
@@ -139,7 +139,7 @@ class ConditionRowFrame(QFrame):
self.literalStack = QStackedWidget(self) self.literalStack = QStackedWidget(self)
self.literalStack.setFixedHeight(25) self.literalStack.setFixedHeight(25)
self._literalWidgets = {} self._literalWidgets = {}
for vt in VAR_TYPE_ORDER: for vt in getTypeOrder():
w = makeValueWidget(vt, self.literalStack) w = makeValueWidget(vt, self.literalStack)
self._literalWidgets[vt] = w self._literalWidgets[vt] = w
self.literalStack.addWidget(w) self.literalStack.addWidget(w)
@@ -272,7 +272,7 @@ class ActionStepFrame(QFrame):
layout = QHBoxLayout(self) layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2) layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4) layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_TYPES, min_width=70, parent=self) self.opTypeCombo = makeComboWidget(ACTION_OPTIONS, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo) layout.addWidget(self.opTypeCombo)
layout.addWidget(makeLabel("设置", self)) layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self) self.targetCombo = QComboBox(self)
@@ -305,7 +305,7 @@ class ActionStepFrame(QFrame):
self.targetCombo.blockSignals(True) self.targetCombo.blockSignals(True)
self.targetCombo.clear() self.targetCombo.clear()
for p in PRESET_VARIABLES: for p in getPresetVars():
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"): if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue continue
info = self._varMgr.getInfoByName(p["name"]) info = self._varMgr.getInfoByName(p["name"])
@@ -322,10 +322,10 @@ class ActionStepFrame(QFrame):
self._literalWidgets = {} self._literalWidgets = {}
self._offsetWidgets = {} self._offsetWidgets = {}
for vt in VAR_TYPE_ORDER: for vt in getTypeOrder():
self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack) self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack)
self.valueStack.addWidget(self._literalWidgets[vt]) self.valueStack.addWidget(self._literalWidgets[vt])
if vt in ARITH_TYPES: if getArithType(vt):
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack) self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack)
self.valueStack.addWidget(self._offsetWidgets[vt]) self.valueStack.addWidget(self._offsetWidgets[vt])
else: else:
+3 -3
View File
@@ -18,7 +18,7 @@ from PySide6.QtCore import (
from base.MsgBase import MsgBase from base.MsgBase import MsgBase
from operators.AutoLib import AutoLib from operators.AutoLib import AutoLib
from utils.JSONReader import JSONReader from utils.JSONReader import JSONReader
from autoscript import execute, registerDefaultTargetVars from autoscript import createEngine
class AutoLibWorker(MsgBase, QThread): class AutoLibWorker(MsgBase, QThread):
@@ -219,8 +219,8 @@ class TimerTaskWorker(AutoLibWorker):
continue continue
for user in group.get("users", []): for user in group.get("users", []):
try: try:
registerDefaultTargetVars() engine = createEngine()
execute(auto_script, user) engine.execute(auto_script, user)
affected_count += 1 affected_count += 1
except ValueError as e: except ValueError as e:
self._showTrace( self._showTrace(
+1 -1
View File
@@ -108,7 +108,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.AutoScriptHelpButton = QPushButton("?") self.AutoScriptHelpButton = QPushButton("?")
self.AutoScriptHelpButton.setFixedSize(20, 20) self.AutoScriptHelpButton.setFixedSize(20, 20)
self.AutoScriptHelpButton.setToolTip( self.AutoScriptHelpButton.setToolTip(
"AutoScript 是一种轻量级 DSL\n" "AutoScript 是一种轻量级 DSL 语言,基于 Lua 实现。\n"
"用于在重复定时任务执行前,对用户的预约数据进行预处理\n" "用于在重复定时任务执行前,对用户的预约数据进行预处理\n"
"\n" "\n"
"点击查看完整在线文档" "点击查看完整在线文档"
+2 -2
View File
@@ -31,7 +31,7 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import QCloseEvent from PySide6.QtGui import QCloseEvent
from managers.driver.WebDriverManager import ( from managers.driver.WebDriverManager import (
instance as webdriverManagerInstance, instance as webdriverInstance,
WebDriverManager, WebDriverManager,
WebDriverInfo, WebDriverInfo,
WebDriverType, WebDriverType,
@@ -261,7 +261,7 @@ class ALWebDriverDownloadDialog(QDialog):
): ):
try: try:
self.__driver_manager = webdriverManagerInstance(self.__driver_dir) self.__driver_manager = webdriverInstance(self.__driver_dir)
except ValueError as e: except ValueError as e:
QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}") QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
self.reject() self.reject()
+3
View File
@@ -3,6 +3,9 @@
<file>icons/AutoLibrary_Logo_64.svg</file> <file>icons/AutoLibrary_Logo_64.svg</file>
<file>icons/AutoLibrary_Logo_128.svg</file> <file>icons/AutoLibrary_Logo_128.svg</file>
<file>icons/Copy.svg</file>
<file>icons/Reset.svg</file>
<file>translators/qtbase_zh_CN.qm</file> <file>translators/qtbase_zh_CN.qm</file>
</qresource> </qresource>
</RCC> </RCC>
+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7H7V5H13V7Z" fill="currentColor"/>
<path d="M13 11H7V9H13V11Z" fill="currentColor"/>
<path d="M7 15H13V13H7V15Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.817 11.186a8.94 8.94 0 0 0-1.355-3.219 9.053 9.053 0 0 0-2.43-2.43 8.95 8.95 0 0 0-3.219-1.355 9.028 9.028 0 0 0-1.838-.18V2L8 5l3.975 3V6.002c.484-.002.968.044 1.435.14a6.961 6.961 0 0 1 2.502 1.053 7.005 7.005 0 0 1 1.892 1.892A6.967 6.967 0 0 1 19 13a7.032 7.032 0 0 1-.55 2.725 7.11 7.11 0 0 1-.644 1.188 7.2 7.2 0 0 1-.858 1.039 7.028 7.028 0 0 1-3.536 1.907 7.13 7.13 0 0 1-2.822 0 6.961 6.961 0 0 1-2.503-1.054 7.002 7.002 0 0 1-1.89-1.89A6.996 6.996 0 0 1 5 13H3a9.02 9.02 0 0 0 1.539 5.034 9.096 9.096 0 0 0 2.428 2.428A8.95 8.95 0 0 0 12 22a9.09 9.09 0 0 0 1.814-.183 9.014 9.014 0 0 0 3.218-1.355 8.886 8.886 0 0 0 1.331-1.099 9.228 9.228 0 0 0 1.1-1.332A8.952 8.952 0 0 0 21 13a9.09 9.09 0 0 0-.183-1.814z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B