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

Compare commits

...

27 Commits

Author SHA1 Message Date
KenanZhu 5e898180c7 refactor(style): 统一项目代码风格,整理导入顺序、间距规范与方法排列
- GUI 模块统一 QtCore → QtGui → QtWidgets 导入排列,各类独占一行按字母排序
- 统一类间两空行、类内方法间一空行、函数间一空行的间距规范
- 统一方法排列顺序:__init__ → setupUi → connectSignals → public → Slot → private
- 统一 _widgets 中 ConditionRowFrame/ActionStepFrame 方法命名(populate* / toScript / updateValueWidget)
- LibTimeSelector 迁入 operators/abs 抽象层

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:14:27 +08:00
KenanZhu a03ab38279 refactor(autoscript): 完善 Lua 错误分类与 Date/Time 严格校验,清理死代码并补齐类型注解
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 01:02:17 +08:00
KenanZhu 4761cade26 refactor(gui): 统一资源路径前缀并更新新版 SVG Logo 2026-05-23 20:05:39 +08:00
KenanZhu 531b05651e refactor(gui): 重构更新 AutoLibrary Logo 样式为全新设计样式 2026-05-23 19:26:00 +08:00
KenanZhu 3cea7df736 refactor(gui): 编排编辑窗口适配 Lua 引擎新接口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:22:49 +08:00
KenanZhu a0fd03f12f refactor(autoscript): ASEngine 迁移至 Lua 沙箱引擎,强化类型安全与异常处理
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:22:36 +08:00
KenanZhu 9b47886e5b fix(autoscript): SET 赋值强制强类型检查,禁止跨类型隐式转换
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:22:06 +08:00
KenanZhu 82738be99a feat(gui): 编辑窗口支持调试运行与动态模拟目标数据输入
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:21:52 +08:00
KenanZhu e097b5afc9 refactor(gui): 编排窗口简化为纯代码生成器,移除脚本解析与预检逻辑
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 04:21:40 +08:00
KenanZhu fe7453fe02 feat(gui): 编排窗口支持算术表达式解析与回显
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:27:59 +08:00
KenanZhu 1d4b03d162 feat(autoscript): 支持算术表达式与变量参与加减运算
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:27:43 +08:00
KenanZhu 4642916fd5 fix(gui): 修正编排窗口日期映射 CURRENT_DATE 误识别为前天的问题 2026-05-18 20:47:35 +08:00
KenanZhu 5800437ba2 fix(gui): 编排窗口代码生成统一使用 END IF 结束块 2026-05-18 20:43:48 +08:00
KenanZhu 23467c1d3d feat(autoscript): 支持 // 行内注释与完整注释行解析 2026-05-18 20:13:46 +08:00
KenanZhu b8c0a29c59 fix(gui): 调整定时任务对话框布局边距与间距 2026-05-18 17:59:04 +08:00
KenanZhu 87787ad3dc style(gui): 编辑器高亮配色更改为 VSCode C 风格并为布尔字面量独立配色 2026-05-18 17:59:00 +08:00
KenanZhu e800f6ece1 refactor(gui): 统一 setupUi 命名并调整按钮布局 2026-05-18 16:01:22 +08:00
KenanZhu 600a304ab8 style(gui): 规范编排对话框属性命名并消除冗余代码 2026-05-18 16:01:16 +08:00
KenanZhu c038c8005d refactor(autoscript): 公开 splitTopLevel 并导出常量,消除冗余委托与重复变量 2026-05-18 16:01:10 +08:00
KenanZhu 6cf182c8c8 refactor(gui): 编排窗口迁移至新包并移除旧的预览/编排对话框 2026-05-18 11:15:35 +08:00
KenanZhu 33c0f4414c fix(autoscript): 为异常添加行号信息并补充类型兼容性检查 2026-05-17 02:58:47 +08:00
KenanZhu 2843300cf9 refactor(autoscript): 使用观察者模式解耦解析与预检查/编排流程 2026-05-17 01:48:25 +08:00
KenanZhu 9bdc9a3de9 refactor(autoscript): 使用 ASTokenizer 和 NodeVisitor 重构解析与执行流程 2026-05-17 01:33:22 +08:00
KenanZhu 500ddd41c5 refactor(autoscript): 替换 dsl 包为 autoscript 引擎模块 2026-05-12 11:49:43 +08:00
KenanZhu 14c6db3384 refactor(config): 引入 ConfigPath 值对象消除 ConfigType/ConfigKey 的消费者 API 冗余 2026-05-10 16:14:36 +08:00
KenanZhu bbd97970a6 refactor(modules): 将 AutoScriptEngine 移至 dsl/,ConfigUtils 移至 managers/config/,修复单一职责和依赖倒置问题 2026-05-10 15:33:10 +08:00
github-actions[bot] 22d3c3462c chore(release): merge release/v1.3.0 to main [auto release commit] 2026-05-09 06:08:33 +00:00
58 changed files with 3394 additions and 2043 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# AutoLibrary # AutoLibrary
--- ---
![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_128x128.ico) ![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_Logo_128.svg)
[![GitHub stars](https://img.shields.io/github/stars/KenanZhu/AutoLibrary.svg?style=social&label=Star)](https://github.com/KenanZhu/AutoLibrary) [![GitHub stars](https://img.shields.io/github/stars/KenanZhu/AutoLibrary.svg?style=social&label=Star)](https://github.com/KenanZhu/AutoLibrary)
![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary?label=license) ![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary?label=license)
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -22,7 +22,7 @@ def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
translator = QTranslator() translator = QTranslator()
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"): if translator.load(":/res/translators/qtbase_zh_CN.ts"):
app.installTranslator(translator) app.installTranslator(translator)
app.setStyle('Fusion') app.setStyle('Fusion')
app.setApplicationName("AutoLibrary") app.setApplicationName("AutoLibrary")
+458
View File
@@ -0,0 +1,458 @@
# -*- 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,
)
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"]
# Engine state
_TARGET_VARS: dict[str, dict] = {}
_lua = None
# 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": "当前时间"},
}
# 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(
):
"""
Return the sandboxed Lua runtime singleton.
"""
global _lua
if _lua is None:
_lua = _LuaRuntime(unpack_returned_tuples = True)
_sandbox(_lua)
_registerHelpers(_lua)
return _lua
def _sandbox(
lua,
) -> None:
"""
Remove dangerous Lua globals while keeping os.date / os.time for date-time helpers.
"""
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
""")
def _registerHelpers(
lua,
) -> None:
"""
Inject Date / Time helpers as pure Lua functions.
Date values are os.time timestamps (seconds since epoch).
Time values are minutes since midnight (0-1439).
This keeps Date / Time as native Lua numbers during script execution,
enabling type-safe arithmetic (+, -) and comparisons (<, <=, ==, ~=).
"""
lua.execute("""
function date(y, m, d)
return os.time({year = y, month = m, day = d})
end
function time(h, m)
return h * 60 + m
end
function CURRENT_DATE()
local now = os.date("*t")
return os.time({year = now.year, month = now.month, day = now.day})
end
function CURRENT_TIME()
local now = os.date("*t")
return now.hour * 60 + now.min
end
function date_add(date_val, n)
return date_val + n * 86400
end
function time_add(time_val, n)
return (time_val + n * 60) % 1440
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 _to_time(hm_str)
local h, m = hm_str:match("(%d+):(%d+)")
return h * 60 + m
end
-- pull helpers: native type -> string
function _from_date(ts)
return os.date("%Y-%m-%d", ts)
end
function _from_time(m)
return string.format("%02d:%02d", math.floor(m / 60), m % 60)
end
""")
def _navigatePath(
data: dict,
key_path: list,
default = None,
):
"""
Walk *key_path* into *data* and return the value at the leaf.
"""
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 _assignPath(
data: dict,
key_path: list,
value,
) -> None:
"""
Walk *key_path* into *data* and set *value* at the leaf.
"""
d = data
for key in key_path[:-1]:
d = d.setdefault(key, {})
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,
value,
) -> None:
"""
Validate that *value* matches the declared variable type.
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.
"""
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():
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)
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,
) -> 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}")
+80
View File
@@ -0,0 +1,80 @@
# -*- 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 autoscript.ASEngine import (
execute,
addTargetVar,
resetEngine,
META_VARS,
)
__all__ = [
"execute",
"addTargetVar",
"resetEngine",
"registerDefaultTargetVars",
"buildMockTargetData",
"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 = [
("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,
"Date": "2099-01-01",
"Time": "00:00",
"Int": 0,
"Float": 0.0,
}
def buildMockTargetData(
) -> dict:
"""
Build a target_data dict filled with type-appropriate mock values
for all registered target variables.
"""
data = {}
for _, var_type, key_path, _ in _TARGET_VAR_DEFS:
d = data
for key in key_path[:-1]:
d = d.setdefault(key, {})
d[key_path[-1]] = _MOCK_TYPE_VALUES.get(var_type, "")
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)
-1
View File
@@ -29,7 +29,6 @@ class LibOperator(MsgBase):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
-4
View File
@@ -58,7 +58,6 @@ class MsgBase:
except RuntimeError: except RuntimeError:
self._logger = None self._logger = None
def _showMsg( def _showMsg(
self, self,
msg: str msg: str
@@ -66,7 +65,6 @@ class MsgBase:
self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}") self._output_queue.put(f"[{self._class_name:<15}] >>> : {msg}")
def _showTrace( def _showTrace(
self, self,
msg: str, msg: str,
@@ -79,7 +77,6 @@ class MsgBase:
if self._logger and not no_log: if self._logger and not no_log:
self._logger.log(level, msg) self._logger.log(level, msg)
def _showLog( def _showLog(
self, self,
msg: str, msg: str,
@@ -89,7 +86,6 @@ class MsgBase:
if self._logger: if self._logger:
self._logger.log(level, msg) self._logger.log(level, msg)
def _waitMsg( def _waitMsg(
self, self,
timeout: float = 1.0 timeout: float = 1.0
+1 -2
View File
@@ -1,8 +1,7 @@
""" """
Base module for the AutoLibrary project. Base module for the AutoLibrary project.
Here are the classes and modules in this package: Here are the classes and modules in this package:
- MsgBase: Base class for messages.\ - MsgBase: Base class for messages.
- LibOperator: Base class for library operators. - LibOperator: Base class for library operators.
""" """
+12 -6
View File
@@ -16,7 +16,7 @@ from managers.config.ConfigManager import instance as configInstance
from managers.driver.WebDriverManager import instance as webdriverInstance from managers.driver.WebDriverManager import instance as webdriverInstance
def initializeLogManager( def _initializeLogManager(
) -> bool: ) -> bool:
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
@@ -27,7 +27,7 @@ def initializeLogManager(
logInstance(log_dir) logInstance(log_dir)
return True return True
def initializeConfigManager( def _initializeConfigManager(
) -> bool: ) -> bool:
logger = logInstance().getLogger("AppInitializer") logger = logInstance().getLogger("AppInitializer")
@@ -49,7 +49,7 @@ def initializeConfigManager(
configInstance(new_config_dir) configInstance(new_config_dir)
return True return True
def initializeWebDriverManager( def _initializeWebDriverManager(
) -> bool: ) -> bool:
logger = logInstance().getLogger("AppInitializer") logger = logInstance().getLogger("AppInitializer")
@@ -66,11 +66,17 @@ def initializeWebDriverManager(
def initializeApp( def initializeApp(
) -> bool: ) -> bool:
"""
Initialize the application components
if not initializeLogManager(): Order:
LogManager -> ConfigManager -> WebDriverManager
"""
if not _initializeLogManager():
return False return False
if not initializeConfigManager(): if not _initializeConfigManager():
return False return False
if not initializeWebDriverManager(): if not _initializeWebDriverManager():
return False return False
return True return True
+9 -12
View File
@@ -9,14 +9,17 @@ See the LICENSE file for details.
""" """
import platform import platform
from PySide6.QtCore import (
Qt,
QTimer
)
from PySide6.QtGui import ( from PySide6.QtGui import (
QIcon, QFont QFont,
QIcon
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QApplication QApplication,
) QDialog
from PySide6.QtCore import (
QTimer, Qt
) )
from gui.ALVersionInfo import ( from gui.ALVersionInfo import (
@@ -38,12 +41,11 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
self.modifyUi() self.modifyUi()
self.connectSignals() self.connectSignals()
def modifyUi( def modifyUi(
self self
): ):
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48)) self.LogoIconLabel.setPixmap(QIcon(":/res/icons/AutoLibrary_Logo_64.svg").pixmap(48, 48))
info_text = self.generateAboutText() info_text = self.generateAboutText()
self.AboutInfoBrowser.setHtml(info_text) self.AboutInfoBrowser.setHtml(info_text)
browser_font = self.AboutInfoBrowser.font() browser_font = self.AboutInfoBrowser.font()
@@ -51,14 +53,12 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
self.AboutInfoBrowser.setFont(browser_font) self.AboutInfoBrowser.setFont(browser_font)
self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction) self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
def connectSignals( def connectSignals(
self self
): ):
self.CopyButton.clicked.connect(self.copyAboutInfo) self.CopyButton.clicked.connect(self.copyAboutInfo)
def generateAboutText( def generateAboutText(
self self
) -> str: ) -> str:
@@ -91,7 +91,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
""" """
return about_text return about_text
def getOSInfo( def getOSInfo(
self self
): ):
@@ -123,7 +122,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
'architecture': architecture 'architecture': architecture
} }
def getQtVersion( def getQtVersion(
self self
): ):
@@ -134,7 +132,6 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
except: except:
return "Unknown" return "Unknown"
def copyAboutInfo( def copyAboutInfo(
self self
): ):
+631
View File
@@ -0,0 +1,631 @@
# -*- 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 copy import deepcopy
from PySide6.QtCore import QDate, Qt, QTime, QTimer, Slot
from PySide6.QtGui import (
QColor,
QFont,
QSyntaxHighlighter,
QTextCharFormat,
)
from PySide6.QtWidgets import (
QApplication,
QComboBox,
QDateEdit,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMessageBox,
QPlainTextEdit,
QPushButton,
QSpinBox,
QSplitter,
QStyle,
QTabWidget,
QTableWidget,
QTableWidgetItem,
QTimeEdit,
QVBoxLayout,
QWidget,
)
from autoscript import (
ALL_VARIABLES,
_MOCK_TYPE_VALUES,
_TARGET_VAR_DEFS,
execute,
registerDefaultTargetVars,
)
class ALScriptHighlighter(QSyntaxHighlighter):
"""
Syntax highlighter for Lua-based AutoScript.
"""
def __init__(
self,
parent = None
):
super().__init__(parent)
self._rules = []
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#569CD6"))
keywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in [
"if", "elseif", "else", "end", "then",
"and", "or", "not",
"local", "function", "return", "nil",
]:
self._rules.append((r"\b" + kw + r"\b", keywordFmt))
boolFmt = QTextCharFormat()
boolFmt.setForeground(QColor("#4FC1FF"))
boolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\btrue\b", boolFmt))
self._rules.append((r"\bfalse\b", boolFmt))
cmpFmt = QTextCharFormat()
cmpFmt.setForeground(QColor("#C586C0"))
cmpFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
self._rules.append((op, cmpFmt))
arithFmt = QTextCharFormat()
arithFmt.setForeground(QColor("#C586C0"))
arithFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
self._rules.append((op, arithFmt))
funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#DCDCAA"))
funcFmt.setFontWeight(QFont.Weight.Normal)
for fn in ["CURRENT_DATE", "CURRENT_TIME", "date_add", "time_add"]:
self._rules.append((r"\b" + fn + r"\b", funcFmt))
varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in ALL_VARIABLES.items()]
for var in var_names:
self._rules.append((r"\b" + var + r"\b", varFmt))
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#CE9178"))
strFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', strFmt))
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#B5CEA8"))
numFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt))
commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#6A9955"))
commentFmt.setFontItalic(True)
self._rules.append((r"--[^\n]*", commentFmt))
def highlightBlock(
self,
text
):
import re
for pattern, fmt in self._rules:
for match in re.finditer(pattern, text, re.IGNORECASE):
start = match.start()
length = match.end() - match.start()
self.setFormat(start, length, fmt)
class _DebugResultDialog(QDialog):
def __init__(
self,
changes: list,
parent = None
):
super().__init__(parent)
self.setWindowTitle("调试运行结果 - AutoLibrary")
self.setMinimumSize(600, 200)
layout = QVBoxLayout(self)
table = QTableWidget(len(changes), 3)
table.setHorizontalHeaderLabels(["目标变量", "原始数据", "运行后数据"])
table.horizontalHeader().setStretchLastSection(True)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
for row, (display_name, name, var_type, before_val, after_val) in enumerate(changes):
label = f"{display_name}: {name}({var_type})"
table.setItem(row, 0, QTableWidgetItem(label))
table.setItem(row, 1, QTableWidgetItem(str(before_val)))
table.setItem(row, 2, QTableWidgetItem(str(after_val)))
layout.addWidget(table)
btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
btnBox.accepted.connect(self.accept)
layout.addWidget(btnBox)
class ALAutoScriptEditDialog(QDialog):
def __init__(
self,
parent = None,
script: str = "",
mockData: dict = None
):
super().__init__(parent)
self._fontSize = 21
self._mockWidgets = {}
self.setupUi()
self.connectSignals()
self.textEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter(
self.textEdit.document()
)
if mockData:
self.setMockData(mockData)
def setupUi(
self
):
self.setWindowTitle("AutoScript 编辑 - AutoLibrary")
self.setMinimumSize(660, 600)
layout = QVBoxLayout(self)
layout.setSpacing(3)
layout.setContentsMargins(3, 3, 3, 3)
toolbarLayout = QHBoxLayout()
self.zoomInBtn = QPushButton("")
self.zoomInBtn.setFixedSize(25, 25)
self.zoomOutBtn = QPushButton("")
self.zoomOutBtn.setFixedSize(25, 25)
self.zoomResetBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_BrowserReload
), ""
)
self.zoomResetBtn.setFixedSize(25, 25)
self.zoomResetBtn.setToolTip("重置缩放")
self.zoomLabel = QLabel(f"{self._fontSize}px")
self.zoomLabel.setFixedHeight(25)
self.orchBtn = QPushButton("编排")
self.orchBtn.setFixedHeight(25)
self.orchBtn.setToolTip("可视化生成 AutoScript 代码并插入到光标位置")
toolbarLayout.addWidget(self.orchBtn)
self.debugBtn = QPushButton("▶ 调试运行")
self.debugBtn.setFixedHeight(25)
self.debugBtn.setToolTip("使用右侧模拟数据执行脚本,查看目标变量变化")
toolbarLayout.addWidget(self.debugBtn)
sep = QFrame()
sep.setFrameShape(QFrame.Shape.VLine)
sep.setFrameShadow(QFrame.Shadow.Sunken)
sep.setFixedWidth(1)
toolbarLayout.addWidget(sep)
toolbarLayout.addWidget(self.zoomInBtn)
toolbarLayout.addWidget(self.zoomOutBtn)
toolbarLayout.addWidget(self.zoomResetBtn)
toolbarLayout.addWidget(self.zoomLabel)
toolbarLayout.addStretch()
self.copyBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_FileDialogDetailedView
), ""
)
self.copyBtn.setFixedSize(25, 25)
self.copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self.copyBtn)
layout.addLayout(toolbarLayout)
self.textEdit = QPlainTextEdit(self)
self.textEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap
)
self.textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
layout.addWidget(self.textEdit)
self.createButtonPanel(layout)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
layout.addWidget(self.btnBox)
def createButtonPanel(
self,
parent_layout
):
splitter = QSplitter(Qt.Orientation.Horizontal)
tabWidget = QTabWidget()
tabWidget.setMaximumHeight(150)
basicWidget = QWidget()
basicLayout = QGridLayout(basicWidget)
basicLayout.setSpacing(4)
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"),
]
self.addButtonsToGrid(basicLayout, controlButtons, 0, 0, 3)
assignButtons = [
("赋值 (=)", " = "),
]
self.addButtonsToGrid(basicLayout, assignButtons, 1, 2, 3)
tabWidget.addTab(basicWidget, "基本语法")
operatorWidget = QWidget()
operatorLayout = QGridLayout(operatorWidget)
operatorLayout.setSpacing(4)
operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [
("加 (+)", " + "),
("减 (-)", " - "),
]
self.addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 3)
compareButtons = [
("等于 (==)", " == "),
("不等于 (~=)", " ~= "),
("大于 (>)", " > "),
("小于 (<)", " < "),
("大于等于 (>=)", " >= "),
("小于等于 (<=)", " <= "),
]
self.addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 3)
logic_buttons = [
("且 (and)", " and "),
("或 (or)", " or "),
]
self.addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 3)
tabWidget.addTab(operatorWidget, "运算符")
literalWidget = QWidget()
literalLayout = QGridLayout(literalWidget)
literalLayout.setSpacing(4)
literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [
("真 (true)", "true"),
("假 (false)", "false"),
]
self.addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 3)
dateTimeButtons = [
("日期", '"2099-01-01"'),
("时间", '"00:00"'),
]
self.addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 3)
hintButtons = [
("字符串", '"请输入文本"'),
("数字", "123"),
("注释", "-- 请输入注释"),
]
self.addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3)
tabWidget.addTab(literalWidget, "字面量")
varWidget = QWidget()
varLayout = QGridLayout(varWidget)
varLayout.setSpacing(4)
varLayout.setContentsMargins(4, 4, 4, 4)
varLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
varButtons = [
(display_name, name) for display_name, (name, _) in ALL_VARIABLES.items()
]
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, 1)
splitter.setSizes([530, 530])
parent_layout.addWidget(splitter)
def addButtonsToGrid(
self,
grid_layout,
buttons,
start_row,
start_col,
max_columns
):
col = start_col
row = start_row
for btn_text, template in buttons:
btn = QPushButton(btn_text)
btn.setProperty("template", template)
btn.clicked.connect(self.insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(25)
btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col)
col += 1
if col >= start_col + max_columns:
col = start_col
row += 1
def createMockPanel(
self
) -> QGroupBox:
group = QGroupBox("模拟目标数据")
form = QFormLayout(group)
form.setSpacing(4)
form.setContentsMargins(5, 10, 5, 5)
self._mockWidgets = {}
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS:
default = _MOCK_TYPE_VALUES.get(var_type, "")
widget = self.makeMockInput(var_type, default)
label = QLabel(f"{display_name}: {name}({var_type})")
form.addRow(label, widget)
self._mockWidgets[name] = (widget, var_type, key_path)
return group
def makeMockInput(
self,
var_type: str,
default
) -> QWidget:
if var_type == "String":
w = QLineEdit()
w.setText(str(default))
return w
if var_type == "Boolean":
w = QComboBox()
w.addItems(["", ""])
w.setCurrentIndex(0 if default else 1)
return w
if var_type == "Date":
w = QDateEdit()
w.setCalendarPopup(True)
w.setDisplayFormat("yyyy-MM-dd")
w.setDate(QDate.fromString(str(default), "yyyy-MM-dd"))
return w
if var_type == "Time":
w = QTimeEdit()
w.setDisplayFormat("HH:mm")
w.setTime(QTime.fromString(str(default), "HH:mm"))
return w
if var_type == "Int":
w = QSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setValue(int(default) if default else 0)
return w
if var_type == "Float":
w = QDoubleSpinBox()
w.setMinimum(-999999.0)
w.setMaximum(999999.0)
w.setDecimals(2)
w.setValue(float(default) if default else 0.0)
return w
w = QLineEdit()
w.setText(str(default))
return w
def getMockData(
self
) -> dict:
data = {}
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS:
widget, _, _ = self._mockWidgets[name]
value = self.getMockValue(widget, var_type)
d = data
for key in key_path[:-1]:
d = d.setdefault(key, {})
d[key_path[-1]] = value
return data
def setMockData(
self,
data: dict
):
if not data:
return
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS:
d = data
try:
for key in key_path:
d = d[key]
except (KeyError, TypeError):
continue
widget, _, _ = self._mockWidgets[name]
self.setMockValue(widget, var_type, d)
def getMockValue(
self,
widget: QWidget,
var_type: str
):
if var_type == "Boolean":
return widget.currentIndex() == 0
if var_type == "Date":
return widget.date().toString("yyyy-MM-dd")
if var_type == "Time":
return widget.time().toString("HH:mm")
if var_type == "Int":
return widget.value()
if var_type == "Float":
return widget.value()
return widget.text()
def setMockValue(
self,
widget: QWidget,
var_type: str,
value
):
if var_type == "Boolean":
widget.setCurrentIndex(0 if value else 1)
elif var_type == "Date":
widget.setDate(QDate.fromString(str(value), "yyyy-MM-dd"))
elif var_type == "Time":
widget.setTime(QTime.fromString(str(value), "HH:mm"))
elif var_type == "Int":
widget.setValue(int(value))
elif var_type == "Float":
widget.setValue(float(value))
else:
widget.setText(str(value))
def connectSignals(
self
):
self.btnBox.accepted.connect(self.accept)
self.btnBox.rejected.connect(self.reject)
self.orchBtn.clicked.connect(self.onOpenOrchDialog)
self.debugBtn.clicked.connect(self.onDebugRun)
self.zoomInBtn.clicked.connect(self.onZoomIn)
self.zoomOutBtn.clicked.connect(self.onZoomOut)
self.zoomResetBtn.clicked.connect(self.onZoomReset)
self.copyBtn.clicked.connect(self.onCopy)
def getScript(
self
) -> str:
return self.textEdit.toPlainText()
def updateFontSize(
self
):
self.textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
self.zoomLabel.setText(f"{self._fontSize}px")
@Slot()
def insertTemplate(
self
):
btn = self.sender()
if not isinstance(btn, QPushButton):
return
template = btn.property("template")
if not template:
return
cursor = self.textEdit.textCursor()
cursor.insertText(template)
@Slot()
def onZoomIn(
self
):
self._fontSize = min(self._fontSize + 2, 40)
self.updateFontSize()
@Slot()
def onZoomOut(
self
):
self._fontSize = max(self._fontSize - 2, 8)
self.updateFontSize()
@Slot()
def onZoomReset(
self
):
self._fontSize = 21
self.updateFontSize()
@Slot()
def onCopy(
self
):
clipboard = QApplication.clipboard()
clipboard.setText(self.textEdit.toPlainText())
original = self.copyBtn.text()
self.copyBtn.setText("已复制")
self.copyBtn.setEnabled(False)
QTimer.singleShot(2000, lambda: (
self.copyBtn.setText(original),
self.copyBtn.setEnabled(True)
))
@Slot()
def onOpenOrchDialog(
self
):
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
dlg = ALAutoScriptOrchDialog(self)
if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript()
if script:
cursor = self.textEdit.textCursor()
cursor.insertText(script)
dlg.deleteLater()
@Slot()
def onDebugRun(
self
):
script = self.textEdit.toPlainText().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空。")
return
target_data = self.getMockData()
before = deepcopy(target_data)
try:
registerDefaultTargetVars()
execute(script, target_data)
except ValueError as e:
QMessageBox.warning(self, "运行错误", str(e))
return
changes = []
for name, var_type, key_path, display_name in _TARGET_VAR_DEFS:
before_val = before
after_val = target_data
try:
for key in key_path:
before_val = before_val[key]
after_val = after_val[key]
except (KeyError, TypeError):
continue
if before_val != after_val:
changes.append((display_name, name, var_type, before_val, after_val))
if not changes:
QMessageBox.information(self, "调试运行", "目标变量未发生变化。")
return
dlg = _DebugResultDialog(changes, self)
dlg.exec()
dlg.deleteLater()
-884
View File
@@ -1,884 +0,0 @@
# -*- 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 PySide6.QtCore import QTime, QDate, Slot
from PySide6.QtWidgets import (
QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QPushButton, QScrollArea, QTimeEdit,
QDateEdit, QLineEdit, QSpinBox, QDoubleSpinBox,
QStackedWidget, QFrame, QDialogButtonBox,
QGroupBox, QSizePolicy
)
from utils.AutoScriptEngine import AutoScriptEngine
VARIABLE_META = AutoScriptEngine.VARIABLE_META
_VAR_COMBO_ITEMS = [
(display, varname, vartype)
for display, (varname, vartype) in VARIABLE_META.items()
]
_VAR_COMBO_ITEMS_SET = [
(display, varname, vartype)
for display, (varname, vartype) in VARIABLE_META.items()
if not varname.startswith("CURRENT_")
]
OP_ITEMS = [
("等于", ".EQ."),
("不等于", ".NEQ."),
("大于", ".BGT."),
("小于", ".BLT."),
("大于等于", ".BGE."),
("小于等于", ".BLE."),
]
def _makeVarCombo(
) -> QComboBox:
cb = QComboBox()
for display, varname, vartype in _VAR_COMBO_ITEMS:
cb.addItem(display, (varname, vartype))
cb.setMinimumWidth(120)
cb.setFixedHeight(25)
return cb
def _makeSetVarCombo(
) -> QComboBox:
cb = QComboBox()
for display, varname, vartype in _VAR_COMBO_ITEMS_SET:
cb.addItem(display, (varname, vartype))
cb.setMinimumWidth(120)
cb.setFixedHeight(25)
return cb
def _makeOpCombo(
) -> QComboBox:
cb = QComboBox()
for display, op in OP_ITEMS:
cb.addItem(display, op)
cb.setMinimumWidth(80)
cb.setFixedHeight(25)
return cb
def _makeValueWidget(
data_type: str
) -> QWidget:
if data_type == "Time":
w = QTimeEdit()
w.setDisplayFormat("HH:mm")
w.setMinimumWidth(100)
w.setFixedHeight(25)
elif data_type == "Date":
w = QDateEdit()
w.setDisplayFormat("yyyy-MM-dd")
w.setCalendarPopup(True)
w.setMinimumWidth(130)
w.setFixedHeight(25)
elif data_type == "Integer":
w = QSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setMinimumWidth(100)
w.setFixedHeight(25)
elif data_type == "Float":
w = QDoubleSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setDecimals(2)
w.setMinimumWidth(100)
w.setFixedHeight(25)
elif data_type == "Boolean":
w = QComboBox()
w.addItem(".TRUE.", ".TRUE.")
w.addItem(".FALSE.", ".FALSE.")
w.setMinimumWidth(100)
w.setFixedHeight(25)
else:
w = QLineEdit()
w.setPlaceholderText("输入值")
w.setMinimumWidth(120)
w.setFixedHeight(25)
return w
def _makeActionValueWidget(
data_type: str
) -> QWidget:
if data_type == "Date":
w = QComboBox()
w.addItem("今天", "today")
w.addItem("明天", "tomorrow")
w.setFixedHeight(25)
w.setMinimumWidth(100)
w._is_date_action = True
return w
if data_type == "Time":
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
modeCombo = QComboBox()
modeCombo.addItem("固定时间", "fixed")
modeCombo.addItem("相对当前", "relative")
modeCombo.setFixedHeight(25)
stack = QStackedWidget()
timeEdit = QTimeEdit()
timeEdit.setDisplayFormat("HH:mm")
timeEdit.setFixedHeight(25)
spinBox = QSpinBox()
spinBox.setRange(0, 23)
spinBox.setSuffix("小时")
spinBox.setFixedHeight(25)
stack.addWidget(timeEdit)
stack.addWidget(spinBox)
modeCombo.currentIndexChanged.connect(
lambda i: stack.setCurrentIndex(i)
)
layout.addWidget(modeCombo)
layout.addWidget(stack)
container._modeCombo = modeCombo
container._timeEdit = timeEdit
container._spinBox = spinBox
container._isActionTime = True
return container
return _makeValueWidget(data_type)
def _getValueFromWidget(
w: QWidget
) -> str:
if getattr(w, '_isActionTime', False):
if w._modeCombo.currentData() == "fixed":
return w._timeEdit.time().toString("HH:mm")
else:
return f"+{w._spinBox.value()}"
if isinstance(w, QTimeEdit):
return w.time().toString("HH:mm")
if isinstance(w, QDateEdit):
return w.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox):
return w.currentText()
if isinstance(w, QSpinBox):
return str(w.value())
if isinstance(w, QDoubleSpinBox):
return str(w.value())
if isinstance(w, QLineEdit):
return w.text()
return ""
def _encodeValueStr(
raw_value: str,
data_type: str
) -> str:
if data_type == "Time":
if raw_value.startswith("+"):
return raw_value
return f"TIME({raw_value})"
elif data_type == "Date":
if raw_value == "今天":
return "CURRENT_DATE"
elif raw_value == "明天":
return "CURRENT_DATE + 1"
return f"DATE({raw_value})"
elif data_type == "Boolean":
return raw_value
elif data_type == "String":
escaped = raw_value.replace("'", "''")
return f"'{escaped}'"
else:
return raw_value
def _setWidgetValue(
w: QWidget,
vartype: str,
expr: str
):
import re
s = expr.strip()
if getattr(w, '_isActionTime', False):
timeMatch = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
if timeMatch:
w._modeCombo.setCurrentIndex(0)
parts = timeMatch.group(1).split(":")
w._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
return
relMatch = re.match(r"^\+(\d+)$", s)
if relMatch:
w._modeCombo.setCurrentIndex(1)
w._spinBox.setValue(int(relMatch.group(1)))
return
return
if getattr(w, '_is_date_action', False) and isinstance(w, QComboBox):
if s.upper() in ("CURRENT_DATE", "TODAY"):
w.setCurrentIndex(0)
elif s.upper() in ("CURRENT_DATE + 1", "TOMORROW"):
w.setCurrentIndex(1)
else:
dateMatch = re.match(
r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE
)
if dateMatch:
from datetime import datetime, timedelta
dateStr = dateMatch.group(1)
today = datetime.now().strftime("%Y-%m-%d")
tomorrow = (
datetime.now() + timedelta(days=1)
).strftime("%Y-%m-%d")
if dateStr == today:
w.setCurrentIndex(0)
elif dateStr == tomorrow:
w.setCurrentIndex(1)
return
if vartype == "Time":
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
if m and isinstance(w, QTimeEdit):
parts = m.group(1).split(":")
w.setTime(QTime(int(parts[0]), int(parts[1])))
elif vartype == "Date":
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE)
if m and isinstance(w, QDateEdit):
parts = m.group(1).split("-")
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
elif vartype == "Boolean" and isinstance(w, QComboBox):
for i in range(w.count()):
if w.itemData(i) == s.upper():
w.setCurrentIndex(i)
break
elif vartype == "Integer" and isinstance(w, QSpinBox):
try:
w.setValue(int(s))
except ValueError:
pass
elif vartype == "Float" and isinstance(w, QDoubleSpinBox):
try:
w.setValue(float(s))
except ValueError:
pass
elif isinstance(w, QLineEdit):
inner = s
if (inner.startswith("'") and inner.endswith("'")) or \
(inner.startswith('"') and inner.endswith('"')):
inner = inner[1:-1].replace("''", "'")
w.setText(inner)
class ActionStepFrame(QFrame):
def __init__(
self,
parent=None
):
super().__init__(parent)
self.setupUi()
self.connectSignals()
self.onTargetChanged(0)
def setupUi(
self
):
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.targetCombo = _makeSetVarCombo()
self.valueWidgetStack = QStackedWidget()
self.valueWidgetStack.setFixedHeight(25)
self.initValueStack()
setLabel = QLabel("设置")
setLabel.setFixedHeight(25)
layout.addWidget(setLabel)
layout.addWidget(self.targetCombo)
toLabel = QLabel("")
toLabel.setFixedHeight(25)
layout.addWidget(toLabel)
layout.addWidget(self.valueWidgetStack)
self.deleteBtn = QPushButton("×")
self.deleteBtn.setFixedSize(24, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
def connectSignals(
self
):
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
def initValueStack(
self
):
self._valueWidgets = {}
for _, _, vartype in _VAR_COMBO_ITEMS:
if vartype not in self._valueWidgets:
w = _makeActionValueWidget(vartype)
self._valueWidgets[vartype] = w
self.valueWidgetStack.addWidget(w)
self.valueWidgetStack.setCurrentWidget(
self._valueWidgets.get("String", self.valueWidgetStack.widget(0))
)
def getTarget(
self
) -> str:
data = self.targetCombo.currentData()
return data[0] if data else ""
def getTargetType(
self
) -> str:
data = self.targetCombo.currentData()
return data[1] if data else "String"
def getValueRaw(
self
) -> str:
currentType = self.getTargetType()
w = self._valueWidgets.get(currentType)
if w:
return _getValueFromWidget(w)
return ""
def toScriptLine(
self
) -> str:
target = self.getTarget()
if not target:
return ""
rawVal = self.getValueRaw()
targetType = self.getTargetType()
encoded = _encodeValueStr(rawVal, targetType)
if targetType == "Time" and rawVal.startswith("+"):
hours = rawVal[1:]
return f" {target} .ADD. {hours}"
return f" SET {target} = {encoded}"
def loadFromScript(
self,
targetVar: str,
valueExpr: str
):
for idx in range(self.targetCombo.count()):
data = self.targetCombo.itemData(idx)
if data and data[0] == targetVar:
self.targetCombo.setCurrentIndex(idx)
break
self.setValueFromExpr(valueExpr)
def setValueFromExpr(
self,
expr: str
):
targetType = self.getTargetType()
w = self._valueWidgets.get(targetType)
if not w:
return
_setWidgetValue(w, targetType, expr)
@Slot(int)
def onTargetChanged(
self,
idx
):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
if data:
_, vartype = data
w = self._valueWidgets.get(vartype)
if w:
self.valueWidgetStack.setCurrentWidget(w)
class ConditionalBlock(QGroupBox):
def __init__(
self,
blockIndex: int,
parent=None
):
super().__init__(parent)
self.blockIndex = blockIndex
self._actionWidgets = []
self.setupUi()
self.connectSignals()
self.onOperandChanged(0)
def setupUi(
self
):
self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(
QSizePolicy.Policy.Preferred,
QSizePolicy.Policy.Fixed
)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(4)
mainLayout.setContentsMargins(5, 5, 5, 5)
headerLayout = QHBoxLayout()
self.typeCombo = QComboBox()
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
typeLabel = QLabel("类型:")
typeLabel.setFixedHeight(25)
headerLayout.addWidget(typeLabel)
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块")
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget()
self.conditionWidget.setFixedHeight(60)
condLayout = QHBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(0, 0, 0, 0)
ifLabel = QLabel("如果")
ifLabel.setFixedHeight(25)
condLayout.addWidget(ifLabel)
self.operandCombo = _makeVarCombo()
condLayout.addWidget(self.operandCombo)
self.opCombo = _makeOpCombo()
condLayout.addWidget(self.opCombo)
self.condValueStack = QStackedWidget()
self.condValueStack.setFixedHeight(25)
self._condValueWidgets = {}
for vartype in ["Time", "Date", "String", "Integer", "Float", "Boolean"]:
w = _makeValueWidget(vartype)
self._condValueWidgets[vartype] = w
self.condValueStack.addWidget(w)
self.condValueStack.setCurrentWidget(self._condValueWidgets.get("String"))
condLayout.addWidget(self.condValueStack)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:")
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self.actionsLayout = QVBoxLayout()
self.actionsLayout.setSpacing(2)
mainLayout.addLayout(self.actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤")
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
def connectSignals(
self
):
self.operandCombo.currentIndexChanged.connect(self.onOperandChanged)
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addActionBtn.clicked.connect(self.addActionStep)
def removeActionStep(
self,
step: ActionStepFrame
):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self.actionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
def getBlockType(
self
) -> str:
return self.typeCombo.currentData()
def toScriptLines(
self
) -> list:
blockType = self.getBlockType()
lines = []
if blockType in ("IF", "ELSE IF"):
operand = self.operandCombo.currentData()
operandName = operand[0] if operand else ""
operandType = operand[1] if operand else "String"
opSym = self.opCombo.currentData()
rawVal = _getValueFromWidget(
self._condValueWidgets.get(operandType, QLineEdit())
)
encodedVal = _encodeValueStr(rawVal, operandType)
if blockType == "IF":
lines.append(f"IF({operandName} {opSym} {encodedVal}) THEN")
else:
lines.append(f"ELSE IF({operandName} {opSym} {encodedVal}) THEN")
else:
lines.append("ELSE")
for step in self._actionWidgets:
scriptLine = step.toScriptLine()
if scriptLine:
lines.append(scriptLine)
return lines
def getConditionSummary(
self
) -> str:
bt = self.getBlockType()
if bt == "ELSE":
return "ELSE"
operandData = self.operandCombo.currentData()
if not operandData:
return bt
operandDisplay = self.operandCombo.currentText()
opDisplay = self.opCombo.currentText()
rawVal = self.getConditionRawValuePreview()
return f"{bt} ({operandDisplay} {opDisplay} {rawVal})"
def getConditionRawValuePreview(
self
) -> str:
data = self.operandCombo.currentData()
if not data:
return ""
_, vartype = data
w = self._condValueWidgets.get(vartype)
if w:
return _getValueFromWidget(w)
return ""
def countActionSteps(
self
) -> int:
return len(self._actionWidgets)
@Slot(int)
def onOperandChanged(
self,
idx
):
if idx < 0:
return
data = self.operandCombo.itemData(idx)
if data:
_, vartype = data
w = self._condValueWidgets.get(vartype)
if w:
self.condValueStack.setCurrentWidget(w)
@Slot(int)
def onTypeChanged(
self,
idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText("执行步骤:" if isCond else "ELSE 执行步骤:")
@Slot()
def addActionStep(
self
):
step = ActionStepFrame(self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self.actionsLayout.addWidget(step)
class ALAutoScriptOrchDialog(QDialog):
def __init__(
self,
parent=None,
existingScript: str = ""
):
super().__init__(parent)
self._blocks: list[ConditionalBlock] = []
self.modifyUi()
self.connectSignals()
if existingScript and existingScript.strip():
self.loadFromScript(existingScript)
else:
self.addBlock()
self._scrollLayout.addStretch()
def modifyUi(
self
):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(420, 400)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scrollContent = QWidget()
self._scrollLayout = QVBoxLayout(scrollContent)
self._scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
addBlockLayout = QHBoxLayout()
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
addBlockLayout.addStretch()
addBlockLayout.addWidget(self.addBlockBtn)
addBlockLayout.addStretch()
mainLayout.addLayout(addBlockLayout)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.accept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
def removeBlock(
self,
block: ConditionalBlock
):
if block in self._blocks:
self._blocks.remove(block)
self._scrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
def getScript(
self
) -> str:
parts = []
for i, block in enumerate(self._blocks):
blockType = block.getBlockType()
if blockType == "IF" and i > 0:
parts.append("ENDIF")
lines = block.toScriptLines()
parts.extend(lines)
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("ENDIF")
return "\n".join(parts)
def getScriptPreview(
self
) -> str:
s = self.getScript()
if len(s) > 10:
return s[:7] + "..."
return s
def loadFromScript(
self,
script: str
):
import re
lines = [l.strip() for l in script.split("\n") if l.strip()]
if not lines:
self.addBlock()
return
currentBlock = None
currentBlockType = None
actionsBuffer = []
def flushBlock():
nonlocal currentBlock, currentBlockType, actionsBuffer
if currentBlock is None:
return
typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2}
idx = typeIdxMap.get(currentBlockType, 0)
currentBlock.typeCombo.setCurrentIndex(idx)
currentBlock.onTypeChanged(idx)
for oldStep in list(currentBlock._actionWidgets):
currentBlock.removeActionStep(oldStep)
for target, valueExpr in actionsBuffer:
currentBlock.addActionStep()
step = currentBlock._actionWidgets[-1]
step.loadFromScript(target, valueExpr)
self._blocks.clear()
while self._scrollLayout.count():
item = self._scrollLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
for line in lines:
upper = line.upper()
ifMatch = re.match(r"^IF\((.+)\)\s*THEN\s*$", upper)
if ifMatch:
flushBlock()
currentBlockType = "IF"
actionsBuffer = []
self.addBlock()
currentBlock = self._blocks[-1]
self.parseConditionToBlock(currentBlock, ifMatch.group(1))
continue
elifIfMatch = re.match(r"^ELSE\s+IF\((.+)\)\s*THEN\s*$", upper)
if elifIfMatch:
flushBlock()
currentBlockType = "ELSE IF"
actionsBuffer = []
self.addBlock()
currentBlock = self._blocks[-1]
self.parseConditionToBlock(currentBlock, elifIfMatch.group(1))
continue
if upper == "ELSE":
flushBlock()
currentBlockType = "ELSE"
actionsBuffer = []
self.addBlock()
currentBlock = self._blocks[-1]
currentBlock.conditionWidget.setVisible(False)
continue
setMatch = re.match(r"^SET\s+(\w+)\s*=\s*(.+)$", line, re.IGNORECASE)
if setMatch:
target = setMatch.group(1).strip()
valueExpr = setMatch.group(2).strip()
actionsBuffer.append((target, valueExpr))
continue
addMatch = re.match(r"^(\w+)\s+\.ADD\.\s+(\d+)$", line, re.IGNORECASE)
if addMatch:
target = addMatch.group(1).strip()
hours = addMatch.group(2).strip()
actionsBuffer.append((target, f"+{hours}"))
continue
if upper in ("ENDIF", "END IF"):
flushBlock()
currentBlock = None
currentBlockType = None
actionsBuffer = []
continue
flushBlock()
if not self._blocks:
self.addBlock()
def parseConditionToBlock(
self,
block: ConditionalBlock,
condStr: str
):
condStr = condStr.strip()
for _, opSym in OP_ITEMS:
idx = condStr.upper().find(opSym)
if idx >= 0:
leftPart = condStr[:idx].strip()
rightPart = condStr[idx + len(opSym):].strip()
for ci in range(block.operandCombo.count()):
data = block.operandCombo.itemData(ci)
if data and data[0] == leftPart:
block.operandCombo.setCurrentIndex(ci)
break
for oi in range(block.opCombo.count()):
if block.opCombo.itemData(oi) == opSym:
block.opCombo.setCurrentIndex(oi)
break
opData = block.operandCombo.currentData()
vartype = opData[1] if opData else "String"
w = block._condValueWidgets.get(vartype)
if w:
_setWidgetValue(w, vartype, rightPart)
return
@Slot()
def addBlock(
self
):
block = ConditionalBlock(len(self._blocks), self)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
self._blocks.append(block)
block.addActionStep()
if self._scrollLayout.count() > 0:
lastItem = self._scrollLayout.itemAt(
self._scrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self._scrollLayout.insertWidget(
self._scrollLayout.count() - 1, block
)
return
self._scrollLayout.addWidget(block)
@@ -0,0 +1,3 @@
from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog
__all__ = ["ALAutoScriptOrchDialog"]
+266
View File
@@ -0,0 +1,266 @@
# -*- 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.
"""
"""
Conditional block widget for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._widgets import (
ActionStepFrame,
ConditionRowFrame,
)
class ConditionalBlock(QGroupBox):
def __init__(
self,
blockIndex: int,
varMgr = None,
parent = None
):
super().__init__(parent)
self.blockIndex = blockIndex
self._varMgr = varMgr
self._actionWidgets = []
self._conditionRows = []
self.setupUi()
self.connectSignals()
self.addInitialConditionRow()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout()
headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self)
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
self.typeCombo.setFixedHeight(25)
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
headerLayout.addWidget(QLabel("类型:", self))
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块", self)
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
)
condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6)
self.condRowsLayout = QVBoxLayout()
self.condRowsLayout.setSpacing(4)
condLayout.addLayout(self.condRowsLayout)
self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget)
self.addCondBtn.setFixedHeight(25)
condLayout.addWidget(self.addCondBtn)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:", self)
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self.actionsLayout = QVBoxLayout()
self.actionsLayout.setSpacing(4)
mainLayout.addLayout(self.actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤", self)
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
self.setUpdatesEnabled(True)
def connectSignals(
self
):
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addCondBtn.clicked.connect(self.addConditionRow)
self.addActionBtn.clicked.connect(self.addActionStep)
def addInitialConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=True, parent=self
)
self._conditionRows.append(row)
self.condRowsLayout.addWidget(row)
def addConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=False, parent=self
)
row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row))
self._conditionRows.append(row)
self.condRowsLayout.addWidget(row)
def removeConditionRow(
self,
row: ConditionRowFrame
):
if row in self._conditionRows and len(self._conditionRows) > 1:
self._conditionRows.remove(row)
self.condRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
def addActionStep(
self
):
step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self.actionsLayout.addWidget(step)
def removeActionStep(
self,
step: ActionStepFrame
):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self.actionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
def getBlockType(
self
) -> str:
return self.typeCombo.currentData()
def getConditionRows(
self
):
return list(self._conditionRows)
def getActionSteps(
self
):
return list(self._actionWidgets)
def countActionSteps(
self
) -> int:
return len(self._actionWidgets)
def toScript(
self
) -> list:
"""
Generate Lua script lines for this conditional block.
"""
blockType = self.getBlockType()
lines = []
if blockType in ("IF", "ELSE IF"):
condTexts = [
r.toScript() for r in self._conditionRows if r.toScript()
]
if not condTexts:
condTexts = ["true"]
if len(condTexts) == 1:
combined = condTexts[0]
else:
parts = []
for i, ct in enumerate(condTexts):
if i > 0:
logic = self._conditionRows[i].getLogic() or "and"
parts.append(f" {logic} ")
parts.append(f"({ct})")
combined = "".join(parts)
if blockType == "IF":
lines.append(f"if {combined} then")
else:
lines.append(f"elseif {combined} then")
else:
lines.append("else")
for step in self._actionWidgets:
scriptLine = step.toScript()
if scriptLine:
lines.append(scriptLine)
return lines
def refreshVarCombos(
self
):
for row in self._conditionRows:
row.refreshVarCombos()
for step in self._actionWidgets:
step.refreshVarCombos()
def setPrevBlockType(
self,
prevType: str | None
):
model = self.typeCombo.model()
if model is None:
return
for data in ("ELSE IF", "ELSE"):
idx = self.typeCombo.findData(data)
if idx < 0:
continue
item = model.item(idx)
shouldEnable = prevType != "ELSE"
item.setEnabled(shouldEnable)
if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"):
self.typeCombo.setCurrentIndex(0)
@Slot(int)
def onTypeChanged(
self,
_idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:"
)
+164
View File
@@ -0,0 +1,164 @@
# -*- 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.
"""
"""
Orchestration dialog for visually composing AutoScript scripts.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._helpers import VariableManager
from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock
class ALAutoScriptOrchDialog(QDialog):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._blocks = []
self._varMgr = VariableManager(self)
self.setupUi()
self.connectSignals()
self.addBlock()
self.scrollLayout.addStretch()
def setupUi(
self
):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(640, 600)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame)
scrollContent = QWidget()
self.scrollLayout = QVBoxLayout(scrollContent)
self.scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
mainLayout.addWidget(self.addBlockBtn)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.onAccept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
def updateBlockTypeRestrictions(
self
):
prevType = None
for block in self._blocks:
block.setPrevBlockType(prevType)
prevType = block.getBlockType()
def addBlock(
self
):
block = ConditionalBlock(
len(self._blocks), self._varMgr, parent=self
)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
block.typeCombo.currentIndexChanged.connect(self.updateBlockTypeRestrictions)
block.addActionStep()
self._blocks.append(block)
self.updateBlockTypeRestrictions()
if self.scrollLayout.count() > 0:
lastItem = self.scrollLayout.itemAt(
self.scrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self.scrollLayout.insertWidget(
self.scrollLayout.count() - 1, block
)
return
self.scrollLayout.addWidget(block)
def removeBlock(
self,
block: ConditionalBlock
):
if len(self._blocks) <= 1:
QMessageBox.information(self, "提示", "至少保留一个判断块。")
return
if block in self._blocks:
self._blocks.remove(block)
self.scrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
for i, blk in enumerate(self._blocks):
blk.blockIndex = i
if i == 0:
blk.typeCombo.setEnabled(False)
blk.typeCombo.setCurrentIndex(0)
else:
blk.typeCombo.setEnabled(True)
blk.refreshVarCombos()
self.updateBlockTypeRestrictions()
def getScript(
self
) -> str:
"""
Generate the complete Lua script from all blocks.
"""
parts = []
prevType = None
for block in self._blocks:
blockType = block.getBlockType()
if blockType == "IF" and prevType is not None:
parts.append("end")
lines = block.toScript()
parts.extend(lines)
prevType = blockType
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("end")
return "\n".join(parts)
@Slot()
def onAccept(
self
):
script = self.getScript().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。")
return
self.accept()
+771
View File
@@ -0,0 +1,771 @@
# -*- 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.
"""
"""
Helper utilities and constants for the AutoScript orchestration dialog.
"""
import re
from PySide6.QtCore import (
QObject,
QDate,
QTime
)
from PySide6.QtWidgets import (
QComboBox,
QDateEdit,
QDoubleSpinBox,
QHBoxLayout,
QLabel,
QLineEdit,
QSizePolicy,
QSpinBox,
QStackedWidget,
QTimeEdit,
QWidget,
)
from autoscript import (
ALL_VARIABLES,
)
# Types that support arithmetic operations (add/sub)
ARITH_TYPES = {"Date", "Time", "Int", "Float"}
VAR_TYPE_ORDER = [
"String",
"Int",
"Float",
"Boolean",
"Date",
"Time"
]
PRESET_VARIABLES = [
{
"name": name.upper(),
"type": vtype,
"display": display
}
for display, (name, vtype) in ALL_VARIABLES.items()
]
PRESET_NAMES = {
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"),
("或者 (or)", "or"),
]
ACTION_TYPES = [
("设置为", "set"),
("增加", "add"),
("减少", "sub"),
]
DATE_RELATIVE_OPTIONS = [
("前天", "day_before_yesterday"),
("昨天", "yesterday"),
("今天", "today"),
("明天", "tomorrow"),
("后天", "day_after_tomorrow")
]
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"),
]
class _DateInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._dynamicItems = {} # index -> raw expression, for one-way parsed items
self.setupUi()
def setupUi(
self
):
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
self._modeCombo = QComboBox(self)
self._modeCombo.addItem("相对日期", "relative")
self._modeCombo.addItem("绝对日期", "absolute")
self._modeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self)
self._relCombo = QComboBox(self)
for display, data in DATE_RELATIVE_OPTIONS:
self._relCombo.addItem(display, data)
self._relCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo)
self._dateEdit = QDateEdit(self)
self._dateEdit.setDisplayFormat("yyyy-MM-dd")
self._dateEdit.setCalendarPopup(True)
self._dateEdit.setFixedHeight(25)
self._stack.addWidget(self._dateEdit)
self._modeCombo.currentIndexChanged.connect(
lambda i: self._stack.setCurrentIndex(i)
)
layout.addWidget(self._modeCombo)
layout.addWidget(self._stack)
layout.addStretch()
_RE_DATE_ADD_CURRENT = re.compile(
r'^date_add\(CURRENT_DATE\(\),\s*(-?\d+)\)$', re.IGNORECASE
)
def getValue(
self
) -> str:
mode = self._modeCombo.currentData()
if mode == "relative":
idx = self._relCombo.currentIndex()
if idx in self._dynamicItems:
return self._dynamicItems[idx]
return self._relCombo.currentText()
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):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._timeEdit = QTimeEdit(self)
self._timeEdit.setDisplayFormat("HH:mm")
self._timeEdit.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._timeEdit)
def getValue(
self
) -> str:
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):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self)
for display, data in DATE_OFFSET_UNITS:
self._unitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addWidget(self._spinBox)
layout.addWidget(self._unitCombo)
layout.addStretch()
def getValue(
self
) -> str:
return str(self.getOffsetDays())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetDays(
self
) -> int:
val = self._spinBox.value()
unit = self._unitCombo.currentData()
if unit == "weeks":
return val * 7
if unit == "months":
return val * 30
if unit == "years":
return val * 365
return val
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
class _TimeOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setSuffix(" 小时")
self._spinBox.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._spinBox)
def getValue(
self
) -> str:
return str(self.getOffsetHours())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetHours(
self
) -> int:
return self._spinBox.value()
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
class VariableManager(QObject):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._vars = []
self._nameMap = {}
self._initPresetVars()
def _initPresetVars(
self
):
for p in PRESET_VARIABLES:
entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry)
self._nameMap[p["name"]] = entry
def getInfoByName(
self,
name: str
):
return self._nameMap.get(name.upper().strip())
def populateCombo(
self,
combo: QComboBox
):
currentData = combo.currentData()
combo.blockSignals(True)
combo.clear()
for entry in self._vars:
combo.addItem(
entry["display"],
(entry["name"], entry["type"])
)
if currentData:
for i in range(combo.count()):
d = combo.itemData(i)
if d and d[0] == currentData[0]:
combo.setCurrentIndex(i)
break
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(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "String":
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
if var_type == "Boolean":
w = QComboBox(parent)
w.addItem("是 (true)", "true")
w.addItem("否 (false)", "false")
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateInputContainer(parent)
if var_type == "Time":
return _TimeInputContainer(parent)
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
def makeOffsetWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateOffsetContainer(parent)
if var_type == "Time":
return _TimeOffsetContainer(parent)
w = QLabel("(不支持该操作)", parent)
w.setFixedHeight(25)
return w
def makeVarRefCombo(
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
cb.setFixedHeight(25)
cb.setMinimumWidth(120)
cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return cb
def makeComboWidget(
items,
min_width: int = 80,
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
for display, data in items:
cb.addItem(display, data)
cb.setFixedHeight(25)
cb.setMinimumWidth(min_width)
return cb
def makeLabel(
text: str,
parent: QWidget = None,
width: int = None
) -> QLabel:
lbl = QLabel(text, parent)
lbl.setFixedHeight(25)
if width:
lbl.setFixedWidth(width)
return lbl
def getValueFromWidget(
w: QWidget
) -> str:
if hasattr(w, "getValue"):
return w.getValue()
if isinstance(w, QTimeEdit):
return w.time().toString("HH:mm")
if isinstance(w, QDateEdit):
return w.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox):
return w.currentData() or w.currentText()
if isinstance(w, QSpinBox):
return str(w.value())
if isinstance(w, QDoubleSpinBox):
return str(w.value())
if isinstance(w, QLineEdit):
return w.text()
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(
raw_value: str,
var_type: str
) -> str:
"""
Encode a raw widget value as a Lua expression.
Arithmetic expressions (A + B) are passed through for numeric types;
Date/Time arithmetic is translated to ``date_add()`` / ``time_add()`` calls.
"""
if var_type in ("Date", "Time"):
return encodeDateOrTime(str(raw_value), var_type)
if isinstance(raw_value, bool):
return "true" if raw_value else "false"
s = str(raw_value)
if isArithExpr(s):
return s
if var_type == "Boolean":
up = s.upper().strip()
if up in ("TRUE", "FALSE"):
return up.lower()
return "true" if raw_value else "false"
if var_type == "String":
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
return s
def encodeDateOrTime(
raw_value: str,
var_type: str
) -> str:
"""
Translate a date/time widget value into a Lua expression.
"""
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
if m_arith:
left = m_arith.group(1).strip().upper()
sign = m_arith.group(2)
right = m_arith.group(3).strip()
operand = right if sign == "+" else f"-{right}"
if left == "CURRENT_DATE":
return f"date_add(CURRENT_DATE(), {operand})"
if left == "CURRENT_TIME":
return f"time_add(CURRENT_TIME(), {operand})"
if var_type == "Date":
return f"date_add({left}, {operand})"
if var_type == "Time":
return f"time_add({left}, {operand})"
return f"{left} {sign} {right}"
if up == "CURRENT_DATE":
return "CURRENT_DATE()"
if up == "CURRENT_TIME":
return "CURRENT_TIME()"
_REL_MAP = {
"前天": "date_add(CURRENT_DATE(), -2)",
"昨天": "date_add(CURRENT_DATE(), -1)",
"今天": "CURRENT_DATE()",
"明天": "date_add(CURRENT_DATE(), 1)",
"后天": "date_add(CURRENT_DATE(), 2)",
}
if s in _REL_MAP:
return _REL_MAP[s]
if var_type == "Date":
m_date = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", s)
if m_date:
y, m, d = int(m_date.group(1)), int(m_date.group(2)), int(m_date.group(3))
return f"date({y}, {m}, {d})"
if var_type == "Time":
m_time = re.match(r"^(\d{1,2}):(\d{2})$", s)
if m_time:
h, m = int(m_time.group(1)), int(m_time.group(2))
return f"time({h}, {m})"
if re.match(r"^[+-]?\d+$", s):
return s
if re.match(r"^[A-Za-z_]\w*$", s):
return 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)
_RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$')
_RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$')
def isArithExpr(
expr: str
) -> bool:
"""
Return True if expr looks like a two-operand arithmetic expression (A ± B).
"""
s = expr.strip()
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
+464
View File
@@ -0,0 +1,464 @@
# -*- 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.
"""
"""
Widget components for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QStackedWidget
)
from gui.ALAutoScriptOrchDialog._helpers import (
ACTION_TYPES,
ARITH_TYPES,
COMPARE_OPERATORS,
LOGIC_OPERATORS,
PRESET_VARIABLES,
VAR_TYPE_ORDER,
encodeValueStr,
getValueFromWidget,
makeComboWidget,
makeLabel,
makeOffsetWidget,
makeValueWidget,
makeVarRefCombo,
)
class ConditionRowFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
isFirst: bool = False,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._isFirst = isFirst
self._isBoolMode = False
self._rawRhsExpr = ""
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(32)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
if self._isFirst:
self.logicCombo = None
else:
self.logicCombo = makeComboWidget(LOGIC_OPERATORS, min_width=110, parent=self)
layout.addWidget(self.logicCombo)
self.leftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25)
self.leftVarCombo.setMinimumWidth(120)
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPERATORS, min_width=80, parent=self)
layout.addWidget(self.opCombo)
self._compTypeCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._compTypeCombo)
self.rhsStack = QStackedWidget(self)
self.rhsStack.setFixedHeight(25)
self.initLiteralStack()
self.rhsVarCombo = makeVarRefCombo(self)
self.rhsStack.addWidget(self.rhsVarCombo)
self.rhsStack.setCurrentIndex(0)
layout.addWidget(self.rhsStack)
if not self._isFirst:
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
else:
self.deleteBtn = None
layout.addStretch()
self.setUpdatesEnabled(True)
def populateLeftVarCombo(
self
):
wasBool = self._isBoolMode
boolName = None
if wasBool:
data = self.leftVarCombo.currentData()
if data:
boolName = data[0]
self._varMgr.populateCombo(self.leftVarCombo)
# Append boolean literal sentinels at the end
self.leftVarCombo.insertSeparator(self.leftVarCombo.count())
self.leftVarCombo.addItem("true", ("true", "Boolean"))
self.leftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName:
for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci)
if d and d[0] == boolName:
self.leftVarCombo.setCurrentIndex(ci)
break
def populateRHSVarCombo(
self
):
self._varMgr.populateCombo(self.rhsVarCombo)
def initLiteralStack(
self
):
self.literalStack = QStackedWidget(self)
self.literalStack.setFixedHeight(25)
self._literalWidgets = {}
for vt in VAR_TYPE_ORDER:
w = makeValueWidget(vt, self.literalStack)
self._literalWidgets[vt] = w
self.literalStack.addWidget(w)
self.literalStack.setCurrentWidget(self._literalWidgets.get("String"))
self.rhsStack.addWidget(self.literalStack)
def connectSignals(
self
):
self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
def getLogic(
self
) -> str:
return self.logicCombo.currentData() if self.logicCombo else ""
def updateRHSLiteralWidget(
self,
vartype: str
):
if vartype not in self._literalWidgets:
vartype = "String"
self.literalStack.setCurrentWidget(self._literalWidgets[vartype])
def toScript(
self
) -> str:
data = self.leftVarCombo.currentData()
if self._isBoolMode and data:
return data[0]
if not data:
return ""
name, vartype = data
# CURRENT_DATE / CURRENT_TIME are Lua functions — call them, not reference them
if name in ("CURRENT_DATE", "CURRENT_TIME"):
name = f"{name}()"
opSym = self.opCombo.currentData()
if self._rawRhsExpr:
return f"{name} {opSym} {self._rawRhsExpr}"
isVarRef = (self._compTypeCombo.currentData() == "variable")
if isVarRef:
rd = self.rhsVarCombo.currentData()
if rd:
rhsName = rd[0]
if rhsName in ("CURRENT_DATE", "CURRENT_TIME"):
rhsName = f"{rhsName}()"
return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip()
if rhsText:
return f"{name} {opSym} {rhsText}"
return ""
w = self._literalWidgets.get(vartype)
if w:
rawVal = getValueFromWidget(w)
encoded = encodeValueStr(rawVal, vartype)
return f"{name} {opSym} {encoded}"
return ""
def refreshVarCombos(
self
):
self.populateLeftVarCombo()
self.populateRHSVarCombo()
@Slot(int)
def onLeftVarChanged(
self,
idx
):
self._rawRhsExpr = ""
if idx < 0:
return
data = self.leftVarCombo.itemData(idx)
if not data:
return
name, vartype = data
isBool = name in ("true", "false")
self._isBoolMode = isBool
self.opCombo.setVisible(not isBool)
self._compTypeCombo.setVisible(not isBool)
self.rhsStack.setVisible(not isBool)
if not isBool:
self.updateRHSLiteralWidget(vartype)
@Slot(int)
def onCompTypeChanged(
self,
idx
):
self._rawRhsExpr = ""
isVar = (self._compTypeCombo.currentData() == "variable")
self.rhsStack.setCurrentIndex(1 if isVar else 0)
if isVar:
self.populateRHSVarCombo()
class ActionStepFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._currentTargetType = "String"
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_TYPES, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo)
layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self)
self.targetCombo.setFixedHeight(25)
self.targetCombo.setMinimumWidth(120)
self.populateTargetCombo()
layout.addWidget(self.targetCombo)
layout.addWidget(makeLabel("", self))
self.valueSrcCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self.valueSrcCombo)
self.valueStack = QStackedWidget(self)
self.valueStack.setFixedHeight(25)
self.initValueStacks()
layout.addWidget(self.valueStack)
self.existingVarCombo = makeVarRefCombo(self)
self.existingVarCombo.setVisible(False)
layout.addWidget(self.existingVarCombo)
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
self.setUpdatesEnabled(True)
def populateTargetCombo(
self
):
self.targetCombo.blockSignals(True)
self.targetCombo.clear()
for p in PRESET_VARIABLES:
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue
info = self._varMgr.getInfoByName(p["name"])
if info:
self.targetCombo.addItem(
info["display"],
(info["name"], info["type"])
)
self.targetCombo.blockSignals(False)
def initValueStacks(
self
):
self._literalWidgets = {}
self._offsetWidgets = {}
for vt in VAR_TYPE_ORDER:
self._literalWidgets[vt] = makeValueWidget(vt, self.valueStack)
self.valueStack.addWidget(self._literalWidgets[vt])
if vt in ARITH_TYPES:
self._offsetWidgets[vt] = makeOffsetWidget(vt, self.valueStack)
self.valueStack.addWidget(self._offsetWidgets[vt])
else:
lbl = QLabel("(不支持该操作)", self.valueStack)
lbl.setFixedHeight(25)
self._offsetWidgets[vt] = lbl
self.valueStack.addWidget(lbl)
def connectSignals(
self
):
self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
self.valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
def getTargetName(
self
) -> str:
data = self.targetCombo.currentData()
return data[0] if data else ""
def updateValueWidget(
self
):
op = self.opTypeCombo.currentData()
isArith = (op in ("add", "sub"))
actualType = self._currentTargetType
if isArith and actualType in self._offsetWidgets:
self.valueStack.setCurrentWidget(self._offsetWidgets[actualType])
elif actualType in self._literalWidgets:
self.valueStack.setCurrentWidget(self._literalWidgets[actualType])
else:
self.valueStack.setCurrentWidget(self._literalWidgets.get("String"))
def toScript(
self
) -> str:
"""
Generate a single line of Lua script from the current widget state.
"""
target = self.getTargetName()
op = self.opTypeCombo.currentData()
if op == "pass":
return " -- pass"
if not target:
return ""
rawVal = self.getValueRaw()
vartype = self._currentTargetType
if op == "set":
encoded = encodeValueStr(rawVal, vartype)
return f" {target} = {encoded}"
elif op == "add":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays()
return f" {target} = date_add({target}, {days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours()
return f" {target} = time_add({target}, {hours})"
return f" {target} = {target} + {rawVal}"
elif op == "sub":
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays()
return f" {target} = date_add({target}, -{days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours()
return f" {target} = time_add({target}, -{hours})"
return f" {target} = {target} - {rawVal}"
return ""
def getValueRaw(
self
) -> str:
if self.valueSrcCombo.currentData() == "variable":
data = self.existingVarCombo.currentData()
return data[0] if data else ""
w = self.valueStack.currentWidget()
if w:
return getValueFromWidget(w)
return ""
def refreshVarCombos(
self
):
currentData = self.targetCombo.currentData()
self.populateTargetCombo()
if currentData:
for i in range(self.targetCombo.count()):
d = self.targetCombo.itemData(i)
if d and d[0] == currentData[0]:
self.targetCombo.setCurrentIndex(i)
break
self._varMgr.populateCombo(self.existingVarCombo)
@Slot(int)
def onTargetChanged(
self,
idx
):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
if not data:
return
_, vartype = data
self._currentTargetType = vartype
self.updateValueWidget()
self.onValueSrcChanged(self.valueSrcCombo.currentIndex())
@Slot(int)
def onOpTypeChanged(
self,
idx
):
self.updateValueWidget()
@Slot(int)
def onValueSrcChanged(
self,
idx
):
isVar = (self.valueSrcCombo.currentData() == "variable")
self.valueStack.setVisible(not isVar)
self.existingVarCombo.setVisible(isVar)
if isVar:
self._varMgr.populateCombo(self.existingVarCombo)
else:
self.updateValueWidget()
-226
View File
@@ -1,226 +0,0 @@
# -*- 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 PySide6.QtCore import Slot
from PySide6.QtGui import (
QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QIcon
)
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
QDialogButtonBox, QPushButton, QLabel, QApplication, QStyle
)
class ALScriptHighlighter(QSyntaxHighlighter):
def __init__(
self,
parent=None
):
super().__init__(parent)
self._rules = []
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#316BFF"))
keywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
"SET", "PASS", "THEN"]:
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
self._rules.append((pattern, keywordFmt))
literalFmt = QTextCharFormat()
literalFmt.setForeground(QColor("#C2185B"))
literalFmt.setFontWeight(QFont.Weight.Bold)
for lit in [".TRUE.", ".FALSE."]:
self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt))
opFmt = QTextCharFormat()
opFmt.setForeground(QColor("#9C27B0"))
for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]:
self._rules.append((op, opFmt))
varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#E65100"))
for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME",
"RESERVE_DATE", "USERNAME", "USER_ENABLE",
"PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]:
self._rules.append((r"\b" + var + r"\b", varFmt))
funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#2E7D32"))
self._rules.append((r"\bTIME\([^)]+\)", funcFmt))
self._rules.append((r"\bDATE\([^)]+\)", funcFmt))
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#388E3C"))
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#D32F2F"))
self._rules.append((r"\b\d+\b", numFmt))
commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#999999"))
commentFmt.setFontItalic(True)
self._rules.append((r"//[^\n]*", commentFmt))
def highlightBlock(
self,
text
):
import re
for pattern, fmt in self._rules:
for match in re.finditer(pattern, text, re.IGNORECASE):
start = match.start()
length = match.end() - match.start()
self.setFormat(start, length, fmt)
class ALAutoScriptPreviewDialog(QDialog):
def __init__(
self,
parent=None,
script: str = ""
):
super().__init__(parent)
self.__fontSize = 13
self.modifyUi()
self.connectSignals()
self._textEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter(
self._textEdit.document()
)
def modifyUi(
self
):
self.setWindowTitle("AutoScript 预览 - AutoLibrary")
self.setMinimumSize(520, 360)
layout = QVBoxLayout(self)
toolbarLayout = QHBoxLayout()
self._zoomInBtn = QPushButton("")
self._zoomInBtn.setFixedSize(30, 25)
self._zoomOutBtn = QPushButton("")
self._zoomOutBtn.setFixedSize(30, 25)
self._zoomResetBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_BrowserReload
), ""
)
self._zoomResetBtn.setFixedSize(30, 25)
self._zoomResetBtn.setToolTip("重置缩放")
self._zoomLabel = QLabel(f"{self.__fontSize}px")
self._zoomLabel.setFixedHeight(25)
toolbarLayout.addWidget(self._zoomInBtn)
toolbarLayout.addWidget(self._zoomOutBtn)
toolbarLayout.addWidget(self._zoomResetBtn)
toolbarLayout.addWidget(self._zoomLabel)
toolbarLayout.addStretch()
self._copyBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_FileDialogDetailedView
), ""
)
self._copyBtn.setFixedSize(30, 25)
self._copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self._copyBtn)
layout.addLayout(toolbarLayout)
self._textEdit = QPlainTextEdit(self)
self._textEdit.setReadOnly(True)
self._textEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap
)
self._textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
" font-size: 13px;"
"}"
)
layout.addWidget(self._textEdit)
self._btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Close
)
self._btnBox.button(
QDialogButtonBox.StandardButton.Close
).setText("关闭")
layout.addWidget(self._btnBox)
def connectSignals(
self
):
self._btnBox.rejected.connect(self.reject)
self._zoomInBtn.clicked.connect(self.onZoomIn)
self._zoomOutBtn.clicked.connect(self.onZoomOut)
self._zoomResetBtn.clicked.connect(self.onZoomReset)
self._copyBtn.clicked.connect(self.onCopy)
def updateFontSize(
self
):
font = self._textEdit.font()
font.setPointSize(self.__fontSize)
self._textEdit.setFont(font)
self._textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self.__fontSize}px;"
"}"
)
self._zoomLabel.setText(f"{self.__fontSize}px")
@Slot()
def onZoomIn(
self
):
self.__fontSize = min(self.__fontSize + 2, 40)
self.updateFontSize()
@Slot()
def onZoomOut(
self
):
self.__fontSize = max(self.__fontSize - 2, 8)
self.updateFontSize()
@Slot()
def onZoomReset(
self
):
self.__fontSize = 13
self.updateFontSize()
@Slot()
def onCopy(
self
):
clipboard = QApplication.clipboard()
clipboard.setText(self._textEdit.toPlainText())
original = self._copyBtn.text()
self._copyBtn.setText("已复制")
self._copyBtn.setEnabled(False)
from PySide6.QtCore import QTimer
QTimer.singleShot(2000, lambda: (
self._copyBtn.setText(original),
self._copyBtn.setEnabled(True)
))
+36 -50
View File
@@ -10,27 +10,46 @@ See the LICENSE file for details.
import os import os
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo QDate,
) QDir,
from PySide6.QtWidgets import ( QFileInfo,
QDialog, QWidget, QLineEdit, QMessageBox, QFileDialog, Qt,
QTreeWidgetItem, QMenu, QInputDialog QTime,
Signal,
Slot
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent, QAction QAction,
QCloseEvent
)
from PySide6.QtWidgets import (
QDialog,
QFileDialog,
QInputDialog,
QLineEdit,
QMenu,
QMessageBox,
QTreeWidgetItem,
QWidget
) )
import managers.config.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
from utils.ConfigUtils import ConfigUtils
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
from gui.ALSeatMapTable import ALSeatMapTable from gui.ALSeatMapTable import ALSeatMapTable
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType from gui.ALUserTreeWidget import (
ALUserTreeItemType,
ALUserTreeWidget
)
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from interfaces.ConfigProvider import (
CfgKey,
ConfigProvider
)
from managers.config.ConfigUtils import ConfigUtils
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
class ALConfigWidget(QWidget, Ui_ALConfigWidget): class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -43,7 +62,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
): ):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr = ConfigManager.instance() self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
self.__config_paths = ConfigUtils.getAutomationConfigPaths() self.__config_paths = ConfigUtils.getAutomationConfigPaths()
self.__config_data = {"run": {}, "user": {}} self.__config_data = {"run": {}, "user": {}}
@@ -53,7 +72,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if not self.initializeConfigs(): if not self.initializeConfigs():
self.close() self.close()
def modifyUi( def modifyUi(
self self
): ):
@@ -69,7 +87,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.initializeFloorRoomMap() self.initializeFloorRoomMap()
self.initializeUserInfoWidget() self.initializeUserInfoWidget()
def connectSignals( def connectSignals(
self self
): ):
@@ -93,7 +110,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent( def showEvent(
self, self,
event event
@@ -117,7 +133,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return result return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
@@ -126,7 +141,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.configWidgetIsClosed.emit() self.configWidgetIsClosed.emit()
super().closeEvent(event) super().closeEvent(event)
def initializeFloorRoomMap( def initializeFloorRoomMap(
self self
): ):
@@ -160,7 +174,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"五层": ["五层考研"] "五层": ["五层考研"]
} }
def initializeConfigToWidget( def initializeConfigToWidget(
self, self,
which: str, which: str,
@@ -175,7 +188,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setUsersToTreeWidget(config_data) self.setUsersToTreeWidget(config_data)
self.CurrentUserConfigEdit.setText(self.__config_paths["user"]) self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
def initializeConfig( def initializeConfig(
self, self,
which: str which: str
@@ -209,7 +221,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
is_success = False is_success = False
return is_success return is_success
def initializeConfigs( def initializeConfigs(
self self
) -> bool: ) -> bool:
@@ -222,7 +233,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.initializeConfigToWidget(which, self.__config_data[which]) self.initializeConfigToWidget(which, self.__config_data[which])
return is_success return is_success
def defaultRunConfig( def defaultRunConfig(
self self
) -> dict: ) -> dict:
@@ -246,7 +256,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
} }
} }
def defaultUserConfig( def defaultUserConfig(
self self
) -> dict: ) -> dict:
@@ -256,7 +265,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
] ]
} }
def collectRunConfigFromWidget( def collectRunConfigFromWidget(
self self
) -> dict: ) -> dict:
@@ -278,7 +286,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
run_config["mode"]["run_mode"] = run_mode run_config["mode"]["run_mode"] = run_mode
return run_config return run_config
def setRunConfigToWidget( def setRunConfigToWidget(
self, self,
run_config: dict run_config: dict
@@ -317,7 +324,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"文件可能被意外修改或已经损坏\n" "文件可能被意外修改或已经损坏\n"
) )
def initializeUserInfoWidget( def initializeUserInfoWidget(
self self
): ):
@@ -342,7 +348,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.MaxRenewTimeDiffSpinBox.setValue(30) self.MaxRenewTimeDiffSpinBox.setValue(30)
self.PreferLateRenewTimeCheckBox.setChecked(False) self.PreferLateRenewTimeCheckBox.setChecked(False)
def collectUserFromWidget( def collectUserFromWidget(
self self
) -> dict: ) -> dict:
@@ -375,7 +380,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked() user["reserve_info"]["renew_time"]["prefer_early"] = not self.PreferLateRenewTimeCheckBox.isChecked()
return user return user
def collectUsersFromTreeWidget( def collectUsersFromTreeWidget(
self self
) -> dict: ) -> dict:
@@ -398,7 +402,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config["groups"].append(group_config) user_config["groups"].append(group_config)
return user_config return user_config
def setUserToWidget( def setUserToWidget(
self, self,
user: dict user: dict
@@ -440,7 +443,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"文件可能被意外修改或已经损坏\n" "文件可能被意外修改或已经损坏\n"
) )
def setUsersToTreeWidget( def setUsersToTreeWidget(
self, self,
users: dict users: dict
@@ -482,7 +484,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
finally: finally:
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
def loadRunConfig( def loadRunConfig(
self, self,
run_config_path: str run_config_path: str
@@ -506,7 +507,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) )
return None return None
def saveRunConfig( def saveRunConfig(
self, self,
run_config_path: str, run_config_path: str,
@@ -528,7 +528,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) )
return False return False
def loadUserConfig( def loadUserConfig(
self, self,
user_config_path: str user_config_path: str
@@ -562,7 +561,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) )
return None return None
def saveUserConfig( def saveUserConfig(
self, self,
user_config_path: str, user_config_path: str,
@@ -584,7 +582,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) )
return False return False
def saveConfigs( def saveConfigs(
self, self,
run_config_path: str, run_config_path: str,
@@ -607,7 +604,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return False return False
return True return True
def loadConfig( def loadConfig(
self, self,
config_path: str config_path: str
@@ -636,7 +632,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
except: except:
return False return False
def addGroup( def addGroup(
self, self,
group_name: str = "" group_name: str = ""
@@ -653,7 +648,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return group_item return group_item
def delGroup( def delGroup(
self, self,
group_item: QTreeWidgetItem = None group_item: QTreeWidgetItem = None
@@ -666,7 +660,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
index = self.UserTreeWidget.indexOfTopLevelItem(group_item) index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index) self.UserTreeWidget.takeTopLevelItem(index)
def addUser( def addUser(
self, self,
group_item: QTreeWidgetItem = None group_item: QTreeWidgetItem = None
@@ -721,7 +714,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
return user_item return user_item
def delUser( def delUser(
self, self,
user_item: QTreeWidgetItem = None user_item: QTreeWidgetItem = None
@@ -737,7 +729,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if parent_item.childCount() == 0: if parent_item.childCount() == 0:
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
def renameItem( def renameItem(
self, self,
item: QTreeWidgetItem, item: QTreeWidgetItem,
@@ -861,7 +852,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
is_checked = item.checkState(1) == Qt.CheckState.Checked is_checked = item.checkState(1) == Qt.CheckState.Checked
item.setText(1, "" if is_checked else "跳过") item.setText(1, "" if is_checked else "跳过")
def showTreeMenu( def showTreeMenu(
self, self,
menu: QMenu menu: QMenu
@@ -871,7 +861,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
add_group_action.triggered.connect(self.addGroup) add_group_action.triggered.connect(self.addGroup)
menu.addAction(add_group_action) menu.addAction(add_group_action)
def showGroupMenu( def showGroupMenu(
self, self,
menu: QMenu, menu: QMenu,
@@ -891,7 +880,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if group_item.checkState(1) == Qt.CheckState.Unchecked: if group_item.checkState(1) == Qt.CheckState.Unchecked:
add_user_action.setEnabled(False) add_user_action.setEnabled(False)
def showUserMenu( def showUserMenu(
self, self,
menu: QMenu, menu: QMenu,
@@ -951,7 +939,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if browser_driver_path: if browser_driver_path:
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path)) self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
@Slot() @Slot()
def onAutoDownloadWebDriverButtonClicked( def onAutoDownloadWebDriverButtonClicked(
self self
@@ -965,7 +952,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value) self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path))) self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
@Slot() @Slot()
def onBrowseCurrentRunConfigButtonClicked( def onBrowseCurrentRunConfigButtonClicked(
self self
@@ -985,13 +971,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setRunConfigToWidget(data) self.setRunConfigToWidget(data)
self.__config_paths["run"] = run_config_path self.__config_paths["run"] = run_config_path
self.CurrentRunConfigEdit.setText(run_config_path) self.CurrentRunConfigEdit.setText(run_config_path)
paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.run_path.paths", []) paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, [])
if run_config_path not in paths: if run_config_path not in paths:
paths.append(run_config_path) paths.append(run_config_path)
index = len(paths) - 1 index = len(paths) - 1
else: else:
index = paths.index(run_config_path) index = paths.index(run_config_path)
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths}) self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.ROOT, {"current": index, "paths": paths})
else: else:
QMessageBox.warning( QMessageBox.warning(
self, self,
@@ -1020,13 +1006,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setUsersToTreeWidget(data) self.setUsersToTreeWidget(data)
self.__config_paths["user"] = user_config_path self.__config_paths["user"] = user_config_path
self.CurrentUserConfigEdit.setText(user_config_path) self.CurrentUserConfigEdit.setText(user_config_path)
paths = self.__cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation.user_path.paths", []) paths = self.__cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.USER_PATH.PATHS, [])
if user_config_path not in paths: if user_config_path not in paths:
paths.append(user_config_path) paths.append(user_config_path)
index = len(paths) - 1 index = len(paths) - 1
else: else:
index = paths.index(user_config_path) index = paths.index(user_config_path)
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths}) self.__cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.USER_PATH.ROOT, {"current": index, "paths": paths})
else: else:
QMessageBox.warning( QMessageBox.warning(
self, self,
+26 -26
View File
@@ -10,24 +10,37 @@ See the LICENSE file for details.
import queue import queue
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QTimer, QUrl, QTimer,
) QUrl,
from PySide6.QtWidgets import ( Qt,
QMainWindow, QMenu, QSystemTrayIcon, QMessageBox Signal,
Slot
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices QCloseEvent,
QDesktopServices,
QFont,
QIcon,
QTextCursor
)
from PySide6.QtWidgets import (
QMainWindow,
QMenu,
QMessageBox,
QSystemTrayIcon
) )
from base.MsgBase import MsgBase from base.MsgBase import MsgBase
from utils.ConfigUtils import ConfigUtils
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.resources import ALResource
from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.ALAboutDialog import ALAboutDialog from gui.ALAboutDialog import ALAboutDialog
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker from gui.ALConfigWidget import ALConfigWidget
from gui.ALMainWorkers import (
AutoLibWorker,
TimerTaskWorker
)
from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.resources import ALResource
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from managers.config.ConfigUtils import ConfigUtils
class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow): class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
@@ -59,12 +72,11 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.startTimerTaskPolling() self.startTimerTaskPolling()
self._showLog("主窗口初始化完成") self._showLog("主窗口初始化完成")
def modifyUi( def modifyUi(
self self
): ):
self.icon = QIcon(":/res/icon/icons/AutoLibrary_32x32.ico") self.icon = QIcon(":/res/icons/AutoLibrary_Logo_64.svg")
self.setWindowIcon(self.icon) self.setWindowIcon(self.icon)
self.MessageIOTextEdit.setFont(QFont("Courier New", 10)) self.MessageIOTextEdit.setFont(QFont("Courier New", 10))
self.ManualAction.triggered.connect(self.onManualActionTriggered) self.ManualAction.triggered.connect(self.onManualActionTriggered)
@@ -90,7 +102,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed) self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint) self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered( def onAboutActionTriggered(
self self
): ):
@@ -98,7 +109,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
about_dialog = ALAboutDialog(self) about_dialog = ALAboutDialog(self)
about_dialog.exec() about_dialog.exec()
def onManualActionTriggered( def onManualActionTriggered(
self self
): ):
@@ -106,7 +116,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
url = QUrl("https://www.autolibrary.kenanzhu.com/manuals") url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def setupTray( def setupTray(
self self
): ):
@@ -128,7 +137,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.TrayIcon.activated.connect(self.onTrayIconActivated) self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show() self.TrayIcon.show()
def hideToTray( def hideToTray(
self self
): ):
@@ -141,7 +149,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
2000 2000
) )
def onTrayIconActivated( def onTrayIconActivated(
self, self,
reason: QSystemTrayIcon.ActivationReason reason: QSystemTrayIcon.ActivationReason
@@ -150,7 +157,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
if reason == QSystemTrayIcon.DoubleClick: if reason == QSystemTrayIcon.DoubleClick:
self.showNormal() self.showNormal()
def connectSignals( def connectSignals(
self self
): ):
@@ -162,7 +168,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.SendButton.clicked.connect(self.onSendButtonClicked) self.SendButton.clicked.connect(self.onSendButtonClicked)
self.MessageEdit.returnPressed.connect(self.onSendButtonClicked) self.MessageEdit.returnPressed.connect(self.onSendButtonClicked)
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
@@ -188,7 +193,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self._showLog("主窗口关闭") self._showLog("主窗口关闭")
QMainWindow.closeEvent(self, event) QMainWindow.closeEvent(self, event)
def appendToTextEdit( def appendToTextEdit(
self, self,
text: str text: str
@@ -202,7 +206,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
scrollbar = self.MessageIOTextEdit.verticalScrollBar() scrollbar = self.MessageIOTextEdit.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum()) scrollbar.setValue(scrollbar.maximum())
def startMsgPolling( def startMsgPolling(
self self
): ):
@@ -211,7 +214,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__msg_queue_timer.timeout.connect(self.pollMsgQueue) self.__msg_queue_timer.timeout.connect(self.pollMsgQueue)
self.__msg_queue_timer.start(100) self.__msg_queue_timer.start(100)
def startTimerTaskPolling( def startTimerTaskPolling(
self self
): ):
@@ -220,7 +222,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue) self.__timer_task_timer.timeout.connect(self.pollTimerTaskQueue)
self.__timer_task_timer.start(500) self.__timer_task_timer.start(500)
def pollTimerTaskQueue( def pollTimerTaskQueue(
self self
): ):
@@ -254,7 +255,6 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__is_running_timer_task = False self.__is_running_timer_task = False
pass pass
def setControlButtons( def setControlButtons(
self, self,
config_button_enabled: bool, config_button_enabled: bool,
+3 -8
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 utils.AutoScriptEngine import AutoScriptEngine from autoscript import execute, registerDefaultTargetVars
class AutoLibWorker(MsgBase, QThread): class AutoLibWorker(MsgBase, QThread):
@@ -37,7 +37,6 @@ class AutoLibWorker(MsgBase, QThread):
QThread.__init__(self) QThread.__init__(self)
self.__config_paths = config_paths self.__config_paths = config_paths
def checkTimeAvailable( def checkTimeAvailable(
self, self,
) -> bool: ) -> bool:
@@ -52,7 +51,6 @@ class AutoLibWorker(MsgBase, QThread):
self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO) self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO)
return True return True
def checkConfigPaths( def checkConfigPaths(
self, self,
) -> bool: ) -> bool:
@@ -68,7 +66,6 @@ class AutoLibWorker(MsgBase, QThread):
self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO) self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO)
return True return True
def loadConfigs( def loadConfigs(
self self
) -> bool: ) -> bool:
@@ -101,7 +98,6 @@ class AutoLibWorker(MsgBase, QThread):
) )
return True return True
def run( def run(
self self
): ):
@@ -161,7 +157,6 @@ class TimerTaskWorker(AutoLibWorker):
self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished) self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError) self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
def run( def run(
self self
): ):
@@ -206,7 +201,6 @@ class TimerTaskWorker(AutoLibWorker):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束") self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task) self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
def applyRepeatAutoScript( def applyRepeatAutoScript(
self self
): ):
@@ -225,7 +219,8 @@ class TimerTaskWorker(AutoLibWorker):
continue continue
for user in group.get("users", []): for user in group.get("users", []):
try: try:
AutoScriptEngine.execute(auto_script, user) registerDefaultTargetVars()
execute(auto_script, user)
affected_count += 1 affected_count += 1
except ValueError as e: except ValueError as e:
self._showTrace( self._showTrace(
-2
View File
@@ -63,14 +63,12 @@ class ALSeatFrame(QFrame):
self.toggleSelection() self.toggleSelection()
self.clicked.emit(self.__seat_number) self.clicked.emit(self.__seat_number)
def isSelected( def isSelected(
self self
): ):
return self.__is_selected return self.__is_selected
def toggleSelection(self): def toggleSelection(self):
self.__is_selected = not self.__is_selected self.__is_selected = not self.__is_selected
+10 -13
View File
@@ -8,15 +8,20 @@ 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 PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Slot, Signal Qt,
) Signal,
from PySide6.QtWidgets import ( Slot
QDialog, QLabel, QHBoxLayout, QVBoxLayout,
QPushButton,
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent QCloseEvent
) )
from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout
)
from gui.ALSeatMapView import ALSeatMapView from gui.ALSeatMapView import ALSeatMapView
@@ -42,7 +47,6 @@ class ALSeatMapSelectDialog(QDialog):
self.setupUi() self.setupUi()
self.connectSignals() self.connectSignals()
def setupUi( def setupUi(
self self
): ):
@@ -85,7 +89,6 @@ class ALSeatMapSelectDialog(QDialog):
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton) self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout) self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals( def connectSignals(
self self
): ):
@@ -93,7 +96,6 @@ class ALSeatMapSelectDialog(QDialog):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked) self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent( def showEvent(
self, self,
event event
@@ -117,7 +119,6 @@ class ALSeatMapSelectDialog(QDialog):
return result return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
@@ -131,7 +132,6 @@ class ALSeatMapSelectDialog(QDialog):
self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats()) self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats())
super().closeEvent(event) super().closeEvent(event)
def selectSeat( def selectSeat(
self, self,
seat_number: str seat_number: str
@@ -139,7 +139,6 @@ class ALSeatMapSelectDialog(QDialog):
self.SeatMapGraphicsView.selectSeat(seat_number) self.SeatMapGraphicsView.selectSeat(seat_number)
def selectSeats( def selectSeats(
self, self,
seat_numbers: list[str] seat_numbers: list[str]
@@ -147,14 +146,12 @@ class ALSeatMapSelectDialog(QDialog):
return self.SeatMapGraphicsView.selectSeats(seat_numbers) return self.SeatMapGraphicsView.selectSeats(seat_numbers)
def getSelectedSeats( def getSelectedSeats(
self self
) -> list[str]: ) -> list[str]:
return self.SeatMapGraphicsView.getSelectedSeats() return self.SeatMapGraphicsView.getSelectedSeats()
def clearSelections( def clearSelections(
self self
): ):
+19 -22
View File
@@ -11,11 +11,16 @@ from PySide6.QtCore import (
Qt, Slot, QEvent Qt, Slot, QEvent
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFrame, QWidget, QFrame,
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem QWidget,
QGridLayout,
QGraphicsView,
QGraphicsScene,
QGraphicsItem
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QPainter, QWheelEvent QPainter,
QWheelEvent
) )
from gui.ALSeatFrame import ALSeatFrame from gui.ALSeatFrame import ALSeatFrame
@@ -35,18 +40,6 @@ class ALSeatMapView(QGraphicsView):
self.setupUi() self.setupUi()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
def eventFilter( def eventFilter(
self, self,
watched, watched,
@@ -61,7 +54,6 @@ class ALSeatMapView(QGraphicsView):
return True return True
return super().eventFilter(watched, event) return super().eventFilter(watched, event)
def zoomGraphicsView( def zoomGraphicsView(
self, self,
event: QWheelEvent event: QWheelEvent
@@ -80,7 +72,6 @@ class ALSeatMapView(QGraphicsView):
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.scale(zoom_factor, zoom_factor) self.scale(zoom_factor, zoom_factor)
def setupUi( def setupUi(
self self
): ):
@@ -100,7 +91,6 @@ class ALSeatMapView(QGraphicsView):
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget) self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False) self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
def setupSeatMap( def setupSeatMap(
self self
): ):
@@ -125,7 +115,6 @@ class ALSeatMapView(QGraphicsView):
self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20) self.SeatsContainerLayout.setContentsMargins(20, 20, 20, 20)
self.SeatsContainerWidget.adjustSize() self.SeatsContainerWidget.adjustSize()
def selectSeat( def selectSeat(
self, self,
seat_number: str seat_number: str
@@ -142,7 +131,6 @@ class ALSeatMapView(QGraphicsView):
widget.toggleSelection() widget.toggleSelection()
self.__selected_seats.append(seat_number) self.__selected_seats.append(seat_number)
def selectSeats( def selectSeats(
self, self,
selected_seats: list selected_seats: list
@@ -152,14 +140,12 @@ class ALSeatMapView(QGraphicsView):
for seat_number in selected_seats: for seat_number in selected_seats:
self.selectSeat(seat_number) self.selectSeat(seat_number)
def getSelectedSeats( def getSelectedSeats(
self self
) -> list[str]: ) -> list[str]:
return self.__selected_seats return self.__selected_seats
def clearSelections( def clearSelections(
self self
): ):
@@ -186,3 +172,14 @@ class ALSeatMapView(QGraphicsView):
self.__selected_seats.append(seat_number) self.__selected_seats.append(seat_number)
else: else:
self.__seat_frames[seat_number].toggleSelection() self.__seat_frames[seat_number].toggleSelection()
@staticmethod
def formatSeatNumber(
seat_number: str
) -> str:
if seat_number and not seat_number[-1].isdigit():
digits = seat_number[:-1]
letter = seat_number[-1]
return digits.zfill(3) + letter
return seat_number.zfill(3)
+11 -9
View File
@@ -9,14 +9,20 @@ See the LICENSE file for details.
""" """
from enum import Enum from enum import Enum
from PySide6.QtWidgets import (
QLabel
)
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Property, QPropertyAnimation, QEasingCurve Property,
QEasingCurve,
QPropertyAnimation,
Qt
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QPainter, QColor, QConicalGradient, QPalette QColor,
QConicalGradient,
QPainter,
QPalette
)
from PySide6.QtWidgets import (
QLabel
) )
@@ -44,7 +50,6 @@ class ALStatusLabel(QLabel):
self.setupUi() self.setupUi()
def setupUi( def setupUi(
self self
): ):
@@ -59,14 +64,12 @@ class ALStatusLabel(QLabel):
self.RunningAnimation.setLoopCount(-1) self.RunningAnimation.setLoopCount(-1)
self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear) self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
def isDarkMode( def isDarkMode(
self self
) -> bool: ) -> bool:
return self.palette().color(QPalette.ColorRole.Window).value() < 128 return self.palette().color(QPalette.ColorRole.Window).value() < 128
def getMarkColor( def getMarkColor(
self self
) -> QColor: ) -> QColor:
@@ -111,7 +114,6 @@ class ALStatusLabel(QLabel):
self.__icon_angle = value self.__icon_angle = value
self.update() self.update()
def paintEvent( def paintEvent(
self, self,
event event
+19 -32
View File
@@ -17,7 +17,6 @@ from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
from gui.ALAutoScriptOrchDialog import ALAutoScriptOrchDialog
from utils.TimerUtils import TimerUtils from utils.TimerUtils import TimerUtils
@@ -50,7 +49,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
if self.__edit_timer_task: if self.__edit_timer_task:
self.loadTask(self.__edit_timer_task) self.loadTask(self.__edit_timer_task)
def modifyUi( def modifyUi(
self self
): ):
@@ -58,6 +56,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.TimerTypeComboBox.setCurrentIndex(0) self.TimerTypeComboBox.setCurrentIndex(0)
self.SpecificTimerWidget = QWidget() self.SpecificTimerWidget = QWidget()
self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget) self.SpecificTimerLayout = QHBoxLayout(self.SpecificTimerWidget)
self.SpecificTimerLayout.setContentsMargins(0, 0, 0, 0)
self.SpecificTimerLayout.setSpacing(5)
self.SpecificTimerLayout.addWidget(QLabel("定时时间:")) self.SpecificTimerLayout.addWidget(QLabel("定时时间:"))
self.SpecificDateTimeEdit = QDateTimeEdit() self.SpecificDateTimeEdit = QDateTimeEdit()
self.SpecificDateTimeEdit.setCalendarPopup(True) self.SpecificDateTimeEdit.setCalendarPopup(True)
@@ -69,6 +69,8 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.RelativeTimerWidget = QWidget() self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget) self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.setContentsMargins(0, 0, 0, 0)
self.RelativeTimerLayout.setSpacing(5)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:")) self.RelativeTimerLayout.addWidget(QLabel("相对时间:"))
self.RelativeDaySpinBox = QSpinBox() self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0) self.RelativeDaySpinBox.setMinimum(0)
@@ -98,15 +100,10 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3) self.AutoScriptLayout.setContentsMargins(3, 3, 3, 3)
self.AutoScriptLayout.setSpacing(3) self.AutoScriptLayout.setSpacing(3)
autoScriptBtnLayout = QHBoxLayout() autoScriptBtnLayout = QHBoxLayout()
self.AutoScriptSetButton = QPushButton("设置指令") self.AutoScriptEditButton = QPushButton("编辑")
self.AutoScriptSetButton.setMinimumHeight(25) self.AutoScriptEditButton.setMinimumHeight(25)
self.AutoScriptSetButton.setFixedWidth(130) self.AutoScriptEditButton.setFixedWidth(80)
autoScriptBtnLayout.addWidget(self.AutoScriptSetButton) autoScriptBtnLayout.addWidget(self.AutoScriptEditButton)
self.AutoScriptPreviewButton = QPushButton("预览")
self.AutoScriptPreviewButton.setMinimumHeight(25)
self.AutoScriptPreviewButton.setFixedWidth(60)
self.AutoScriptPreviewButton.setEnabled(False)
autoScriptBtnLayout.addWidget(self.AutoScriptPreviewButton)
autoScriptBtnLayout.addStretch() autoScriptBtnLayout.addStretch()
self.AutoScriptHelpButton = QPushButton("?") self.AutoScriptHelpButton = QPushButton("?")
self.AutoScriptHelpButton.setFixedSize(20, 20) self.AutoScriptHelpButton.setFixedSize(20, 20)
@@ -133,7 +130,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
) )
self.AutoScriptGroupBox.setVisible(False) self.AutoScriptGroupBox.setVisible(False)
self.__auto_script = "" self.__auto_script = ""
self.__mock_target_data = None
def loadTask( def loadTask(
self, self,
@@ -170,10 +167,11 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.__auto_script = auto_script self.__auto_script = auto_script
self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
self.AutoScriptPreviewButton.setEnabled(True) mock_data = task.get("mock_target_data")
if mock_data:
self.__mock_target_data = mock_data
self.ConfirmButton.setText("保存") self.ConfirmButton.setText("保存")
def connectSignals( def connectSignals(
self self
): ):
@@ -182,11 +180,9 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.ConfirmButton.clicked.connect(self.accept) self.ConfirmButton.clicked.connect(self.accept)
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged) self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled) self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
self.AutoScriptSetButton.clicked.connect(self.onSetAutoScript) self.AutoScriptEditButton.clicked.connect(self.onPreviewAutoScript)
self.AutoScriptPreviewButton.clicked.connect(self.onPreviewAutoScript)
self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp) self.AutoScriptHelpButton.clicked.connect(self.onAutoScriptHelp)
def getTimerTask( def getTimerTask(
self self
) -> dict: ) -> dict:
@@ -218,6 +214,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
task_data["status"] = ALTimerTaskStatus.PENDING task_data["status"] = ALTimerTaskStatus.PENDING
task_data["executed"] = False task_data["executed"] = False
task_data["repeat_auto_script"] = self.__auto_script task_data["repeat_auto_script"] = self.__auto_script
task_data["mock_target_data"] = self.__mock_target_data
else: else:
task_data = { task_data = {
"name": name, "name": name,
@@ -230,6 +227,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
"executed": False, "executed": False,
"repeat": self.RepeatCheckBox.isChecked(), "repeat": self.RepeatCheckBox.isChecked(),
"repeat_auto_script": self.__auto_script, "repeat_auto_script": self.__auto_script,
"mock_target_data": self.__mock_target_data,
} }
repeat = self.RepeatCheckBox.isChecked() repeat = self.RepeatCheckBox.isChecked()
@@ -291,28 +289,19 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.AutoScriptGroupBox.setVisible(checked) self.AutoScriptGroupBox.setVisible(checked)
@Slot() @Slot()
def onSetAutoScript(self): def onPreviewAutoScript(self):
dlg = ALAutoScriptOrchDialog(self, existingScript=self.__auto_script) from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog
dlg = ALAutoScriptEditDialog(self, self.__auto_script, self.__mock_target_data)
if dlg.exec() == QDialog.DialogCode.Accepted: if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript() script = dlg.getScript()
self.__auto_script = script self.__auto_script = script
self.__mock_target_data = dlg.getMockData()
if script: if script:
self.AutoScriptStatusLabel.setText("已设置") self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;") self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
self.AutoScriptPreviewButton.setEnabled(True)
else: else:
self.AutoScriptStatusLabel.setText("未设置") self.AutoScriptStatusLabel.setText("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;") self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
self.AutoScriptPreviewButton.setEnabled(False)
dlg.deleteLater()
@Slot()
def onPreviewAutoScript(self):
if not self.__auto_script:
return
from gui.ALAutoScriptPrevDialog import ALAutoScriptPreviewDialog
dlg = ALAutoScriptPreviewDialog(self, self.__auto_script)
dlg.exec()
dlg.deleteLater() dlg.deleteLater()
@Slot() @Slot()
@@ -323,5 +312,3 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
QDesktopServices.openUrl( QDesktopServices.openUrl(
QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript") QUrl("https://www.autolibrary.kenanzhu.com/manuals/autoscript")
) )
+7 -13
View File
@@ -28,15 +28,13 @@ class ALTimerTaskHistoryDialog(QDialog):
): ):
super().__init__(parent) super().__init__(parent)
self.__task_data = task_data self.__task_data = task_data
self.__history = task_data.get("repeat_history", []) self.__history = task_data.get("repeat_history", [])
self.modifyUi() self.setupUi()
self.connectSignals() self.connectSignals()
def setupUi(
def modifyUi(
self self
): ):
@@ -83,7 +81,6 @@ class ALTimerTaskHistoryDialog(QDialog):
ButtonLayout.addWidget(self.CloseButton) ButtonLayout.addWidget(self.CloseButton)
MainLayout.addLayout(ButtonLayout) MainLayout.addLayout(ButtonLayout)
def connectSignals( def connectSignals(
self self
): ):
@@ -91,7 +88,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.CloseButton.clicked.connect(self.accept) self.CloseButton.clicked.connect(self.accept)
self.ClearHistoryButton.clicked.connect(self.onClearHistoryButtonClicked) self.ClearHistoryButton.clicked.connect(self.onClearHistoryButtonClicked)
def loadHistory( def loadHistory(
self self
): ):
@@ -100,6 +96,11 @@ class ALTimerTaskHistoryDialog(QDialog):
for row, record in enumerate(self.__history): for row, record in enumerate(self.__history):
self.addHistoryRow(row, record) self.addHistoryRow(row, record)
def getHistory(
self
) -> list:
return self.__history
def addHistoryRow( def addHistoryRow(
self, self,
@@ -130,13 +131,6 @@ class ALTimerTaskHistoryDialog(QDialog):
self.HistoryTableWidget.setItem(row, 2, DurationItem) self.HistoryTableWidget.setItem(row, 2, DurationItem)
self.HistoryTableWidget.setRowHeight(row, 25) self.HistoryTableWidget.setRowHeight(row, 25)
def getHistory(
self
) -> list:
return self.__history
@Slot() @Slot()
def onClearHistoryButtonClicked( def onClearHistoryButtonClicked(
self self
+30 -30
View File
@@ -15,22 +15,40 @@ from enum import Enum
from datetime import datetime, timedelta from datetime import datetime, timedelta
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QTimer QTimer,
) Qt,
from PySide6.QtWidgets import ( Signal,
QDialog, QWidget, QListWidgetItem, QMessageBox, Slot
QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QMenu
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent, QAction QAction,
QCloseEvent
)
from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QListWidgetItem,
QMenu,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget
) )
import managers.config.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from utils.TimerUtils import TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget from gui.ALTimerTaskAddDialog import (
from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus ALTimerTaskAddDialog,
ALTimerTaskStatus
)
from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from interfaces.ConfigProvider import (
CfgKey,
ConfigProvider
)
from utils.TimerUtils import TimerUtils
class ALTimerTaskItemWidget(QWidget): class ALTimerTaskItemWidget(QWidget):
@@ -51,7 +69,6 @@ class ALTimerTaskItemWidget(QWidget):
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.showContextMenu) self.customContextMenuRequested.connect(self.showContextMenu)
def modifyUi( def modifyUi(
self self
): ):
@@ -190,7 +207,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
): ):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr = ConfigManager.instance() self.__cfg_mgr: ConfigProvider = ConfigManager.instance()
self.__timer_tasks = [] self.__timer_tasks = []
self.__check_timer = None self.__check_timer = None
self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
@@ -202,7 +219,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
if not self.initializeTimerTasks(): if not self.initializeTimerTasks():
raise Exception("定时任务配置文件初始化失败 !") raise Exception("定时任务配置文件初始化失败 !")
def connectSignals( def connectSignals(
self self
): ):
@@ -213,7 +229,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked) self.TimerTaskSortOrderToggleButton.clicked.connect(self.onSortOrderToggleButtonClicked)
self.timerTasksChanged.connect(self.onTimerTasksChanged) self.timerTasksChanged.connect(self.onTimerTasksChanged)
def setupTimer( def setupTimer(
self self
): ):
@@ -222,7 +237,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__check_timer.timeout.connect(self.checkTasks) self.__check_timer.timeout.connect(self.checkTasks)
self.__check_timer.start(500) self.__check_timer.start(500)
def initializeTimerTasks( def initializeTimerTasks(
self self
) -> bool: ) -> bool:
@@ -238,13 +252,12 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
return True return True
return False return False
def getTimerTasks( def getTimerTasks(
self self
) -> list: ) -> list:
try: try:
timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK) timer_tasks = self.__cfg_mgr.get(CfgKey.TIMERTASK.ROOT)
if timer_tasks and "timer_tasks" in timer_tasks: if timer_tasks and "timer_tasks" in timer_tasks:
for task in timer_tasks["timer_tasks"]: for task in timer_tasks["timer_tasks"]:
task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S") task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S")
@@ -263,7 +276,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
) )
return None return None
def setTimerTasks( def setTimerTasks(
self, self,
timer_tasks: list timer_tasks: list
@@ -277,7 +289,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
if "repeat_history" in task: if "repeat_history" in task:
for item in task["repeat_history"]: for item in task["repeat_history"]:
item["result"] = item["result"].value item["result"] = item["result"].value
self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks }) self.__cfg_mgr.set(CfgKey.TIMERTASK.ROOT, { "timer_tasks": timer_tasks })
return True return True
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
@@ -287,7 +299,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
) )
return False return False
def showEvent( def showEvent(
self, self,
event event
@@ -311,7 +322,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
return result return result
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent event: QCloseEvent
@@ -321,7 +331,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.timerTaskManageWidgetIsClosed.emit() self.timerTaskManageWidgetIsClosed.emit()
event.ignore() event.ignore()
def sortTimerTasks( def sortTimerTasks(
self, self,
policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME, policy: SortPolicy = SortPolicy.BY_EXECUTE_TIME,
@@ -344,7 +353,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
reverse = order is Qt.SortOrder.DescendingOrder reverse = order is Qt.SortOrder.DescendingOrder
) )
def updateStat( def updateStat(
self self
): ):
@@ -371,7 +379,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.ExecutedTaskLabel.setText(f"已执行:{executed}") self.ExecutedTaskLabel.setText(f"已执行:{executed}")
self.InvalidTaskLabel.setText(f"无效的:{invalid}") self.InvalidTaskLabel.setText(f"无效的:{invalid}")
def updateTimerTaskList( def updateTimerTaskList(
self self
): ):
@@ -394,7 +401,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.TimerTasksListWidget.addItem(item) self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget) self.TimerTasksListWidget.setItemWidget(item, widget)
def addTask( def addTask(
self self
): ):
@@ -405,7 +411,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.append(timer_task) self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def editTask( def editTask(
self, self,
timer_task: dict timer_task: dict
@@ -438,7 +443,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
f"已记录次数:{history_count}" f"已记录次数:{history_count}"
) )
def deleteTask( def deleteTask(
self, self,
timer_task: dict timer_task: dict
@@ -467,7 +471,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
] ]
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def clearAllTasks( def clearAllTasks(
self self
): ):
@@ -529,7 +532,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.clear() self.__timer_tasks.clear()
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def showTaskHistory( def showTaskHistory(
self, self,
task: dict task: dict
@@ -539,7 +541,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
if dialog.exec() == QDialog.DialogCode.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def checkTasks( def checkTasks(
self self
): ):
@@ -613,7 +614,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
break break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def onRepeatTimerTaskIs( def onRepeatTimerTaskIs(
self, self,
status: ALTimerTaskStatus, status: ALTimerTaskStatus,
+12 -10
View File
@@ -10,14 +10,22 @@ See the LICENSE file for details.
from enum import Enum from enum import Enum
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, QSize, QCoreApplication, QRect, QPoint Qt,
QSize,
QCoreApplication,
QRect,
QPoint
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractScrollArea, QAbstractItemView, QAbstractScrollArea,
QTreeWidget, QTreeWidgetItem QAbstractItemView,
QTreeWidget,
QTreeWidgetItem
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QDragEnterEvent, QDragMoveEvent, QDropEvent QDragEnterEvent,
QDragMoveEvent,
QDropEvent
) )
@@ -39,7 +47,6 @@ class ALUserTreeWidget(QTreeWidget):
self.setupUi() self.setupUi()
self.translateUi() self.translateUi()
def setupUi( def setupUi(
self self
): ):
@@ -70,7 +77,6 @@ class ALUserTreeWidget(QTreeWidget):
self.header().setHighlightSections(False) self.header().setHighlightSections(False)
self.header().setProperty(u"showSortIndicator", True) self.header().setProperty(u"showSortIndicator", True)
def translateUi( def translateUi(
self self
): ):
@@ -78,7 +84,6 @@ class ALUserTreeWidget(QTreeWidget):
___qtreewidgetitem = self.headerItem() ___qtreewidgetitem = self.headerItem()
___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None)); ___qtreewidgetitem.setText(1, QCoreApplication.translate("ALConfigWidget", u"\u72b6\u6001", None));
@staticmethod @staticmethod
def isDragPositionValid( def isDragPositionValid(
target_rect: QRect, target_rect: QRect,
@@ -90,7 +95,6 @@ class ALUserTreeWidget(QTreeWidget):
y_offset < target_rect.height()*0.8) y_offset < target_rect.height()*0.8)
return valid return valid
def dragEnterEvent( def dragEnterEvent(
self, self,
event: QDragEnterEvent event: QDragEnterEvent
@@ -98,7 +102,6 @@ class ALUserTreeWidget(QTreeWidget):
super().dragEnterEvent(event) super().dragEnterEvent(event)
def dragMoveEvent( def dragMoveEvent(
self, self,
event: QDragMoveEvent event: QDragMoveEvent
@@ -136,7 +139,6 @@ class ALUserTreeWidget(QTreeWidget):
return return
event.acceptProposedAction() event.acceptProposedAction()
def dropEvent( def dropEvent(
self, self,
event: QDropEvent event: QDropEvent
+137 -140
View File
@@ -11,20 +11,30 @@ import threading
from typing import Optional from typing import Optional
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Slot, QThread, Signal Qt,
Slot,
QThread,
Signal
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QLabel, QComboBox, QProgressBar, QDialog,
QPushButton, QVBoxLayout, QHBoxLayout, QLabel,
QMessageBox, QFrame, QLineEdit QComboBox,
) QProgressBar,
from PySide6.QtGui import ( QPushButton,
QCloseEvent QVBoxLayout,
QHBoxLayout,
QMessageBox,
QFrame,
QLineEdit
) )
from PySide6.QtGui import QCloseEvent
from managers.driver.WebDriverManager import ( from managers.driver.WebDriverManager import (
instance as webdriver_manager_instance, instance as webdriverManagerInstance,
WebDriverManager, WebDriverInfo, WebDriverType, WebDriverManager,
WebDriverInfo,
WebDriverType,
WebDriverStatus WebDriverStatus
) )
from gui.ALStatusLabel import ALStatusLabel from gui.ALStatusLabel import ALStatusLabel
@@ -142,7 +152,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.initializeDriverManager() self.initializeDriverManager()
self.refreshDriverList() self.refreshDriverList()
def showEvent( def showEvent(
self, self,
event event
@@ -165,7 +174,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.move(target_pos) self.move(target_pos)
return result return result
def setupUi( def setupUi(
self self
): ):
@@ -237,7 +245,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.ControlLayout.addWidget(self.ConfirmButton) self.ControlLayout.addWidget(self.ConfirmButton)
self.MainLayout.addLayout(self.ControlLayout) self.MainLayout.addLayout(self.ControlLayout)
def connectSignals( def connectSignals(
self self
): ):
@@ -249,18 +256,16 @@ class ALWebDriverDownloadDialog(QDialog):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked) self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged) self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
def initializeDriverManager( def initializeDriverManager(
self self
): ):
try: try:
self.__driver_manager = webdriver_manager_instance(self.__driver_dir) self.__driver_manager = webdriverManagerInstance(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()
def refreshDriverList( def refreshDriverList(
self self
): ):
@@ -270,18 +275,20 @@ class ALWebDriverDownloadDialog(QDialog):
self.__driver_manager.refresh() self.__driver_manager.refresh()
self.__driver_infos = self.__driver_manager.getDriverInfos() self.__driver_infos = self.__driver_manager.getDriverInfos()
self.DriverComboBox.clear() self.DriverComboBox.clear()
installed = 0
installed_idx = 0 installed_idx = 0
for i, driver_info in enumerate(self.__driver_infos): for i, driver_info in enumerate(self.__driver_infos):
if driver_info.driver_status == WebDriverStatus.INSTALLED:
installed_idx = i # get the installed driver index
display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}" display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
if driver_info.driver_status == WebDriverStatus.INSTALLED:
installed += 1
installed_idx = i # get the installed driver index
display_text += " : 已安装"
self.DriverComboBox.addItem(display_text) self.DriverComboBox.addItem(display_text)
count = len(self.__driver_infos) count = len(self.__driver_infos)
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:") self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器{installed} 个已安装驱动")
if self.__driver_infos: if self.__driver_infos:
self.DriverComboBox.setCurrentIndex(installed_idx) self.DriverComboBox.setCurrentIndex(installed_idx)
def onDriverComboBoxChanged( def onDriverComboBoxChanged(
self, self,
index: int index: int
@@ -294,6 +301,116 @@ class ALWebDriverDownloadDialog(QDialog):
self.updateProgressBarStates(driver_info) self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info) self.updateButtonStates(driver_info)
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.stop()
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def onThreadFinished(
self
):
if self.__download_thread:
self.__download_thread.deleteLater()
self.__download_thread = None
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status:
case WebDriverStatus.NOT_INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case WebDriverStatus.INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case WebDriverStatus.DOWNLOADING:
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case WebDriverStatus.ERROR:
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateProgressBarStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.ProgressBar.setValue(0)
self.ProgressText.setText("未安装")
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.ProgressBar.setValue(100)
self.ProgressText.setText("已安装")
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
pass # update by worker thread
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.ProgressBar.setValue(0)
self.ProgressText.setText("下载失败")
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.RefreshButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
self.RefreshButton.setEnabled(False)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
@Slot() @Slot()
def onRefreshButtonClicked( def onRefreshButtonClicked(
@@ -302,7 +419,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.refreshDriverList() self.refreshDriverList()
@Slot() @Slot()
def onDeleteButtonClicked( def onDeleteButtonClicked(
self self
@@ -355,7 +471,7 @@ class ALWebDriverDownloadDialog(QDialog):
self.__download_thread.downloadFinished.connect(self.onDownloadFinished) self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
self.__download_thread.downloadError.connect(self.onDownloadError) self.__download_thread.downloadError.connect(self.onDownloadError)
self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled) self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
self.__download_thread.finished.connect(self.__onThreadFinished) self.__download_thread.finished.connect(self.onThreadFinished)
self.__download_thread.start() self.__download_thread.start()
@Slot() @Slot()
@@ -406,7 +522,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.updateButtonStates(driver_info) self.updateButtonStates(driver_info)
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message) QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
@Slot() @Slot()
def onDownloadCancelled( def onDownloadCancelled(
self self
@@ -423,7 +538,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.updateButtonStates(driver_info) self.updateButtonStates(driver_info)
self.ProgressText.setText("下载已取消") self.ProgressText.setText("下载已取消")
@Slot() @Slot()
def onConfirmButtonClicked( def onConfirmButtonClicked(
self self
@@ -439,7 +553,6 @@ class ALWebDriverDownloadDialog(QDialog):
self.__confirmed = True self.__confirmed = True
self.accept() self.accept()
@Slot() @Slot()
def onCancelButtonClicked( def onCancelButtonClicked(
self self
@@ -458,119 +571,3 @@ class ALWebDriverDownloadDialog(QDialog):
self.__confirmed = False self.__confirmed = False
self.__selected_driver_info = None self.__selected_driver_info = None
self.reject() self.reject()
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.stop()
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def __onThreadFinished(
self
):
if self.__download_thread:
self.__download_thread.deleteLater()
self.__download_thread = None
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status:
case WebDriverStatus.NOT_INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case WebDriverStatus.INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case WebDriverStatus.DOWNLOADING:
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case WebDriverStatus.ERROR:
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateProgressBarStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.ProgressBar.setValue(0)
self.ProgressText.setText("未安装")
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.ProgressBar.setValue(100)
self.ProgressText.setText("已安装")
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
pass # update by worker thread
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.ProgressBar.setValue(0)
self.ProgressText.setText("下载失败")
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.RefreshButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
self.RefreshButton.setEnabled(False)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
+4 -4
View File
@@ -1,8 +1,8 @@
<RCC> <RCC>
<qresource prefix="/res/icon"> <qresource prefix="/res">
<file>icons/AutoLibrary_32x32.ico</file> <file>icons/AutoLibrary_Logo_64.svg</file>
</qresource> <file>icons/AutoLibrary_Logo_128.svg</file>
<qresource prefix="/res/trans">
<file>translators/qtbase_zh_CN.qm</file> <file>translators/qtbase_zh_CN.qm</file>
</qresource> </qresource>
</RCC> </RCC>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

+119
View File
@@ -0,0 +1,119 @@
# -*- 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 dataclasses import dataclass
from enum import Enum
from typing import Any, Optional, Protocol
class ConfigType(Enum):
"""
Config type enum. Values represent the default filename.
"""
GLOBAL = "autolibrary.json"
BULLETIN = "bulletin.json"
TIMERTASK = "timer_task.json"
@dataclass(frozen=True)
class ConfigPath:
"""
A typed configuration path that carries both the config file
and the dot-separated key in a single object.
Consumers pass this directly to ConfigProvider.get/set,
eliminating the need to import ConfigType separately.
"""
config_type: ConfigType
key: str = ""
class CfgKey:
"""
Type-safe hierarchical configuration key constants.
Each leaf is a ConfigPath that can be passed directly to
``ConfigProvider.get()`` or ``ConfigProvider.set()``.
Usage::
CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS
# -> ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths")
config.get(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, [])
config.set(CfgKey.GLOBAL.AUTOMATION.RUN_PATH.PATHS, value)
"""
class GLOBAL:
class AUTOMATION:
ROOT = ConfigPath(ConfigType.GLOBAL, "automation")
class RUN_PATH:
ROOT = ConfigPath(ConfigType.GLOBAL, "automation.run_path")
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.run_path.current")
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.run_path.paths")
class USER_PATH:
ROOT = ConfigPath(ConfigType.GLOBAL, "automation.user_path")
CURRENT = ConfigPath(ConfigType.GLOBAL, "automation.user_path.current")
PATHS = ConfigPath(ConfigType.GLOBAL, "automation.user_path.paths")
class TIMERTASK:
ROOT = ConfigPath(ConfigType.TIMERTASK, "")
TIMER_TASKS = ConfigPath(ConfigType.TIMERTASK, "timer_tasks")
class BULLETIN:
ROOT = ConfigPath(ConfigType.BULLETIN, "")
BULLETIN = ConfigPath(ConfigType.BULLETIN, "bulletin")
LAST_SYNC_TIME = ConfigPath(ConfigType.BULLETIN, "last_sync_time")
class ConfigProvider(Protocol):
"""
Abstract interface for configuration storage access.
Concrete implementations (e.g. ConfigManager) conform to
this protocol structurally rather than through explicit
inheritance.
"""
def get(
self,
key: ConfigPath,
default: Optional[Any] = None
) -> Any:
"""
Retrieve a configuration value.
Args:
key: A ConfigPath object specifying which config file
and key to read from.
default: Fallback value if the key is not found.
Returns:
The configuration value at the given key path.
"""
...
def set(
self,
key: ConfigPath,
value: Any = None
) -> None:
"""
Set a configuration value and persist to disk.
Args:
key: A ConfigPath object specifying which config file
and key to write to.
value: The value to store.
"""
...
+11
View File
@@ -0,0 +1,11 @@
"""
Interfaces module for the AutoLibrary project.
Defines abstract interfaces (Protocols) and shared type definitions
used across layers to decouple consumers from concrete implementations.
Key components:
- ConfigProvider: Abstract interface for configuration access.
- ConfigType: Enumeration of configuration file types.
- ConfigKey: Type-safe hierarchical key constants for config lookups.
"""
+12 -29
View File
@@ -10,26 +10,17 @@ See the LICENSE file for details.
import os import os
import threading import threading
from enum import Enum
from typing import Any, Optional from typing import Any, Optional
from utils.JSONReader import JSONReader from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter from utils.JSONWriter import JSONWriter
from interfaces.ConfigProvider import ConfigType, ConfigPath
# This config manager class only responsible for global and other # This config manager class only responsible for global and other
# unconfigurable config files. # unconfigurable config files.
class ConfigType(Enum):
"""
Config type class. Values represent the default filename.
"""
GLOBAL = "autolibrary.json" # Global config file.
BULLETIN = "bulletin.json" # Bulletin board config file.
TIMERTASK = "timer_task.json" # Timer task config file.
class ConfigTemplate: class ConfigTemplate:
""" """
Config template class. Config template class.
@@ -42,7 +33,6 @@ class ConfigTemplate:
self.__config_type = config_type self.__config_type = config_type
def template( def template(
self self
) -> dict: ) -> dict:
@@ -92,7 +82,6 @@ class ConfigManager:
self.initialize() self.initialize()
def initialize( def initialize(
self self
): ):
@@ -100,7 +89,6 @@ class ConfigManager:
for config_type in ConfigType: for config_type in ConfigType:
self.load(config_type) self.load(config_type)
def load( def load(
self, self,
config_type: ConfigType config_type: ConfigType
@@ -117,47 +105,42 @@ class ConfigManager:
self.__config_data[config_type.value] = ConfigTemplate(config_type).template() self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
JSONWriter(config_path, self.__config_data[config_type.value]) JSONWriter(config_path, self.__config_data[config_type.value])
def get( def get(
self, self,
config_type: ConfigType, key: ConfigPath,
key: str = "",
default: Optional[Any] = None default: Optional[Any] = None
) -> Any: ) -> Any:
with self.__config_lock: with self.__config_lock:
config_data = self.__config_data[config_type.value] config_data = self.__config_data[key.config_type.value]
if key == "": if key.key == "":
return config_data return config_data
keys = key.split('.') keys = key.key.split('.')
for k in keys[:-1]: for k in keys[:-1]:
config_data = config_data.get(k, None) config_data = config_data.get(k, None)
if config_data is None: if config_data is None:
return default return default
return config_data.get(keys[-1], default) return config_data.get(keys[-1], default)
def set( def set(
self, self,
config_type: ConfigType, key: ConfigPath,
key: str = "",
value: Any = None value: Any = None
): ):
with self.__config_lock: with self.__config_lock:
root_data = self.__config_data[config_type.value] root_data = self.__config_data[key.config_type.value]
if key == "": if key.key == "":
self.__config_data[config_type.value] = value self.__config_data[key.config_type.value] = value
else: else:
keys = key.split('.') keys = key.key.split('.')
config_data = root_data config_data = root_data
for k in keys[:-1]: for k in keys[:-1]:
if k not in config_data: if k not in config_data:
config_data[k] = {} config_data[k] = {}
config_data = config_data[k] config_data = config_data[k]
config_data[keys[-1]] = value config_data[keys[-1]] = value
self.save(config_type) self.save(key.config_type)
def save( def save(
self, self,
@@ -167,7 +150,6 @@ class ConfigManager:
config_path = os.path.join(self.__config_dir, config_type.value) config_path = os.path.join(self.__config_dir, config_type.value)
JSONWriter(config_path, self.__config_data[config_type.value]) JSONWriter(config_path, self.__config_data[config_type.value])
def configDir( def configDir(
self self
) -> str: ) -> str:
@@ -180,6 +162,7 @@ _config_manager_instance : ConfigManager | None = None
# Singleton instance of ConfigManager. # Singleton instance of ConfigManager.
_instance_lock = threading.Lock() _instance_lock = threading.Lock()
def instance( def instance(
config_dir: str = "" config_dir: str = ""
) -> ConfigManager: ) -> ConfigManager:
@@ -11,6 +11,8 @@ import os
import managers.config.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from interfaces.ConfigProvider import CfgKey
class ConfigUtils: class ConfigUtils:
""" """
Config utilities class. Config utilities class.
@@ -29,7 +31,7 @@ class ConfigUtils:
cfg_mgr = ConfigManager.instance() # config manager instance cfg_mgr = ConfigManager.instance() # config manager instance
config_paths = {"run": "", "user": ""} config_paths = {"run": "", "user": ""}
auto_config = cfg_mgr.get(ConfigManager.ConfigType.GLOBAL, "automation", {}) auto_config = cfg_mgr.get(CfgKey.GLOBAL.AUTOMATION.ROOT, {})
for cfg_type in ["run", "user"]: for cfg_type in ["run", "user"]:
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", []) paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0) index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
@@ -42,5 +44,5 @@ class ConfigUtils:
config_paths[cfg_type] = paths[index] config_paths[cfg_type] = paths[index]
data = {"current": index, "paths": paths} data = {"current": index, "paths": paths}
auto_config[f"{cfg_type}_path"] = data auto_config[f"{cfg_type}_path"] = data
cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation", auto_config) cfg_mgr.set(CfgKey.GLOBAL.AUTOMATION.ROOT, auto_config)
return config_paths return config_paths
+1 -2
View File
@@ -41,6 +41,7 @@ class WebBrowserArch(Enum):
MACX86_64 = 6 MACX86_64 = 6
MACARM = 7 MACARM = 7
@dataclass @dataclass
class WebBrowserInfo: class WebBrowserInfo:
""" """
@@ -70,7 +71,6 @@ class WebBrowserArchDetector:
pass pass
def detect( def detect(
self self
) -> WebBrowserArch: ) -> WebBrowserArch:
@@ -123,7 +123,6 @@ class WebBrowserDetector:
self.browser_arch = WebBrowserArchDetector().detect() self.browser_arch = WebBrowserArchDetector().detect()
self.browser_infos : list[WebBrowserInfo] = [] self.browser_infos : list[WebBrowserInfo] = []
def detect( def detect(
self self
) -> list[WebBrowserInfo]: ) -> list[WebBrowserInfo]:
+23 -31
View File
@@ -95,7 +95,6 @@ class WebDriverName:
self.driver_type = driver_type self.driver_type = driver_type
def __str__( def __str__(
self self
) -> str: ) -> str:
@@ -125,7 +124,6 @@ class WebDriverExecName:
self.driver_type = driver_type self.driver_type = driver_type
self.arch = arch self.arch = arch
def __str__( def __str__(
self self
) -> str: ) -> str:
@@ -200,7 +198,6 @@ class WebDriverURL:
self.arch = arch self.arch = arch
self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch)) self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
def __str__( def __str__(
self self
) -> str: ) -> str:
@@ -250,31 +247,6 @@ class WebDriverDownloader:
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True) self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch)) self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
def download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
try:
# downlaod file : 0% - 98%
if not self._download(progress_callback, cancel_event=cancel_event):
return None
# verify file : 98% - 99%
if not self._verify(progress_callback):
progress_callback(0, 100, 0.0, "验证失败")
return None
# extract file : 99% - 100%
driver_path = self._extract(progress_callback)
if not driver_path:
progress_callback(0, 100, 0.0, "解压失败")
return None
return driver_path
except Exception as e:
raise e
def _download( def _download(
self, self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None, progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
@@ -352,7 +324,6 @@ class WebDriverDownloader:
continue continue
raise e raise e
def _verify( def _verify(
self, self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None progress_callback: Optional[Callable[[float, int, float, str], None]] = None
@@ -361,7 +332,6 @@ class WebDriverDownloader:
progress_callback(98, 100, 0.0, "验证完成") progress_callback(98, 100, 0.0, "验证完成")
return True return True
def _extract( def _extract(
self, self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None progress_callback: Optional[Callable[[float, int, float, str], None]] = None
@@ -397,7 +367,6 @@ class WebDriverDownloader:
except Exception: except Exception:
return None return None
def _cleanup( def _cleanup(
self, self,
driver_file: Path driver_file: Path
@@ -410,6 +379,29 @@ class WebDriverDownloader:
else: else:
item.unlink() item.unlink()
def download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
try:
# downlaod file : 0% - 98%
if not self._download(progress_callback, cancel_event=cancel_event):
return None
# verify file : 98% - 99%
if not self._verify(progress_callback):
progress_callback(0, 100, 0.0, "验证失败")
return None
# extract file : 99% - 100%
driver_path = self._extract(progress_callback)
if not driver_path:
progress_callback(0, 100, 0.0, "解压失败")
return None
return driver_path
except Exception as e:
raise e
class ChromeDriverDownloader(WebDriverDownloader): class ChromeDriverDownloader(WebDriverDownloader):
""" """
-16
View File
@@ -81,7 +81,6 @@ class WebDriverManager:
self.initialize() self.initialize()
def initialize( def initialize(
self self
): ):
@@ -93,7 +92,6 @@ class WebDriverManager:
self._checkDriverStatus() self._checkDriverStatus()
self.__initialized = True self.__initialized = True
def _detectBrowsers( def _detectBrowsers(
self self
): ):
@@ -105,7 +103,6 @@ class WebDriverManager:
for info in browser_infos for info in browser_infos
] ]
def _checkDriverStatus( def _checkDriverStatus(
self self
): ):
@@ -117,7 +114,6 @@ class WebDriverManager:
driver_info.driver_path = driver_path driver_info.driver_path = driver_path
driver_info.driver_status = WebDriverStatus.INSTALLED driver_info.driver_status = WebDriverStatus.INSTALLED
def _mapWebBrowserTypeToDriver( def _mapWebBrowserTypeToDriver(
self, self,
browser_type: WebBrowserType browser_type: WebBrowserType
@@ -132,7 +128,6 @@ class WebDriverManager:
else: else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}") raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapWebBrowserArchToDriver( def _mapWebBrowserArchToDriver(
self, self,
browser_type: WebBrowserType, browser_type: WebBrowserType,
@@ -199,7 +194,6 @@ class WebDriverManager:
else: else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}") raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapFirefoxDriverVersion( def _mapFirefoxDriverVersion(
self, self,
version: str version: str
@@ -240,7 +234,6 @@ class WebDriverManager:
except Exception as e: except Exception as e:
raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
def _getDriverInfo( def _getDriverInfo(
self, self,
browser_info: WebBrowserInfo browser_info: WebBrowserInfo
@@ -256,7 +249,6 @@ class WebDriverManager:
driver_info.browser_version = browser_info.browser_version driver_info.browser_version = browser_info.browser_version
return driver_info return driver_info
def _getDriverPath( def _getDriverPath(
self, self,
driver_info: WebDriverInfo driver_info: WebDriverInfo
@@ -286,7 +278,6 @@ class WebDriverManager:
driver_path = driver_dir/exe_name driver_path = driver_dir/exe_name
return driver_path return driver_path
def refresh( def refresh(
self self
): ):
@@ -294,7 +285,6 @@ class WebDriverManager:
self._detectBrowsers() self._detectBrowsers()
self._checkDriverStatus() self._checkDriverStatus()
def getDriverInfos( def getDriverInfos(
self self
) -> list[WebDriverInfo]: ) -> list[WebDriverInfo]:
@@ -302,7 +292,6 @@ class WebDriverManager:
with self.__lock: with self.__lock:
return self.__driver_infos.copy() return self.__driver_infos.copy()
def getDriverInfo( def getDriverInfo(
self, self,
driver_type: WebDriverType driver_type: WebDriverType
@@ -315,7 +304,6 @@ class WebDriverManager:
if info.driver_type == driver_type if info.driver_type == driver_type
] ]
def getDriverPath( def getDriverPath(
self, self,
driver_info: WebDriverInfo driver_info: WebDriverInfo
@@ -325,7 +313,6 @@ class WebDriverManager:
return driver_info.driver_path return driver_info.driver_path
return None return None
def installDriver( def installDriver(
self, self,
driver_info: WebDriverInfo, driver_info: WebDriverInfo,
@@ -390,7 +377,6 @@ class WebDriverManager:
driver_info.driver_status = WebDriverStatus.ERROR driver_info.driver_status = WebDriverStatus.ERROR
raise e raise e
def cancelDriverDownload( def cancelDriverDownload(
self, self,
driver_info: WebDriverInfo driver_info: WebDriverInfo
@@ -411,7 +397,6 @@ class WebDriverManager:
except Exception: except Exception:
return False return False
def uninstallDriver( def uninstallDriver(
self, self,
driver_info: WebDriverInfo, driver_info: WebDriverInfo,
@@ -441,7 +426,6 @@ class WebDriverManager:
driver_info.driver_status = WebDriverStatus.ERROR driver_info.driver_status = WebDriverStatus.ERROR
raise raise
def driverDir( def driverDir(
self self
) -> str: ) -> str:
+1 -4
View File
@@ -89,7 +89,6 @@ class LogManager:
self.initialize() self.initialize()
def initialize( def initialize(
self self
): ):
@@ -139,7 +138,6 @@ class LogManager:
self.__initialized = True self.__initialized = True
def getLogger( def getLogger(
self, self,
name: Optional[str] = None name: Optional[str] = None
@@ -149,7 +147,6 @@ class LogManager:
return self.__logger.getChild(name) return self.__logger.getChild(name)
return self.__logger return self.__logger
def setLevel( def setLevel(
self, self,
level: int level: int
@@ -158,7 +155,6 @@ class LogManager:
if self.__logger: if self.__logger:
self.__logger.setLevel(level) self.__logger.setLevel(level)
def logDir( def logDir(
self self
) -> str: ) -> str:
@@ -171,6 +167,7 @@ _log_manager_instance = None
# Singleton instance lock. # Singleton instance lock.
_instance_lock = threading.Lock() _instance_lock = threading.Lock()
def instance( def instance(
log_dir: str = "" log_dir: str = ""
) -> LogManager: ) -> LogManager:
-7
View File
@@ -49,7 +49,6 @@ class AutoLib(MsgBase):
raise Exception("浏览器驱动URL初始化失败 !") raise Exception("浏览器驱动URL初始化失败 !")
self.__initLibOperators() self.__initLibOperators()
def __initBrowserDriver( def __initBrowserDriver(
self self
) -> bool: ) -> bool:
@@ -142,7 +141,6 @@ class AutoLib(MsgBase):
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}") self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
return True return True
def __initLibOperators( def __initLibOperators(
self self
): ):
@@ -157,7 +155,6 @@ class AutoLib(MsgBase):
self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver) self.__lib_checkin = LibCheckin(self._input_queue, self._output_queue, self.__driver)
self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver) self.__lib_renew = LibRenew(self._input_queue, self._output_queue, self.__driver)
def __waitResponseLoad( def __waitResponseLoad(
self self
) -> bool: ) -> bool:
@@ -184,7 +181,6 @@ class AutoLib(MsgBase):
self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR) self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR)
return False return False
def __initDriverUrl( def __initDriverUrl(
self, self,
) -> bool: ) -> bool:
@@ -207,7 +203,6 @@ class AutoLib(MsgBase):
return False return False
return True return True
def __run( def __run(
self, self,
username: str, username: str,
@@ -292,7 +287,6 @@ class AutoLib(MsgBase):
return -1 return -1
return result return result
def run( def run(
self, self,
user_config: dict user_config: dict
@@ -339,7 +333,6 @@ class AutoLib(MsgBase):
) )
return return
def close( def close(
self self
) -> bool: ) -> bool:
-12
View File
@@ -33,7 +33,6 @@ class LibChecker(LibOperator):
self.__driver = driver self.__driver = driver
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
@@ -50,7 +49,6 @@ class LibChecker(LibOperator):
seconds = int(seconds%60) seconds = int(seconds%60)
return f"{hours}{minutes}{seconds}" return f"{hours}{minutes}{seconds}"
def __navigateToReserveRecordPage( def __navigateToReserveRecordPage(
self self
) -> bool: ) -> bool:
@@ -67,7 +65,6 @@ class LibChecker(LibOperator):
return False return False
return True return True
def __decodeReserveTime( def __decodeReserveTime(
self, self,
time_element time_element
@@ -105,7 +102,6 @@ class LibChecker(LibOperator):
} }
} }
def __decodeReserveInfo( def __decodeReserveInfo(
self, self,
info_elements info_elements
@@ -133,7 +129,6 @@ class LibChecker(LibOperator):
"status": status, "status": status,
} }
def __decodeReserveRecord( def __decodeReserveRecord(
self, self,
reservation reservation
@@ -160,7 +155,6 @@ class LibChecker(LibOperator):
"info": info "info": info
} }
def __loadReserveRecords( def __loadReserveRecords(
self self
) -> list: ) -> list:
@@ -177,7 +171,6 @@ class LibChecker(LibOperator):
self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR) self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR)
return None return None
def __showMoreReserveRecords( def __showMoreReserveRecords(
self self
) -> bool: ) -> bool:
@@ -203,7 +196,6 @@ class LibChecker(LibOperator):
self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR) self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR)
return False return False
def __getReserveRecord( def __getReserveRecord(
self, self,
wanted_date: str, wanted_date: str,
@@ -253,7 +245,6 @@ class LibChecker(LibOperator):
break break
return None return None
def canReserve( def canReserve(
self, self,
date: str date: str
@@ -270,7 +261,6 @@ class LibChecker(LibOperator):
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约") self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
return False return False
def canCheckin( def canCheckin(
self self
) -> bool: ) -> bool:
@@ -307,7 +297,6 @@ class LibChecker(LibOperator):
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到") self._showTrace(f"用户在 {date} 没有有效预约记录, 无法签到")
return False return False
def canRenew( def canRenew(
self self
) -> tuple[bool, dict]: ) -> tuple[bool, dict]:
@@ -335,7 +324,6 @@ class LibChecker(LibOperator):
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return False, None return False, None
def postRenewCheck( def postRenewCheck(
self, self,
record: dict record: dict
-3
View File
@@ -31,7 +31,6 @@ class LibCheckin(LibOperator):
self.__driver = driver self.__driver = driver
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
@@ -87,7 +86,6 @@ class LibCheckin(LibOperator):
ok_btn.click() ok_btn.click()
return False return False
def __enableCheckinBtn( def __enableCheckinBtn(
self self
) -> bool: ) -> bool:
@@ -112,7 +110,6 @@ class LibCheckin(LibOperator):
self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING) self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING)
return result return result
def checkin( def checkin(
self, self,
username: str username: str
-1
View File
@@ -33,7 +33,6 @@ class LibCheckout(LibOperator):
self.__driver = driver self.__driver = driver
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
-8
View File
@@ -34,7 +34,6 @@ class LibLogin(LibOperator):
self.__driver = driver self.__driver = driver
self.__ddddocr = ddddocr.DdddOcr() self.__ddddocr = ddddocr.DdddOcr()
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
@@ -58,7 +57,6 @@ class LibLogin(LibOperator):
) )
return False return False
def __fillLogInElements( def __fillLogInElements(
self, self,
username: str, username: str,
@@ -78,7 +76,6 @@ class LibLogin(LibOperator):
return False return False
return True return True
def __autoRecognizeCaptcha( def __autoRecognizeCaptcha(
self self
) -> str: ) -> str:
@@ -100,7 +97,6 @@ class LibLogin(LibOperator):
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR) self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return "" return ""
def __manualRecognizeCaptcha( def __manualRecognizeCaptcha(
self self
) -> str: ) -> str:
@@ -118,7 +114,6 @@ class LibLogin(LibOperator):
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR) self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
return "" return ""
def __refreshCaptcha( def __refreshCaptcha(
self self
): ):
@@ -134,7 +129,6 @@ class LibLogin(LibOperator):
self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR) self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR)
return False return False
def __solveCaptcha( def __solveCaptcha(
self, self,
auto_captcha: bool = True auto_captcha: bool = True
@@ -158,7 +152,6 @@ class LibLogin(LibOperator):
) )
return "" return ""
def __fillCaptchaElement( def __fillCaptchaElement(
self, self,
captcha_text: str captcha_text: str
@@ -173,7 +166,6 @@ class LibLogin(LibOperator):
self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR) self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR)
return False return False
def login( def login(
self, self,
username: str, username: str,
-2
View File
@@ -28,14 +28,12 @@ class LibLogout(LibOperator):
self.__driver = driver self.__driver = driver
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
return True return True
def logout( def logout(
self, self,
username: str username: str
+1 -7
View File
@@ -14,7 +14,7 @@ from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from base.LibTimeSelector import LibTimeSelector from operators.abs.LibTimeSelector import LibTimeSelector
class LibRenew(LibTimeSelector): class LibRenew(LibTimeSelector):
@@ -30,7 +30,6 @@ class LibRenew(LibTimeSelector):
self.__driver = driver self.__driver = driver
def _waitResponseLoad( def _waitResponseLoad(
self self
) -> bool: ) -> bool:
@@ -38,7 +37,6 @@ class LibRenew(LibTimeSelector):
self.__driver.refresh() self.__driver.refresh()
return True return True
def __waitRenewDialog( def __waitRenewDialog(
self self
) -> bool: ) -> bool:
@@ -77,7 +75,6 @@ class LibRenew(LibTimeSelector):
return False return False
return True return True
def __selectNearestTime( def __selectNearestTime(
self, self,
record: dict, record: dict,
@@ -116,7 +113,6 @@ class LibRenew(LibTimeSelector):
self._showTrace(f"当前可供续约的时间有: {free_times}") self._showTrace(f"当前可供续约的时间有: {free_times}")
return False return False
def __validateAndAdjustRenewTime( def __validateAndAdjustRenewTime(
self, self,
end_time: str, end_time: str,
@@ -139,7 +135,6 @@ class LibRenew(LibTimeSelector):
return True return True
return True return True
def __confirmRenewal( def __confirmRenewal(
self, self,
best_opt, best_opt,
@@ -167,7 +162,6 @@ class LibRenew(LibTimeSelector):
self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR) self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR)
return False return False
def renew( def renew(
self, self,
username: str, username: str,
+3 -22
View File
@@ -15,7 +15,7 @@ from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from base.LibTimeSelector import LibTimeSelector from operators.abs.LibTimeSelector import LibTimeSelector
class LibReserve(LibTimeSelector): class LibReserve(LibTimeSelector):
@@ -48,7 +48,6 @@ class LibReserve(LibTimeSelector):
"8": "五层考研" "8": "五层考研"
} }
def _waitResponseLoad( def _waitResponseLoad(
self, self,
) -> bool: ) -> bool:
@@ -99,7 +98,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR) self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR)
return False return False
def __containRequiredInfo( def __containRequiredInfo(
self, self,
reserve_info: dict reserve_info: dict
@@ -135,7 +133,6 @@ class LibReserve(LibTimeSelector):
) )
return False return False
def __isValidDate( def __isValidDate(
self, self,
reserve_info: dict reserve_info: dict
@@ -157,7 +154,6 @@ class LibReserve(LibTimeSelector):
reserve_info["date"] = cur_date_str reserve_info["date"] = cur_date_str
return True return True
def __isValidBeginTime( def __isValidBeginTime(
self, self,
reserve_info: dict reserve_info: dict
@@ -177,7 +173,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True") self._showTrace(f"是否优先选择更早开始时间未指定, 自动设置为 True")
return True return True
def __isValidExpectDuration( def __isValidExpectDuration(
self, self,
reserve_info: dict reserve_info: dict
@@ -192,7 +187,6 @@ class LibReserve(LibTimeSelector):
self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时") self._showTrace("需要满足预约持续时间, 但未指定, 使用默认时长为 4 小时")
return True return True
def __isValidEndTime( def __isValidEndTime(
self, self,
reserve_info: dict reserve_info: dict
@@ -222,7 +216,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True") self._showTrace(f"是否优先选择较晚结束时间未指定, 自动设置为 True")
return True return True
def __finalCheck( def __finalCheck(
self, self,
reserve_info: dict reserve_info: dict
@@ -275,7 +268,6 @@ class LibReserve(LibTimeSelector):
reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60) reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60)
return True return True
def __checkReserveInfo( def __checkReserveInfo(
self, self,
reserve_info: dict reserve_info: dict
@@ -305,7 +297,6 @@ class LibReserve(LibTimeSelector):
) )
return True return True
def __clickElement( def __clickElement(
self, self,
trigger_locator: tuple, trigger_locator: tuple,
@@ -330,7 +321,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(fail_msg) self._showTrace(fail_msg)
return False return False
def __clickElementByJS( def __clickElementByJS(
self, self,
trigger_locator_id: str, trigger_locator_id: str,
@@ -364,7 +354,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(fail_msg) self._showTrace(fail_msg)
return result return result
def __selectDate( def __selectDate(
self, self,
date_str: str date_str: str
@@ -384,7 +373,6 @@ class LibReserve(LibTimeSelector):
fail_msg=f"选择日期失败 ! : {date_str} 不可用" fail_msg=f"选择日期失败 ! : {date_str} 不可用"
) )
def __selectPlace( def __selectPlace(
self, self,
place: str place: str
@@ -406,7 +394,6 @@ class LibReserve(LibTimeSelector):
fail_msg=f"选择预约场所失败 ! : {display_place} 不可用" fail_msg=f"选择预约场所失败 ! : {display_place} 不可用"
) )
def __selectFloor( def __selectFloor(
self, self,
floor: str floor: str
@@ -427,7 +414,6 @@ class LibReserve(LibTimeSelector):
fail_msg=f"选择楼层失败 ! : {display_floor} 不可用" fail_msg=f"选择楼层失败 ! : {display_floor} 不可用"
) )
def __selectRoom( def __selectRoom(
self, self,
room: str room: str
@@ -453,7 +439,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR) self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
return False return False
def __selectSeat( def __selectSeat(
self, self,
seat_id: str seat_id: str
@@ -492,7 +477,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR) self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR)
return False return False
def __selectNearestTime( def __selectNearestTime(
self, self,
time_id: str, time_id: str,
@@ -547,7 +531,6 @@ class LibReserve(LibTimeSelector):
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
return -1 return -1
def __selectSeatTime( def __selectSeatTime(
self, self,
begin_time: dict, begin_time: dict,
@@ -583,7 +566,7 @@ class LibReserve(LibTimeSelector):
# If 'satisfy_duration' is True, select end time based on actual begin time # If 'satisfy_duration' is True, select end time based on actual begin time
if satisfy_duration: if satisfy_duration:
exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expect_duration)) exp_end_mins = int(self.__validateAndAdjustEndTime(act_beg_mins, expect_duration))
exp_end_tm_str = self._minsToTimeStr(exp_end_mins) exp_end_tm_str = self._minsToTimeStr(exp_end_mins)
self._showTrace( self._showTrace(
f"需要满足期望预约持续时间: {expect_duration} 小时, " f"需要满足期望预约持续时间: {expect_duration} 小时, "
@@ -607,8 +590,7 @@ class LibReserve(LibTimeSelector):
) )
return True return True
def __validateAndAdjustEndTime(
def validateAndAdjustEndTime(
self, self,
begin_mins: int, begin_mins: int,
duration: int duration: int
@@ -627,7 +609,6 @@ class LibReserve(LibTimeSelector):
) )
return expect_end_mins return expect_end_mins
def reserve( def reserve(
self, self,
username: str, username: str,
@@ -16,7 +16,7 @@ from base.LibOperator import LibOperator
class LibTimeSelector(LibOperator): class LibTimeSelector(LibOperator):
""" """
Base class for time selection operations. Abstract base class for time selection operations.
This class provides common time selection logic for reservation and renewal This class provides common time selection logic for reservation and renewal
operations, including time conversion utilities and best time option finding. operations, including time conversion utilities and best time option finding.
@@ -60,7 +60,6 @@ class LibTimeSelector(LibOperator):
hour, minute = divmod(int(mins), 60) hour, minute = divmod(int(mins), 60)
return f"{hour:02d}:{minute:02d}" return f"{hour:02d}:{minute:02d}"
def _formatTimeRelation( def _formatTimeRelation(
self, self,
abs_diff: int, abs_diff: int,
@@ -78,7 +77,6 @@ class LibTimeSelector(LibOperator):
else: else:
return f"正好等于 {time_type}" return f"正好等于 {time_type}"
def _findBestTimeOption( def _findBestTimeOption(
self, self,
time_options: list, time_options: list,
+6
View File
@@ -0,0 +1,6 @@
"""
Abstract layer class of the LibOperator
Here are the classes and modules in this package:
- LibTimeSelector: Abstract base class for time selection operations.
"""
-386
View File
@@ -1,386 +0,0 @@
# -*- 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.
"""
import re
from datetime import datetime, timedelta
class AutoScriptEngine:
"""
AutoScript script engine.
Parses and executes AutoScript a lightweight scripting DSL
used in repeatable timer tasks to preprocess user reservation
data before automation runs.
Supports IF/ELSE IF/ELSE/END IF control flow, SET assignments,
.ADD./.SUB. operations on Date/Time fields, and rich comparison
operators (.EQ. .NEQ. .BGT. .BLT. .BGE. .BLE.).
Examples:
>>> engine = AutoScriptEngine
>>> user = {
... "username": "test",
... "enabled": True,
... "reserve_info": {"date": "2026-05-07"}
... }
>>> engine.execute(
... 'IF(CURRENT_TIME .BGT. TIME(19:00))\\n'
... ' RESERVE_DATE .ADD. 1\\n'
... 'END IF',
... user
... )
"""
COMPARE_OPS = { # compare operators
".EQ." : lambda a, b: a == b,
".NEQ.": lambda a, b: a != b,
".BGT.": lambda a, b: a > b,
".BLT.": lambda a, b: a < b,
".BGE.": lambda a, b: a >= b,
".BLE.": lambda a, b: a <= b,
}
VARIABLE_META = { # variable metadata
"预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
"预约结束时间": ("RESERVE_END_TIME", "Time"),
"预约日期": ("RESERVE_DATE", "Date"),
"用户名": ("USERNAME", "String"),
"用户启用": ("USER_ENABLE", "Boolean"),
"当前时间": ("CURRENT_TIME", "Time"),
"当前日期": ("CURRENT_DATE", "Date"),
}
_FIELD_TYPE_MAP = {meta[0]: meta[1] for meta in VARIABLE_META.values()}
@staticmethod
def execute(
script_text: str,
user_data: dict
):
"""
Execute an AutoScript against the given user data.
The script is parsed line-by-line. All modifications are
applied directly to ``user_data`` in-place.
Args:
script_text (str): Raw AutoScript source code.
user_data (dict): User data dictionary to read from and
write to. Must conform to the standard user profile
structure (username, enabled, reserve_info, etc.).
Raises:
ValueError: On any syntax or type error encountered
during parsing or execution.
"""
if not script_text or not script_text.strip():
return
lines = [l.strip() for l in script_text.split("\n") if l.strip()]
if not lines:
return
if_stack = []
for line in lines:
upper_line = line.upper().strip()
if upper_line.startswith("IF("):
cond_end = _findConditionEnd(upper_line)
if cond_end < 0:
raise ValueError("AutoScript 语法错误: IF 缺少右括号")
condition_str = line[3:cond_end].strip()
matched = AutoScriptEngine._evaluateCondition(
condition_str, user_data
)
if_stack.append([matched, matched])
elif upper_line.startswith("ELSE IF("):
if not if_stack:
raise ValueError("AutoScript 语法错误: ELSE IF 前缺少 IF")
cond_end = _findConditionEnd(upper_line)
if cond_end < 0:
raise ValueError("AutoScript 语法错误: ELSE IF 缺少右括号")
condition_str = line[8:cond_end].strip()
_, has_matched = if_stack[-1]
if not has_matched:
matched = AutoScriptEngine._evaluateCondition(
condition_str, user_data
)
if_stack[-1] = [matched, matched]
else:
if_stack[-1][0] = False
elif upper_line == "ELSE":
if not if_stack:
raise ValueError("AutoScript 语法错误: ELSE 前缺少 IF")
_, has_matched = if_stack[-1]
if not has_matched:
if_stack[-1] = [True, True]
else:
if_stack[-1][0] = False
elif upper_line in ("ENDIF", "END IF"):
if not if_stack:
raise ValueError("AutoScript 语法错误: ENDIF/END IF 前缺少 IF")
if_stack.pop()
elif upper_line.startswith("SET "):
should_execute = (
all(ctx[0] for ctx in if_stack) if if_stack else True
)
if should_execute:
AutoScriptEngine._executeSet(line, user_data)
elif upper_line == "PASS":
continue
else:
should_execute = (
all(ctx[0] for ctx in if_stack) if if_stack else True
)
if should_execute:
AutoScriptEngine._executeOperation(line, user_data)
if if_stack:
raise ValueError("AutoScript 语法错误: IF 与 ENDIF/END IF 不匹配")
@staticmethod
def _resolveField(
field_name: str,
user_data: dict
):
upper_name = field_name.upper().strip()
if upper_name == "CURRENT_DATE":
return datetime.now().strftime("%Y-%m-%d")
elif upper_name == "CURRENT_TIME":
return datetime.now().strftime("%H:%M")
elif upper_name == "USERNAME":
return user_data.get("username", "")
elif upper_name == "USER_ENABLE":
return user_data.get("enabled", False)
elif upper_name == "RESERVE_DATE":
return user_data.get("reserve_info", {}).get("date", "")
elif upper_name == "RESERVE_BEGIN_TIME":
return (
user_data
.get("reserve_info", {})
.get("begin_time", {})
.get("time", "")
)
elif upper_name == "RESERVE_END_TIME":
return (
user_data
.get("reserve_info", {})
.get("end_time", {})
.get("time", "")
)
return ""
@staticmethod
def _resolveValue(
value_str: str,
user_data: dict
):
s = value_str.strip()
time_match = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE)
if time_match:
h, m = time_match.group(1), time_match.group(2)
return f"{int(h):02d}:{int(m):02d}"
date_match = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE)
if date_match:
y, mo, d = date_match.group(1), date_match.group(2), date_match.group(3)
return f"{int(y):04d}-{int(mo):02d}-{int(d):02d}"
if s.upper() == ".TRUE.":
return True
if s.upper() == ".FALSE.":
return False
if s.startswith("'") and s.endswith("'"):
inner = s[1:-1].replace("''", "'")
return inner
if s.startswith('"') and s.endswith('"'):
return s[1:-1]
relDate = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE)
if relDate:
days = int(relDate.group(1))
return (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
relTime = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE)
if relTime:
hours = int(relTime.group(1))
return (datetime.now() + timedelta(hours=hours)).strftime("%H:%M")
try:
return int(s)
except ValueError:
pass
try:
return float(s)
except ValueError:
pass
resolved = AutoScriptEngine._resolveField(s, user_data)
return resolved
@staticmethod
def _setField(
field_name: str,
value: str,
user_data: dict
):
upper_name = field_name.upper().strip()
if upper_name == "RESERVE_DATE":
user_data.setdefault("reserve_info", {})["date"] = value
elif upper_name == "RESERVE_BEGIN_TIME":
ri = user_data.setdefault("reserve_info", {})
ri.setdefault("begin_time", {})["time"] = value
elif upper_name == "RESERVE_END_TIME":
ri = user_data.setdefault("reserve_info", {})
ri.setdefault("end_time", {})["time"] = value
elif upper_name == "USERNAME":
user_data["username"] = value
elif upper_name == "USER_ENABLE":
if isinstance(value, bool):
user_data["enabled"] = value
else:
user_data["enabled"] = (str(value).upper() == "TRUE")
@staticmethod
def _evaluateCondition(
condition_str: str,
user_data: dict
) -> bool:
for op, cmp_func in AutoScriptEngine.COMPARE_OPS.items():
if op not in condition_str.upper():
continue
idx = condition_str.upper().find(op)
parts = [condition_str[:idx], condition_str[idx + len(op):]]
if len(parts) != 2:
continue
field_name = parts[0].strip()
value_str = parts[1].strip()
left_val = AutoScriptEngine._resolveField(field_name, user_data)
right_val = AutoScriptEngine._resolveValue(value_str, user_data)
try:
return cmp_func(left_val, right_val)
except TypeError:
raise ValueError(
f"AutoScript 语法错误: 无法比较 "
f"'{field_name}' ({type(left_val).__name__}) "
f"'{value_str}' ({type(right_val).__name__})"
)
return False
@staticmethod
def _executeSet(
line: str,
user_data: dict
):
rest = line[3:].strip()
eq_idx = rest.find("=")
if eq_idx < 0:
return
field_name = rest[:eq_idx].strip()
value_str = rest[eq_idx + 1:].strip()
if not field_name:
return
resolved = AutoScriptEngine._resolveValue(value_str, user_data)
AutoScriptEngine._setField(field_name, resolved, user_data)
@staticmethod
def _executeOperation(
line: str,
user_data: dict
):
parts = line.split()
if len(parts) < 3:
return
field_name = parts[0].upper().strip()
op = parts[1].upper().strip()
raw_value = parts[2].strip()
field_type = AutoScriptEngine._FIELD_TYPE_MAP.get(field_name)
if not field_type:
raise ValueError(
f"AutoScript 语法错误: 未知字段 '{field_name}'"
)
try:
num_value = float(raw_value) if "." in raw_value else int(raw_value)
except (ValueError, TypeError):
raise ValueError(
f"AutoScript 语法错误: 无效操作数 '{raw_value}'"
)
if field_type == "Date":
date_str = AutoScriptEngine._resolveField(field_name, user_data)
if not date_str:
return
try:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
except (ValueError, TypeError):
return
if op == ".ADD.":
date_obj += timedelta(days=num_value)
elif op == ".SUB.":
date_obj -= timedelta(days=num_value)
else:
raise ValueError(
f"AutoScript 语法错误: Date 类型不支持操作 '{op}'"
)
AutoScriptEngine._setField(
field_name, date_obj.strftime("%Y-%m-%d"), user_data
)
elif field_type == "Time":
time_str = AutoScriptEngine._resolveField(field_name, user_data)
if not time_str:
return
try:
time_obj = datetime.strptime(time_str, "%H:%M")
except (ValueError, TypeError):
return
if op == ".ADD.":
time_obj += timedelta(hours=num_value)
elif op == ".SUB.":
time_obj -= timedelta(hours=num_value)
else:
raise ValueError(
f"AutoScript 语法错误: Time 类型不支持操作 '{op}'"
)
AutoScriptEngine._setField(
field_name, time_obj.strftime("%H:%M"), user_data
)
elif field_type in ("String", "Boolean"):
raise ValueError(
f"AutoScript 语法错误: '{field_type}' 类型字段不支持操作运算"
)
else:
raise ValueError(
f"AutoScript 语法错误: 未知字段类型 '{field_type}'"
)
def _findConditionEnd(
upper_line: str
) -> int:
"""
Find the index of the closing parenthesis that matches the
opening parenthesis in a condition expression, handling nested
parentheses and optional ``THEN`` keyword.
Args:
upper_line (str): The uppercased line text containing the
condition, e.g. ``"IF(A .BGT. B) THEN"``.
Returns:
int: Index of the matching ``)``, or ``-1`` if no match
is found.
"""
line = upper_line.rstrip()
if line.endswith(" THEN"):
line = line[:-5].rstrip()
paren_depth = 0
start_found = False
for i, ch in enumerate(line):
if ch == "(":
paren_depth += 1
start_found = True
elif ch == ")":
paren_depth -= 1
if start_found and paren_depth == 0:
return i
return -1
-4
View File
@@ -42,7 +42,6 @@ class JSONReader:
self.__json_data = None self.__json_data = None
self.__read() self.__read()
def __read( def __read(
self self
): ):
@@ -59,7 +58,6 @@ class JSONReader:
except Exception as e: except Exception as e:
raise Exception(f"读取文件时发生未知错误: {e}") from e raise Exception(f"读取文件时发生未知错误: {e}") from e
def read( def read(
self self
) -> bool: ) -> bool:
@@ -70,14 +68,12 @@ class JSONReader:
return False return False
return True return True
def data( def data(
self self
) -> dict: ) -> dict:
return self.__json_data.copy() return self.__json_data.copy()
def path( def path(
self self
) -> str: ) -> str:
-3
View File
@@ -46,7 +46,6 @@ class JSONWriter:
self.__json_data = json_data.copy() if json_data is not None else {} self.__json_data = json_data.copy() if json_data is not None else {}
self.__write() self.__write()
def __write( def __write(
self self
): ):
@@ -63,7 +62,6 @@ class JSONWriter:
except Exception as e: except Exception as e:
raise Exception(f"写入文件时发生未知错误: {e}") from e raise Exception(f"写入文件时发生未知错误: {e}") from e
def write( def write(
self self
) -> bool: ) -> bool:
@@ -74,7 +72,6 @@ class JSONWriter:
return False return False
return True return True
def path( def path(
self self
) -> str: ) -> str:
-2
View File
@@ -5,6 +5,4 @@
- TimerUtils: Timer utils class for the AutoLibrary project. - TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project. - JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project. - JSONWriter: JSON writer class for the AutoLibrary project.
- ConfigUtils: Config utils class for the AutoLibrary project.
- AutoScriptEngine: AutoScript script engine class for the AutoLibrary project.
""" """