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

Compare commits

..

90 Commits

Author SHA1 Message Date
KenanZhu 1cd39ec84c docs(readme): 更新 “后续会有哪些功能?” 部分,可重复性定时任务功能已完成 2026-03-17 16:08:54 +08:00
Kenan Zhu 73aab7b957 feat(LibCheckin, gui.*): 支持校园网环境下图书馆远程签到;定时任务管理支持可重复性定时任务 (#5)
- feat(LibCheckin): 支持校园网环境下图书馆远程签到 
- feat(TimerUtils): 新增重复任务时间计算工具 
- feat(TimerTask): 新增任务执行历史对话框 
- feat(TimerTaskAddDialog): 添加重复任务 UI 支持 
- feat(TimerTaskManageWidget): 实现重复任务执行与历史记录 
- fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局
- fix(ALTimerTaskAddDialog): 修改定时任务时间类型中相对时间控件的布局样式 
- fix(ALTimerTaskAddDialog): 删除定时任务数据中多余的字段 ‘repeat_records’
- fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局 
- style(ALTimerTaskManageWidget): 统一 import 语句的格式 
- refactor(ALTimerTaskItemWidget): 一些变量重构
- optimze(gui): 优化删除按钮样式,使其更加醒目;优化 ALTimerTaskManageWidget 的宽度 
- optimize(gui): 优化定时任务管理功能 
- fix(ALTimerTaskManageWidget): 修复删除任务的信号槽参数传递问题
2026-03-17 16:04:59 +08:00
KenanZhu 0a94c344d5 ci(workflows): 修复 Release 工作流的触发条件
- 当创建 'release/v*' 分支时,自动进行 Release 构建

/! Release 流程必须手动创建分支,工作流结束后会将对应分支提交合并
/! 到 main 分支上,且对应分支会被删除
2026-03-17 15:46:32 +08:00
KenanZhu 68e002ba8e fix(ALTimerTaskManageWidget): 修复删除任务的信号槽参数传递问题
- 修复删除任务的信号槽参数传递问题,此次修复通过 lambda 表达式将当前的 task 作为参数传递,避免了闭包陷阱。
2026-03-17 15:27:03 +08:00
KenanZhu 94dc22819f optimize(gui): 优化定时任务管理功能
- 优化任务历史查看对话框的界面布局和交互体验
- 新增任务状态枚举值以支持更完整的状态管理
- 统一重复任务执行后的历史记录处理逻辑
- 增强删除任务时的确认机制,删除可重复任务前展示详细执行记录
- 完善批量清除任务的验证流程,检查运行中任务并确认重复任务删除
2026-03-17 14:51:55 +08:00
KenanZhu d55d2075cb optimze(gui): 优化删除按钮样式,使其更加醒目;优化 ALTimerTaskManageWidget 的宽度
- 优化了 ALConfigWidget, ALTimerTaskManageWidget 中的删除按钮样式(字体颜色更改为红色),使其更加醒目
- 优化了 ALTimerTaskManageWidget 的宽度,使其适应内容宽度
2026-03-17 14:46:19 +08:00
KenanZhu 82744e3a2d refactor(ALTimerTaskItemWidget): 一些变量重构 2026-03-17 14:42:47 +08:00
KenanZhu 67493349dd style(ALTimerTaskManageWidget): 统一 import 语句的格式
- 对 gui.ALTimerTaskAddDialog 的 import 语句进行格式化
2026-03-17 14:42:07 +08:00
KenanZhu 0aea9b1540 fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局
- 对 (b73242be00) 的补充提交
2026-03-17 14:39:01 +08:00
KenanZhu c02c6ddbe3 fix(ALTimerTaskAddDialog): 删除定时任务数据中多余的字段 ‘repeat_records’ 2026-03-17 14:37:33 +08:00
KenanZhu c679a1c79e fix(ALTimerTaskAddDialog): 修改定时任务时间类型中相对时间控件的布局样式
- 由栅格布局改为水平布局,该区域的高度与绝对时间控件的高度一致
2026-03-17 14:35:47 +08:00
KenanZhu b73242be00 fix(ALTimerTaskAddDialog): 修改添加定时任务对话框的重复选项的 Label 描述和布局 2026-03-17 14:33:37 +08:00
KenanZhu 9accf5ddc1 ci(workflows): 添加 push 触发器 2026-03-16 21:20:54 +08:00
KenanZhu 883859d1f9 feat(TimerTaskManageWidget): 实现重复任务执行与历史记录
- onTimerTaskIsExecuted/onTimerTaskIsError 添加历史记录
- 历史记录包含:execute_time、executed_time、result、duration
- 重复任务执行后自动计算并更新下次执行时间
2026-03-16 21:17:48 +08:00
KenanZhu f37bcf836b feat(TimerTaskAddDialog): 添加重复任务 UI 支持
- UI 添加重复配置控件:复选框、周一到周日复选框
- 新增 onRepeatCheckBoxToggled 槽函数控制日期选择显示
- getTimerTask 支持提取重复配置(日期、时分秒)
- 调用 TimerUtils 计算首次执行时间
- 重构导入语句格式
2026-03-16 21:16:46 +08:00
KenanZhu b0d1c0e99e feat(TimerTask): 新增任务执行历史对话框
- 新增 ALTimerTaskHistoryDialog 显示重复任务执行历史
- 支持查看执行时间、运行结果、运行耗时
- 提供清空历史记录功能
- 表格显示:执行时间、结果、耗时(秒/s)、uuid
2026-03-16 21:15:56 +08:00
KenanZhu 5af6120be8 feat(TimerUtils): 新增重复任务时间计算工具
- 新增 TimerUtils.calculateNextRepeatTime 方法
- 支持基于重复日期和目标时间计算下次执行时间
- 如果当天在重复日期且目标时间未过,则返回今天;否则查找下一个匹配日期
2026-03-16 21:15:15 +08:00
KenanZhu 60e055f6bb docs(readme): 添加 build-test 状态图标 2026-03-16 17:08:47 +08:00
KenanZhu 01e8100774 feat(LibCheckin): 支持校园网环境下图书馆远程签到
- 新增 __enableCheckinBtn 方法,通过 JavaScript 移除签到按钮的 disabled 属性
- 在检测到签到按钮不可用时,自动尝试启用按钮而非直接失败
- 支持在校园网环境下无需连接图书馆网络即可完成签到
- 优化签到流程的用户提示信息"
2026-03-16 16:55:52 +08:00
Kenan Zhu cf8493565e ci(workflows): 修改一些字符格式 (#2) 2026-03-16 16:40:13 +08:00
KenanZhu 24bb76d039 ci(workflows): 修改一些字符格式 2026-03-16 16:26:34 +08:00
KenanZhu 7111411115 ci(workflows): 优化 CI/CD 工作流配置
- 新增 build-test.yml 用于PR测试构建
- 升级 actions/checkout 和 actions/upload-artifact 到 v6 版本
- 完善 release.yml 的清理流程和摘要输出
2026-03-16 15:59:39 +08:00
KenanZhu 7df6a9157d refactor(LibReserve, LibRenew): 提取时间选择公共逻辑到 LibTimeSelector 基类
将 LibReserve 和 LibRenew 中重复的时间转换和选择逻辑提取到
LibTimeSelector 基类,消除代码重复,提升可维护性。

主要变更:
- 新增 LibTimeSelector 基类,提供时间转换和最佳时间选择算法
- LibReserve 和 LibRenew 继承 LibTimeSelector,移除重复代码
- 拆分过长方法,提升代码可读性
- 修正方法命名 __selectNearstTime -> __selectNearestTime

同时修复续约功能业务逻辑漏洞:
- 新增续约时间上限校验,防止续约时间超过图书馆闭馆时间(23:30)
2026-03-14 14:48:35 +08:00
KenanZhu ebe3910df5 fix(AutoLib): 修复自动预约,签到和续约功能的顺序处理逻辑问题
边缘情况下,即用户当前的预约时间满足签到或者续约的时间范围要求时,预期的处理顺序是先进行预约,再进行签到或者续约。
该提交修复了对这种情况的处理逻辑,确保先进行预约,再进行签到或者续约。
2026-03-10 11:00:01 +08:00
KenanZhu 84367e4abe chore(*): 更新网站地址为 www.autolibrary.kenanzhu.com 2026-03-10 10:59:13 +08:00
KenanZhu 3a50991860 fix(ALMainWindow): 修复程序最小化到托盘图标后,退出菜单异常处理问题 2026-03-10 10:58:27 +08:00
KenanZhu e4482b01da fix(ALMainWindow): 修复托盘图标初始化问题
1. 修复托盘图标初始化上下文菜单的重复调用问题
2. 修复托盘图标初始化忘记更改消息方法的问题
2026-03-05 07:55:36 +08:00
KenanZhu c06e0e05da fix(ALMainWindow): 修复定时任务的消息通知图标与运行状态不一致的问题 2026-03-05 07:54:18 +08:00
KenanZhu ff083884b6 style(utils.ConfigManager): 添加一些注释,并为 getBaseConfigDir 添加文档字符串。 2026-03-04 23:53:31 +08:00
KenanZhu 9ae89b61a4 chore(utils.ConfigManager): 将 ConfigManager 类的 appDir 重命名为 configDir 2026-03-04 23:52:28 +08:00
KenanZhu 2152cc46a3 style(*): 修改 ConfigManager 模块的 import 方式,并移除未使用的 import 语句 2026-03-04 23:52:01 +08:00
github-actions[bot] 95a3ae2a24 chore(release): v1.1.0 [auto release commit] 2026-02-26 15:04:42 +00:00
KenanZhu 896242a1e3 fix(Main, ALConfigWidget): 修复配置文件初始化问题 2026-02-26 22:59:26 +08:00
KenanZhu fd96fc235e ci(workflows): 修复 build.yml 中 Generate 'Main.spec' 步骤中的 name 参数 2026-02-26 21:27:35 +08:00
KenanZhu 25aab588a8 feat(utils): 添加 ConfigManager 与 JSON 配置读写,替换旧实现
add:
- src/utils/ConfigManager.py
- src/utils/JSONReader.py
- src/utils/JSONWriter.py
remove:
- src/utils/ConfigReader.py
- src/utils/ConfigWriter.py
refactor:
- 更新调用方以使用 ConfigManager / JSONReader / JSONWriter(见 ALConfigWidget.py、ALMainWindow.py、ALTimerTaskManageWidget.py、ALMainWorkers.py 等)
- 统一方法命名(initlize* -> initialize*)、改进错误提示与配置路径管理

BREAKING CHANGE: 删除 ConfigReader/ConfigWriter,外部调用需改为 JSONReader/JSONWriter 或通过 ConfigManager 访问配置
2026-02-26 21:18:18 +08:00
KenanZhu 6e1b8e6b10 ci(workflows): 修改 build.yml 中 PyInstaller 打包参数,发布压缩包修改为为文件夹模式 2026-02-24 17:39:49 +08:00
KenanZhu 5f2327cf61 style(gui.*): 修改一些 import 顺序和格式 2026-02-23 22:26:52 +08:00
KenanZhu 96e7adabb0 docs(readme): 修改自述文件 2026-02-23 00:07:58 +08:00
KenanZhu 42afbbe694 docs(readme): 修改自述文件 2026-02-22 23:19:42 +08:00
KenanZhu 3777970332 docs(readme): 修改自述文件,完善使用说明 2026-02-22 00:24:47 +08:00
KenanZhu 9fb28e1368 ci(release.yml): 修改 release.yml 中发布说明的默认内容 2026-02-21 23:32:19 +08:00
KenanZhu 4aeca08ce8 chore(ALMainWindow, ALMainWorkers): 修改统一部分函数和变量的命名 2026-02-21 23:18:17 +08:00
KenanZhu a1ff85256a refactor(ALConfigWidget, ALTimerTaskManageWidget): 优化界面的错误异常处理 2026-02-21 15:38:56 +08:00
KenanZhu 169de92d5b chore(ALConfigWidget): 删除了未使用的方法 def defaultGroup() 和 def defaultUsers() 2026-02-21 15:10:36 +08:00
KenanZhu 5ca4a14a14 chore(*): 更改一些界面类方法,局部变量和信号的命名:
(ALConfigWidget):
def initlizeDefaultConfigPaths() 中 script_path 和 script_dir 分别改为 executable_path 和 executable_dir
def fillUserTree() 更改为 def setUsersToTreeWidget()
def collectUserFromUserInfoWidget() 更改为 def collectUserFromWidget()
def collectUserConfigFromUserTreeWidget 更改为 def collectUsersFromTreeWidget()
交换了一些方法的位置

(ALSeatMapSelectDialog):
信号 seatMapSelectDialogClosed 改为 seatMapSelectDialogIsClosed

(ALTimerTaskManageWidget):
信号 timerTaskManageWidgetClosed 改为 timerTaskManageWidgetIsClosed

(ALMainWindow):
def __init__() 中 script_path 和 script_dir 分别改为 executable_path 和 executable_dir
更改 ALSeatMapSelectDialog 和 ALTimerTaskManageWidget 中相关的信号命名
2026-02-21 14:26:54 +08:00
KenanZhu 155b3fe3ca style(LibRenew): 删除多余注释,修改部分注释的格式 2026-02-19 17:09:15 +08:00
KenanZhu 99d454a566 refactor(LibChecker, AutoLib): 重构 LibChecker 类中 canRenew 方法的返回值类型:
将 canRenew 方法的返回值类型指定为 tuple(bool, dict),并随之修改返回值以及调用
模块的调用逻辑。
2026-02-19 17:05:42 +08:00
github-actions[bot] 3963b3f2e6 chore(release): v1.0.5 [auto release commit] 2026-02-16 07:04:57 +00:00
KenanZhu f2a05809bd ci(batchs): 修复编译脚本中的路径问题 2026-02-16 15:00:35 +08:00
KenanZhu b55a0c06a5 refactor(ALConfigWidget, ALTimerTaskManageWidget): 重构配置和定时器任务管理窗口的配置显式初始化
修改后配置文件的初始化将不再通过 QMessageBox 提示用户,界面将只在初始化失败时显示错误信息。
2026-02-16 14:17:58 +08:00
KenanZhu 2496c4e367 fix(ALMainWindow): 修复配置按钮状态问题 2026-02-16 13:02:40 +08:00
KenanZhu de30559af1 chore(ALTimerTaskManageWidget): 更改信号函数命名 2026-02-16 13:02:01 +08:00
KenanZhu e1c2efc8c0 chore(utils): 配置文件读写器异常改为中文 2026-02-16 13:01:34 +08:00
KenanZhu 26a70cdceb ci(batchs): 修复 *.sh 编译脚本中项目路径问题 2026-02-11 20:17:04 +08:00
KenanZhu ce14be2555 chore(*): 重构项目文件目录结构
- 将 src/gui 目录下的 Qt 资源文件移动到 src/gui/resources 目录下
- 将 src/gui 目录下的 Qt UI 设计文件移动到 src/gui/resources/ui 目录下
- 将 src/gui/icons 目录下的图标文件移动到 src/gui/resources/icons 目录下
- 将 src/gui/translators 目录下移动到 src/gui/resources/translators 目录下
- 将 src/gui/configs 目录移动到 templates 目录下
- 将 document, driver, model 目录重命名为 manuals, drivers, models
- 由于上述目录移动和重命名,相应的更改了代码和批处理脚本中的文件路径
2026-02-11 20:00:51 +08:00
KenanZhu eda16f01f1 refactor(gui): chore(gui): 对部分界面类进行重构,将 ALSeatMapView 提取到单独文件,将 ALSeatMapWidget 重替换为 ALSeatMapSelectDialog
: 对文件名进行重命名,以更贴近各自功能,ALTimerTaskWidget 重命名为 ALTimerTaskManageWidget;ALAddTimerTaskDialog 重命名为 ALTimerTaskAddDialog
2026-02-03 15:03:33 +08:00
KenanZhu 22f806bfb0 chore(*): 更新有关帮助手册的链接 2026-01-30 22:10:00 +08:00
KenanZhu d26852eaaf chore(*): 更新网站地址为 www.autolibrary.top 2026-01-30 22:04:29 +08:00
KenanZhu 2ffe620532 optimize(AutoLib): 优化图书馆登录页面加载超时处理逻辑 2026-01-26 16:11:26 +08:00
KenanZhu fe42d3cd98 fix(AutoLib): 修复浏览器驱动初始化的异常控制 2026-01-26 16:10:15 +08:00
KenanZhu 0795939aa3 docs(readme): 交换使用方法与注意事项的顺序 2026-01-23 17:57:41 +08:00
KenanZhu 8b6baf9b6a refactor(ALMainWindow): 重构主窗口类的消息队列能力,修改为直接从 MsgBase 继承 2026-01-20 17:45:32 +08:00
KenanZhu 7098d7075f refactor(ALMainWorkers): 重构主工作线程的父类初始化方式 2026-01-20 17:43:52 +08:00
KenanZhu be3942ea2f optimize(MsgBase): 优化消息队列能力基类,增加小数秒精度时间戳,移除无用方法 '_inputMsg' 2026-01-20 17:41:34 +08:00
KenanZhu 7e3a089e21 refactor(gui.ALSeatMapWidget): 重构座位选图控件
将座位图提取为 ALSeatMapView 类,并添加缩放限制
2026-01-18 13:50:48 +08:00
github-actions[bot] f3d68c40cb chore(release): v1.0.4 [auto release commit] 2026-01-17 18:18:22 +00:00
KenanZhu 0ceff677e4 docs(readme): 更新自述文档内容 2026-01-18 02:14:01 +08:00
KenanZhu 6f6b415bff refactor(ALMainWindow, ALMainWorkers): 重构 Qt 信号函数的命名 2026-01-18 02:08:12 +08:00
KenanZhu 735f31830d refactor(gui.*): 统一界面控件颜色风格 2026-01-18 02:08:12 +08:00
KenanZhu 7be5afeae1 style(gui.ALSeatFrame): 一些格式问题 2026-01-18 02:08:12 +08:00
KenanZhu 3d6978c9c2 optimize(gui.*): 优化界面组件的布局和样式 2026-01-18 02:08:12 +08:00
github-actions[bot] db7a868598 chore(release): v1.0.3 [auto release commit] 2026-01-17 17:52:03 +00:00
KenanZhu f1e0334ce3 docs(MsgBase, LibOperator): 添加并完善类文档注释 2026-01-16 23:41:25 +08:00
KenanZhu b9411261ea style(ALMainWorkers): 一些格式更改 2026-01-16 23:25:42 +08:00
KenanZhu fa737711d4 optimize(ConfigReader, ConfigWriter): 优化配置文件读写类逻辑,完善异常处理,添加注释文档 2026-01-16 23:23:03 +08:00
KenanZhu 79e2128fca style(operators.*): 显式指定浏览器驱动类型为 WebDriver 2026-01-16 23:21:36 +08:00
KenanZhu 128c8e7a83 style(*): 移除未使用的 import 语句 2026-01-16 22:37:26 +08:00
KenanZhu 6474f6e3bb style(*): 格式化一些界面类的构造函数 2026-01-16 22:33:01 +08:00
KenanZhu ba60a5d884 style(comment): 修改一些注释格式 2026-01-15 17:08:54 +08:00
KenanZhu 4d8f8130dc chore(operators.__init__): 添加 LibChecker 类的简介 2026-01-13 22:40:45 +08:00
KenanZhu eba99cab9f fix(ALSeatMapWidget): 修复座位图选择的确定取消逻辑 2026-01-13 22:01:16 +08:00
KenanZhu aa7a806ff7 fix(gui): 修复一些界面问题 2026-01-12 14:22:20 +08:00
KenanZhu bb180f8c8e fix(ALConfigWidget, LibReserve): 修改二楼楼层区域名称
将 二层外环 改为 二层西区
2026-01-09 14:06:36 +08:00
KenanZhu 107ed41b58 chore(*): 更新 license 和版权信息为 2025 - 2026 年 2026-01-09 14:00:25 +08:00
github-actions[bot] 43b87db4eb chore(release): v1.0.2 [auto release commit] 2026-01-05 04:05:04 +00:00
KenanZhu ae23f65e5a fix(AutoLib): 修复并完善对不同浏览器驱动的支持,目前支持的浏览器驱动为 Edge、Chrome、Firefox
之前的代码只支持 Edge 浏览器驱动,现在完善了对 Chrome、Firefox 浏览器驱动的支持
2026-01-05 11:59:33 +08:00
KenanZhu a7b9c340ae refactor(ALConfigWidget): 初始化的默认浏览器驱动路径改为空 2026-01-05 11:58:15 +08:00
KenanZhu 96d733d2ed fix(ALConfigWidget): 修复配置界面错误字符 2026-01-05 11:43:16 +08:00
KenanZhu 65cb951ada ci(workflow): 优化 update-version.yml 中的版本信息更新逻辑
对 ALVersionInfo.py 文件的更新逻辑进行差异化处理,分别为 commit-release 和 build 阶段生成不同的 artifact:

* 针对 commit-release 阶段:将所有与构建(build)相关的字段值设置为 'local' 或 'null'
* 针对 build 阶段:保留完整的版本信息字段

这种差异化处理确保其后续提交的 release commit 不会包含构建相关的版本信息。
2026-01-04 10:05:57 +08:00
KenanZhu 94ce3433a3 ci(workflows): 重构 CI/CD 工作流执行配置 2026-01-03 14:33:49 +08:00
71 changed files with 3667 additions and 1823 deletions
-236
View File
@@ -1,236 +0,0 @@
name: Build and Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
update-version-info:
uses: ./.github/workflows/update-version-info.yml
permissions:
contents: write
with:
tag_name: ${{ github.ref_name }}
ref: ${{ github.ref }}
commit-and-move-tag:
needs: update-version-info
if: ${{ needs.update-version-info.outputs.has_changes == 'true' }}
uses: ./.github/workflows/commit-and-move-tag.yml
permissions:
contents: write
with:
tag_name: ${{ needs.update-version-info.outputs.tag_name }}
version: ${{ needs.update-version-info.outputs.version }}
file_path: src/gui/ALVersionInfo.py
build-and-release:
runs-on: windows-latest
needs: [update-version-info, commit-and-move-tag]
if: always() && needs.update-version-info.result == 'success'
permissions:
contents: write
steps:
- name: Checkout code with updated version info
uses: actions/checkout@v4
with:
ref: main
- name: Get version info from previous job
id: get_tag
run: |
$tagName = "${{ needs.update-version-info.outputs.tag_name }}"
$version = "${{ needs.update-version-info.outputs.version }}"
echo "TAG_NAME=$tagName" >> $env:GITHUB_OUTPUT
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
Write-Host "✓ Tag: $tagName"
Write-Host "✓ Version: $version"
shell: pwsh
- name: Verify ALVersionInfo.py was updated
run: |
$versionInfoFile = "src/gui/ALVersionInfo.py"
Write-Host "Verifying $versionInfoFile content:"
Write-Host "================================"
Get-Content $versionInfoFile | Write-Host
Write-Host "================================"
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
- name: Fix ddddocr compatibility and copy model files
run: |
$ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))"
Write-Host "ddddocr package location: $ddddocrPath"
$initFile = Join-Path $ddddocrPath "__init__.py"
if (Test-Path $initFile) {
Write-Host "Fixing ddddocr compatibility in: $initFile"
(Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile
Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
} else {
Write-Error "✗ ddddocr __init__.py not found"
exit 1
}
if (-not (Test-Path "model")) {
New-Item -ItemType Directory -Path "model" | Out-Null
Write-Host "✓ Created model directory"
}
$onnxSource = Join-Path $ddddocrPath "common.onnx"
$onnxDest = "model/common.onnx"
if (Test-Path $onnxSource) {
Copy-Item $onnxSource $onnxDest -Force
Write-Host "✓ Copied ONNX model from: $onnxSource"
Write-Host "✓ ONNX model copied to: $onnxDest"
} else {
Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource"
exit 1
}
if (Test-Path $onnxDest) {
$fileSize = (Get-Item $onnxDest).Length / 1MB
Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)"
} else {
Write-Error "✗ Failed to copy model file"
exit 1
}
shell: pwsh
- name: Compile Qt UI files
run: |
cd src/gui/batchs
./compile_ui.bat
shell: cmd
- name: Compile Qt Resource files
run: |
cd src/gui/batchs
./compile_rc.bat
shell: cmd
- name: Generate Main.spec dynamically
run: |
$version = "${{ steps.get_tag.outputs.VERSION }}"
$exeName = "AutoLibrary-$version"
Write-Host "Generating Main.spec for version: $version"
Write-Host "Executable name: $exeName"
$specLines = @(
"# -*- mode: python ; coding: utf-8 -*-"
""
""
"a = Analysis("
" ['src\\Main.py'],"
" pathex=[],"
" binaries=[],"
" datas=["
" ('model\\common.onnx', 'ddddocr'),"
" ('src\\gui\\icons\\AutoLibrary_32x32.ico', 'gui\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
" hooksconfig={},"
" runtime_hooks=[],"
" excludes=[],"
" noarchive=False,"
" optimize=0,"
")"
"pyz = PYZ(a.pure)"
""
"exe = EXE("
" pyz,"
" a.scripts,"
" a.binaries,"
" a.datas,"
" [],"
" name='$exeName',"
" debug=False,"
" bootloader_ignore_signals=False,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" runtime_tmpdir=None,"
" console=False,"
" disable_windowed_traceback=False,"
" argv_emulation=False,"
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\icons\\AutoLibrary_32x32.ico'],"
")"
)
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec generated successfully"
Write-Host "`n=== Generated Main.spec ==="
Get-Content "Main.spec" | Write-Host
Write-Host "==========================`n"
shell: pwsh
- name: Build with PyInstaller
run: |
pyinstaller Main.spec
- name: Create Release Archive
run: |
$tagName = "${{ steps.get_tag.outputs.TAG_NAME }}"
$version = "${{ steps.get_tag.outputs.VERSION }}"
$exeName = "AutoLibrary-$version.exe"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
Write-Host "Looking for executable: dist/$exeName"
if (Test-Path "dist/$exeName") {
Compress-Archive -Path "dist/$exeName" -DestinationPath $zipName
Write-Host "✓ Created release archive: $zipName"
} else {
Write-Error "✗ Executable not found: dist/$exeName"
Write-Host "Files in dist directory:"
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
exit 1
}
shell: pwsh
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_tag.outputs.TAG_NAME }}
name: AutoLibrary ${{ steps.get_tag.outputs.TAG_NAME }}
files: |
AutoLibrary.${{ steps.get_tag.outputs.TAG_NAME }}-windows-x86_64.zip
draft: false
prerelease: false
generate_release_notes: true
body: |
### 下载获取
- **Windows x86_64**: `AutoLibrary.${{ steps.get_tag.outputs.TAG_NAME }}-windows-x86_64.zip`
### 如何使用
1. 下载 `AutoLibrary.${{ steps.get_tag.outputs.TAG_NAME }}-windows-x86_64.zip` 文件
2. 解压到任意目录
3. 下载对应浏览器的驱动文件
4. 运行 `AutoLibrary-${{ steps.get_tag.outputs.VERSION }}.exe` (首次运行会初始化配置文件)
5. 按照提示操作即可
更多详情请访问 [AutoLibrary 网站](http://autolibrary.cv) 和查看 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)
---
**完整更新日志见下方自动生成的 Release Notes**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+221
View File
@@ -0,0 +1,221 @@
name: Build Test
# This workflow builds the application for testing purposes.
# It is triggered when a pull request is opened, synchronized, or reopened against the main branch.
on:
push:
branches:
- main
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
# Allow manual trigger for testing
workflow_dispatch:
#
# Build Windows
#
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- name: Get version info
id: get_version
run: |
$version = "pr-test"
$tagName = "pr-test"
Write-Host "✓ Mode: Pull Request Test Build"
Write-Host "✓ Tag: $tagName"
Write-Host "✓ Version: $version"
"VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"TAG_NAME=$tagName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
- name: Solve ddddocr compatibility and copy model files
run: |
$ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))"
Write-Host "ddddocr package location: $ddddocrPath"
$initFile = Join-Path $ddddocrPath "__init__.py"
if (Test-Path $initFile) {
Write-Host "Fixing ddddocr compatibility in: $initFile"
(Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile
Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
} else {
Write-Error "✗ ddddocr __init__.py not found"
exit 1
}
if (-not (Test-Path "models")) {
New-Item -ItemType Directory -Path "models" | Out-Null
Write-Host "✓ Created models directory"
}
$onnxSource = Join-Path $ddddocrPath "common.onnx"
$onnxDest = "models/common.onnx"
if (Test-Path $onnxSource) {
Copy-Item $onnxSource $onnxDest -Force
Write-Host "✓ Copied ONNX model from: $onnxSource"
Write-Host "✓ ONNX model copied to: $onnxDest"
} else {
Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource"
exit 1
}
if (Test-Path $onnxDest) {
$fileSize = (Get-Item $onnxDest).Length / 1MB
Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)"
} else {
Write-Error "✗ Failed to copy model file"
exit 1
}
shell: pwsh
- name: Compile Qt Resource files
run: |
cd batchs
./compile_rc.bat
shell: cmd
- name: Compile Qt UI files
run: |
cd batchs
./compile_ui.bat
shell: cmd
- name: Generate 'Main.spec'
run: |
$version = "${{ steps.get_version.outputs.VERSION }}"
$exeName = "AutoLibrary-$version"
Write-Host "Generating Main.spec for version: $version"
Write-Host "Executable name: $exeName"
$specLines = @(
"# -*- mode: python ; coding: utf-8 -*-"
""
""
"a = Analysis("
" ['src\\Main.py'],"
" pathex=[],"
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
" hooksconfig={},"
" runtime_hooks=[],"
" excludes=[],"
" noarchive=False,"
" optimize=0,"
")"
"pyz = PYZ(a.pure)"
""
"exe = EXE("
" pyz,"
" a.scripts,"
" name='AutoLibrary',"
" debug=False,"
" bootloader_ignore_signals=False,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" runtime_tmpdir=None,"
" console=False,"
" disable_windowed_traceback=False,"
" argv_emulation=False,"
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
")"
""
"coll = COLLECT("
" exe,"
" a.binaries,"
" a.datas,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" name='$exeName'"
")"
)
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
shell: pwsh
- name: Build with PyInstaller
run: |
pyinstaller Main.spec
- name: Zip windows release
id: zip_release
run: |
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
$version = "${{ steps.get_version.outputs.VERSION }}"
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
Compress-Archive -Path "$distDir/*" -DestinationPath $zipName
Write-Host "✓ Created release archive (directory mode): $zipName"
} else {
Write-Error "✗ Distribution directory not found: $distDir"
Write-Host "Files in dist directory:"
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
exit 1
}
shell: pwsh
- name: Archive artifacts
uses: actions/upload-artifact@v6
with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: |
${{ steps.zip_release.outputs.ZIP_NAME }}
retention-days: 7
- name: Upload build summary
run: |
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Pull Request #${{ github.event.pull_request.number }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Branch: ${{ github.event.pull_request.head.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
+257
View File
@@ -0,0 +1,257 @@
name: Build
# This workflow compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
#
# It is triggered when called by the release workflow.
on:
workflow_call:
inputs:
tag_name:
description: 'Tag name'
required: false
type: string
version:
description: 'Version number'
required: false
type: string
is_test:
description: 'Whether this is a test build (not a release)'
required: false
type: string
default: 'true'
#
# Build Windows
#
jobs:
build-windows:
runs-on: windows-latest
outputs:
tag_name: ${{ steps.get_version.outputs.TAG_NAME }}
version: ${{ steps.get_version.outputs.VERSION }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
# here we download the build version of ALVersionInfo.py from artifacts
# and replace the committed version
- name: Download build version of ALVersionInfo.py
uses: actions/download-artifact@v6
with:
name: updated-version-info-for-build
path: src/gui/
- name: Get version info
id: get_version
run: |
$isTest = "${{ inputs.is_test }}"
if ($isTest -eq "true") {
$version = "test"
$tagName = "test"
Write-Host "✓ Mode: Test Build"
} else {
$version = "${{ inputs.version }}"
$tagName = "${{ inputs.tag_name }}"
if ([string]::IsNullOrEmpty($version)) {
$version = "test"
$tagName = "test"
Write-Host "✓ Mode: Independent Build (No inputs provided)"
}
}
Write-Host "✓ Tag: $tagName"
Write-Host "✓ Version: $version"
"VERSION=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"TAG_NAME=$tagName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
shell: pwsh
- name: Verify 'ALVersionInfo.py' was updated
run: |
$versionInfoFile = "src/gui/ALVersionInfo.py"
Write-Host "Verifying $versionInfoFile content:"
Write-Host "=================================="
Get-Content $versionInfoFile | Write-Host
Write-Host "=================================="
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirement.txt
- name: Solve ddddocr compatibility and copy model files
run: |
$ddddocrPath = python -c "import ddddocr, os; print(os.path.dirname(ddddocr.__file__))"
Write-Host "ddddocr package location: $ddddocrPath"
$initFile = Join-Path $ddddocrPath "__init__.py"
if (Test-Path $initFile) {
Write-Host "Fixing ddddocr compatibility in: $initFile"
(Get-Content $initFile) -replace 'Image\.ANTIALIAS', 'Image.Resampling.LANCZOS' | Set-Content $initFile
Write-Host "✓ Fixed: Image.ANTIALIAS -> Image.Resampling.LANCZOS"
} else {
Write-Error "✗ ddddocr __init__.py not found"
exit 1
}
if (-not (Test-Path "models")) {
New-Item -ItemType Directory -Path "models" | Out-Null
Write-Host "✓ Created models directory"
}
$onnxSource = Join-Path $ddddocrPath "common.onnx"
$onnxDest = "models/common.onnx"
if (Test-Path $onnxSource) {
Copy-Item $onnxSource $onnxDest -Force
Write-Host "✓ Copied ONNX model from: $onnxSource"
Write-Host "✓ ONNX model copied to: $onnxDest"
} else {
Write-Error "✗ ONNX model not found in ddddocr package: $onnxSource"
exit 1
}
if (Test-Path $onnxDest) {
$fileSize = (Get-Item $onnxDest).Length / 1MB
Write-Host "✓ Model file verified: $onnxDest (Size: $([math]::Round($fileSize, 2)) MB)"
} else {
Write-Error "✗ Failed to copy model file"
exit 1
}
shell: pwsh
- name: Compile Qt Resource files
run: |
cd batchs
./compile_rc.bat
shell: cmd
- name: Compile Qt UI files
run: |
cd batchs
./compile_ui.bat
shell: cmd
- name: Generate 'Main.spec'
run: |
$version = "${{ steps.get_version.outputs.VERSION }}"
$exeName = "AutoLibrary-$version"
Write-Host "Generating Main.spec for version: $version"
Write-Host "Executable name: $exeName"
$specLines = @(
"# -*- mode: python ; coding: utf-8 -*-"
""
""
"a = Analysis("
" ['src\\Main.py'],"
" pathex=[],"
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
" hooksconfig={},"
" runtime_hooks=[],"
" excludes=[],"
" noarchive=False,"
" optimize=0,"
")"
"pyz = PYZ(a.pure)"
""
"exe = EXE("
" pyz,"
" a.scripts,"
" name='AutoLibrary',"
" debug=False,"
" bootloader_ignore_signals=False,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" runtime_tmpdir=None,"
" console=False,"
" disable_windowed_traceback=False,"
" argv_emulation=False,"
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
")"
""
"coll = COLLECT("
" exe,"
" a.binaries,"
" a.datas,"
" strip=False,"
" upx=True,"
" upx_exclude=[],"
" name='$exeName'"
")"
)
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
shell: pwsh
- name: Build with PyInstaller
run: |
pyinstaller Main.spec
- name: Zip windows release
id: zip_release
run: |
$tagName = "${{ steps.get_version.outputs.TAG_NAME }}"
$version = "${{ steps.get_version.outputs.VERSION }}"
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
Compress-Archive -Path "$distDir/*" -DestinationPath $zipName
Write-Host "✓ Created release archive (directory mode): $zipName"
} else {
Write-Error "✗ Distribution directory not found: $distDir"
Write-Host "Files in dist directory:"
Get-ChildItem "dist" | ForEach-Object { Write-Host " - $($_.Name)" }
exit 1
}
shell: pwsh
- name: Archive artifacts
uses: actions/upload-artifact@v6
with:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: |
${{ steps.zip_release.outputs.ZIP_NAME }}
retention-days: ${{ github.event_name != 'workflow_call' && 7 || 90 }}
- name: Upload build summary
if: ${{ github.event_name != 'workflow_call' }}
run: |
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Ref: ${{ github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
-102
View File
@@ -1,102 +0,0 @@
name: Commit and Move Tag
on:
workflow_call:
inputs:
tag_name:
description: 'Tag name to move (e.g., v1.0.0)'
required: true
type: string
version:
description: 'Version number for commit message'
required: true
type: string
file_path:
description: 'File path to commit'
required: true
type: string
outputs:
new_commit_sha:
description: 'The new commit SHA after moving the tag'
value: ${{ jobs.commit-and-move-tag.outputs.new_commit_sha }}
jobs:
commit-and-move-tag:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
new_commit_sha: ${{ steps.commit_info.outputs.commit_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Download modified file
uses: actions/download-artifact@v4
with:
name: updated-version-info
path: downloaded-file/
- name: Replace file with updated version
run: |
FILE_PATH="${{ inputs.file_path }}"
FILE_NAME=$(basename "$FILE_PATH")
TARGET_DIR=$(dirname "$FILE_PATH")
mkdir -p "$TARGET_DIR"
cp "downloaded-file/$FILE_NAME" "$FILE_PATH"
echo "✓ File replaced: $FILE_PATH"
echo ""
echo "=== Updated file content ==="
cat "$FILE_PATH"
echo "============================"
- name: Commit changes
id: commit_changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
FILE_PATH="${{ inputs.file_path }}"
VERSION="${{ inputs.version }}"
if [ ! -f "$FILE_PATH" ]; then
echo "✗ Error: File $FILE_PATH not found"
exit 1
fi
git add "$FILE_PATH"
git commit -m "chore(release): v${VERSION} [auto release commit]"
echo "✓ Changes committed"
- name: Push to main branch
run: |
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
if [ -z "$MAIN_BRANCH" ]; then
MAIN_BRANCH="main"
fi
echo "Pushing to branch: ${MAIN_BRANCH}"
git push origin HEAD:${MAIN_BRANCH}
echo "✓ Changes pushed to ${MAIN_BRANCH}"
- name: Move tag to new commit
run: |
TAG_NAME="${{ inputs.tag_name }}"
echo "Moving tag ${TAG_NAME} to the new commit..."
git tag -f ${TAG_NAME}
git push origin ${TAG_NAME} --force
echo "✓ Tag ${TAG_NAME} moved to commit $(git rev-parse --short HEAD)"
- name: Output commit info
id: commit_info
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✓ New commit SHA: $COMMIT_SHA"
+153
View File
@@ -0,0 +1,153 @@
name: Commit Release
# This workflow commits version changes in 'ALVersionInfo.py' (get from artifacts) and
# creates/moves the release tag to this new release commit.
#
# It is triggered when called by the release workflow.
on:
workflow_call:
inputs:
tag_name:
description: 'Tag name to create/move (e.g., v1.0.0 or v1.0.0-rc1)'
required: true
type: string
version:
description: 'Version number for commit message'
required: true
type: string
file_path:
description: 'File path to commit'
required: true
type: string
create_tag:
description: 'Whether to create new tag (true) or move existing tag (false)'
required: false
type: string
default: 'false'
is_rc:
description: 'Whether this is a release candidate (pre-release)'
required: false
type: string
default: 'false'
ref:
description: 'Git ref to checkout (release branch)'
required: true
type: string
outputs:
tag_name:
description: 'The tag name created/moved'
value: ${{ inputs.tag_name }}
version:
description: 'Version number for commit message'
value: ${{ inputs.version }}
new_commit_sha:
description: 'The new commit SHA after creating/moving the tag'
value: ${{ jobs.commit-release.outputs.new_commit_sha }}
branch_name:
description: 'The branch name where the commit was made'
value: ${{ jobs.commit-release.outputs.branch_name }}
jobs:
commit-release:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
new_commit_sha: ${{ steps.commit_info.outputs.commit_sha }}
branch_name: ${{ steps.push_release.outputs.branch_name }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
# here we download the commit version of ALVersionInfo.py from artifacts
# and replace the original file with it.
- name: Download commit version of ALVersionInfo.py
uses: actions/download-artifact@v6
with:
name: updated-version-info-for-commit
path: downloaded-file/
- name: Replace file with updated version
run: |
FILE_PATH="${{ inputs.file_path }}"
FILE_NAME=$(basename "$FILE_PATH")
TARGET_DIR=$(dirname "$FILE_PATH")
mkdir -p "$TARGET_DIR"
cp "downloaded-file/$FILE_NAME" "$FILE_PATH"
echo "✓ File replaced: $FILE_PATH"
echo ""
echo "Updated file content ==================="
cat "$FILE_PATH"
echo "========================================"
- name: Commit changes
id: commit_changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
FILE_PATH="${{ inputs.file_path }}"
VERSION="${{ inputs.version }}"
if [ ! -f "$FILE_PATH" ]; then
echo "✗ Error: File $FILE_PATH not found"
exit 1
fi
git add "$FILE_PATH"
git commit -m "chore(release): v${VERSION} [auto release commit]"
echo "✓ Changes committed"
- name: Push to release branch
id: push_release
run: |
# Get the release branch name from the input ref
BRANCH_NAME=$(echo "${{ inputs.ref }}" | sed 's|refs/heads/||')
if [ -z "$BRANCH_NAME" ]; then
echo "✗ Error: Could not determine branch name from ref: ${{ inputs.ref }}"
exit 1
fi
echo "Pushing to branch: ${BRANCH_NAME}"
git push origin HEAD:${BRANCH_NAME}
echo "✓ Changes pushed to ${BRANCH_NAME}"
# Output branch name for downstream jobs
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create tag for release
if: ${{ inputs.create_tag == 'true' }}
run: |
TAG_NAME="${{ inputs.tag_name }}"
IS_RC="${{ inputs.is_rc }}"
echo "Creating new tag ${TAG_NAME} at this commit..."
echo "Release type: $([ "$IS_RC" = "true" ] && echo "Release Candidate (Pre-release)" || echo "Stable Release")"
git tag ${TAG_NAME}
git push origin ${TAG_NAME}
echo "✓ Tag ${TAG_NAME} created at commit $(git rev-parse --short HEAD)"
- name: Move tag to new release commit
if: ${{ inputs.create_tag != 'true' }}
run: |
TAG_NAME="${{ inputs.tag_name }}"
echo "Moving tag ${TAG_NAME} to the new commit..."
git tag -f ${TAG_NAME}
git push origin ${TAG_NAME} --force
echo "✓ Tag ${TAG_NAME} moved to commit $(git rev-parse --short HEAD)"
- name: Output commit info
id: commit_info
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✓ New commit SHA: $COMMIT_SHA"
+322
View File
@@ -0,0 +1,322 @@
name: Release
# This workflow automates the complete release process for AutoLibrary application
# It is triggered when a new release branch is created (release/vX.Y.Z or release/vX.Y.Z-rc*)
#
# Workflow Steps:
# START >
# 1. Extract Version:
# Extracts version number from branch name:
# - release/v1.0.0 -> v1.0.0 (stable release)
# - release/v1.0.0-rc1 -> v1.0.0 (release candidate)
# 2. Update Version:
# Updates version information in 'ALVersionInfo.py' with build metadata and archives
# the updated version file as an artifact.
#
# for more information, please refer to the comment in the workflow 'update-version.yml'
# 3. Commit Release:
# Commits version changes to release branch and creates the release tag.
# 4. Build:
# Compiles the application for Windows platform using PyInstaller, and
# archives the built artifacts as 'AutoLibrary.<tag_name>-windows-x86_64.zip'.
# 5. Release:
# Creates GitHub release with generated artifacts and release notes
# < END
#
# 6. Merge back:
# Merges release branch back to main branch, and clean/delete the release branch
#
# The workflow ensures version consistency between source code, built artifacts, and GitHub releases
# while maintaining proper commit history and tag management.
#
# IMPORTANT: This workflow only triggers on branch CREATION, not on pushes to release branches.
# If you need to fix issues on a release branch, delete the tag, merge fixes to main,
# and create a new release branch.
on:
push:
branches:
- 'release/v*'
jobs:
#
# Start :
# virtual job that indacates the start of the release process
#
start:
runs-on: ubuntu-latest
steps:
- name: Start release
run: |
echo "✓ Starting release"
echo "Branch: ${{ github.ref_name }}"
echo "Ref: ${{ github.ref }}"
#
# Extract version :
# this job extracts version from branch name
#
extract-version:
needs:
- start
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.extract.outputs.tag_name }}
version: ${{ steps.extract.outputs.version }}
is_rc: ${{ steps.extract.outputs.is_rc }}
steps:
- name: Extract version from branch name
id: extract
run: |
BRANCH_NAME="${{ github.ref_name }}"
# Validate branch name starts with 'release/v'
if ! echo "$BRANCH_NAME" | grep -qE '^release/v'; then
echo "✗ Error: Branch '$BRANCH_NAME' does not start with 'release/v'"
echo "✗ This workflow should only be triggered by release branches"
exit 1
fi
# Extract version from branch name:
# - release/v1.0.0 -> v1.0.0 (stable)
# - release/v1.0.0-rc1 -> v1.0.0 (release candidate)
# - release/v1.0.0-alpha.1 -> v1.0.0-alpha.1 (pre-release)
if echo "$BRANCH_NAME" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
# Stable release: release/v1.0.0 -> v1.0.0
TAG_NAME=$(echo "$BRANCH_NAME" | sed -E 's|^release/(v[0-9]+\.[0-9]+\.[0-9]+)$|\1|')
IS_RC=false
elif echo "$BRANCH_NAME" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+-'; then
# Pre-release: release/v1.0.0-rc1 -> v1.0.0-rc1
TAG_NAME=$(echo "$BRANCH_NAME" | sed -E 's|^release/(v[0-9]+\.[0-9]+\.[0-9]+-.*)$|\1|')
IS_RC=true
else
echo "✗ Error: Branch '$BRANCH_NAME' does not match expected format"
echo "✗ Expected format: release/vX.Y.Z or release/vX.Y.Z-rcX"
exit 1
fi
VERSION="${TAG_NAME#v}"
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "IS_RC=$IS_RC" >> $GITHUB_OUTPUT
echo "✓ Branch: $BRANCH_NAME"
echo "✓ Tag: $TAG_NAME"
echo "✓ Version: $VERSION"
echo "✓ Is RC: $IS_RC"
#
# Update version :
# this job updates the version in the file 'ALVersionInfo.py'
#
update-version:
needs:
- extract-version
uses: ./.github/workflows/update-version.yml
permissions:
contents: write
with:
tag_name: ${{ needs.extract-version.outputs.tag_name }}
ref: ${{ github.ref }}
#
# Commit release :
# this job commits the updated version file to main and creates
# the release tag (not moving an existing tag)
#
commit-release:
needs:
- extract-version
- update-version
if: ${{ needs.update-version.outputs.has_changes == 'true' }}
uses: ./.github/workflows/commit-release.yml
permissions:
contents: write
with:
tag_name: ${{ needs.extract-version.outputs.tag_name }}
version: ${{ needs.extract-version.outputs.version }}
file_path: src/gui/ALVersionInfo.py
create_tag: 'true'
is_rc: ${{ needs.extract-version.outputs.is_rc }}
ref: ${{ github.ref }}
#
# Build :
# this job builds the application artifacts and archives them
build:
needs:
- update-version
- commit-release
if: always() && needs.update-version.result == 'success' && needs.commit-release.result == 'success'
uses: ./.github/workflows/build.yml
permissions:
contents: write
with:
tag_name: ${{ needs.update-version.outputs.tag_name }}
version: ${{ needs.update-version.outputs.version }}
is_test: 'false'
#
# Release :
# this job creates a GitHub release and uploads the archive files
release:
runs-on: ubuntu-latest
needs:
- build
- extract-version
if: always() && needs.build.result == 'success'
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v6
with:
name: AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64
path: artifacts/
- name: Create release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.extract-version.outputs.tag_name }}
name: AutoLibrary ${{ needs.extract-version.outputs.tag_name }}
files: |
artifacts/AutoLibrary.${{ needs.extract-version.outputs.tag_name }}-windows-x86_64.zip
draft: false
prerelease: ${{ needs.extract-version.outputs.is_rc == 'true' }}
generate_release_notes: true
body: |
---
**完整更新日志见下方自动生成的 Release Notes**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# End :
# virtual job that indacates the end of the release process
#
end:
needs:
- release
runs-on: ubuntu-latest
steps:
- name: End release
run: |
echo "✓ Ending release"
#
# Merge Back :
# this job merges the release branch to main after successful release
#
merge-back:
needs:
- release
- extract-version
- commit-release
if: ${{ needs.release.result == 'success' && needs.commit-release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Merge release branch to main
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Use the release branch name from the original trigger
BRANCH_NAME="${{ needs.extract-version.outputs.tag_name }}"
# Extract branch name: v1.0.0 -> release/v1.0.0
if [[ ! "$BRANCH_NAME" =~ ^release/ ]]; then
BRANCH_NAME="release/${BRANCH_NAME}"
fi
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
if [ -z "$MAIN_BRANCH" ]; then
MAIN_BRANCH="main"
fi
echo "Merging ${BRANCH_NAME} to ${MAIN_BRANCH}..."
echo "Current commit info:"
git log --oneline -3
# Fetch all branches including the release branch
git fetch origin ${BRANCH_NAME}
git fetch origin ${MAIN_BRANCH}
# Checkout main branch
git checkout ${MAIN_BRANCH}
# Show branch status before merge
echo "=== Branch status before merge ==="
git log --oneline --graph --all -5
echo "=== Diff between ${MAIN_BRANCH} and origin/${BRANCH_NAME} ==="
git diff ${MAIN_BRANCH} origin/${BRANCH_NAME} --stat || echo "No differences found"
# Force create a merge commit even if there are no changes
# This ensures the release history is properly recorded
git merge origin/${BRANCH_NAME} \
--no-ff \
-m "chore(release): merge ${BRANCH_NAME} to ${MAIN_BRANCH} [auto release commit]"
# Show merge result
echo "=== Merge result ==="
git log --oneline --graph -3
# Push to main
git push origin ${MAIN_BRANCH}
echo "✓ Successfully merged ${BRANCH_NAME} to ${MAIN_BRANCH}"
- name: Delete release branch
run: |
BRANCH_NAME="${{ needs.extract-version.outputs.tag_name }}"
# Extract branch name: v1.0.0 -> release/v1.0.0
if [[ ! "$BRANCH_NAME" =~ ^release/ ]]; then
BRANCH_NAME="release/${BRANCH_NAME}"
fi
echo "Deleting release branch: ${BRANCH_NAME}"
git push origin --delete ${BRANCH_NAME}
echo "✓ Deleted branch ${BRANCH_NAME}"
- name: Release cleanup summary
run: |
BRANCH_NAME="${{ github.ref_name }}"
TAG_NAME="${{ needs.extract-version.outputs.tag_name }}"
MAIN_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
if [ -z "$MAIN_BRANCH" ]; then
MAIN_BRANCH="main"
fi
echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Actions Performed:" >> $GITHUB_STEP_SUMMARY
echo "- Merged \`${BRANCH_NAME}\` to \`${MAIN_BRANCH}\`" >> $GITHUB_STEP_SUMMARY
echo "- Deleted release branch \`${BRANCH_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Release Details:" >> $GITHUB_STEP_SUMMARY
echo "- Tag: \`${TAG_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo "- Version: \`${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Release Type: $([ "${{ needs.extract-version.outputs.is_rc }}" = "true" ] && echo "Release Candidate" || echo "Stable Release")" >> $GITHUB_STEP_SUMMARY
@@ -1,10 +1,18 @@
name: Update Version Info name: Update Version
# This workflow updates version information in 'ALVersionInfo.py' with build metadata.
# In progress, it will generate two version files, the first one is locate in 'src/gui/ALVersionInfo.py',
# and the second one is locate in 'src/gui/temp/ALVersionInfo.py'. The first one is use
# in the release process, it only update the version and tag name. The commit and build infomation
# is 'local' or 'null'. All of them will finally archive as artifacts.
#
# It is triggered when called by the release workflow.
on: on:
workflow_call: workflow_call:
inputs: inputs:
tag_name: tag_name:
description: 'Tag name (e.g., v1.0.0)' description: 'Tag name'
required: true required: true
type: string type: string
ref: ref:
@@ -14,16 +22,16 @@ on:
outputs: outputs:
tag_name: tag_name:
description: 'The tag name' description: 'The tag name'
value: ${{ jobs.update-version-info.outputs.tag_name }} value: ${{ jobs.update-version.outputs.tag_name }}
version: version:
description: 'The version number' description: 'The version number'
value: ${{ jobs.update-version-info.outputs.version }} value: ${{ jobs.update-version.outputs.version }}
has_changes: has_changes:
description: 'Whether ALVersionInfo.py was modified' description: 'Whether ALVersionInfo.py was modified'
value: ${{ jobs.update-version-info.outputs.has_changes }} value: ${{ jobs.update-version.outputs.has_changes }}
jobs: jobs:
update-version-info: update-version:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -34,7 +42,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
fetch-depth: 0 fetch-depth: 0
@@ -59,24 +67,30 @@ jobs:
echo "✓ Commit SHA: $COMMIT_SHA" echo "✓ Commit SHA: $COMMIT_SHA"
echo "✓ Commit Date: $COMMIT_DATE" echo "✓ Commit Date: $COMMIT_DATE"
- name: Create 'temp' directory
run: |
echo "Creating temp directory..."
mkdir -p "src/gui/temp"
echo "✓ temp directory created successfully"
- name: Update ALVersionInfo.py with version info - name: Update ALVersionInfo.py with version info
run: | run: |
VERSION="${{ steps.get_version.outputs.VERSION }}" VERSION="${{ steps.get_version.outputs.VERSION }}"
TAG_NAME="${{ steps.get_version.outputs.TAG_NAME }}" TAG_NAME="${{ steps.get_version.outputs.TAG_NAME }}"
COMMIT_SHA="${{ steps.get_version.outputs.COMMIT_SHA }}" COMMIT_SHA="${{ steps.get_version.outputs.COMMIT_SHA }}"
COMMIT_DATE="${{ steps.get_version.outputs.COMMIT_DATE }}" COMMIT_DATE="${{ steps.get_version.outputs.COMMIT_DATE }}"
APP_INFO_FILE="src/gui/ALVersionInfo.py" VER_INFO_BUILDFILE="src/gui/temp/ALVersionInfo.py"
VER_INFO_COMMITFILE="src/gui/ALVersionInfo.py"
BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC') BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
echo "Updating $APP_INFO_FILE with build information..." echo "Updating ALVersionInfo.py files with build information..."
{ {
echo '# -*- coding: utf-8 -*-' echo '# -*- coding: utf-8 -*-'
echo '' echo ''
echo '"""' echo '"""'
echo ' The contents of this file will automatically be updated by the' echo ' The contents of this file will automatically be updated by the'
echo ' workflow process. Do not edit manually.' echo ' workflow process. Do not edit manually.'
echo ' ' echo ''
echo ' This file is auto-generated during the workflow process.' echo ' This file is auto-generated during the workflow process.'
echo " Last updated: ${BUILD_DATE}" echo " Last updated: ${BUILD_DATE}"
echo '"""' echo '"""'
@@ -87,13 +101,39 @@ jobs:
echo "AL_COMMIT_DATE = \"${COMMIT_DATE}\" # time zone : UTC" echo "AL_COMMIT_DATE = \"${COMMIT_DATE}\" # time zone : UTC"
echo "AL_BUILD_DATE = \"${BUILD_DATE}\" # time zone : UTC" echo "AL_BUILD_DATE = \"${BUILD_DATE}\" # time zone : UTC"
echo 'AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"' echo 'AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"'
} > "$APP_INFO_FILE" } > "$VER_INFO_BUILDFILE"
echo " ALVersionInfo.py updated successfully" echo "Updating ALVersionInfo.py for release commit..."
{
echo '# -*- coding: utf-8 -*-'
echo ''
echo '"""'
echo ' The contents of this file will automatically be updated by the'
echo ' workflow process. Do not edit manually.'
echo ''
echo ' This file is auto-generated during the workflow process.'
echo " Last updated: ${BUILD_DATE}"
echo '"""'
echo ''
echo "AL_VERSION = \"${VERSION}\""
echo "AL_TAG = \"${TAG_NAME}\""
echo "AL_COMMIT_SHA = \"local\""
echo "AL_COMMIT_DATE = \"null\" # time zone : UTC"
echo "AL_BUILD_DATE = \"null\" # time zone : UTC"
echo 'AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"'
} > "$VER_INFO_COMMITFILE"
echo "✓ ALVersionInfo.py files updated successfully"
echo "" echo ""
echo "=== Updated ALVersionInfo.py ===" echo "Build version file location: $VER_INFO_BUILDFILE"
cat "$APP_INFO_FILE" echo "Commit version file location: $VER_INFO_COMMITFILE"
echo "==========================" echo ""
echo "Build version ALVersionInfo.py content ="
cat "$VER_INFO_BUILDFILE"
echo ""
echo "Commit version ALVersionInfo.py content "
cat "$VER_INFO_COMMITFILE"
echo "========================================"
- name: Check if ALVersionInfo.py was modified - name: Check if ALVersionInfo.py was modified
id: check_changes id: check_changes
@@ -106,10 +146,18 @@ jobs:
echo "✓ ALVersionInfo.py has been modified" echo "✓ ALVersionInfo.py has been modified"
fi fi
- name: Upload modified ALVersionInfo.py - name: Upload modified ALVersionInfo.py ready for build
if: steps.check_changes.outputs.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: updated-version-info name: updated-version-info-for-build
path: src/gui/temp/ALVersionInfo.py
retention-days: 1
- name: Upload modified ALVersionInfo.py ready for commit
if: steps.check_changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v6
with:
name: updated-version-info-for-commit
path: src/gui/ALVersionInfo.py path: src/gui/ALVersionInfo.py
retention-days: 1 retention-days: 1
+11 -11
View File
@@ -6,16 +6,16 @@
__pycache__/ __pycache__/
build/ build/
dist/ dist/
model/*.onnx
driver/*.exe models/*.*
src/gui/configs/*.json drivers/*.*
src/gui/translators/qtbase_zh_CN.qm !models/*.md
src/gui/AutoLibraryResources.py !drivers/*.md
src/gui/AutoLibraryResource.py !templates/*.md
src/gui/Ui_ALMainWindow.py !templates/configs/*.md
src/gui/Ui_ALConfigWidget.py
src/gui/Ui_ALTimerTaskWidget.py src/gui/resources/ui/Ui_*.py
src/gui/Ui_ALAddTimerTaskDialog.py src/gui/resources/translators/qtbase_zh_CN.qm
src/gui/Ui_ALAboutDialog.py src/gui/resources/ALResource.py
Main.spec Main.spec
@@ -3,6 +3,7 @@ chcp 65001 >nul
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
cd /d "%~dp0.." cd /d "%~dp0.."
cd src/gui/resources
echo [AutoLibrary compile] 检查翻译文件... echo [AutoLibrary compile] 检查翻译文件...
if exist translators ( if exist translators (
@@ -18,12 +19,11 @@ if exist translators (
pyside6-lrelease "%%f" pyside6-lrelease "%%f"
if !errorlevel! equ 0 ( if !errorlevel! equ 0 (
echo [AutoLibrary compile] 翻译文件 "%%f" 编译成功,输出文件: "!qm_filename!" echo [AutoLibrary compile] 翻译文件 "%%f" 编译成功,输出文件: "!qm_filename!"
) else ( ) else (
echo [AutoLibrary compile] 翻译文件 "%%f" 编译失败 echo [AutoLibrary compile] 翻译文件 "%%f" 编译失败
) )
) )
echo.
) else ( ) else (
echo [AutoLibrary compile] 未找到任何 .ts 翻译文件 echo [AutoLibrary compile] 未找到任何 .ts 翻译文件
) )
@@ -52,11 +52,10 @@ for %%f in (*.qrc) do (
pyside6-rcc "%%f" -o "!output_file!" pyside6-rcc "%%f" -o "!output_file!"
if !errorlevel! equ 0 ( if !errorlevel! equ 0 (
echo [AutoLibrary compile] 文件 "%%f" 编译成功,输出文件: "!output_file!" echo [AutoLibrary compile] 文件 "%%f" 编译成功,输出文件: "!output_file!"
) else ( ) else (
echo [AutoLibrary compile] 文件 "%%f" 编译失败 echo [AutoLibrary compile] 文件 "%%f" 编译失败
) )
echo.
) )
echo [AutoLibrary compile] 所有操作完成。 echo [AutoLibrary compile] 所有操作完成。
+7 -11
View File
@@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")" PRJECT_DIR="$SCRIPT_DIR/.."
cd "$PARENT_DIR"
cd "$PRJECT_DIR/src/gui/resources"
echo "[AutoLibrary compile] 检查翻译文件..." echo "[AutoLibrary compile] 检查翻译文件..."
if [ -d "translators" ]; then if [ -d "translators" ]; then
@@ -10,7 +11,6 @@ if [ -d "translators" ]; then
ts_files=(*.ts) ts_files=(*.ts)
ts_count=${#ts_files[@]} ts_count=${#ts_files[@]}
# 如果第一个元素是"*.ts"(表示没有匹配),则数量为0
if [ "$ts_count" -eq 1 ] && [ "${ts_files[0]}" = "*.ts" ]; then if [ "$ts_count" -eq 1 ] && [ "${ts_files[0]}" = "*.ts" ]; then
ts_count=0 ts_count=0
fi fi
@@ -23,12 +23,11 @@ if [ -d "translators" ]; then
echo "[AutoLibrary compile] 正在编译翻译文件: \"$file\" -> \"$qm_file\"" echo "[AutoLibrary compile] 正在编译翻译文件: \"$file\" -> \"$qm_file\""
if pyside6-lrelease "$file"; then if pyside6-lrelease "$file"; then
echo "[AutoLibrary compile] 翻译文件 \"$file\" 编译成功,输出文件: \"$qm_file\"" echo "[AutoLibrary compile] 翻译文件 \"$file\" 编译成功,输出文件: \"$qm_file\""
else else
echo "[AutoLibrary compile] 翻译文件 \"$file\" 编译失败" echo "[AutoLibrary compile] 翻译文件 \"$file\" 编译失败"
fi fi
done done
echo
else else
echo "[AutoLibrary compile] 未找到任何 .ts 翻译文件" echo "[AutoLibrary compile] 未找到任何 .ts 翻译文件"
fi fi
@@ -36,7 +35,6 @@ if [ -d "translators" ]; then
else else
echo "[AutoLibrary compile] 未找到 translators 目录" echo "[AutoLibrary compile] 未找到 translators 目录"
fi fi
echo
file_count=$(ls *.qrc 2>/dev/null | wc -l) file_count=$(ls *.qrc 2>/dev/null | wc -l)
@@ -46,7 +44,6 @@ if [ $file_count -eq 0 ]; then
fi fi
echo "[AutoLibrary compile] 找到 $file_count 个 .qrc 文件,开始编译..." echo "[AutoLibrary compile] 找到 $file_count 个 .qrc 文件,开始编译..."
echo
for file in *.qrc; do for file in *.qrc; do
base_name=$(basename "$file" .qrc) base_name=$(basename "$file" .qrc)
@@ -54,11 +51,10 @@ for file in *.qrc; do
echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\"" echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\""
if pyside6-rcc "$file" -o "$output_file"; then if pyside6-rcc "$file" -o "$output_file"; then
echo "[AutoLibrary compile] 文件 \"$file\" 编译成功,输出文件: \"$output_file\"" echo "[AutoLibrary compile] 文件 \"$file\" 编译成功,输出文件: \"$output_file\""
else else
echo "[AutoLibrary compile] 文件 \"$file\" 编译失败" echo "[AutoLibrary compile] 文件 \"$file\" 编译失败"
fi fi
echo
done done
echo "[AutoLibrary compile] 所有操作完成。" echo "[AutoLibrary compile] 所有操作完成。"
@@ -3,6 +3,7 @@ chcp 65001 >nul
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
cd /d "%~dp0.." cd /d "%~dp0.."
cd src/gui/resources/ui
set count=0 set count=0
for %%f in (*.ui) do set /a count+=1 for %%f in (*.ui) do set /a count+=1
@@ -23,11 +24,10 @@ for %%f in (*.ui) do (
pyside6-uic "%%f" -o "!output_file!" pyside6-uic "%%f" -o "!output_file!"
if !errorlevel! equ 0 ( if !errorlevel! equ 0 (
echo [AutoLibrary compile] 文件 "%%f" 编译成功,输出文件: "!output_file!" echo [AutoLibrary compile] 文件 "%%f" 编译成功,输出文件: "!output_file!"
) else ( ) else (
echo [AutoLibrary compile] 文件 "%%f" 编译失败 echo [AutoLibrary compile] 文件 "%%f" 编译失败
) )
echo.
) )
echo [AutoLibrary compile] 所有操作完成。 echo [AutoLibrary compile] 所有操作完成。
+5 -6
View File
@@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PARENT_DIR="$(dirname "$SCRIPT_DIR")" PRJECT_DIR="$SCRIPT_DIR/.."
cd "$PARENT_DIR"
cd "$PRJECT_DIR/src/gui/resources/ui"
file_count=$(ls *.ui 2>/dev/null | wc -l) file_count=$(ls *.ui 2>/dev/null | wc -l)
@@ -12,7 +13,6 @@ if [ $file_count -eq 0 ]; then
fi fi
echo "[AutoLibrary compile] 找到 $file_count 个 .ui 文件,开始编译..." echo "[AutoLibrary compile] 找到 $file_count 个 .ui 文件,开始编译..."
echo
for file in *.ui; do for file in *.ui; do
base_name=$(basename "$file" .ui) base_name=$(basename "$file" .ui)
@@ -20,11 +20,10 @@ for file in *.ui; do
echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\"" echo "[AutoLibrary compile] 正在编译: \"$file\" -> \"$output_file\""
if pyside6-uic "$file" -o "$output_file"; then if pyside6-uic "$file" -o "$output_file"; then
echo "[AutoLibrary compile] 文件 \"$file\" 编译成功,输出文件: \"$output_file\"" echo "[AutoLibrary compile] 文件 \"$file\" 编译成功,输出文件: \"$output_file\""
else else
echo "[AutoLibrary compile] 文件 \"$file\" 编译失败" echo "[AutoLibrary compile] 文件 \"$file\" 编译失败"
fi fi
echo
done done
echo "[AutoLibrary compile] 所有操作完成。" echo "[AutoLibrary compile] 所有操作完成。"
+1
View File
@@ -0,0 +1 @@
This folder is used to store the batch scripts.
-1
View File
@@ -1 +0,0 @@
For more infomation, please visit our website: https://www.autolibrary.cv
-1
View File
@@ -1 +0,0 @@
This folder is used to store the browser driver using by selenium.
+1
View File
@@ -0,0 +1 @@
This folder is used to store the browser drivers using by selenium.
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2025 KenanZhu Copyright 2025 - 2026 KenanZhu
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+3
View File
@@ -0,0 +1,3 @@
This folder is used to store the manuals.
Our manuals are available at https://www.autolibrary.kenanzhu.com/manuals
-1
View File
@@ -1 +0,0 @@
This folder is used to store the model using by ddddocr.
+1
View File
@@ -0,0 +1 @@
This folder is used to store the models using by ddddocr.
+39 -36
View File
@@ -2,14 +2,16 @@
# AutoLibrary # AutoLibrary
--- ---
![AutoLibrary Logo](./src/gui/icons/AutoLibrary_128x128.ico) ![AutoLibrary Logo](./src/gui/resources/icons/AutoLibrary_128x128.ico)
![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary) [![GitHub stars](https://img.shields.io/github/stars/KenanZhu/AutoLibrary.svg?style=social&label=Star)](https://github.com/KenanZhu/AutoLibrary)
![Issue](https://img.shields.io/github/issues/KenanZhu/AutoLibrary) ![License](https://img.shields.io/github/license/KenanZhu/AutoLibrary?label=license)
![Release](https://img.shields.io/github/v/release/KenanZhu/AutoLibrary) [![Build](https://img.shields.io/github/actions/workflow/status/KenanZhu/AutoLibrary/build-test.yml?label=build-test&logo=github-actions&logoColor=white)](https://github.com/KenanZhu/AutoLibrary/actions/workflows/build-test.yml)
![Download](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total) [![Release](https://img.shields.io/github/actions/workflow/status/KenanZhu/AutoLibrary/release.yml?label=release&logo=github-actions&logoColor=white)](https://github.com/KenanZhu/AutoLibrary/actions/workflows/release.yml)
[![Release](https://img.shields.io/github/v/release/KenanZhu/AutoLibrary?label=latest&logo=github&logoColor=white)](https://github.com/KenanZhu/AutoLibrary/releases/latest)
![Downloads](https://img.shields.io/github/downloads/KenanZhu/AutoLibrary/total?label=downloads)
了解更多请访问 [_AutoLibrary 网站_](http://autolibrary.cv) 了解更多请访问 [_AutoLibrary 网站_](http://www.autolibrary.kenanzhu.com)
--- ---
@@ -21,36 +23,18 @@
4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组 4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行 5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行
*1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://autolibrary.cv/docs/manual_lists.html)* *1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)*
### 特点
#### 关于预约等操作的注意事项
工具会自动处理登录过程的验证码识别过程,正常情况下单次识别准确率在 90% 以上,如遇验证码识别错误,大概率是校园网网络环境不佳导致的。
只要确保处于校园网网络环境内,工具都是可以正常运行的。操作处理速度基本上取决于校园网的网络环境,一般情况下在 3-4 秒(不考虑硬件差异)左右即可完成一个用户的操作,完全满足正常使用需求。
> [!NOTE]
> 工具仅作为正常的预约,签到和续约的图书馆辅助工具,请勿干扰图书馆的正常运行(如故意预约多个座位,或同时预约大量的用户等,对此影响图书馆正常运行本工具概不负责,请在善用工具方便自己的情况下尽量不用影响其他同学的使用)。
#### 关于批量操作的注意事项
批量操作时,建议将需要操作的用户分成多个组,每个组的用户数量不要超过 4 人(即一整张桌子的数量),否则会影响操作效率,大量用户同时预约会一定程度上增加图书馆服务器的压力,影响正常使用。根据需要在用户管理界面中可以勾选本次操作是否跳过该用户,以提高运行效率。
#### 关于定时任务的注意事项
定时任务会在指定的时间自动运行,运行时会根据当前预约信息进行操作。一般情况下不建议设置两个运行开始时间比较接近的定时任务,否则后一个任务会等待前一个任务完成后才会运行,按照队列的顺序执行。
### 如何使用 ### 如何使用
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases)。 1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)。
2. 解压下载的文件到任意目录。 2. 解压下载的文件到任意目录。
3. 下载对应浏览器的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动 3. 下载对应浏览器类型和版本(具体操作请参考适用软件版本的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动
4. 运行 `AutoLibrary.exe` 文件 4. 运行 `AutoLibrary-[主版本号].[次版本号].[修订版本号].Z.exe` 文件 (如 `AutoLibrary-1.0.0.exe`
5. 按照提示操作即可 5. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮
6. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://autolibrary.cv/docs/manual_lists.html) 中对应软件版本的内容。 *注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals) 中对应软件版本的内容。
#### 平台支持 & 编译步骤 #### 平台支持 & 编译步骤
@@ -58,7 +42,7 @@
1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。 1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。
2. 安装 pyside6 selenium ddddocr 库,命令为 `pip install pyside6 selenium ddddocr` 2. 安装 pyside6 selenium ddddocr 库,命令为 `pip install pyside6 selenium ddddocr`
3.`src/gui/batchs` 目录下运行 `compile_ui.bat` linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。 3.`batchs` 目录下运行 `compile_ui.bat` linux 和 macOS 系统使用 `compile_ui.sh`) 文件来编译 Qt 的 UI 文件。
4. 在上一步相同目录内运行 `compile_rc.bat` linux 和 macOS 系统使用 `compile_rc.sh` 文件来编译 Qt 的资源文件。 4. 在上一步相同目录内运行 `compile_rc.bat` linux 和 macOS 系统使用 `compile_rc.sh` 文件来编译 Qt 的资源文件。
5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。 5. 待上述步骤完成后,运行 `src/Main.py` 文件即可。
@@ -77,6 +61,25 @@ def classification(self, img: bytes):
[1](@ref)[pillow 中已经删除或已经弃用的常量](https://pillow.ac.cn/en/stable/deprecations.html#constants) [1](@ref)[pillow 中已经删除或已经弃用的常量](https://pillow.ac.cn/en/stable/deprecations.html#constants)
### 注意事项
#### 关于预约等操作
工具会自动处理登录过程的验证码识别过程,正常情况下单次识别准确率在 90% 以上,如遇验证码识别错误,大概率是校园网网络环境不佳导致的。
只要确保处于校园网网络环境内,工具都是可以正常运行的。操作处理速度基本上取决于校园网的网络环境,一般情况下在 3-4 秒(不考虑硬件差异)左右即可完成一个用户的操作,完全满足正常使用需求。
> [!NOTE]
> 工具仅作为正常的预约,签到和续约的图书馆辅助工具,请勿干扰图书馆的正常运行(如故意预约多个座位,或同时预约大量的用户等,对此影响图书馆正常运行本工具概不负责,请在善用工具方便自己的情况下尽量不用影响其他同学的使用)。
#### 关于批量操作
批量操作时,建议将需要操作的用户分成多个组,每个组的用户数量不要超过 4 人(即一整张桌子的数量),否则会影响操作效率,大量用户同时预约会一定程度上增加图书馆服务器的压力,影响正常使用。根据需要在用户管理界面中可以勾选本次操作是否跳过该用户,以提高运行效率。
#### 关于定时任务
定时任务会在指定的时间自动运行,运行时会根据当前预约信息进行操作。一般情况下不建议设置两个运行开始时间比较接近的定时任务,否则后一个任务会等待前一个任务完成后才会运行,按照队列的顺序执行。
### Q&A ### Q&A
#### 为什么开发这个工具? #### 为什么开发这个工具?
@@ -97,11 +100,11 @@ def classification(self, img: bytes):
#### 后续会有哪些功能? #### 后续会有哪些功能?
当前 v1.0.0 版本的功能对于正常使用已经足够,不过后续会着重考虑完善 2-4 人预约时的使用体验,暂时有以下构想: 当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想:
1. 2-4 人一起预约时,往往会偏向于预约并排或对面的整个空座位,这时候工具会按照一定策略查询搜索符合条件的座位,并预约并排或对面的整个座位,而不是各自独立预约 - 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息
2. 预约时会考虑到组内用户的预约时间是否冲突,若冲突则会提示用户是否继续预约,若用户选择继续预约,则会按需要调整预约时间,再进行预约。 - ~~优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。~~ (已完成)
3. 对于比较固定的用户,会考虑在定时任务管理中添加如 ‘每日任务’ ‘每周任务’ 等选项,用户可以根据需要设置定时任务重复的日期范围,自动完成预约,签到,续约等操作 - 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。 不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
+16 -3
View File
@@ -1,20 +1,31 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import os
import sys import sys
from PySide6.QtCore import QTranslator from PySide6.QtCore import QTranslator, QStandardPaths, QDir
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from gui.ALMainWindow import ALMainWindow from gui.ALMainWindow import ALMainWindow
from gui import AutoLibraryResource from gui.resources import ALResource
from utils.ConfigManager import instance
def initializeConfigManager():
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
config_dir = os.path.join(app_dir, "config")
if not QDir(config_dir).exists():
QDir().mkpath(config_dir)
instance(config_dir)
def main(): def main():
@@ -23,6 +34,8 @@ def main():
if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"): if translator.load(":/res/trans/translators/qtbase_zh_CN.ts"):
app.installTranslator(translator) app.installTranslator(translator)
app.setStyle('Fusion') app.setStyle('Fusion')
app.setApplicationName("AutoLibrary")
initializeConfigManager()
window = ALMainWindow() window = ALMainWindow()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())
+8 -1
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -13,6 +13,13 @@ from base.MsgBase import MsgBase
class LibOperator(MsgBase): class LibOperator(MsgBase):
"""
Base abstract class for library operation.
This class provides the foundation for library-related operations, inheriting
message handling and tracing abilities from MsgBase. It serves as an abstract
base class that must be subclassed to implement specific library functionality.
"""
def __init__( def __init__(
self, self,
+135
View File
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 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 queue
from base.LibOperator import LibOperator
class LibTimeSelector(LibOperator):
"""
Base class for time selection operations.
This class provides common time selection logic for reservation and renewal
operations, including time conversion utilities and best time option finding.
"""
def __init__(
self,
input_queue: queue.Queue,
output_queue: queue.Queue
):
super().__init__(input_queue, output_queue)
@staticmethod
def _timeToMins(
time_str: str
) -> int:
"""
Convert time string "HH:MM" to minutes since midnight.
"""
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def _minsToTime(
mins: int
) -> str:
"""
Convert minutes since midnight to time string "HH:MM".
"""
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def _formatTimeRelation(
self,
abs_diff: int,
actual_diff: int,
time_type: str
) -> str:
"""
Format time difference relation string.
"""
if actual_diff < 0:
return f"早了 {abs_diff} 分钟"
elif actual_diff > 0:
return f"晚了 {abs_diff} 分钟"
else:
return f"正好等于 {time_type}"
def _findBestTimeOption(
self,
time_options: list,
target_time: int,
max_time_diff: int,
prefer_earlier: bool,
is_reserve: bool = True
) -> tuple:
"""
Find the best time option from available times.
Args:
time_options: List of WebElement time options
target_time: Target time in minutes
max_time_diff: Maximum acceptable time difference in minutes
prefer_earlier: If True, prefer earlier times when diffs are equal
is_reserve: If True, parse 'time' attribute; if False, parse 'id' attribute
Returns:
Tuple of (best_time_element, best_time_text, actual_diff, free_times_list)
or (None, None, None, []) if no suitable option found
"""
free_times = []
best_time_diff = max_time_diff
best_actual_diff = None
best_time_opt = None
for time_opt in time_options:
# Parse time value based on context
if is_reserve:
time_attr = time_opt.get_attribute("time")
if time_attr == "now":
from datetime import datetime
now = datetime.now()
time_val = now.hour * 60 + now.minute
elif time_attr and time_attr.isdigit():
time_val = int(time_attr)
else:
continue
else:
# Renewal context: parse 'id' attribute
time_attr = time_opt.get_attribute("id")
if not (time_attr and time_attr.isdigit()):
continue
time_val = int(time_attr)
free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTime(time_val))
actual_diff = time_val - target_time
abs_diff = abs(actual_diff)
# Update best option if current is better
if (abs_diff < best_time_diff or
(abs_diff == best_time_diff and
((prefer_earlier and actual_diff <= 0) or
(not prefer_earlier and actual_diff >= 0)))):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
return (best_time_opt, best_time_opt.text.strip(), best_actual_diff, free_times)
return (None, None, None, free_times)
+19 -15
View File
@@ -1,17 +1,33 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import time
import queue import queue
import datetime
class MsgBase: class MsgBase:
"""
Base class for message and trace abilities (thread-safe).
This class provides the foundation for message handling and tracing
abilities based on the provided input and output queues. It enables
thread-safe communication between components using queue-based messaging.
Args:
input_queue (queue.Queue): The input queue for receiving messages.
output_queue (queue.Queue): The output queue for sending messages.
Usage:
This class must be initialized with input and output queues. The queue
provider (the caller of this class or its subclasses) must explicitly
implement queue polling to retrieve and process messages.
"""
def __init__( def __init__(
self, self,
@@ -37,7 +53,7 @@ class MsgBase:
msg: str msg: str
): ):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}") self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
@@ -51,15 +67,3 @@ class MsgBase:
return msg return msg
except queue.Empty: except queue.Empty:
return None return None
def _inputMsg(
self,
timeout: float = 1.0
) -> bool:
try:
self._input_queue.get(timeout=timeout)
return True
except queue.Empty:
return False
+12 -11
View File
@@ -1,17 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import sys
import platform import platform
from PySide6.QtGui import ( from PySide6.QtGui import (
QIcon QIcon, QFont
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QApplication QDialog, QApplication
@@ -23,9 +22,8 @@ from PySide6.QtCore import (
from gui.ALVersionInfo import ( from gui.ALVersionInfo import (
AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE AL_VERSION, AL_COMMIT_SHA, AL_COMMIT_DATE, AL_BUILD_DATE
) )
from gui.Ui_ALAboutDialog import Ui_ALAboutDialog from gui.resources.ui.Ui_ALAboutDialog import Ui_ALAboutDialog
from gui.resources import ALResource
from gui import AutoLibraryResource
class ALAboutDialog(QDialog, Ui_ALAboutDialog): class ALAboutDialog(QDialog, Ui_ALAboutDialog):
@@ -47,8 +45,11 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48)) self.LogoIconLabel.setPixmap(QIcon(":/res/icon/icons/AutoLibrary_32x32.ico").pixmap(48, 48))
info_text = self.generateAboutText() info_text = self.generateAboutText()
self.AboutInfoEdit.setHtml(info_text) self.AboutInfoBrowser.setHtml(info_text)
self.AboutInfoEdit.setTextInteractionFlags(Qt.TextBrowserInteraction) browser_font = self.AboutInfoBrowser.font()
browser_font.setFamily("Courier New")
self.AboutInfoBrowser.setFont(browser_font)
self.AboutInfoBrowser.setTextInteractionFlags(Qt.TextBrowserInteraction)
def connectSignals( def connectSignals(
@@ -60,7 +61,7 @@ class ALAboutDialog(QDialog, Ui_ALAboutDialog):
def generateAboutText( def generateAboutText(
self self
): ) -> str:
os_info = self.getOSInfo() os_info = self.getOSInfo()
about_text = f""" about_text = f"""
@@ -81,7 +82,7 @@ System architecture: {os_info['architecture']}<br>
<h4>Project Information:</h4> <h4>Project Information:</h4>
License: MIT License<br> License: MIT License<br>
Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br> Project repository: <a href="https://www.github.com/KenanZhu/AutoLibrary" style="text-decoration: none;">https://www.github.com/KenanZhu/AutoLibrary</a><br>
Project website: <a href="https://www.autolibrary.cv/" style="text-decoration: none;">https://www.autolibrary.cv/</a><br> Project website: <a href="https://www.autolibrary.kenanzhu.com" style="text-decoration: none;">https://www.autolibrary.kenanzhu.com</a><br>
<h4>Author Information:</h4> <h4>Author Information:</h4>
Developer: KenanZhu<br> Developer: KenanZhu<br>
@@ -138,7 +139,7 @@ GitHub: <a href="https://www.github.com/KenanZhu" style="text-decoration: none;"
self self
): ):
about_text = self.AboutInfoEdit.toPlainText() about_text = self.AboutInfoBrowser.toPlainText()
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
clipboard.setText(about_text) clipboard.setText(about_text)
original_text = self.CopyButton.text() original_text = self.CopyButton.text()
-249
View File
@@ -1,249 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALAddTimerTaskDialog</class>
<widget class="QDialog" name="ALAddTimerTaskDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>添加定时任务 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TaskNameLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TaskNameLabel">
<property name="minimumSize">
<size>
<width>60</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>任务名称:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="TaskNameLineEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="TimerConfigGroupBox">
<property name="title">
<string>定时设置</string>
</property>
<layout class="QVBoxLayout" name="TimerConfigLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TimerTypeLabel">
<property name="text">
<string>定时类型:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTypeComboBox">
<item>
<property name="text">
<string>特定时间</string>
</property>
</item>
<item>
<property name="text">
<string>相对时间</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="TaskConfigGroupBox">
<property name="title">
<string>运行设置</string>
</property>
<layout class="QGridLayout" name="TaskConfigLayout">
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<property name="spacing">
<number>5</number>
</property>
<item row="0" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="text">
<string>静默运行</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="text">
<string>运行前提示</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ControLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="CancelButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>取消</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ConfirmButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+221 -202
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -14,48 +14,42 @@ from PySide6.QtCore import (
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QLineEdit, QMessageBox, QFileDialog, QDialog, QWidget, QLineEdit, QMessageBox, QFileDialog,
QTreeWidgetItem, QMenu, QInputDialog QTreeWidgetItem, QMenu, QInputDialog
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent, QAction QCloseEvent, QAction
) )
from gui.Ui_ALConfigWidget import Ui_ALConfigWidget import utils.ConfigManager as ConfigManager
from gui.ALSeatMapWidget import ALSeatMapWidget
from gui.ALSeatMapTable import seats_maps
from gui.ALUserTreeWidget import TreeItemType
from gui.ALUserTreeWidget import ALUserTreeWidget
from utils.ConfigReader import ConfigReader from utils.JSONReader import JSONReader
from utils.ConfigWriter import ConfigWriter from utils.JSONWriter import JSONWriter
from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
from gui.ALSeatMapTable import ALSeatMapTable
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
class ALConfigWidget(QWidget, Ui_ALConfigWidget): class ALConfigWidget(QWidget, Ui_ALConfigWidget):
configWidgetCloseSingal = Signal(dict) configWidgetIsClosed = Signal()
def __init__( def __init__(
self, self,
parent = None, parent = None,
config_paths = {
"run": "",
"user": ""
}
): ):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr = ConfigManager.instance()
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.__config_data = {"run": {}, "user": {}}
self.setupUi(self) self.setupUi(self)
self.__config_paths = config_paths
self.__config_data = {"run": {}, "user": {}}
self.__seat_map_widget = None
self.modifyUi() self.modifyUi()
self.connectSignals() self.connectSignals()
self.initlizeFloorRoomMap() if not self.initializeConfigs():
self.initlizeDefaultConfigPaths()
if not self.initlizeConfigs():
self.close() self.close()
@@ -71,8 +65,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserListLayout.insertWidget(0, self.UserTreeWidget) self.UserListLayout.insertWidget(0, self.UserTreeWidget)
self.UserTreeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.UserTreeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.UserTreeWidget.customContextMenuRequested.connect(self.onUserTreeWidgetContextMenu) self.UserTreeWidget.customContextMenuRequested.connect(self.onUserTreeWidgetContextMenu)
self.initlizeFloorRoomMap() self.initializeFloorRoomMap()
self.initilizeUserInfoWidget() self.initializeUserInfoWidget()
def connectSignals( def connectSignals(
@@ -127,11 +121,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
event: QCloseEvent event: QCloseEvent
): ):
self.configWidgetCloseSingal.emit(self.__config_paths) self.configWidgetIsClosed.emit()
super().closeEvent(event) super().closeEvent(event)
def initlizeFloorRoomMap( def initializeFloorRoomMap(
self self
): ):
@@ -143,12 +137,12 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
} }
self.__room_map = { self.__room_map = {
"1": "二层内环", "1": "二层内环",
"2": "二层外环", "2": "二层西区",
"3": "三层内环", "3": "三层内环",
"4": "三层外环", "4": "三层外环",
"5": "四层内环", "5": "四层内环",
"6": "四层外环", "6": "四层外环",
"7": "四层期刊", "7": "四层期刊",
"8": "五层考研" "8": "五层考研"
} }
self.__floor_rmap = { self.__floor_rmap = {
@@ -158,26 +152,14 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
v: k for k, v in self.__room_map.items() v: k for k, v in self.__room_map.items()
} }
self.__floor_room_map = { self.__floor_room_map = {
"二层": ["二层内环", "二层外环"], "二层": ["二层内环", "二层西区"],
"三层": ["三层内环", "三层外环"], "三层": ["三层内环", "三层外环"],
"四层": ["四层内环", "四层外环", "四层期刊"], "四层": ["四层内环", "四层外环", "四层期刊"],
"五层": ["五层考研"] "五层": ["五层考研"]
} }
def initlizeDefaultConfigPaths( def initializeConfigToWidget(
self
):
script_path = sys.executable
script_dir = QFileInfo(script_path).absoluteDir()
self.__default_config_paths = {
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json"))
}
def initlizeConfigToWidget(
self, self,
which: str, which: str,
config_data: dict config_data: dict
@@ -187,23 +169,22 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.setRunConfigToWidget(config_data) self.setRunConfigToWidget(config_data)
self.CurrentRunConfigEdit.setText(self.__config_paths["run"]) self.CurrentRunConfigEdit.setText(self.__config_paths["run"])
elif which == "user": elif which == "user":
self.initilizeUserInfoWidget() self.initializeUserInfoWidget()
self.fillUserTree(config_data) self.setUsersToTreeWidget(config_data)
self.CurrentUserConfigEdit.setText(self.__config_paths["user"]) self.CurrentUserConfigEdit.setText(self.__config_paths["user"])
def initlizeConfig( def initializeConfig(
self, self,
which: str which: str
) -> bool: ) -> bool:
msg = "" msg = "" # no use for now
is_success = True is_success = True
if which == "run": if which == "run":
run_config_path = self.__config_paths[which] run_config_path = self.__config_paths[which]
if not os.path.exists(run_config_path): if not os.path.exists(run_config_path):
self.__config_data[which] = self.defaultRunConfig() self.__config_data[which] = self.defaultRunConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveRunConfig(self.__config_paths[which], self.__config_data[which]): if self.saveRunConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"运行配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n" msg += f"运行配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else: else:
@@ -216,7 +197,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
user_config_path = self.__config_paths[which] user_config_path = self.__config_paths[which]
if not os.path.exists(user_config_path): if not os.path.exists(user_config_path):
self.__config_data[which] = self.defaultUserConfig() self.__config_data[which] = self.defaultUserConfig()
self.__config_paths[which] = self.__default_config_paths[which]
if self.saveUserConfig(self.__config_paths[which], self.__config_data[which]): if self.saveUserConfig(self.__config_paths[which], self.__config_data[which]):
msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n" msg += f"用户配置文件已初始化, 文件路径: \n{self.__config_paths[which]}\n"
else: else:
@@ -225,27 +205,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.__config_data[which] = self.loadUserConfig(user_config_path) self.__config_data[which] = self.loadUserConfig(user_config_path)
if self.__config_data[which] is None: if self.__config_data[which] is None:
is_success = False is_success = False
if msg:
QMessageBox.information(
self,
"提示 - AutoLibrary",
f"配置文件初始化完成: \n{msg}"
)
return is_success return is_success
def initlizeConfigs( def initializeConfigs(
self self
) -> bool: ) -> bool:
is_success = True is_success = True
for which in ["run", "user"]: for which in ["run", "user"]:
if not self.__config_paths[which]: if not self.initializeConfig(which):
self.__config_paths[which] = self.__default_config_paths[which]
if not self.initlizeConfig(which):
is_success = False is_success = False
break break
self.initlizeConfigToWidget(which, self.__config_data[which]) self.initializeConfigToWidget(which, self.__config_data[which])
return is_success return is_success
@@ -264,7 +236,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
}, },
"web_driver": { "web_driver": {
"driver_type": "edge", "driver_type": "edge",
"driver_path": "msedgedriver.exe", "driver_path": "",
"headless": False "headless": False
}, },
"mode": { "mode": {
@@ -278,27 +250,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> dict: ) -> dict:
return { return {
"groups": [] "groups": [
} ]
def defaultGroup(
self
) -> dict:
return {
"name": "默认分组",
"enabled": True,
"users": []
}
def defaultUsers(
self
) -> dict:
return {
"users": []
} }
@@ -329,21 +282,41 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
run_config: dict run_config: dict
): ):
self.HostUrlEdit.setText(run_config["library"]["host_url"]) try:
self.LoginUrlEdit.setText(run_config["library"]["login_url"]) self.HostUrlEdit.setText(run_config["library"]["host_url"])
self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"]) self.LoginUrlEdit.setText(run_config["library"]["login_url"])
self.LoginAttemptSpinBox.setValue(run_config["login"]["max_attempt"]) self.AutoCaptchaCheckBox.setChecked(run_config["login"]["auto_captcha"])
self.BrowserTypeComboBox.setCurrentText(run_config["web_driver"]["driver_type"]) self.LoginAttemptSpinBox.setValue(run_config["login"]["max_attempt"])
driver_path = os.path.abspath(run_config["web_driver"]["driver_path"]) self.BrowserTypeComboBox.setCurrentText(run_config["web_driver"]["driver_type"])
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path)) if run_config["web_driver"]["driver_path"]:
self.HeadlessCheckBox.setChecked(run_config["web_driver"]["headless"]) driver_path = os.path.abspath(run_config["web_driver"]["driver_path"])
run_mode = run_config["mode"]["run_mode"] else:
self.AutoReserveCheckBox.setChecked(run_mode&0x01) driver_path = ""
self.AutoCheckinCheckBox.setChecked(run_mode&0x02) self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(driver_path))
self.AutoRenewalCheckBox.setChecked(run_mode&0x04) self.HeadlessCheckBox.setChecked(run_config["web_driver"]["headless"])
run_mode = run_config["mode"]["run_mode"]
self.AutoReserveCheckBox.setChecked(run_mode&0x01)
self.AutoCheckinCheckBox.setChecked(run_mode&0x02)
self.AutoRenewalCheckBox.setChecked(run_mode&0x04)
except KeyError as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"运行配置文件读取键 '{e}' 时发生错误 ! :\n"
f"文件路径: {self.__config_paths['run']}\n"
"文件可能被意外修改或已经损坏\n"
)
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"运行配置文件读取键 '{e}' 时发生未知错误 ! :\n"
f"文件路径: {self.__config_paths['run']}\n"
"文件可能被意外修改或已经损坏\n"
)
def initilizeUserInfoWidget( def initializeUserInfoWidget(
self self
): ):
@@ -368,7 +341,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.PreferLateRenewTimeCheckBox.setChecked(False) self.PreferLateRenewTimeCheckBox.setChecked(False)
def collectUserFromUserInfoWidget( def collectUserFromWidget(
self self
) -> dict: ) -> dict:
@@ -401,7 +374,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return user return user
def collectUserConfigFromUserTreeWidget( def collectUsersFromTreeWidget(
self self
) -> dict: ) -> dict:
@@ -448,13 +421,64 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.ExpectRenewDurationSpinBox.setValue(user["reserve_info"]["renew_time"]["expect_duration"]) self.ExpectRenewDurationSpinBox.setValue(user["reserve_info"]["renew_time"]["expect_duration"])
self.MaxRenewTimeDiffSpinBox.setValue(user["reserve_info"]["renew_time"]["max_diff"]) self.MaxRenewTimeDiffSpinBox.setValue(user["reserve_info"]["renew_time"]["max_diff"])
self.PreferLateRenewTimeCheckBox.setChecked(not user["reserve_info"]["renew_time"]["prefer_early"]) self.PreferLateRenewTimeCheckBox.setChecked(not user["reserve_info"]["renew_time"]["prefer_early"])
except: except KeyError as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
"用户配置文件读取发生错误 !\n"\ f"用户配置文件读取'{e}'发生错误 ! :\n"
f"用户: {user['username']} 配置文件可能已损坏" f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
) )
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
def setUsersToTreeWidget(
self,
users: dict
):
self.UserTreeWidget.clear()
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
try:
if "groups" in users:
for group_config in users["groups"]:
group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
group_item.setText(0, group_config["name"])
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]:
user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
user_item.setText(0, user_config["username"])
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
user_item.setData(0, Qt.UserRole, user_config)
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
user_item.setDisabled(not group_config.get("enabled", True))
group_item.setExpanded(True)
except KeyError as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取键 '{e}' 时发生错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
except Exception as e:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
f"用户配置文件读取键 '{e}' 时发生未知错误 ! :\n"
f"文件路径: {self.__config_paths['user']}\n"
"文件可能被意外修改或已经损坏\n"
)
finally:
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
def loadRunConfig( def loadRunConfig(
@@ -465,18 +489,18 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
try: try:
if not run_config_path or not os.path.exists(run_config_path): if not run_config_path or not os.path.exists(run_config_path):
raise Exception("文件路径不存在") raise Exception("文件路径不存在")
run_config = ConfigReader(run_config_path).getConfigs() run_config = JSONReader(run_config_path).data()
if run_config and "library" in run_config\ if run_config and "library" in run_config\
and "web_driver" in run_config\ and "web_driver" in run_config\
and "login" in run_config: and "login" in run_config:
return run_config return run_config
return None else:
return None
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"运行配置文件读取发生错误 ! : {e}\n"\ f"运行配置文件读取发生错误 ! :\n{e}"
f"文件路径: {run_config_path}"
) )
return None return None
@@ -492,14 +516,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
raise Exception("文件路径为空") raise Exception("文件路径为空")
if not run_config_data or not isinstance(run_config_data, dict): if not run_config_data or not isinstance(run_config_data, dict):
raise Exception("运行配置数据为空或类型错误") raise Exception("运行配置数据为空或类型错误")
ConfigWriter(run_config_path, run_config_data) JSONWriter(run_config_path, run_config_data)
return True return True
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"配置文件写入发生错误 ! : {e}\n"\ f"配置文件写入发生错误 ! : \n{e}"
f"文件路径: {run_config_path}"
) )
return False return False
@@ -512,11 +535,11 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
try: try:
if not user_config_path or not os.path.exists(user_config_path): if not user_config_path or not os.path.exists(user_config_path):
raise Exception("文件路径不存在") raise Exception("文件路径不存在")
user_config = ConfigReader(user_config_path).getConfigs() user_config = JSONReader(user_config_path).data()
if user_config and "groups" in user_config: if user_config and "groups" in user_config:
return user_config return user_config
# compatibility with old version config format # compatibility with old version config format
if user_config and "users" in user_config: elif user_config and "users" in user_config:
user_config = { user_config = {
"groups": [ "groups": [
{ {
@@ -527,13 +550,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
] ]
} }
return user_config return user_config
return None else:
return None
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"用户配置文件读取发生错误 ! : {e}\n"\ f"用户配置文件读取发生错误 ! :\n{e}"
f"文件路径: {user_config_path}"
) )
return None return None
@@ -549,14 +572,13 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
raise Exception("文件路径为空") raise Exception("文件路径为空")
if not user_config_data or not isinstance(user_config_data, dict): if not user_config_data or not isinstance(user_config_data, dict):
raise Exception("用户配置数据为空或类型错误") raise Exception("用户配置数据为空或类型错误")
ConfigWriter(user_config_path, user_config_data) JSONWriter(user_config_path, user_config_data)
return True return True
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"用户配置文件写入发生错误 ! : {e}\n"\ f"用户配置文件写入发生错误 ! :\n{e}"
f"文件路径: \n{user_config_path}"
) )
return False return False
@@ -568,7 +590,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
) -> bool: ) -> bool:
if user_config_path: if user_config_path:
self.__config_data["user"] = self.collectUserConfigFromUserTreeWidget() self.__config_data["user"] = self.collectUsersFromTreeWidget()
if not self.saveUserConfig( if not self.saveUserConfig(
user_config_path, user_config_path,
self.__config_data["user"] self.__config_data["user"]
@@ -607,45 +629,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return True return True
if user_config is not None: if user_config is not None:
self.__config_data["user"].update(user_config) self.__config_data["user"].update(user_config)
self.fillUserTree(self.__config_data["user"]) self.setUsersToTreeWidget(self.__config_data["user"])
return True return True
except: except:
return False return False
def fillUserTree(
self,
user_config_data: dict
):
self.UserTreeWidget.clear()
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
try:
if "groups" in user_config_data:
for group_config in user_config_data["groups"]:
group_item = QTreeWidgetItem(self.UserTreeWidget, TreeItemType.GROUP.value)
group_item.setText(0, group_config["name"])
group_item.setFlags(group_item.flags() | Qt.ItemIsEditable)
group_item.setCheckState(1, Qt.Checked if group_config.get("enabled", True) else Qt.Unchecked)
for user_config in group_config["users"]:
user_item = QTreeWidgetItem(group_item, TreeItemType.USER.value)
user_item.setText(0, user_config["username"])
user_item.setText(1, "" if user_config.get("enabled", True) else "跳过")
user_item.setData(0, Qt.UserRole, user_config)
user_item.setCheckState(1, Qt.Checked if user_config.get("enabled", True) else Qt.Unchecked)
user_item.setDisabled(not group_config.get("enabled", True))
group_item.setExpanded(True)
finally:
self.UserTreeWidget.itemChanged.connect(self.onUserTreeWidgetItemChanged)
def addGroup( def addGroup(
self, self,
group_name: str = "" group_name: str = ""
) -> QTreeWidgetItem: ) -> QTreeWidgetItem:
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
group_item = QTreeWidgetItem(self.UserTreeWidget, TreeItemType.GROUP.value) group_item = QTreeWidgetItem(self.UserTreeWidget, ALUserTreeItemType.GROUP.value)
if not group_name: if not group_name:
group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}" group_name = f"新分组-{self.UserTreeWidget.topLevelItemCount()}"
group_item.setText(0, group_name) group_item.setText(0, group_name)
@@ -656,6 +652,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return group_item return group_item
def delGroup(
self,
group_item: QTreeWidgetItem = None
):
if group_item is None:
return
if group_item.type() != ALUserTreeItemType.GROUP.value:
return
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index)
def addUser( def addUser(
self, self,
group_item: QTreeWidgetItem = None group_item: QTreeWidgetItem = None
@@ -665,7 +674,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
current_item = self.UserTreeWidget.currentItem() current_item = self.UserTreeWidget.currentItem()
if current_item is None: if current_item is None:
group_item = self.addGroup() group_item = self.addGroup()
if group_item.type() == TreeItemType.USER.value: if group_item.type() == ALUserTreeItemType.USER.value:
group_item = group_item.parent() group_item = group_item.parent()
if group_item.checkState(1) == Qt.CheckState.Unchecked: if group_item.checkState(1) == Qt.CheckState.Unchecked:
return None return None
@@ -699,7 +708,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
} }
} }
self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged) self.UserTreeWidget.itemChanged.disconnect(self.onUserTreeWidgetItemChanged)
user_item = QTreeWidgetItem(group_item, TreeItemType.USER.value) user_item = QTreeWidgetItem(group_item, ALUserTreeItemType.USER.value)
user_item.setText(0, new_user["username"]) user_item.setText(0, new_user["username"])
user_item.setText(1, "") user_item.setText(1, "")
user_item.setData(0, Qt.UserRole, new_user) user_item.setData(0, Qt.UserRole, new_user)
@@ -718,7 +727,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if user_item is None: if user_item is None:
return return
if user_item.type() != TreeItemType.USER.value: if user_item.type() != ALUserTreeItemType.USER.value:
return return
parent_item = user_item.parent() parent_item = user_item.parent()
index = parent_item.indexOfChild(user_item) index = parent_item.indexOfChild(user_item)
@@ -727,19 +736,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
def delGroup(
self,
group_item: QTreeWidgetItem = None
):
if group_item is None:
return
if group_item.type() != TreeItemType.GROUP.value:
return
index = self.UserTreeWidget.indexOfTopLevelItem(group_item)
self.UserTreeWidget.takeTopLevelItem(index)
def renameItem( def renameItem(
self, self,
item: QTreeWidgetItem, item: QTreeWidgetItem,
@@ -759,7 +755,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if not ok or not new_name: if not ok or not new_name:
return return
item.setText(0, new_name) item.setText(0, new_name)
if item.type() == TreeItemType.GROUP.value: if item.type() == ALUserTreeItemType.GROUP.value:
item.setText(0, new_name) item.setText(0, new_name)
else: else:
user = item.data(0, Qt.UserRole) user = item.data(0, Qt.UserRole)
@@ -768,7 +764,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
item.setData(0, Qt.UserRole, user) item.setData(0, Qt.UserRole, user)
self.setUserToWidget(user) self.setUserToWidget(user)
@Slot() @Slot()
def onShowPasswordCheckBoxChecked( def onShowPasswordCheckBoxChecked(
self, self,
@@ -790,19 +785,6 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.RoomComboBox.addItems(self.__floor_room_map[floor]) self.RoomComboBox.addItems(self.__floor_room_map[floor])
self.RoomComboBox.setCurrentIndex(0) self.RoomComboBox.setCurrentIndex(0)
@Slot()
def onSeatMapWidgetClosed(
self,
selected_seats: list[str]
):
self.__seat_map_widget.seatMapWidgetClosed.disconnect(self.onSeatMapWidgetClosed)
self.__seat_map_widget.deleteLater()
self.__seat_map_widget = None
if len(selected_seats) == 0:
return
self.SeatIDEdit.setText(",".join(selected_seats))
@Slot() @Slot()
def onSelectSeatsButtonClicked( def onSelectSeatsButtonClicked(
self self
@@ -812,18 +794,19 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
room = self.RoomComboBox.currentText() room = self.RoomComboBox.currentText()
floor_idx = self.__floor_rmap[floor] floor_idx = self.__floor_rmap[floor]
room_idx = self.__room_rmap[room] room_idx = self.__room_rmap[room]
if self.__seat_map_widget is None: dialog = ALSeatMapSelectDialog(
self.__seat_map_widget = ALSeatMapWidget( self,
self, floor,
floor, room,
room, ALSeatMapTable[floor_idx][room_idx]
seats_maps[floor_idx][room_idx] )
) dialog.selectSeats(self.SeatIDEdit.text().split(","))
self.__seat_map_widget.seatMapWidgetClosed.connect(self.onSeatMapWidgetClosed) if dialog.exec() == QDialog.DialogCode.Accepted:
self.__seat_map_widget.show() selected_seats = dialog.getSelectedSeats()
self.__seat_map_widget.raise_() if len(selected_seats) == 0:
self.__seat_map_widget.activateWindow() self.SeatIDEdit.clear()
self.__seat_map_widget.selectSeats(self.SeatIDEdit.text().split(",")) return
self.SeatIDEdit.setText(",".join(dialog.getSelectedSeats()))
@Slot() @Slot()
def onUserTreeWidgetCurrentItemChanged( def onUserTreeWidgetCurrentItemChanged(
@@ -835,8 +818,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
# cant effectively update the data of each user, due to the # cant effectively update the data of each user, due to the
# possiblity of frequency edit. we just let the QListWidget # possiblity of frequency edit. we just let the QListWidget
# help us. # help us.
if previous and previous.type() == TreeItemType.USER.value: if previous and previous.type() == ALUserTreeItemType.USER.value:
user = self.collectUserFromUserInfoWidget() user = self.collectUserFromWidget()
if user: if user:
self.UsernameEdit.textEdited.disconnect() self.UsernameEdit.textEdited.disconnect()
user["enabled"] = previous.checkState(1) == Qt.Checked user["enabled"] = previous.checkState(1) == Qt.Checked
@@ -844,15 +827,15 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
previous.setText(1, "" if user.get("enabled", True) else "跳过") previous.setText(1, "" if user.get("enabled", True) else "跳过")
previous.setData(0, Qt.UserRole, user) previous.setData(0, Qt.UserRole, user)
if current is None: if current is None:
self.initilizeUserInfoWidget() self.initializeUserInfoWidget()
return return
if current.type() == TreeItemType.USER.value: if current.type() == ALUserTreeItemType.USER.value:
user = current.data(0, Qt.UserRole) user = current.data(0, Qt.UserRole)
if user: if user:
self.setUserToWidget(user) self.setUserToWidget(user)
self.UsernameEdit.textEdited.connect(lambda text: current.setText(0, text)) self.UsernameEdit.textEdited.connect(lambda text: current.setText(0, text))
else: else:
self.initilizeUserInfoWidget() self.initializeUserInfoWidget()
@Slot() @Slot()
def onUserTreeWidgetItemChanged( def onUserTreeWidgetItemChanged(
@@ -865,7 +848,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
return return
if column != 1: if column != 1:
return return
if item.type() == TreeItemType.GROUP.value: if item.type() == ALUserTreeItemType.GROUP.value:
is_checked = item.checkState(1) == Qt.CheckState.Checked is_checked = item.checkState(1) == Qt.CheckState.Checked
for i in range(item.childCount()): for i in range(item.childCount()):
child = item.child(i) child = item.child(i)
@@ -930,7 +913,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
menu = QMenu(self.UserTreeWidget) menu = QMenu(self.UserTreeWidget)
if current_item is None: if current_item is None:
self.showTreeMenu(menu) self.showTreeMenu(menu)
elif current_item.type() == TreeItemType.GROUP.value: elif current_item.type() == ALUserTreeItemType.GROUP.value:
self.showGroupMenu(menu, current_item) self.showGroupMenu(menu, current_item)
else: else:
self.showUserMenu(menu, current_item) self.showUserMenu(menu, current_item)
@@ -979,9 +962,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)[0] )[0]
if run_config_path: if run_config_path:
run_config_path = QDir.toNativeSeparators(run_config_path) run_config_path = QDir.toNativeSeparators(run_config_path)
if self.loadConfig(run_config_path): data = self.loadRunConfig(run_config_path)
if data is not None:
self.__config_data["run"].update(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", [])
if run_config_path not in paths:
paths.append(run_config_path)
index = len(paths) - 1
else:
index = paths.index(run_config_path)
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.run_path", {"current": index, "paths": paths})
else:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"运行配置文件读取发生错误 ! :\n"\
"无法从选择的运行配置文件中加载数据 ! :\n"\
"可能选择了错误的配置文件类型"
)
@Slot() @Slot()
def onBrowseCurrentUserConfigButtonClicked( def onBrowseCurrentUserConfigButtonClicked(
@@ -996,9 +997,27 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
)[0] )[0]
if user_config_path: if user_config_path:
user_config_path = QDir.toNativeSeparators(user_config_path) user_config_path = QDir.toNativeSeparators(user_config_path)
if self.loadConfig(user_config_path): data = self.loadUserConfig(user_config_path)
if data is not None:
self.__config_data["user"].update(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", [])
if user_config_path not in paths:
paths.append(user_config_path)
index = len(paths) - 1
else:
index = paths.index(user_config_path)
self.__cfg_mgr.set(ConfigManager.ConfigType.GLOBAL, "automation.user_path", {"current": index, "paths": paths})
else:
QMessageBox.warning(
self,
"警告 - AutoLibrary",
"用户配置文件读取发生错误 ! :\n"\
"无法从选择的用户配置文件中加载数据 ! :\n"\
"可能选择了错误的配置文件类型"
)
@Slot() @Slot()
def onBrowseExportRunConfigButtonClicked( def onBrowseExportRunConfigButtonClicked(
@@ -1085,9 +1104,9 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if run_exists or user_exists: if run_exists or user_exists:
exist_files = [] exist_files = []
if run_exists: if run_exists:
exist_files.append(run_config_path) exist_files.append(f"运行配置文件: \n{run_config_path}")
if user_exists: if user_exists:
exist_files.append(user_config_path) exist_files.append(f"用户配置文件: \n{user_config_path}")
reply = QMessageBox.information( reply = QMessageBox.information(
self, self,
"提示 - AutoLibrary", "提示 - AutoLibrary",
@@ -1103,8 +1122,8 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
"run": run_config_path, "run": run_config_path,
"user": user_config_path "user": user_config_path
} }
self.initlizeConfigToWidget("run", self.__config_data["run"]) self.initializeConfigToWidget("run", self.__config_data["run"])
self.initlizeConfigToWidget("user", self.__config_data["user"]) self.initializeConfigToWidget("user", self.__config_data["user"])
@Slot() @Slot()
def onConfirmButtonClicked( def onConfirmButtonClicked(
@@ -1112,7 +1131,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
): ):
current_item = self.UserTreeWidget.currentItem() current_item = self.UserTreeWidget.currentItem()
if current_item and current_item.type() == TreeItemType.USER.value: if current_item and current_item.type() == ALUserTreeItemType.USER.value:
self.UserTreeWidget.setCurrentItem(None) self.UserTreeWidget.setCurrentItem(None)
if self.saveConfigs( if self.saveConfigs(
self.__config_paths["run"], self.__config_paths["run"],
@@ -1121,7 +1140,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
QMessageBox.information( QMessageBox.information(
self, self,
"提示 - AutoLibrary", "提示 - AutoLibrary",
"配置文件保存成功 !\n" "配置文件保存成功 ! :\n"
f"运行配置文件路径: \n{self.__config_paths['run']}\n"\ f"运行配置文件路径: \n{self.__config_paths['run']}\n"\
f"用户配置文件路径: \n{self.__config_paths['user']}" f"用户配置文件路径: \n{self.__config_paths['user']}"
) )
@@ -1129,7 +1148,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
"配置文件保存失败, 请检查文件路径权限" "配置文件保存失败 !\n"
) )
self.close() self.close()
+77 -97
View File
@@ -1,41 +1,39 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import os
import sys
import time
import queue import queue
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, Signal, Slot, QDir, QFileInfo, QTimer, QUrl, Qt, Signal, Slot, QTimer, QUrl,
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QMainWindow, QMenu, QSystemTrayIcon QMainWindow, QMenu, QSystemTrayIcon, QMessageBox
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
) )
from gui.Ui_ALMainWindow import Ui_ALMainWindow import utils.ConfigManager as ConfigManager
from base.MsgBase import MsgBase
from gui.resources.ui.Ui_ALMainWindow import Ui_ALMainWindow
from gui.resources import ALResource
from gui.ALConfigWidget import ALConfigWidget from gui.ALConfigWidget import ALConfigWidget
from gui.ALTimerTaskWidget import ALTimerTaskWidget from gui.ALTimerTaskManageWidget import ALTimerTaskManageWidget
from gui.ALAboutDialog import ALAboutDialog from gui.ALAboutDialog import ALAboutDialog
from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker from gui.ALMainWorkers import TimerTaskWorker, AutoLibWorker
from gui import AutoLibraryResource
from utils.ConfigReader import ConfigReader class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
from utils.ConfigWriter import ConfigWriter
class ALMainWindow(QMainWindow, Ui_ALMainWindow):
# signal : timer task
timerTaskIsRunning = Signal(dict) timerTaskIsRunning = Signal(dict)
timerTaskIsExecuted = Signal(dict) timerTaskIsExecuted = Signal(dict)
timerTaskIsError = Signal(dict) timerTaskIsError = Signal(dict)
@@ -44,26 +42,18 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self self
): ):
super().__init__() MsgBase.__init__(self, queue.Queue(), queue.Queue())
self.__class_name = self.__class__.__name__ QMainWindow.__init__(self)
self.__cfg_mgr = ConfigManager.instance()
self.setupUi(self)
self.__input_queue = queue.Queue()
self.__output_queue = queue.Queue()
self.__timer_task_queue = queue.Queue() self.__timer_task_queue = queue.Queue()
script_path = sys.executable self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
script_dir = QFileInfo(script_path).absoluteDir() self.__alTimerTaskManageWidget = None
self.__config_paths = {
"run": QDir.toNativeSeparators(script_dir.absoluteFilePath("run.json")),
"user": QDir.toNativeSeparators(script_dir.absoluteFilePath("user.json")),
"timer_task": QDir.toNativeSeparators(script_dir.absoluteFilePath("timer_task.json")),
}
self.__alTimerTaskWidget = None
self.__alConfigWidget = None self.__alConfigWidget = None
self.__auto_lib_thread = None self.__auto_lib_thread = None
self.__current_timer_task_thread = None self.__current_timer_task_thread = None
self.__is_running_timer_task = False self.__is_running_timer_task = False
self.setupUi(self)
self.modifyUi() self.modifyUi()
self.setupTray() self.setupTray()
self.connectSignals() self.connectSignals()
@@ -82,13 +72,24 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.AboutAction.triggered.connect(self.onAboutActionTriggered) self.AboutAction.triggered.connect(self.onAboutActionTriggered)
# initialize timer task widget, but not show it # initialize timer task widget, but not show it
self.__alTimerTaskWidget = ALTimerTaskWidget(self, self.__config_paths["timer_task"]) try:
self.timerTaskIsRunning.connect(self.__alTimerTaskWidget.onTimerTaskIsRunning) self.__alTimerTaskManageWidget = ALTimerTaskManageWidget(self)
self.timerTaskIsExecuted.connect(self.__alTimerTaskWidget.onTimerTaskIsExecuted) except Exception as e:
self.timerTaskIsError.connect(self.__alTimerTaskWidget.onTimerTaskIsError) QMessageBox.critical(
self.__alTimerTaskWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady) self,
self.__alTimerTaskWidget.timerTaskWidgetClosed.connect(self.onTimerTaskWidgetClosed) "错误 - AutoLibrary",
self.__alTimerTaskWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint) f"初始化定时任务功能失败: \n{e}"
)
self.__alTimerTaskManageWidget = None
self.TimerTaskManageWidgetButton.setEnabled(False)
self.TimerTaskManageWidgetButton.setToolTip("定时任务功能初始化失败, 请检查配置文件。")
return
self.timerTaskIsRunning.connect(self.__alTimerTaskManageWidget.onTimerTaskIsRunning)
self.timerTaskIsExecuted.connect(self.__alTimerTaskManageWidget.onTimerTaskIsExecuted)
self.timerTaskIsError.connect(self.__alTimerTaskManageWidget.onTimerTaskIsError)
self.__alTimerTaskManageWidget.timerTaskIsReady.connect(self.onTimerTaskIsReady)
self.__alTimerTaskManageWidget.timerTaskManageWidgetIsClosed.connect(self.onTimerTaskManageWidgetClosed)
self.__alTimerTaskManageWidget.setWindowFlags(Qt.WindowType.Window|Qt.WindowType.WindowCloseButtonHint)
def onAboutActionTriggered( def onAboutActionTriggered(
@@ -103,7 +104,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self self
): ):
url = QUrl("https://www.autolibrary.cv/docs/manual_lists.html") url = QUrl("https://www.autolibrary.kenanzhu.com/manuals")
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
@@ -112,22 +113,19 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
): ):
if not QSystemTrayIcon.isSystemTrayAvailable(): if not QSystemTrayIcon.isSystemTrayAvailable():
self.showTraceSignal.emit( self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标")
"系统不支持系统托盘功能, 无法创建系统托盘图标。"
)
return return
self.TrayIcon = QSystemTrayIcon(self.icon, self) self.TrayIcon = QSystemTrayIcon(self.icon, self)
self.TrayIcon.setToolTip("AutoLibrary") self.TrayIcon.setToolTip("AutoLibrary")
self.TrayMenu = QMenu() self.TrayMenu = QMenu()
self.TrayMenu.addAction("显示主窗口", self.showNormal) self.TrayMenu.addAction("显示主窗口", self.showNormal)
self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskWidgetButtonClicked) self.TrayMenu.addAction("显示定时窗口", self.onTimerTaskManageWidgetButtonClicked)
self.TrayMenu.addAction("最小化到托盘", self.hideToTray) self.TrayMenu.addAction("最小化到托盘", self.hideToTray)
self.TrayMenu.addSeparator() self.TrayMenu.addSeparator()
self.TrayMenu.addAction("退出", self.close) self.TrayMenu.addAction("退出", self.close)
self.TrayIcon.setContextMenu(self.TrayMenu) self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.setContextMenu(self.TrayMenu)
self.TrayIcon.activated.connect(self.onTrayIconActivated) self.TrayIcon.activated.connect(self.onTrayIconActivated)
self.TrayIcon.show() self.TrayIcon.show()
@@ -159,7 +157,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
): ):
self.ConfigButton.clicked.connect(self.onConfigButtonClicked) self.ConfigButton.clicked.connect(self.onConfigButtonClicked)
self.TimerTaskWidgetButton.clicked.connect(self.onTimerTaskWidgetButtonClicked) self.TimerTaskManageWidgetButton.clicked.connect(self.onTimerTaskManageWidgetButtonClicked)
self.StartButton.clicked.connect(self.onStartButtonClicked) self.StartButton.clicked.connect(self.onStartButtonClicked)
self.StopButton.clicked.connect(self.onStopButtonClicked) self.StopButton.clicked.connect(self.onStopButtonClicked)
self.SendButton.clicked.connect(self.onSendButtonClicked) self.SendButton.clicked.connect(self.onSendButtonClicked)
@@ -171,6 +169,10 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
event: QCloseEvent event: QCloseEvent
): ):
if not self.isVisible():
self.showNormal()
event.ignore()
return
if self.__msg_queue_timer and self.__msg_queue_timer.isActive(): if self.__msg_queue_timer and self.__msg_queue_timer.isActive():
self.__msg_queue_timer.stop() self.__msg_queue_timer.stop()
if self.__timer_task_timer and self.__timer_task_timer.isActive(): if self.__timer_task_timer and self.__timer_task_timer.isActive():
@@ -178,13 +180,13 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
if self.__is_running_timer_task: if self.__is_running_timer_task:
self.__current_timer_task_thread.wait(2000) self.__current_timer_task_thread.wait(2000)
self.__current_timer_task_thread.deleteLater() self.__current_timer_task_thread.deleteLater()
if self.__alTimerTaskWidget: if self.__alTimerTaskManageWidget:
self.__alTimerTaskWidget.close() self.__alTimerTaskManageWidget.close()
self.__alTimerTaskWidget.deleteLater() self.__alTimerTaskManageWidget.deleteLater()
if self.__alConfigWidget: if self.__alConfigWidget:
self.__alConfigWidget.close() self.__alConfigWidget.close()
# the config widget is already deleted in the 'self.onConfigWidgetClosed' # the config widget is already deleted in the 'self.onConfigWidgetClosed'
super().closeEvent(event) QMainWindow.closeEvent(self, event)
def appendToTextEdit( def appendToTextEdit(
@@ -231,7 +233,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.timerTaskIsRunning.emit(timer_task) self.timerTaskIsRunning.emit(timer_task)
self.__timer_task_timer.stop() self.__timer_task_timer.stop()
self.__is_running_timer_task = True self.__is_running_timer_task = True
self.setControlButtons(True, True, False) self.setControlButtons(None, True, False)
if not timer_task["silent"]: if not timer_task["silent"]:
self.TrayIcon.showMessage( self.TrayIcon.showMessage(
"定时任务 - AutoLibrary", "定时任务 - AutoLibrary",
@@ -242,11 +244,11 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.showNormal() self.showNormal()
self.__current_timer_task_thread = TimerTaskWorker( self.__current_timer_task_thread = TimerTaskWorker(
timer_task, timer_task,
self.__input_queue, self._input_queue,
self.__output_queue, self._output_queue,
self.__config_paths self.__config_paths
) )
self.__current_timer_task_thread.finishedSignal_TimerWorker.connect(self.onTimerTaskFinished) self.__current_timer_task_thread.timerTaskWorkerIsFinished.connect(self.onTimerTaskFinished)
self.__current_timer_task_thread.start() self.__current_timer_task_thread.start()
except queue.Empty: except queue.Empty:
self.__is_running_timer_task = False self.__is_running_timer_task = False
@@ -268,23 +270,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
if start_button_enabled is not None: if start_button_enabled is not None:
self.StartButton.setEnabled(start_button_enabled) self.StartButton.setEnabled(start_button_enabled)
@Slot()
def showMsg(
self,
msg: str
):
self.__output_queue.put(f"[{self.__class_name:<15}] >>> : {msg}")
@Slot()
def showTrace(
self,
msg: str
):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.__output_queue.put(f"{timestamp}-[{self.__class_name:<15}] : {msg}")
@Slot() @Slot()
def pollMsgQueue( def pollMsgQueue(
self self
@@ -292,30 +277,28 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
try: try:
while True: while True:
msg = self.__output_queue.get_nowait() msg = self._output_queue.get_nowait()
self.appendToTextEdit(msg) self.appendToTextEdit(msg)
except queue.Empty: except queue.Empty:
pass pass
@Slot() @Slot()
def onTimerTaskWidgetClosed( def onTimerTaskManageWidgetClosed(
self self
): ):
self.TimerTaskWidgetButton.setEnabled(True) self.TimerTaskManageWidgetButton.setEnabled(True)
@Slot(dict) @Slot(dict)
def onConfigWidgetClosed( def onConfigWidgetClosed(
self, self
config_paths: dict
): ):
if self.__alConfigWidget: if self.__alConfigWidget:
self.__alConfigWidget.configWidgetCloseSingal.disconnect(self.onConfigWidgetClosed) self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
self.__alConfigWidget.deleteLater() self.__alConfigWidget.deleteLater()
self.__alConfigWidget = None self.__alConfigWidget = None
self.setControlButtons(True, None, None) self.setControlButtons(True, None, None)
self.__config_paths = config_paths
@Slot(dict) @Slot(dict)
def onTimerTaskIsReady( def onTimerTaskIsReady(
@@ -333,7 +316,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
): ):
self.__current_timer_task_thread.wait(1000) self.__current_timer_task_thread.wait(1000)
self.__current_timer_task_thread.finishedSignal_TimerWorker.disconnect(self.onTimerTaskFinished) self.__current_timer_task_thread.timerTaskWorkerIsFinished.disconnect(self.onTimerTaskFinished)
self.__current_timer_task_thread.deleteLater() self.__current_timer_task_thread.deleteLater()
self.__current_timer_task_thread = None self.__current_timer_task_thread = None
self.setControlButtons(None, False, True) self.setControlButtons(None, False, True)
@@ -343,10 +326,10 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.TrayIcon.showMessage( self.TrayIcon.showMessage(
"定时任务 - AutoLibrary", "定时任务 - AutoLibrary",
f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}", f"\n定时任务 '{timer_task['name']}' 执行{'失败' if is_error else '完成'}",
QSystemTrayIcon.MessageIcon.Information, QSystemTrayIcon.MessageIcon.Warning if is_error else QSystemTrayIcon.MessageIcon.Information,
1000 1000
) )
self.showTrace( self._showTrace(
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}" f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
) )
if not is_error: if not is_error:
@@ -355,14 +338,14 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.timerTaskIsError.emit(timer_task) self.timerTaskIsError.emit(timer_task)
@Slot() @Slot()
def onTimerTaskWidgetButtonClicked( def onTimerTaskManageWidgetButtonClicked(
self self
): ):
self.__alTimerTaskWidget.show() self.__alTimerTaskManageWidget.show()
self.__alTimerTaskWidget.raise_() self.__alTimerTaskManageWidget.raise_()
self.__alTimerTaskWidget.activateWindow() self.__alTimerTaskManageWidget.activateWindow()
self.TimerTaskWidgetButton.setEnabled(False) self.TimerTaskManageWidgetButton.setEnabled(False)
@Slot() @Slot()
def onConfigButtonClicked( def onConfigButtonClicked(
@@ -370,11 +353,8 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
): ):
if self.__alConfigWidget is None: if self.__alConfigWidget is None:
self.__alConfigWidget = ALConfigWidget( self.__alConfigWidget = ALConfigWidget(self)
self, self.__alConfigWidget.configWidgetIsClosed.connect(self.onConfigWidgetClosed)
self.__config_paths
)
self.__alConfigWidget.configWidgetCloseSingal.connect(self.onConfigWidgetClosed)
self.__alConfigWidget.show() self.__alConfigWidget.show()
self.__alConfigWidget.raise_() self.__alConfigWidget.raise_()
self.__alConfigWidget.activateWindow() self.__alConfigWidget.activateWindow()
@@ -388,12 +368,12 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
self.setControlButtons(None, True, False) self.setControlButtons(None, True, False)
if self.__auto_lib_thread is None: if self.__auto_lib_thread is None:
self.__auto_lib_thread = AutoLibWorker( self.__auto_lib_thread = AutoLibWorker(
self.__input_queue, self._input_queue,
self.__output_queue, self._output_queue,
self.__config_paths self.__config_paths
) )
self.__auto_lib_thread.finishedSignal.connect(self.onStopButtonClicked) self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked)
self.__auto_lib_thread.finishedWithErrorSignal.connect(self.onStopButtonClicked) self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
self.__auto_lib_thread.start() self.__auto_lib_thread.start()
@Slot() @Slot()
@@ -402,11 +382,11 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
): ):
if self.__auto_lib_thread: if self.__auto_lib_thread:
self.showTrace("正在停止操作......") self._showTrace("正在停止操作......")
self.__auto_lib_thread.wait(2000) self.__auto_lib_thread.wait(2000)
self.showTrace("操作已停止") self._showTrace("操作已停止")
self.__auto_lib_thread.finishedSignal.disconnect(self.onStopButtonClicked) self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.finishedWithErrorSignal.disconnect(self.onStopButtonClicked) self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
self.__auto_lib_thread.deleteLater() self.__auto_lib_thread.deleteLater()
self.__auto_lib_thread = None self.__auto_lib_thread = None
self.setControlButtons(None, False, True) self.setControlButtons(None, False, True)
@@ -419,6 +399,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
msg = self.MessageEdit.text().strip() msg = self.MessageEdit.text().strip()
if not msg: if not msg:
return return
self.showMsg(msg) self._showMsg(msg)
self.__input_queue.put(msg) # put message to input queue self._input_queue.put(msg) # put message to input queue
self.MessageEdit.clear() self.MessageEdit.clear()
+20 -24
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -17,13 +17,13 @@ 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.ConfigReader import ConfigReader from utils.JSONReader import JSONReader
class AutoLibWorker(QThread, MsgBase): class AutoLibWorker(MsgBase, QThread):
finishedSignal = Signal() autoLibWorkerIsFinished = Signal()
finishedWithErrorSignal = Signal() autoLibWorkerFinishedWithError = Signal()
def __init__( def __init__(
self, self,
@@ -32,8 +32,8 @@ class AutoLibWorker(QThread, MsgBase):
config_paths: dict config_paths: dict
): ):
super().__init__(input_queue = input_queue, output_queue = output_queue) MsgBase.__init__(self, input_queue, output_queue)
QThread.__init__(self)
self.__config_paths = config_paths self.__config_paths = config_paths
@@ -69,15 +69,11 @@ class AutoLibWorker(QThread, MsgBase):
self._showTrace( self._showTrace(
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}" f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
) )
self.__run_config = ConfigReader( self.__run_config = JSONReader(self.__config_paths["run"]).data()
self.__config_paths["run"]
).getConfigs()
self._showTrace( self._showTrace(
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}" f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
) )
self.__user_config = ConfigReader( self.__user_config = JSONReader(self.__config_paths["user"]).data()
self.__config_paths["user"]
).getConfigs()
if self.__run_config is None or self.__user_config is None: if self.__run_config is None or self.__user_config is None:
self._showTrace("配置文件加载失败, 请检查配置文件是否正确") self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
self._showTrace("配置文件加载失败, 请检查配置文件是否正确") self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
@@ -120,17 +116,17 @@ class AutoLibWorker(QThread, MsgBase):
) )
except Exception as e: except Exception as e:
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}") self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
self.finishedWithErrorSignal.emit() self.autoLibWorkerFinishedWithError.emit()
return return
if auto_lib: if auto_lib:
auto_lib.close() auto_lib.close()
self._showTrace("AutoLibrary 运行结束") self._showTrace("AutoLibrary 运行结束")
self.finishedSignal.emit() self.autoLibWorkerIsFinished.emit()
class TimerTaskWorker(AutoLibWorker): class TimerTaskWorker(AutoLibWorker):
finishedSignal_TimerWorker = Signal(bool, dict) timerTaskWorkerIsFinished = Signal(bool, dict)
def __init__( def __init__(
self, self,
@@ -141,10 +137,10 @@ class TimerTaskWorker(AutoLibWorker):
): ):
super().__init__(input_queue, output_queue, config_paths) super().__init__(input_queue, output_queue, config_paths)
self.__timer_task = timer_task self.__timer_task = timer_task
self.finishedSignal.connect(self.onTimerTaskIsFinished)
self.finishedWithErrorSignal.connect(self.onTimerTaskIsError) self.autoLibWorkerIsFinished.connect(self.onTimerTaskIsFinished)
self.autoLibWorkerFinishedWithError.connect(self.onTimerTaskFinishedWithError)
def run( def run(
self self
@@ -153,18 +149,18 @@ class TimerTaskWorker(AutoLibWorker):
self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行") self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行")
super().run() super().run()
@Slot(dict) @Slot()
def onTimerTaskIsError( def onTimerTaskFinishedWithError(
self self
): ):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常") self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
self.finishedSignal_TimerWorker.emit(True, self.__timer_task) self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
@Slot(dict) @Slot()
def onTimerTaskIsFinished( def onTimerTaskIsFinished(
self self
): ):
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束") self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
self.finishedSignal_TimerWorker.emit(False, self.__timer_task) self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
+15 -13
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -22,12 +22,13 @@ class ALSeatFrame(QFrame):
def __init__( def __init__(
self, self,
seat_number, seat_number,
parent=None parent = None
): ):
super().__init__(parent) super().__init__(parent)
self.__seat_number = seat_number self.__seat_number = seat_number
self.__is_selected = False self.__is_selected = False
self.setupUi() self.setupUi()
def setupUi( def setupUi(
@@ -39,18 +40,19 @@ class ALSeatFrame(QFrame):
self.setLineWidth(2) self.setLineWidth(2)
self.setStyleSheet(""" self.setStyleSheet("""
QFrame { QFrame {
background-color: #4196EB; background-color: #2294FF;
border: 2px solid #4196EB; border: 2px solid #2294FF;
border-radius: 5px; border-radius: 5px;
} }
QLabel { QLabel {
color: #F0F0F0; color: #FFFFFF;
font-weight: bold; font-weight: bold;
} }
""") """)
self.label = QLabel(self.__seat_number, self) self.setCursor(Qt.CursorShape.PointingHandCursor)
self.label.setAlignment(Qt.AlignCenter) self.Label = QLabel(self.__seat_number, self)
self.label.setGeometry(0, 0, 60, 40) self.Label.setAlignment(Qt.AlignCenter)
self.Label.setGeometry(0, 0, 60, 40)
def mousePressEvent( def mousePressEvent(
self, self,
@@ -76,24 +78,24 @@ class ALSeatFrame(QFrame):
self.setStyleSheet(""" self.setStyleSheet("""
QFrame { QFrame {
background-color: #4CAF50; background-color: #4CAF50;
border: 2px solid #388E3C; border: 2px solid #4CAF50;
border-radius: 5px; border-radius: 5px;
color: white; color: white;
} }
QLabel { QLabel {
color: #F0F0F0; color: #FFFFFF;
font-weight: bold; font-weight: bold;
} }
""") """)
else: else:
self.setStyleSheet(""" self.setStyleSheet("""
QFrame { QFrame {
background-color: #4196EB; background-color: #2294FF;
border: 2px solid #4196EB; border: 2px solid #2294FF;
border-radius: 5px; border-radius: 5px;
} }
QLabel { QLabel {
color: #F0F0F0; color: #FFFFFF;
font-weight: bold; font-weight: bold;
} }
""") """)
+178
View File
@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 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 (
Qt, Slot, Signal
)
from PySide6.QtWidgets import (
QDialog, QLabel, QHBoxLayout, QVBoxLayout,
QPushButton,
)
from PySide6.QtGui import (
QCloseEvent
)
from gui.ALSeatMapView import ALSeatMapView
class ALSeatMapSelectDialog(QDialog):
seatMapSelectDialogIsClosed = Signal(list)
def __init__(
self,
parent: QDialog = None,
floor: str = "",
room: str = "",
seats_data: str = ""
):
super().__init__(parent)
self.__floor = floor
self.__room = room
self.__seats_data = seats_data
self.__confirmed = False
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setModal(True)
self.setMinimumSize(800, 600)
self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.SeatMapWidgetMainLayout.setContentsMargins(5, 5, 5, 5)
self.SeatMapWidgetMainLayout.setSpacing(5)
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
self.SeatMapGraphicsView = ALSeatMapView(None, self.__seats_data)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
)
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setAutoDefault(True)
self.ConfirmButton.setDefault(True)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.SeatMapWidgetControlLayout = QHBoxLayout()
self.SeatMapWidgetControlLayout.setContentsMargins(0, 0, 0, 0)
self.SeatMapWidgetControlLayout.setSpacing(5)
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals(
self
):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent(
self,
event: QCloseEvent
):
if not self.__confirmed:
self.clearSelections()
self.reject()
else:
self.accept()
self.seatMapSelectDialogIsClosed.emit(self.getSelectedSeats())
super().closeEvent(event)
def selectSeat(
self,
seat_number: str
):
self.SeatMapGraphicsView.selectSeat(seat_number)
def selectSeats(
self,
seat_numbers: list[str]
) -> bool:
return self.SeatMapGraphicsView.selectSeats(seat_numbers)
def getSelectedSeats(
self
) -> list[str]:
return self.SeatMapGraphicsView.getSelectedSeats()
def clearSelections(
self
):
self.SeatMapGraphicsView.clearSelections()
@Slot()
def onConfirmButtonClicked(
self
):
self.__confirmed = True
self.accept()
@Slot()
def onCancelButtonClicked(
self
):
self.__confirmed = False
self.reject()
+1 -1
View File
@@ -1,4 +1,4 @@
seats_maps = { ALSeatMapTable = {
"2": { "2": {
"1": """ "1": """
,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,, ,,,,,,,,,,,039A,039B,,040A,040B,,041A,041B,,042A,042B,,043A,043B,,044A,044B,,,,,,,,,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -8,40 +8,32 @@ 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, QEvent Qt, Slot, QEvent
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFrame, QWidget, QLabel, QHBoxLayout, QVBoxLayout, QFrame, QWidget,
QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem, QGridLayout, QGraphicsView, QGraphicsScene, QGraphicsItem
QPushButton,
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QPainter, QWheelEvent, QCloseEvent QPainter, QWheelEvent
) )
from gui.ALSeatFrame import ALSeatFrame from gui.ALSeatFrame import ALSeatFrame
class ALSeatMapWidget(QWidget): class ALSeatMapView(QGraphicsView):
seatMapWidgetClosed = Signal(list)
def __init__( def __init__(
self, self,
parent: QWidget = None, parent: QWidget = None,
floor: str = "",
room: str = "",
seats_data: dict = {}, seats_data: dict = {},
): ):
super().__init__(parent) super().__init__(parent)
self.__floor = floor
self.__room = room
self.__seats_data = seats_data self.__seats_data = seats_data
self.__selected_seats = [] self.__selected_seats = []
self.__seat_frames = {} self.__seat_frames = {}
self.setupUi() self.setupUi()
self.connectSignals()
@staticmethod @staticmethod
def formatSeatNumber( def formatSeatNumber(
@@ -55,106 +47,13 @@ class ALSeatMapWidget(QWidget):
return seat_number.zfill(3) return seat_number.zfill(3)
def setupUi(
self
):
self.setWindowFlags(Qt.WindowType.Window)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(800, 600)
self.resize(800, 600)
self.setWindowTitle(f"选择楼层座位 - AutoLibrary")
self.SeatMapWidgetMainLayout = QVBoxLayout(self)
self.TitleLabel = QLabel(f"楼层座位分布图: {self.__floor}-{self.__room}")
self.TitleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.TitleLabel.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
self.SeatMapWidgetMainLayout.addWidget(self.TitleLabel)
self.SeatMapGraphicsView = QGraphicsView(self)
self.SeatMapGraphicsScene = QGraphicsScene(self)
self.SeatMapGraphicsView.setScene(self.SeatMapGraphicsScene)
self.SeatMapGraphicsView.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
self.SeatMapGraphicsView.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.SeatMapGraphicsView.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.SeatMapGraphicsView.viewport().installEventFilter(self)
self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.createSeatMap()
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
self.SeatMapWidgetMainLayout.addWidget(self.SeatMapGraphicsView)
self.TipsLabel = QLabel(
" 点击座位进行选择/取消选择, 最多选择1个座位 \n"
" [操作方法: Ctrl+鼠标滚轮缩放 | 滚轮/拖拽/方向键 移动]"
)
self.TipsLabel.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.TipsLabel.setStyleSheet("color: #666; margin: 5px;")
self.SeatMapWidgetMainLayout.addWidget(self.TipsLabel)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.SeatMapWidgetControlLayout = QHBoxLayout()
self.SeatMapWidgetControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.SeatMapWidgetControlLayout.addWidget(self.CancelButton)
self.SeatMapWidgetControlLayout.addWidget(self.ConfirmButton)
self.SeatMapWidgetMainLayout.addLayout(self.SeatMapWidgetControlLayout)
def connectSignals(
self
):
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
def showEvent(
self,
event
):
result = super().showEvent(event)
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def closeEvent(
self,
event: QCloseEvent
):
self.seatMapWidgetClosed.emit(self.__selected_seats)
super().closeEvent(event)
def eventFilter( def eventFilter(
self, self,
watched, watched,
event event
): ):
if (watched is self.SeatMapGraphicsView.viewport() and if (watched is self.viewport() and
event.type() == QEvent.Type.Wheel and event.type() == QEvent.Type.Wheel and
event.modifiers() == Qt.KeyboardModifier.ControlModifier event.modifiers() == Qt.KeyboardModifier.ControlModifier
): ):
@@ -169,12 +68,40 @@ class ALSeatMapWidget(QWidget):
): ):
delta = event.angleDelta().y() delta = event.angleDelta().y()
min_scale = 0.1
max_scale = 4.0
current_scale = self.transform().m11()
zoom_factor = 1.2 if delta > 0 else 1/1.2 zoom_factor = 1.2 if delta > 0 else 1/1.2
self.SeatMapGraphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) target_scale = current_scale*zoom_factor
self.SeatMapGraphicsView.scale(zoom_factor, zoom_factor) if target_scale < min_scale and delta < 0:
return
if target_scale > max_scale and delta > 0:
return
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.scale(zoom_factor, zoom_factor)
def createSeatMap( def setupUi(
self
):
self.SeatMapGraphicsScene = QGraphicsScene(self)
self.setScene(self.SeatMapGraphicsScene)
self.setRenderHint(QPainter.RenderHint.LosslessImageRendering)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.viewport().installEventFilter(self)
self.SeatsContainerWidget = QWidget()
self.SeatsContainerLayout = QGridLayout(self.SeatsContainerWidget)
self.setupSeatMap()
self.ContainerProxy = self.SeatMapGraphicsScene.addWidget(self.SeatsContainerWidget)
self.ContainerProxy.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
def setupSeatMap(
self self
): ):
@@ -259,18 +186,3 @@ class ALSeatMapWidget(QWidget):
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()
@Slot()
def onConfirmButtonClicked(
self
):
self.close()
@Slot()
def onCancelButtonClicked(
self
):
self.clearSelections()
self.close()
@@ -1,36 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import os
import sys
import time
import uuid import uuid
import queue
from enum import Enum from enum import Enum
from datetime import datetime, timedelta from datetime import datetime, timedelta
from PySide6.QtCore import ( from PySide6.QtCore import Slot, QDateTime
Qt, Signal, Slot, QDateTime from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtWidgets import (
QLabel, QDialog, QWidget, QSpinBox, QVBoxLayout,
QHBoxLayout, QGridLayout, QDateTimeEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from gui.Ui_ALAddTimerTaskDialog import Ui_ALAddTimerTaskDialog from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
import utils.TimerUtils as TimerUtils
class TimerTaskStatus(Enum): class ALTimerTaskStatus(Enum):
PENDING = "等待中" PENDING = "等待中"
READY = "已就绪" READY = "已就绪"
@@ -38,9 +27,10 @@ class TimerTaskStatus(Enum):
EXECUTED = "已执行" EXECUTED = "已执行"
ERROR = "执行失败" ERROR = "执行失败"
OUTDATED = "已过期" OUTDATED = "已过期"
UNKNOWN = "未知"
class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog): class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
def __init__( def __init__(
self, self,
@@ -50,8 +40,8 @@ class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
self.connectSignals()
self.modifyUi() self.modifyUi()
self.connectSignals()
def modifyUi( def modifyUi(
@@ -71,28 +61,28 @@ class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
self.TimerConfigLayout.addWidget(self.SpecificTimerWidget) self.TimerConfigLayout.addWidget(self.SpecificTimerWidget)
self.RelativeTimerWidget = QWidget() self.RelativeTimerWidget = QWidget()
self.RelativeTimerLayout = QGridLayout(self.RelativeTimerWidget) self.RelativeTimerLayout = QHBoxLayout(self.RelativeTimerWidget)
self.RelativeTimerLayout.addWidget(QLabel("相对时间:"), 0, 0) self.RelativeTimerLayout.addWidget(QLabel("相对时间:"))
self.RelativeDaySpinBox = QSpinBox() self.RelativeDaySpinBox = QSpinBox()
self.RelativeDaySpinBox.setMinimum(0) self.RelativeDaySpinBox.setMinimum(0)
self.RelativeDaySpinBox.setMaximum(365) self.RelativeDaySpinBox.setMaximum(364)
self.RelativeDaySpinBox.setSuffix("") self.RelativeDaySpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox, 1, 0) self.RelativeTimerLayout.addWidget(self.RelativeDaySpinBox)
self.RelativeHourSpinBox = QSpinBox() self.RelativeHourSpinBox = QSpinBox()
self.RelativeHourSpinBox.setMinimum(0) self.RelativeHourSpinBox.setMinimum(0)
self.RelativeHourSpinBox.setMaximum(23) self.RelativeHourSpinBox.setMaximum(23)
self.RelativeHourSpinBox.setSuffix("") self.RelativeHourSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox, 1, 1) self.RelativeTimerLayout.addWidget(self.RelativeHourSpinBox)
self.RelativeMinuteSpinBox = QSpinBox() self.RelativeMinuteSpinBox = QSpinBox()
self.RelativeMinuteSpinBox.setMinimum(0) self.RelativeMinuteSpinBox.setMinimum(0)
self.RelativeMinuteSpinBox.setMaximum(59) self.RelativeMinuteSpinBox.setMaximum(59)
self.RelativeMinuteSpinBox.setSuffix("") self.RelativeMinuteSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox, 1, 2) self.RelativeTimerLayout.addWidget(self.RelativeMinuteSpinBox)
self.RelativeSecondSpinBox = QSpinBox() self.RelativeSecondSpinBox = QSpinBox()
self.RelativeSecondSpinBox.setMinimum(0) self.RelativeSecondSpinBox.setMinimum(0)
self.RelativeSecondSpinBox.setMaximum(59) self.RelativeSecondSpinBox.setMaximum(59)
self.RelativeSecondSpinBox.setSuffix("") self.RelativeSecondSpinBox.setSuffix("")
self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox, 1, 3) self.RelativeTimerLayout.addWidget(self.RelativeSecondSpinBox)
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget) self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
self.RelativeTimerWidget.setVisible(False) self.RelativeTimerWidget.setVisible(False)
@@ -104,6 +94,7 @@ class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
self.CancelButton.clicked.connect(self.reject) self.CancelButton.clicked.connect(self.reject)
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)
def getTimerTask( def getTimerTask(
@@ -128,17 +119,47 @@ class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
minutes = self.RelativeMinuteSpinBox.value(), minutes = self.RelativeMinuteSpinBox.value(),
seconds = self.RelativeSecondSpinBox.value() seconds = self.RelativeSecondSpinBox.value()
) )
return { task_data = {
"name": name, "name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}", "task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
"time_type": self.TimerTypeComboBox.currentText(), "time_type": self.TimerTypeComboBox.currentText(),
"execute_time": execute_time, "execute_time": execute_time,
"silent": silent, "silent": silent,
"add_time": added_time, "add_time": added_time,
"status": TimerTaskStatus.PENDING, "status": ALTimerTaskStatus.PENDING,
"executed": False "executed": False,
"repeat": self.RepeatCheckBox.isChecked(),
} }
if task_data["repeat"]:
repeat_days = []
if self.MonCheckBox.isChecked():
repeat_days.append(0)
if self.TueCheckBox.isChecked():
repeat_days.append(1)
if self.WedCheckBox.isChecked():
repeat_days.append(2)
if self.ThuCheckBox.isChecked():
repeat_days.append(3)
if self.FriCheckBox.isChecked():
repeat_days.append(4)
if self.SatCheckBox.isChecked():
repeat_days.append(5)
if self.SunCheckBox.isChecked():
repeat_days.append(6)
if not repeat_days:
repeat_days = [0, 1, 2, 3, 4, 5, 6]
task_data["repeat_days"] = repeat_days
task_data["repeat_hour"] = execute_time.hour
task_data["repeat_minute"] = execute_time.minute
task_data["repeat_second"] = execute_time.second
task_data["execute_time"] = TimerUtils.calculateNextRepeatTime(
task_data["repeat_days"],
task_data["repeat_hour"],
task_data["repeat_minute"],
task_data["repeat_second"]
)
return task_data
@Slot(int) @Slot(int)
def onTimerTypeComboBoxIndexChanged( def onTimerTypeComboBoxIndexChanged(
@@ -148,3 +169,17 @@ class ALAddTimerTaskWidget(QDialog, Ui_ALAddTimerTaskDialog):
self.SpecificTimerWidget.setVisible(index == 0) self.SpecificTimerWidget.setVisible(index == 0)
self.RelativeTimerWidget.setVisible(index == 1) self.RelativeTimerWidget.setVisible(index == 1)
@Slot(bool)
def onRepeatCheckBoxToggled(
self,
checked: bool
):
self.MonCheckBox.setEnabled(checked)
self.TueCheckBox.setEnabled(checked)
self.WedCheckBox.setEnabled(checked)
self.ThuCheckBox.setEnabled(checked)
self.FriCheckBox.setEnabled(checked)
self.SatCheckBox.setEnabled(checked)
self.SunCheckBox.setEnabled(checked)
+147
View File
@@ -0,0 +1,147 @@
# -*- 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 datetime
from PySide6.QtCore import Slot, Qt
from PySide6.QtWidgets import (
QDialog, QTableWidget, QTableWidgetItem,
QVBoxLayout, QHBoxLayout, QGridLayout,
QPushButton, QLabel, QHeaderView
)
from gui.ALTimerTaskAddDialog import ALTimerTaskStatus
class ALTimerTaskHistoryDialog(QDialog):
def __init__(
self,
parent = None,
task_data: dict = None
):
super().__init__(parent)
self.__task_data = task_data
self.__history = task_data.get("history", [])
self.modifyUi()
self.connectSignals()
def modifyUi(
self
):
self.setWindowTitle("定时任务执行历史 - AutoLibrary")
self.setMinimumSize(300, 300)
self.setMaximumSize(500, 400)
MainLayout = QVBoxLayout(self)
InfoLayout = QGridLayout()
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 14px;")
InfoLayout.addWidget(TaskNameLabel, 0, 0)
TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}")
TaskUUIDLabel.setStyleSheet("font-size: 10px;")
InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
InfoLayout.setColumnStretch(0, 1)
if self.__task_data.get("repeat", False):
RepeatLabel = QLabel("重复任务")
RepeatLabel.setStyleSheet("color: #2294FF; font-weight: bold; font-size: 12px;")
InfoLayout.addWidget(RepeatLabel, 0, 1)
MainLayout.addLayout(InfoLayout)
self.HistoryTableWidget = QTableWidget()
self.HistoryTableWidget.setColumnCount(3)
self.HistoryTableWidget.setHorizontalHeaderLabels(["执行时间", "结果", "耗时(秒/s"])
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
self.HistoryTableWidget.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.HistoryTableWidget.verticalHeader().setVisible(False)
self.HistoryTableWidget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.HistoryTableWidget.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.loadHistory()
MainLayout.addWidget(self.HistoryTableWidget)
ButtonLayout = QHBoxLayout()
ButtonLayout.addStretch()
self.CloseButton = QPushButton("关闭")
self.CloseButton.setFixedSize(80, 25)
self.CloseButton.setDefault(True)
self.ClearHistoryButton = QPushButton("清空历史")
self.ClearHistoryButton.setFixedSize(80, 25)
self.ClearHistoryButton.setStyleSheet("color: #DC0000;")
ButtonLayout.addWidget(self.ClearHistoryButton)
ButtonLayout.addWidget(self.CloseButton)
MainLayout.addLayout(ButtonLayout)
def connectSignals(
self
):
self.CloseButton.clicked.connect(self.accept)
self.ClearHistoryButton.clicked.connect(self.onClearHistoryButtonClicked)
def loadHistory(
self
):
self.HistoryTableWidget.setRowCount(len(self.__history))
for row, record in enumerate(self.__history):
self.addHistoryRow(row, record)
def addHistoryRow(
self,
row: int,
record: dict
):
execute_time = record.get("execute_time", "")
result = record.get("result", ALTimerTaskStatus.UNKNOWN)
duration = record.get("duration", 0)
ExecuteTimeItem = QTableWidgetItem(execute_time)
ExecuteTimeItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 0, ExecuteTimeItem)
ResultItem = QTableWidgetItem(result.value)
ResultItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
match result:
case ALTimerTaskStatus.EXECUTED:
ResultItem.setForeground(Qt.GlobalColor.green)
case ALTimerTaskStatus.ERROR:
ResultItem.setForeground(Qt.GlobalColor.red)
case ALTimerTaskStatus.OUTDATED:
ResultItem.setForeground(Qt.GlobalColor.red)
case _:
ResultItem.setForeground(Qt.GlobalColor.black)
self.HistoryTableWidget.setItem(row, 1, ResultItem)
DurationItem = QTableWidgetItem(f"{duration:.2f}")
DurationItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.HistoryTableWidget.setItem(row, 2, DurationItem)
self.HistoryTableWidget.setRowHeight(row, 25)
@Slot()
def onClearHistoryButtonClicked(
self
):
self.__history.clear()
self.HistoryTableWidget.setRowCount(0)
self.__task_data["history"] = self.__history
def getHistory(
self
) -> list:
return self.__history
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -9,9 +9,7 @@ See the LICENSE file for details.
""" """
import os import os
import sys import sys
import time
import copy import copy
import queue
from enum import Enum from enum import Enum
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -24,24 +22,18 @@ from PySide6.QtWidgets import (
QHBoxLayout, QVBoxLayout, QLabel, QPushButton QHBoxLayout, QVBoxLayout, QLabel, QPushButton
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QCloseEvent, QScreen QCloseEvent
) )
from gui.Ui_ALTimerTaskWidget import Ui_ALTimerTaskWidget import utils.ConfigManager as ConfigManager
from gui.ALAddTimerTaskDialog import ALAddTimerTaskWidget, TimerTaskStatus import utils.TimerUtils as TimerUtils
from utils.ConfigReader import ConfigReader from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
from utils.ConfigWriter import ConfigWriter from gui.ALTimerTaskAddDialog import ALTimerTaskAddDialog, ALTimerTaskStatus
from gui.ALTimerTaskHistoryDialog import ALTimerTaskHistoryDialog
class SortPolicy(Enum): class ALTimerTaskItemWidget(QWidget):
BY_NAME = "按名称"
BY_ADD_TIME = "按添加时间"
BY_EXECUTE_TIME = "按执行时间"
class TimerTaskItemWidget(QWidget):
def __init__( def __init__(
self, self,
@@ -50,8 +42,8 @@ class TimerTaskItemWidget(QWidget):
): ):
super().__init__(parent) super().__init__(parent)
self.__timer_task = timer_task self.__timer_task = timer_task
self.modifyUi() self.modifyUi()
@@ -71,40 +63,52 @@ class TimerTaskItemWidget(QWidget):
TaskNameLabel.setFont(TaskNameLabelFont) TaskNameLabel.setFont(TaskNameLabelFont)
TaskNameLabel.setFixedHeight(25) TaskNameLabel.setFixedHeight(25)
self.TaskInfoLayout.addWidget(TaskNameLabel) self.TaskInfoLayout.addWidget(TaskNameLabel)
ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") ExecuteTimeStr = self.__timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}") if self.__timer_task.get("repeat", False):
ExecuteTimeLabel.setStyleSheet("color: gray;") repeat_days = self.__timer_task.get("repeat_days", [])
repeat_hour = self.__timer_task.get("repeat_hour", 0)
repeat_minute = self.__timer_task.get("repeat_minute", 0)
repeat_second = self.__timer_task.get("repeat_second", 0)
if len(repeat_days) == 7:
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}"
ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每日 {time_str})")
else:
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
selected_days = [day_names[d] for d in repeat_days]
time_str = f"{repeat_hour:02d}:{repeat_minute:02d}:{repeat_second:02d}"
ExecuteTimeLabel = QLabel(f"下次执行时间: {ExecuteTimeStr} (每{','.join(selected_days)} {time_str})")
else:
ExecuteTimeLabel = QLabel(f"执行时间: {ExecuteTimeStr}")
ExecuteTimeLabel.setStyleSheet("color: #969696;")
ExecuteTimeLabel.setFixedHeight(20) ExecuteTimeLabel.setFixedHeight(20)
self.TaskInfoLayout.addWidget(ExecuteTimeLabel) self.TaskInfoLayout.addWidget(ExecuteTimeLabel)
self.ItemWidgetLayout.addLayout(self.TaskInfoLayout) self.ItemWidgetLayout.addLayout(self.TaskInfoLayout)
self.ItemWidgetLayout.addStretch() self.ItemWidgetLayout.addStretch()
match self.__timer_task["status"]: match self.__timer_task["status"]:
case TimerTaskStatus.PENDING: case ALTimerTaskStatus.PENDING:
TaskStatusText = "等待中" TaskStatusText = "等待中"
TaskStatusColor = "#FF9800" TaskStatusColor = "#FF9800"
case TimerTaskStatus.READY: case ALTimerTaskStatus.READY:
TaskStatusText = "已就绪" TaskStatusText = "已就绪"
TaskStatusColor = "#316BFF" TaskStatusColor = "#316BFF"
case TimerTaskStatus.RUNNING: case ALTimerTaskStatus.RUNNING:
TaskStatusText = "执行中" TaskStatusText = "执行中"
TaskStatusColor = "#2294FF" TaskStatusColor = "#2294FF"
case TimerTaskStatus.EXECUTED: case ALTimerTaskStatus.EXECUTED:
TaskStatusText = "已执行" TaskStatusText = "已执行"
TaskStatusColor = "#4CAF50" TaskStatusColor = "#4CAF50"
case TimerTaskStatus.ERROR: case ALTimerTaskStatus.ERROR:
TaskStatusText = "执行失败" TaskStatusText = "执行失败"
TaskStatusColor = "#FF5722" TaskStatusColor = "#DC0000"
case TimerTaskStatus.OUTDATED: case ALTimerTaskStatus.OUTDATED:
TaskStatusText = "已过期" TaskStatusText = "已过期"
TaskStatusColor = "#FF5722" TaskStatusColor = "#DC0000"
TaskStatusLabel = QLabel(TaskStatusText) TaskStatusLabel = QLabel(TaskStatusText)
TaskStatusLabel.setStyleSheet(f""" TaskStatusLabel.setStyleSheet(f"""
QLabel {{ QLabel {{
background-color: {TaskStatusColor}; background-color: {TaskStatusColor};
color: white; color: #FFFFFF;
border-radius: 5px; border-radius: 5px;
font-weight: bold; font-weight: bold;
}} }}
@@ -119,7 +123,7 @@ class TimerTaskItemWidget(QWidget):
TaskModeLabel.setStyleSheet(f""" TaskModeLabel.setStyleSheet(f"""
QLabel {{ QLabel {{
background-color: {TaskModeColor}; background-color: {TaskModeColor};
color: white; color: #FFFFFF;
border-radius: 5px; border-radius: 5px;
font-weight: bold; font-weight: bold;
}} }}
@@ -128,40 +132,49 @@ class TimerTaskItemWidget(QWidget):
TaskModeLabel.setFixedSize(60, 25) TaskModeLabel.setFixedSize(60, 25)
self.ItemWidgetLayout.addWidget(TaskModeLabel) self.ItemWidgetLayout.addWidget(TaskModeLabel)
if self.__timer_task.get("repeat", False):
self.HistoryButton = QPushButton("历史")
self.HistoryButton.setFixedSize(80, 25)
self.ItemWidgetLayout.addWidget(self.HistoryButton)
self.DeleteButton = QPushButton("删除") self.DeleteButton = QPushButton("删除")
self.DeleteButton.setFixedSize(80, 25) self.DeleteButton.setFixedSize(80, 25)
self.DeleteButton.setStyleSheet("color: #DC0000;")
self.ItemWidgetLayout.addWidget(self.DeleteButton) self.ItemWidgetLayout.addWidget(self.DeleteButton)
if self.__timer_task["status"] == TimerTaskStatus.READY\ if self.__timer_task["status"] == ALTimerTaskStatus.READY\
or self.__timer_task["status"] == TimerTaskStatus.RUNNING: or self.__timer_task["status"] == ALTimerTaskStatus.RUNNING:
self.DeleteButton.setEnabled(False) self.DeleteButton.setEnabled(False)
self.setFixedHeight(55) self.setFixedHeight(55)
class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget): class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
class SortPolicy(Enum):
BY_NAME = "按名称"
BY_ADD_TIME = "按添加时间"
BY_EXECUTE_TIME = "按执行时间"
timerTasksChanged = Signal()
timerTaskIsReady = Signal(dict) timerTaskIsReady = Signal(dict)
timerTaskWidgetClosed = Signal() timerTasksChanged = Signal()
timerTaskManageWidgetIsClosed = Signal()
def __init__( def __init__(
self, self,
parent = None, parent = None
timer_tasks_config_path: str = ""
): ):
super().__init__(parent) super().__init__(parent)
self.__cfg_mgr = ConfigManager.instance()
self.__timer_tasks = [] self.__timer_tasks = []
self.__check_timer = None self.__check_timer = None
self.__sort_policy = SortPolicy.BY_EXECUTE_TIME self.__sort_policy = self.SortPolicy.BY_EXECUTE_TIME
self.__sort_order = Qt.SortOrder.AscendingOrder self.__sort_order = Qt.SortOrder.AscendingOrder
self.__timer_tasks_config_path = timer_tasks_config_path
self.setupUi(self) self.setupUi(self)
self.connectSignals() self.connectSignals()
self.setupTimer() self.setupTimer()
if not self.initializeTimerTasks(): if not self.initializeTimerTasks():
return raise Exception("定时任务配置文件初始化失败 !")
def connectSignals( def connectSignals(
@@ -188,74 +201,63 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
self self
) -> bool: ) -> bool:
timer_tasks = self.loadTimerTasks(self.__timer_tasks_config_path) timer_tasks = self.getTimerTasks()
if timer_tasks is not None: if timer_tasks is not None:
self.__timer_tasks = timer_tasks self.__timer_tasks = timer_tasks
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
return True return True
timer_tasks = [] timer_tasks = []
if self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(timer_tasks)): if self.setTimerTasks(copy.deepcopy(timer_tasks)):
QMessageBox.information(
self,
"信息 - AutoLibrary",
f"定时任务配置文件初始化完成: \n{self.__timer_tasks_config_path}"
)
self.__timer_tasks = timer_tasks self.__timer_tasks = timer_tasks
self.updateTimerTaskList()
return True return True
return False return False
def loadTimerTasks( def getTimerTasks(
self, self
timer_tasks_config_path: str
) -> list: ) -> list:
try: try:
if not timer_tasks_config_path or not os.path.exists(timer_tasks_config_path): timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
raise Exception("定时任务配置文件不存在")
timer_tasks = ConfigReader(timer_tasks_config_path).getConfigs()
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["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S") task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S") task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
task["status"] = TimerTaskStatus(task["status"]) task["status"] = ALTimerTaskStatus(task["status"])
if "history" in task:
for item in task["history"]:
item["result"] = ALTimerTaskStatus(item["result"])
return timer_tasks["timer_tasks"] return timer_tasks["timer_tasks"]
raise Exception("定时任务配置文件格式错误") raise Exception("定时任务配置文件格式错误")
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"加载定时任务配置发生错误 ! : {e}\n"\ f"加载定时任务配置发生错误 ! : \n{e}"
f"文件路径: {timer_tasks_config_path}"
) )
return None return None
def saveTimerTasks( def setTimerTasks(
self, self,
timer_tasks_config_path: str,
timer_tasks: list timer_tasks: list
) -> bool: ) -> bool:
try: try:
if not timer_tasks_config_path:
raise Exception("配置文件路径为空")
for task in timer_tasks: for task in timer_tasks:
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S") task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S") task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
task["status"] = task["status"].value task["status"] = task["status"].value
ConfigWriter( if "history" in task:
timer_tasks_config_path, for item in task["history"]:
{ "timer_tasks": timer_tasks } item["result"] = item["result"].value
) self.__cfg_mgr.set(ConfigManager.ConfigType.TIMERTASK, "", { "timer_tasks": timer_tasks })
return True return True
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"保存定时任务配置发生错误 ! : {e}\n"\ f"保存定时任务配置发生错误 ! : \n{e}"
f"文件路径: {timer_tasks_config_path}"
) )
return False return False
@@ -290,7 +292,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
): ):
self.hide() self.hide()
self.timerTaskWidgetClosed.emit() self.timerTaskManageWidgetIsClosed.emit()
event.ignore() event.ignore()
@@ -300,17 +302,17 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
order: Qt.SortOrder = Qt.SortOrder.AscendingOrder order: Qt.SortOrder = Qt.SortOrder.AscendingOrder
): ):
if policy == SortPolicy.BY_NAME: if policy == self.SortPolicy.BY_NAME:
self.__timer_tasks.sort( self.__timer_tasks.sort(
key = lambda x: x["name"], key = lambda x: x["name"],
reverse = order is Qt.SortOrder.DescendingOrder reverse = order is Qt.SortOrder.DescendingOrder
) )
elif policy == SortPolicy.BY_ADD_TIME: elif policy == self.SortPolicy.BY_ADD_TIME:
self.__timer_tasks.sort( self.__timer_tasks.sort(
key = lambda x: x["add_time"], key = lambda x: x["add_time"],
reverse = order is Qt.SortOrder.DescendingOrder reverse = order is Qt.SortOrder.DescendingOrder
) )
elif policy == SortPolicy.BY_EXECUTE_TIME: elif policy == self.SortPolicy.BY_EXECUTE_TIME:
self.__timer_tasks.sort( self.__timer_tasks.sort(
key = lambda x: x["execute_time"], key = lambda x: x["execute_time"],
reverse = order is Qt.SortOrder.DescendingOrder reverse = order is Qt.SortOrder.DescendingOrder
@@ -327,15 +329,15 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
invalid = 0 invalid = 0
total = len(self.__timer_tasks) total = len(self.__timer_tasks)
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
if timer_task["status"] == TimerTaskStatus.PENDING: if timer_task["status"] == ALTimerTaskStatus.PENDING:
pending += 1 pending += 1
elif timer_task["status"] == TimerTaskStatus.READY\ elif timer_task["status"] == ALTimerTaskStatus.READY\
or timer_task["status"] == TimerTaskStatus.RUNNING: or timer_task["status"] == ALTimerTaskStatus.RUNNING:
in_queue += 1 in_queue += 1
elif timer_task["status"] == TimerTaskStatus.EXECUTED: elif timer_task["status"] == ALTimerTaskStatus.EXECUTED:
executed += 1 executed += 1
elif timer_task["status"] == TimerTaskStatus.ERROR\ elif timer_task["status"] == ALTimerTaskStatus.ERROR\
or timer_task["status"] == TimerTaskStatus.OUTDATED: or timer_task["status"] == ALTimerTaskStatus.OUTDATED:
invalid += 1 invalid += 1
self.TotalTaskLabel.setText(f"总任务:{total}") self.TotalTaskLabel.setText(f"总任务:{total}")
self.PendingTaskLabel.setText(f"待执行:{pending}") self.PendingTaskLabel.setText(f"待执行:{pending}")
@@ -353,10 +355,14 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
item = QListWidgetItem() item = QListWidgetItem()
item.setData(Qt.UserRole, timer_task) item.setData(Qt.UserRole, timer_task)
widget = TimerTaskItemWidget(self, timer_task) widget = ALTimerTaskItemWidget(self, timer_task)
widget.DeleteButton.clicked.connect( widget.DeleteButton.clicked.connect(
lambda _, uuid = timer_task["task_uuid"]: self.deleteTask(uuid) lambda _, task = timer_task: self.deleteTask(task)
) )
if timer_task.get("repeat", False) and hasattr(widget, "HistoryButton"):
widget.HistoryButton.clicked.connect(
lambda _, task = timer_task: self.showTaskHistory(task)
)
item.setSizeHint(widget.size()) item.setSizeHint(widget.size())
self.TimerTasksListWidget.addItem(item) self.TimerTasksListWidget.addItem(item)
self.TimerTasksListWidget.setItemWidget(item, widget) self.TimerTasksListWidget.setItemWidget(item, widget)
@@ -366,18 +372,49 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
self self
): ):
dialog = ALAddTimerTaskWidget(self) dialog = ALTimerTaskAddDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
timer_task = dialog.getTimerTask() timer_task = dialog.getTimerTask()
self.__timer_tasks.append(timer_task) self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def deleteTask( @staticmethod
self, def getTimerTaskDetailMessage(
task_uuid: str timer_task: dict
): ):
return (
f"任务名称:{timer_task["name"]}\n"
f"添加时间:{timer_task["add_time"]}\n"
f"当前状态:{timer_task["status"].value}\n"
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n"
f"已执行次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
)
def deleteTask(
self,
timer_task: dict
):
if timer_task["repeat"]: # when delete a repeat task
msgbox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText("删除可重复性任务将同时删除所有已执行的记录 !\n是否继续 ?")
msgbox.setDetailedText(
"以下可重复性任务将被删除:\n"\
"\n"
f"{self.getTimerTaskDetailMessage(timer_task)}"
)
result = msgbox.exec()
if result != QMessageBox.StandardButton.Yes:
return
task_uuid = timer_task["task_uuid"]
self.__timer_tasks = [ self.__timer_tasks = [
x for x in self.__timer_tasks x for x in self.__timer_tasks
if x["task_uuid"] != task_uuid if x["task_uuid"] != task_uuid
@@ -397,24 +434,66 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
"是否要清除所有定时任务 ?", "是否要清除所有定时任务 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
) )
if result is QMessageBox.StandardButton.No: if result == QMessageBox.StandardButton.No:
return return
# READY and RUNNING tasks cannot be cleared
in_queue_tasks = [ in_queue_tasks = [
x for x in self.__timer_tasks x for x in self.__timer_tasks
if x["status"] == TimerTaskStatus.READY if x["status"] == ALTimerTaskStatus.READY
or x["status"] == TimerTaskStatus.RUNNING or x["status"] == ALTimerTaskStatus.RUNNING
] ]
in_queue_count = len(in_queue_tasks) in_queue_count = len(in_queue_tasks)
if in_queue_count > 0: if in_queue_count > 0:
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
"存在正在执行或已就绪的队列任务,无法清除所有定时任务 !" f"存在 {in_queue_count}正在执行或已就绪的队列任务,无法清除所有定时任务 !"
) )
self.__timer_tasks = in_queue_tasks return
# repeat tasks ask before clear
repeat_tasks = [
x for x in self.__timer_tasks
if x.get("repeat", False)
]
repeat_tasks_count = len(repeat_tasks)
if repeat_tasks_count > 0:
msgbox = QMessageBox(self)
msgbox.setIcon(QMessageBox.Icon.Question)
msgbox.setWindowTitle("警告 - AutoLibrary")
msgbox.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msgbox.setText(
f"存在 {repeat_tasks_count} 个可重复性任务,\n"
"删除可重复性任务将同时删除所有已执行的记录 !\n"
"是否继续 ?"
)
delete_msgs = [
self.getTimerTaskDetailMessage(x) for x in repeat_tasks
]
msgbox.setDetailedText(
"以下可重复性任务将被删除:\n"\
"\n"
f"{"\n\n".join(delete_msgs)}"
)
result = msgbox.exec()
if result != QMessageBox.StandardButton.Yes:
return
# clear all tasks
self.__timer_tasks.clear()
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def showTaskHistory(
self,
task: dict
):
dialog = ALTimerTaskHistoryDialog(self, task)
if dialog.exec() == QDialog.DialogCode.Accepted:
self.timerTasksChanged.emit()
def checkTasks( def checkTasks(
self self
): ):
@@ -425,13 +504,16 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for timer_task in self.__timer_tasks: for timer_task in self.__timer_tasks:
if timer_task["execute_time"] > now: if timer_task["execute_time"] > now:
continue continue
if timer_task["status"] is not TimerTaskStatus.PENDING: if timer_task["status"] is not ALTimerTaskStatus.PENDING:
continue continue
if timer_task["execute_time"] <= now + timedelta(seconds = -5): if timer_task["execute_time"] <= now + timedelta(seconds = -5):
timer_task["status"] = TimerTaskStatus.OUTDATED if timer_task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.OUTDATED, timer_task)
else:
timer_task["status"] = ALTimerTaskStatus.OUTDATED
need_update = True need_update = True
else: else:
timer_task["status"] = TimerTaskStatus.READY timer_task["status"] = ALTimerTaskStatus.READY
self.timerTaskIsReady.emit(timer_task) self.timerTaskIsReady.emit(timer_task)
need_update = True need_update = True
if need_update: if need_update:
@@ -444,9 +526,9 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
): ):
mapping = { mapping = {
0: SortPolicy.BY_NAME, 0: self.SortPolicy.BY_NAME,
1: SortPolicy.BY_ADD_TIME, 1: self.SortPolicy.BY_ADD_TIME,
2: SortPolicy.BY_EXECUTE_TIME 2: self.SortPolicy.BY_EXECUTE_TIME
} }
self.__sort_policy = mapping[policy] self.__sort_policy = mapping[policy]
self.updateTimerTaskList() self.updateTimerTaskList()
@@ -469,7 +551,7 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
self self
): ):
self.saveTimerTasks(self.__timer_tasks_config_path, copy.deepcopy(self.__timer_tasks)) self.setTimerTasks(copy.deepcopy(self.__timer_tasks))
self.updateTimerTaskList() self.updateTimerTaskList()
self.updateStat() self.updateStat()
@@ -482,10 +564,41 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.RUNNING task["status"] = ALTimerTaskStatus.RUNNING
break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
def onRepeatTimerTaskIs(
self,
status: ALTimerTaskStatus,
timer_task: dict
) -> dict:
if "history" not in timer_task:
timer_task["history"] = []
executed_time = datetime.now()
duration = (executed_time - timer_task["execute_time"]).total_seconds()
timer_task["history"].append({
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
"result": status,
"duration": duration if status is ALTimerTaskStatus.EXECUTED else 0,
"uuid": timer_task["task_uuid"]
})
next_time = TimerUtils.calculateNextRepeatTime(
timer_task["repeat_days"],
timer_task["repeat_hour"],
timer_task["repeat_minute"],
timer_task["repeat_second"]
)
if next_time:
timer_task["execute_time"] = next_time
timer_task["status"] = ALTimerTaskStatus.PENDING
timer_task["executed"] = False
else:
timer_task["status"] = status
@Slot(dict) @Slot(dict)
def onTimerTaskIsExecuted( def onTimerTaskIsExecuted(
self, self,
@@ -494,7 +607,11 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.EXECUTED if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task)
else:
task["status"] = ALTimerTaskStatus.EXECUTED
break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@Slot(dict) @Slot(dict)
@@ -505,5 +622,9 @@ class ALTimerTaskWidget(QWidget, Ui_ALTimerTaskWidget):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["task_uuid"] == timer_task["task_uuid"]:
task["status"] = TimerTaskStatus.ERROR if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task)
else:
task["status"] = ALTimerTaskStatus.ERROR
break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
+5 -6
View File
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
from enum import Enum from enum import Enum
from PySide6.QtCore import ( from PySide6.QtCore import (
@@ -22,7 +21,7 @@ from PySide6.QtGui import (
) )
class TreeItemType(Enum): class ALUserTreeItemType(Enum):
GROUP = 0 GROUP = 0
USER = 1 USER = 1
@@ -112,15 +111,15 @@ class ALUserTreeWidget(QTreeWidget):
if source_item is None: if source_item is None:
event.ignore() event.ignore()
return return
if source_item.type() == TreeItemType.GROUP.value: if source_item.type() == ALUserTreeItemType.GROUP.value:
if target_item is not None: if target_item is not None:
event.ignore() event.ignore()
return return
elif source_item.type() == TreeItemType.USER.value: elif source_item.type() == ALUserTreeItemType.USER.value:
if target_item is None: if target_item is None:
event.ignore() event.ignore()
return return
if target_item.type() != TreeItemType.GROUP.value: if target_item.type() != ALUserTreeItemType.GROUP.value:
event.ignore() event.ignore()
return return
if target_item.checkState(1) == Qt.CheckState.Unchecked: if target_item.checkState(1) == Qt.CheckState.Unchecked:
+6 -6
View File
@@ -5,12 +5,12 @@
workflow process. Do not edit manually. workflow process. Do not edit manually.
This file is auto-generated during the workflow process. This file is auto-generated during the workflow process.
Last updated: 2026-01-02 16:38:53 UTC Last updated: 2026-02-26 15:04:28 UTC
""" """
AL_VERSION = "1.0.1" AL_VERSION = "1.1.0"
AL_TAG = "v1.0.1" AL_TAG = "v1.1.0"
AL_COMMIT_SHA = "924db3b" AL_COMMIT_SHA = "local"
AL_COMMIT_DATE = "2026-01-02 16:35:16 UTC" # time zone : UTC AL_COMMIT_DATE = "null" # time zone : UTC
AL_BUILD_DATE = "2026-01-02 16:38:53 UTC" # time zone : UTC AL_BUILD_DATE = "null" # time zone : UTC
AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})" AL_VERSION_FULL = f"{AL_VERSION} ({AL_COMMIT_SHA})"
-1
View File
@@ -1 +0,0 @@
this folder is used to store the batch scripts.
-1
View File
@@ -1 +0,0 @@
this folder is used to store the config files.

Before

Width:  |  Height:  |  Size: 785 KiB

After

Width:  |  Height:  |  Size: 785 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@@ -104,7 +104,7 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QFrame" name="frame"> <widget class="QFrame" name="AboutInfoSpaceFrame">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>56</width> <width>56</width>
@@ -129,21 +129,24 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QTextEdit" name="AboutInfoEdit"> <widget class="QTextBrowser" name="AboutInfoBrowser">
<property name="font"> <property name="frameShadow">
<font> <enum>QFrame::Shadow::Plain</enum>
<family>Courier New</family> </property>
<bold>false</bold> <property name="verticalScrollBarPolicy">
</font> <enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property> </property>
<property name="lineWrapMode"> <property name="lineWrapMode">
<enum>QTextEdit::LineWrapMode::NoWrap</enum> <enum>QTextEdit::LineWrapMode::NoWrap</enum>
</property> </property>
<property name="readOnly"> <property name="openExternalLinks">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="textInteractionFlags"> <property name="openLinks">
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
@@ -63,7 +63,7 @@
</property> </property>
<widget class="QWidget" name="UserConfigWidget"> <widget class="QWidget" name="UserConfigWidget">
<attribute name="title"> <attribute name="title">
<string>用户置</string> <string>用户置</string>
</attribute> </attribute>
<layout class="QHBoxLayout" name="UserConfigWidgetLayout"> <layout class="QHBoxLayout" name="UserConfigWidgetLayout">
<property name="spacing"> <property name="spacing">
@@ -94,19 +94,19 @@
</property> </property>
<layout class="QVBoxLayout" name="UserListLayout"> <layout class="QVBoxLayout" name="UserListLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>5</number>
</property> </property>
<property name="leftMargin"> <property name="leftMargin">
<number>5</number> <number>3</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>5</number> <number>3</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>5</number> <number>3</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>5</number> <number>3</number>
</property> </property>
<item> <item>
<widget class="QTreeWidget" name="UserTreeWidget"> <widget class="QTreeWidget" name="UserTreeWidget">
@@ -178,6 +178,9 @@
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="UserListControlLayout"> <layout class="QHBoxLayout" name="UserListControlLayout">
<property name="spacing">
<number>5</number>
</property>
<item> <item>
<widget class="QPushButton" name="DelUserButton"> <widget class="QPushButton" name="DelUserButton">
<property name="minimumSize"> <property name="minimumSize">
@@ -192,6 +195,11 @@
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #DC0000;
}</string>
</property>
<property name="text"> <property name="text">
<string>删除用户</string> <string>删除用户</string>
</property> </property>
@@ -309,7 +317,7 @@
<item row="2" column="1"> <item row="2" column="1">
<layout class="QHBoxLayout" name="PasswordLayout"> <layout class="QHBoxLayout" name="PasswordLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QLineEdit" name="PasswordEdit"> <widget class="QLineEdit" name="PasswordEdit">
@@ -546,7 +554,7 @@
<item row="4" column="4"> <item row="4" column="4">
<layout class="QHBoxLayout" name="SeatIDLayout"> <layout class="QHBoxLayout" name="SeatIDLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QLineEdit" name="SeatIDEdit"> <widget class="QLineEdit" name="SeatIDEdit">
@@ -703,7 +711,7 @@
<item row="10" column="4"> <item row="10" column="4">
<layout class="QHBoxLayout" name="EndTimeDiffLayout"> <layout class="QHBoxLayout" name="EndTimeDiffLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QSpinBox" name="MaxEndTimeDiffSpinBox"> <widget class="QSpinBox" name="MaxEndTimeDiffSpinBox">
@@ -899,13 +907,13 @@
<item row="7" column="4"> <item row="7" column="4">
<layout class="QHBoxLayout" name="BeginTimeDiffLayout"> <layout class="QHBoxLayout" name="BeginTimeDiffLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QSpinBox" name="MaxBeginTimeDiffSpinBox"> <widget class="QSpinBox" name="MaxBeginTimeDiffSpinBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>65</width> <width>55</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
@@ -1049,10 +1057,23 @@
<item row="15" column="4"> <item row="15" column="4">
<layout class="QHBoxLayout" name="RenewTimeDiffLayout"> <layout class="QHBoxLayout" name="RenewTimeDiffLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QSpinBox" name="MaxRenewTimeDiffSpinBox"/> <widget class="QSpinBox" name="MaxRenewTimeDiffSpinBox">
<property name="minimumSize">
<size>
<width>55</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
</widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="PreferLateRenewTimeCheckBox"> <widget class="QCheckBox" name="PreferLateRenewTimeCheckBox">
@@ -1079,7 +1100,7 @@
</layout> </layout>
</item> </item>
<item row="15" column="1"> <item row="15" column="1">
<widget class="QLabel" name="label"> <widget class="QLabel" name="MaxRenewTimeDiffLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>80</width>
@@ -1107,7 +1128,7 @@
</widget> </widget>
<widget class="QWidget" name="RunConfigWidget"> <widget class="QWidget" name="RunConfigWidget">
<attribute name="title"> <attribute name="title">
<string>运行置</string> <string>运行置</string>
</attribute> </attribute>
<layout class="QGridLayout" name="SystemConfigWidgetLayout"> <layout class="QGridLayout" name="SystemConfigWidgetLayout">
<property name="leftMargin"> <property name="leftMargin">
@@ -1140,10 +1161,10 @@
<property name="rightMargin"> <property name="rightMargin">
<number>3</number> <number>3</number>
</property> </property>
<property name="horizontalSpacing"> <property name="bottomMargin">
<number>3</number> <number>3</number>
</property> </property>
<property name="verticalSpacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
@@ -1340,7 +1361,7 @@
</size> </size>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;详情请参阅 &lt;a href=&quot;https://www.autolibrary.cv/docs/manual_lists.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#69fcff;&quot;&gt;用户手册&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;详情请参阅 &lt;a href=&quot;https://www.autolibrary.kenanzhu.com/manuals&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#69fcff;&quot;&gt;用户手册&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="whatsThis"> <property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -1636,6 +1657,21 @@
<string>当前配置</string> <string>当前配置</string>
</property> </property>
<layout class="QGridLayout" name="CurrentConfigLayout"> <layout class="QGridLayout" name="CurrentConfigLayout">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>5</number>
</property>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLineEdit" name="CurrentRunConfigEdit"> <widget class="QLineEdit" name="CurrentRunConfigEdit">
<property name="minimumSize"> <property name="minimumSize">
@@ -1730,7 +1766,7 @@
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>;;;</string> <string>...</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1762,6 +1798,21 @@
<string>导出路径</string> <string>导出路径</string>
</property> </property>
<layout class="QGridLayout" name="ExportConfigLayout"> <layout class="QGridLayout" name="ExportConfigLayout">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>5</number>
</property>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLineEdit" name="ExportUserConfigEdit"> <widget class="QLineEdit" name="ExportUserConfigEdit">
<property name="minimumSize"> <property name="minimumSize">
@@ -34,13 +34,13 @@
<number>5</number> <number>5</number>
</property> </property>
<property name="leftMargin"> <property name="leftMargin">
<number>3</number> <number>5</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>0</number> <number>0</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>3</number> <number>5</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
@@ -51,7 +51,7 @@
<number>5</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="QPushButton" name="TimerTaskWidgetButton"> <widget class="QPushButton" name="TimerTaskManageWidgetButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>25</width> <width>25</width>
@@ -156,11 +156,9 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="styleSheet"> <property name="styleSheet">
<string notr="true">background-color: rgb(10, 170, 10); <string notr="true">background-color: #0AAA0A;
font: 12pt &quot;Microsoft YaHei UI&quot;; color: #FFFFFF;
color: rgb(255, 255, 255); font: 700 9pt;</string>
font: 9pt &quot;Segoe UI&quot;;
font: 700 9pt &quot;Microsoft YaHei UI&quot;;</string>
</property> </property>
<property name="text"> <property name="text">
<string>启动脚本</string> <string>启动脚本</string>
@@ -0,0 +1,496 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ALTimerTaskAddDialog</class>
<widget class="QDialog" name="ALTimerTaskAddDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>400</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>500</height>
</size>
</property>
<property name="windowTitle">
<string>添加定时任务 - AutoLibrary</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="ALAddTimerTaskLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="TaskNameLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TaskNameLabel">
<property name="minimumSize">
<size>
<width>60</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>任务名称:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="TaskNameLineEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="TimerConfigGroupBox">
<property name="title">
<string>定时设置</string>
</property>
<layout class="QVBoxLayout" name="TimerConfigLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="TimerTypeSelectLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="TimerTypeLabel">
<property name="text">
<string>定时类型:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="TimerTypeComboBox">
<item>
<property name="text">
<string>特定时间</string>
</property>
</item>
<item>
<property name="text">
<string>相对时间</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="TaskConfigGroupBox">
<property name="title">
<string>运行设置</string>
</property>
<layout class="QGridLayout" name="TaskConfigLayout">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<property name="spacing">
<number>5</number>
</property>
<item row="1" column="0">
<widget class="QRadioButton" name="SilentlyRunRadioButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>静默运行</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="ShowBeforeRunRadioButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>运行前提示</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="RepeatConfigGroupBox">
<property name="title">
<string>重复运行</string>
</property>
<layout class="QVBoxLayout" name="RepeatConfigLayout" stretch="1,1,1">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QCheckBox" name="RepeatCheckBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>启用重复执行</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>重复周期(全选或全不选都为每日运行):</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="RepeatCheckBoxLayout" rowstretch="10,10" columnstretch="0,0,0,0" rowminimumheight="25,25">
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="3">
<widget class="QCheckBox" name="ThuCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周四</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="WedCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周三</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="MonCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周一</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="TueCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周二</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="FriCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周五</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="SatCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周六</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="SunCheckBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>周日</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ControLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="ControlSpaceFrame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="CancelButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>取消</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ConfirmButton">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>添加</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -1,31 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"> <ui version="4.0">
<class>ALTimerTaskWidget</class> <class>ALTimerTaskManageWidget</class>
<widget class="QWidget" name="ALTimerTaskWidget"> <widget class="QWidget" name="ALTimerTaskManageWidget">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>500</width>
<height>400</height> <height>400</height>
</rect> </rect>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>400</width> <width>500</width>
<height>400</height> <height>400</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>600</width> <width>800</width>
<height>400</height> <height>400</height>
</size> </size>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>定时任务 - AutoLibrary</string> <string>定时任务管理 - AutoLibrary</string>
</property> </property>
<layout class="QVBoxLayout" name="ALTimerTaskWidgetLayout"> <layout class="QVBoxLayout" name="ALTimerTaskManageWidgetLayout">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
@@ -153,7 +153,7 @@
</property> </property>
<property name="styleSheet"> <property name="styleSheet">
<string notr="true">QLabel { <string notr="true">QLabel {
color: #FF5722 color: #DC0000
}</string> }</string>
</property> </property>
<property name="text"> <property name="text">
@@ -306,6 +306,11 @@
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="styleSheet">
<string notr="true">QPushButton {
color: #DC0000;
}</string>
</property>
<property name="text"> <property name="text">
<string>清除全部</string> <string>清除全部</string>
</property> </property>
+75 -42
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -11,10 +11,13 @@ import os
import queue import queue
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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 selenium.webdriver.edge.service import Service from selenium.webdriver.edge.service import Service as EdgeService
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
from base.MsgBase import MsgBase from base.MsgBase import MsgBase
from operators.LibChecker import LibChecker from operators.LibChecker import LibChecker
@@ -24,8 +27,6 @@ from operators.LibReserve import LibReserve
from operators.LibCheckin import LibCheckin from operators.LibCheckin import LibCheckin
from operators.LibRenew import LibRenew from operators.LibRenew import LibRenew
from utils.ConfigReader import ConfigReader
class AutoLib(MsgBase): class AutoLib(MsgBase):
@@ -41,10 +42,11 @@ class AutoLib(MsgBase):
self.__user_config = None self.__user_config = None
self.__driver = None self.__driver = None
if not self.__initBrowserDriver(): if not self.__initBrowserDriver():
raise Exception("浏览器驱动初始化失败") raise Exception("浏览器驱动初始化失败 !")
else: else:
if not self.__initDriverUrl(): if not self.__initDriverUrl():
raise Exception("浏览器驱动URL初始化失败") self.close()
raise Exception("浏览器驱动URL初始化失败 !")
self.__initLibOperators() self.__initLibOperators()
@@ -53,55 +55,79 @@ class AutoLib(MsgBase):
) -> bool: ) -> bool:
self._showTrace("正在初始化浏览器驱动......") self._showTrace("正在初始化浏览器驱动......")
edge_options = webdriver.EdgeOptions()
web_driver_config = self.__run_config.get("web_driver", None) web_driver_config = self.__run_config.get("web_driver", None)
self.__driver_type = web_driver_config.get("driver_type")
match self.__driver_type.lower():
case "edge":
driver_options = webdriver.EdgeOptions()
case "chrome":
driver_options = webdriver.ChromeOptions()
case "firefox":
driver_options = webdriver.FirefoxOptions()
case _:
self._showTrace(f"不支持的浏览器驱动类型: {self.__driver_type} !")
return False
if not web_driver_config: if not web_driver_config:
self._showTrace("未配置浏览器驱动参数 !") self._showTrace("未配置浏览器驱动参数 !")
return False return False
if web_driver_config.get("headless"): if web_driver_config.get("headless"):
edge_options.add_argument("--headless") driver_options.add_argument("--headless")
edge_options.add_argument("--disable-gpu") driver_options.add_argument("--disable-gpu")
edge_options.add_argument("--no-sandbox") driver_options.add_argument("--no-sandbox")
edge_options.add_argument("--disable-dev-shm-usage") driver_options.add_argument("--disable-dev-shm-usage")
# must be 1920x1080, otherwise the page will cause some elements not accessible # must be 1920x1080, otherwise the page will cause some elements not accessible
edge_options.add_argument("--window-size=1920,1080") driver_options.add_argument("--window-size=1920,1080")
edge_options.add_argument("--remote-allow-origins=*")
# omit ssl errors and verbose log level # omit ssl errors and verbose log level
edge_options.add_argument("--ignore-certificate-errors") driver_options.add_argument("--ignore-certificate-errors")
edge_options.add_argument("--ignore-ssl-errors") driver_options.add_argument("--ignore-ssl-errors")
edge_options.add_argument("--log-level=OFF") driver_options.add_argument("--log-level=OFF")
edge_options.add_argument("--silent") driver_options.add_argument("--silent")
edge_options.add_experimental_option("excludeSwitches", ["enable-automation"]) # set options for chrome and edge
edge_options.add_experimental_option("useAutomationExtension", False) if self.__driver_type.lower() in ["edge", "chrome"]:
edge_options.add_argument("--disable-blink-features=AutomationControlled") driver_options.add_argument("--remote-allow-origins=*")
edge_options.add_argument( driver_options.add_experimental_option("excludeSwitches", ["enable-automation"])
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\ driver_options.add_experimental_option("useAutomationExtension", False)
"AppleWebKit/537.36 (KHTML, like Gecko) "\ driver_options.add_argument("--disable-blink-features=AutomationControlled")
"Chrome/120.0.0.0 "\ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
"Safari/537.36 "\ "AppleWebKit/537.36 (KHTML, like Gecko) "\
"Edg/120.0.0.0" "Chrome/120.0.0.0 "\
) "Safari/537.36"
if self.__driver_type.lower() == "edge":
user_agent += " Edg/120.0.0.0"
# set options for firefox
elif self.__driver_type.lower() == "firefox":
driver_options.set_preference("dom.webdriver.enabled", False)
driver_options.set_preference("useAutomationExtension", False)
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) "\
"Gecko/20100101 Firefox/120.0"
driver_options.add_argument(f"user-agent={user_agent}")
# init browser driver # init browser driver
self.__driver_path = web_driver_config.get("driver_path") self.__driver_path = web_driver_config.get("driver_path")
self.__driver_type = web_driver_config.get("driver_type") if not self.__driver_path:
self._showTrace("未配置浏览器驱动路径 !")
return False
self.__driver_path = os.path.abspath(self.__driver_path) self.__driver_path = os.path.abspath(self.__driver_path)
try: try:
service = None service = None
if self.__driver_path:
service = Service(executable_path=self.__driver_path)
match self.__driver_type.lower(): match self.__driver_type.lower():
case "edge": case "edge":
self.__driver = webdriver.Edge(service=service, options=edge_options) service = EdgeService(executable_path=self.__driver_path)
self.__driver = webdriver.Edge(service=service, options=driver_options)
case "chrome": case "chrome":
self.__driver = webdriver.Chrome(service=service, options=edge_options) service = ChromeService(executable_path=self.__driver_path)
self.__driver = webdriver.Chrome(service=service, options=driver_options)
case "firefox": case "firefox":
self.__driver = webdriver.Firefox(service=service, options=edge_options) self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...")
case _: service = FirefoxService(executable_path=self.__driver_path)
self.__driver = webdriver.Firefox(service=service, options=driver_options)
case _: # actually will not happen, beacuse we have checked it at the initlization
# of 'driver_options'
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}") raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
self.__driver.implicitly_wait(1) self.__driver.implicitly_wait(1)
self.__driver.execute_script( self.__driver.execute_script(
@@ -162,10 +188,16 @@ class AutoLib(MsgBase):
lib_config = self.__run_config.get("library", None) lib_config = self.__run_config.get("library", None)
if not lib_config: if not lib_config:
self._showError("未配置图书馆参数 !") self._showTrace("未配置图书馆参数 !")
return False return False
url = lib_config.get("host_url") + lib_config.get("login_url") url = lib_config.get("host_url") + lib_config.get("login_url")
self.__driver.get(url) self.__driver.set_page_load_timeout(5)
try:
self.__driver.get(url)
except TimeoutException:
self.__driver.execute_script("window.stop();")
self._showTrace(f"图书馆登录页面加载超时 ! 请检查网络环境是否正常")
return False
if not self.__waitResponseLoad(): if not self.__waitResponseLoad():
return False return False
return True return True
@@ -191,9 +223,7 @@ class AutoLib(MsgBase):
login_config.get("auto_captcha", True), login_config.get("auto_captcha", True),
): ):
return 1 return 1
""" # Here, we collect the run mode from the run config.
Here, we collect the run mode from the run config.
"""
run_mode = run_mode_config.get("run_mode", 0) run_mode = run_mode_config.get("run_mode", 0)
run_mode = { run_mode = {
"auto_reserve": run_mode&0x1, "auto_reserve": run_mode&0x1,
@@ -211,7 +241,7 @@ class AutoLib(MsgBase):
self._showTrace(f"用户 {username} 无法预约,已跳过") self._showTrace(f"用户 {username} 无法预约,已跳过")
result = 2 result = 2
# checkin # checkin
if run_mode["auto_checkin"] and result == 2: if run_mode["auto_checkin"] and result != 1:
if self.__lib_checker.canCheckin(): if self.__lib_checker.canCheckin():
if self.__lib_checkin.checkin(username): if self.__lib_checkin.checkin(username):
result = 0 result = 0
@@ -221,8 +251,9 @@ class AutoLib(MsgBase):
self._showTrace(f"用户 {username} 无法签到,已跳过") self._showTrace(f"用户 {username} 无法签到,已跳过")
result = 2 result = 2
# renewal # renewal
if run_mode["auto_renewal"] and result == 2: if run_mode["auto_renewal"] and result != 1:
if record := self.__lib_checker.canRenew(): can_renew, record = self.__lib_checker.canRenew()
if can_renew:
if self.__lib_renew.renew(username, record, reserve_info): if self.__lib_renew.renew(username, record, reserve_info):
if self.__lib_checker.postRenewCheck(record): if self.__lib_checker.postRenewCheck(record):
result = 0 result = 0
@@ -294,6 +325,8 @@ class AutoLib(MsgBase):
) -> bool: ) -> bool:
if self.__driver: if self.__driver:
if self.__driver_type.lower() == "firefox":
self._showTrace(f"Firefox 浏览器驱动关闭略慢, 请耐心等待...")
self.__driver.quit() self.__driver.quit()
self.__driver = None self.__driver = None
self._showTrace(f"浏览器驱动已关闭") self._showTrace(f"浏览器驱动已关闭")
+14 -12
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -13,6 +13,7 @@ import queue
from datetime import datetime, timedelta from datetime import datetime, timedelta
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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
@@ -25,7 +26,7 @@ class LibChecker(LibOperator):
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
@@ -308,7 +309,7 @@ class LibChecker(LibOperator):
def canRenew( def canRenew(
self self
): ) -> tuple[bool, dict]:
# only check the current date # only check the current date
date = time.strftime("%Y-%m-%d", time.localtime()) date = time.strftime("%Y-%m-%d", time.localtime())
@@ -325,26 +326,27 @@ class LibChecker(LibOperator):
) )
if abs(time_diff_seconds) < 120*60: if abs(time_diff_seconds) < 120*60:
self._showTrace(f"{trace_msg}, 可以续约") self._showTrace(f"{trace_msg}, 可以续约")
return record return True, record
else: else:
self._showTrace(f"{trace_msg}, 无法续约") self._showTrace(f"{trace_msg}, 无法续约")
return None return False, None # we do not need to return the record, because if current
# time is not available for renewal, the record is not required
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约") self._showTrace(f"用户在 {date} 没有有效预约记录, 无法续约")
return None return False, None
def postRenewCheck( def postRenewCheck(
self, self,
record: dict record: dict
): ) -> bool:
""" """
Check if the renew operation is successful Check if the renew operation is successful
Args: Args:
record (dict): The expected record after renewal record (dict): The expected record after renewal
Returns: Returns:
bool: True if the renew operation is successful, False otherwise bool: True if the renew operation is successful, False otherwise
""" """
# because the special circumstance that the renew operation # because the special circumstance that the renew operation
# do not show the success message or anything else, # do not show the success message or anything else,
+32 -6
View File
@@ -1,18 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import re
import time import time
import queue import queue
from datetime import datetime, timedelta
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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
@@ -25,7 +24,7 @@ class LibCheckin(LibOperator):
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
@@ -89,6 +88,31 @@ class LibCheckin(LibOperator):
return False return False
def __enableCheckinBtn(
self
) -> bool:
script = """
try {
var checkin_btn = document.getElementById('btnCheckIn');
if (checkin_btn) {
checkin_btn.classList.remove('disabled');
return true;
}
return false;
} catch (e) {
return false;
}
"""
result = self.__driver.execute_script(script)
time.sleep(0.1)
if result:
self._showTrace("签到按钮已启用")
else:
self._showTrace("签到按钮启用失败")
return result
def checkin( def checkin(
self, self,
username: str username: str
@@ -105,8 +129,10 @@ class LibCheckin(LibOperator):
self._showTrace(f"用户 {username} 签到界面加载失败 !") self._showTrace(f"用户 {username} 签到界面加载失败 !")
return False return False
if "disabled" in checkin_btn.get_attribute("class"): if "disabled" in checkin_btn.get_attribute("class"):
self._showTrace("签到按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试") self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......")
return False if not self.__enableCheckinBtn():
self._showTrace(f"签到按钮启用失败 !")
return False
checkin_btn.click() checkin_btn.click()
if self._waitResponseLoad(): if self._waitResponseLoad():
self._showTrace(f"用户 {username} 签到成功 !") self._showTrace(f"用户 {username} 签到成功 !")
+3 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -13,6 +13,7 @@ import queue
from datetime import datetime, timedelta from datetime import datetime, timedelta
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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
@@ -25,7 +26,7 @@ class LibCheckout(LibOperator):
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
+3 -3
View File
@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import time
import queue import queue
import base64 import base64
import ddddocr import ddddocr
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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
@@ -26,7 +26,7 @@ class LibLogin(LibOperator):
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
+3 -4
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
@@ -10,8 +10,7 @@ See the LICENSE file for details.
import queue import queue
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from base.LibOperator import LibOperator from base.LibOperator import LibOperator
@@ -22,7 +21,7 @@ class LibLogout(LibOperator):
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
+79 -92
View File
@@ -1,31 +1,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import os
import time
import queue import queue
from datetime import datetime, timedelta
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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.LibOperator import LibOperator from base.LibTimeSelector import LibTimeSelector
class LibRenew(LibOperator): class LibRenew(LibTimeSelector):
def __init__( def __init__(
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
@@ -40,22 +38,6 @@ class LibRenew(LibOperator):
self.__driver.refresh() self.__driver.refresh()
return True return True
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __waitRenewDialog( def __waitRenewDialog(
self self
@@ -96,87 +78,92 @@ class LibRenew(LibOperator):
return True return True
def __selectNearstTime( def __selectNearestTime(
self, self,
record: dict, record: dict,
reserve_info: dict reserve_info: dict
) -> bool: ) -> bool:
""" """
TODO : this function is too long and too ugly Select the nearest available renewal time.
we need to refactor it to make it more readable.
but may be it is not a good idea to refactor it. :) who knows...
""" """
end_time = record["time"]["end"] end_time = record["time"]["end"]
renew_info = reserve_info["renew_time"] renew_info = reserve_info["renew_time"]
max_diff = renew_info["max_diff"] max_diff = renew_info["max_diff"]
prefer_earlier = renew_info["prefer_early"] prefer_earlier = renew_info["prefer_early"]
target_renew_mins = self.__timeToMins(end_time) + renew_info["expect_duration"]*60 target_renew_mins = self._timeToMins(end_time) + renew_info["expect_duration"]*60
renew_ok_btn = self.__driver.find_element(
By.CSS_SELECTOR, "#extendDiv .btnOK"
)
try:
renew_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR, "#extendDiv .renewal_List li"
)
free_times = []
best_time_diff = max_diff
best_actual_diff = None
best_time_opt = None
if not renew_time_opts: # Validate and adjust target renew time to library closing time
self._showTrace("当前未查询到可用续约时间 !") if not self.__validateAndAdjustRenewTime(end_time, target_renew_mins):
return False
for time_opt in renew_time_opts:
time_attr = time_opt.get_attribute("id")
if time_attr and time_attr.isdigit():
time_val = int(time_attr)
free_times.append(time_opt.text.strip())
else:
continue
actual_diff = time_val - target_renew_mins
abs_diff = abs(actual_diff)
if abs_diff < best_time_diff or (
abs_diff == best_time_diff and (
# 优先选择更早的时间
(prefer_earlier and actual_diff <= 0) or
# 优先选择更晚的时间
(not prefer_earlier and actual_diff >= 0)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于续约时间"
self._showTrace(
f"选择距离期望续约时间最近的 {best_time_opt.text}, "\
f"与期望续约时间相比 {time_relation}"
)
# update the actual renew end time
record["time"]["end"] = best_time_opt.text.strip()
renew_ok_btn.click()
return True
self._showTrace(
"无法选择最近的可用续约时间 !" \
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
)
self._showTrace(
f"当前可供续约的时间有: {free_times}"
)
return False return False
renew_ok_btn = self.__driver.find_element(By.CSS_SELECTOR, "#extendDiv .btnOK")
renew_time_opts = self.__driver.find_elements(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
if not renew_time_opts:
self._showTrace("当前未查询到可用续约时间 !")
return False
# Find best renewal time option
best_opt, best_text, actual_diff, free_times = self._findBestTimeOption(
renew_time_opts, target_renew_mins, max_diff, prefer_earlier, is_reserve=False
)
if best_opt is not None:
return self.__confirmRenewal(best_opt, best_text, actual_diff, record, renew_ok_btn)
self._showTrace(
"无法选择最近的可用续约时间 ! "
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
)
self._showTrace(f"当前可供续约的时间有: {free_times}")
return False
def __validateAndAdjustRenewTime(
self,
end_time: str,
target_renew_mins: int
) -> bool:
"""
Validate and adjust renewal time to library closing time if needed.
"""
LIBRARY_CLOSE_TIME = 1410 # 23:30 in minutes
if target_renew_mins > LIBRARY_CLOSE_TIME:
actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeToMins(end_time)
if actual_renew_duration <= 0:
self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !")
return False
self._showTrace(
f"续约时间已调整至闭馆时间 {self._minsToTime(LIBRARY_CLOSE_TIME)}"
f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟"
)
return True
return True
def __confirmRenewal(
self,
best_opt,
best_text: str,
actual_diff: int,
record: dict,
ok_btn
) -> bool:
"""
Confirm the selected renewal time.
"""
try:
best_opt.click()
abs_diff = abs(actual_diff)
time_relation = self._formatTimeRelation(abs_diff, actual_diff, "续约时间")
self._showTrace(
f"选择距离期望续约时间最近的 {best_text}, "
f"与期望续约时间相比 {time_relation}"
)
record["time"]["end"] = best_text.strip()
ok_btn.click()
return True
except: except:
self._showTrace("查询可用续约时间时发生未知错误 !") self._showTrace("确认续约时发生错误 !")
return False return False
@@ -205,10 +192,10 @@ class LibRenew(LibOperator):
self._showTrace(f"用户 {username} 续约失败 !") self._showTrace(f"用户 {username} 续约失败 !")
# After the renewal, the webpage will display a mask overlay, # After the renewal, the webpage will display a mask overlay,
# so we need to refresh the page for subsequent operations. # so we need to refresh the page for subsequent operations.
self.__driver.refresh() self.__driver.refresh()
return False return False
if not self.__selectNearstTime(record, reserve_info): if not self.__selectNearestTime(record, reserve_info):
self._showTrace(f"用户 {username} 续约失败 !") self._showTrace(f"用户 {username} 续约失败 !")
self.__driver.refresh() self.__driver.refresh()
return False return False
+80 -113
View File
@@ -1,31 +1,30 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2025 KenanZhu. Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved. All rights reserved.
This software is provided "as is", without any warranty of any kind. This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License. You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details. See the LICENSE file for details.
""" """
import re
import time import time
import queue import queue
from datetime import datetime, timedelta
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
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.LibOperator import LibOperator from base.LibTimeSelector import LibTimeSelector
class LibReserve(LibOperator): class LibReserve(LibTimeSelector):
def __init__( def __init__(
self, self,
input_queue: queue.Queue, input_queue: queue.Queue,
output_queue: queue.Queue, output_queue: queue.Queue,
driver: any driver: WebDriver
): ):
super().__init__(input_queue, output_queue) super().__init__(input_queue, output_queue)
@@ -40,12 +39,12 @@ class LibReserve(LibOperator):
} }
self.__room_map = { self.__room_map = {
"1": "二层内环", "1": "二层内环",
"2": "二层外环", "2": "二层西区",
"3": "三层内环", "3": "三层内环",
"4": "三层外环", "4": "三层外环",
"5": "四层内环", "5": "四层内环",
"6": "四层外环", "6": "四层外环",
"7": "四层期刊", "7": "四层期刊",
"8": "五层考研" "8": "五层考研"
} }
@@ -100,22 +99,6 @@ class LibReserve(LibOperator):
self._showTrace(f"预约结果加载失败 !") self._showTrace(f"预约结果加载失败 !")
return False return False
@staticmethod
def __timeToMins(
time_str: str
) -> int:
hour, minute = map(int, time_str.split(":"))
return hour*60 + minute
@staticmethod
def __minsToTime(
mins: int
) -> str:
hour, minute = divmod(mins, 60)
return f"{hour:02d}:{minute:02d}"
def __containRequiredInfo( def __containRequiredInfo(
self, self,
@@ -207,10 +190,10 @@ class LibReserve(LibOperator):
if reserve_info.get("end_time") is None: if reserve_info.get("end_time") is None:
reserve_info["end_time"] = {} reserve_info["end_time"] = {}
if "time" not in reserve_info["end_time"]: if "time" not in reserve_info["end_time"]:
end_mins = self.__timeToMins(reserve_info["begin_time"]["time"]) end_mins = self._timeToMins(reserve_info["begin_time"]["time"])
end_mins = end_mins + int(reserve_info["expect_duration"]*60) end_mins = end_mins + int(reserve_info["expect_duration"]*60)
reserve_info["end_time"] = { reserve_info["end_time"] = {
"time": self.__minsToTime(end_mins), "time": self._minsToTime(end_mins),
"max_diff": 30, "max_diff": 30,
"prefer_early": False "prefer_early": False
} }
@@ -232,8 +215,8 @@ class LibReserve(LibOperator):
): ):
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
begin_mins = self.__timeToMins(begin_time["time"]) begin_mins = self._timeToMins(begin_time["time"])
end_mins = self.__timeToMins(end_time["time"]) end_mins = self._timeToMins(end_time["time"])
# if end time is earlier than begin_time, exchange them # if end time is earlier than begin_time, exchange them
if end_mins < begin_mins: if end_mins < begin_mins:
self._showTrace( self._showTrace(
@@ -242,15 +225,15 @@ class LibReserve(LibOperator):
reserve_info["end_time"] = begin_time reserve_info["end_time"] = begin_time
reserve_info["begin_time"] = end_time reserve_info["begin_time"] = end_time
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"] begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
begin_mins = self.__timeToMins(begin_time["time"]) begin_mins = self._timeToMins(begin_time["time"])
end_mins = self.__timeToMins(end_time["time"]) end_mins = self._timeToMins(end_time["time"])
# ensure the end time is not later than 23:30 # ensure the end time is not later than 23:30
if end_mins > self.__timeToMins("23:30"): if end_mins > self._timeToMins("23:30"):
self._showTrace( self._showTrace(
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30" f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
) )
reserve_info["end_time"]["time"] = "23:30" reserve_info["end_time"]["time"] = "23:30"
end_mins = self.__timeToMins("23:30") end_mins = self._timeToMins("23:30")
# ensure the duration is not longer than 8 hours # ensure the duration is not longer than 8 hours
if reserve_info["satisfy_duration"]: if reserve_info["satisfy_duration"]:
if reserve_info["expect_duration"] > 8: if reserve_info["expect_duration"] > 8:
@@ -267,7 +250,7 @@ class LibReserve(LibOperator):
f"{float((end_mins - begin_mins)/60)} 小时 " f"{float((end_mins - begin_mins)/60)} 小时 "
f"超出最大时长 8 小时, 自动设置为 8 小时" f"超出最大时长 8 小时, 自动设置为 8 小时"
) )
reserve_info["end_time"]["time"] = self.__minsToTime(begin_mins + 8*60) reserve_info["end_time"]["time"] = self._minsToTime(begin_mins + 8*60)
return True return True
@@ -496,6 +479,10 @@ class LibReserve(LibOperator):
prefer_earlier: bool = True prefer_earlier: bool = True
) -> int: ) -> int:
"""
Select the nearest available time option.
"""
# Wait for time options to load
try: try:
WebDriverWait(self.__driver, 2).until( WebDriverWait(self.__driver, 2).until(
EC.presence_of_all_elements_located( EC.presence_of_all_elements_located(
@@ -505,67 +492,34 @@ class LibReserve(LibOperator):
except: except:
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间") self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
return -1 return -1
try:
all_time_opts = self.__driver.find_elements(
By.CSS_SELECTOR,
f"#{time_id} ul li a"
)
free_times = []
best_time_diff = max_time_diff
best_actual_diff = None
best_time_opt = None
if not all_time_opts: # Find best time option
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间") all_time_opts = self.__driver.find_elements(
return -1 By.CSS_SELECTOR,
for time_opt in all_time_opts: f"#{time_id} ul li a"
time_attr = time_opt.get_attribute("time") )
if time_attr == "now": if not all_time_opts:
now = datetime.now() self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
time_val = int(now.hour*60 + now.minute) return -1
elif time_attr and time_attr.isdigit(): best_opt, best_text, actual_diff, free_times = self._findBestTimeOption(
time_val = int(time_attr) all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True
else: )
continue if best_opt is not None:
free_times.append(self.__minsToTime(time_val)) best_opt.click()
actual_diff = time_val - target_time abs_diff = abs(actual_diff)
abs_diff = abs(actual_diff) time_relation = self._formatTimeRelation(abs_diff, actual_diff, time_type)
if abs_diff < best_time_diff or ( target_time += actual_diff
abs_diff == best_time_diff and (
# prefer earlier time
(prefer_earlier and actual_diff <= 0) or
# prefer later time
(not prefer_earlier and actual_diff >= 0)
)
):
best_time_diff = abs_diff
best_actual_diff = actual_diff
best_time_opt = time_opt
if best_time_opt is not None:
best_time_opt.click()
abs_time_diff = abs(best_actual_diff)
if best_actual_diff < 0:
time_relation = f"早了 {abs_time_diff} 分钟"
elif best_actual_diff > 0:
time_relation = f"晚了 {abs_time_diff} 分钟"
else:
time_relation = f"正好等于 {time_type}"
target_time += best_actual_diff
self._showTrace(
f"选择距离期望 {time_type} 最近的 {best_time_opt.text}, "\
f"与期望 {time_type} 相比 {time_relation}"
)
return target_time
self._showTrace( self._showTrace(
f"无法选择最近的 {time_type} {self.__minsToTime(target_time)}, "\ f"选择距离期望 {time_type} 最近的 {best_text}, "
f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟" f"与期望 {time_type} 相比 {time_relation}"
) )
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}") return target_time
return -1 self._showTrace(
except: f"无法选择最近的 {time_type} {self._minsToTime(target_time)}, "
self._showTrace(f"{time_type} {self.__minsToTime(target_time)} 选择失败 !") f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟"
return -1 )
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
return -1
def __selectSeatTime( def __selectSeatTime(
@@ -576,40 +530,35 @@ class LibReserve(LibOperator):
satisfy_duration: bool = True satisfy_duration: bool = True
) -> bool: ) -> bool:
"""Select seat begin and end time."""
expect_begin_time = actual_begin_time = begin_time["time"] expect_begin_time = actual_begin_time = begin_time["time"]
expect_end_time = actual_end_time = end_time["time"] expect_end_time = actual_end_time = end_time["time"]
expect_begin_mins = self.__timeToMins(expect_begin_time) expect_begin_mins = self._timeToMins(expect_begin_time)
actual_begin_mins = expect_begin_mins actual_begin_mins = expect_begin_mins
expect_end_mins = self.__timeToMins(expect_end_time) expect_end_mins = self._timeToMins(expect_end_time)
# select the begin time # Select begin time
if self.__selectNearestTime( if self.__selectNearestTime(
time_id="startTime", # dont change into begin, this is the element in the page time_id="startTime",
time_type="开始时间", time_type="开始时间",
target_time=expect_begin_mins, target_time=expect_begin_mins,
max_time_diff=begin_time["max_diff"], max_time_diff=begin_time["max_diff"],
prefer_earlier=begin_time["prefer_early"] prefer_earlier=begin_time["prefer_early"]
) == -1: ) == -1:
return False return False
else: actual_begin_time = self._minsToTime(expect_begin_mins)
actual_begin_time = self.__minsToTime(expect_begin_mins) actual_begin_mins = self._timeToMins(actual_begin_time)
actual_begin_mins = self.__timeToMins(actual_begin_time)
# if 'satisfy_duration' is True. # If 'satisfy_duration' is True, select end time based on actual begin time
# select the end time based on the begin time
# (because it may be changed under the 'max time diff' strategy) and expect duration.
if satisfy_duration: if satisfy_duration:
expect_end_mins = int(actual_begin_mins + expct_duration*60) expect_end_mins = self.validateAndAdjustEndTime(actual_begin_mins, expct_duration)
if expect_end_mins > self.__timeToMins("23:30"): expect_end_time = self._minsToTime(expect_end_mins)
expect_end_mins = self.__timeToMins("23:30")
self._showTrace(
f"预约持续时间 {expct_duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
expect_end_time = self.__minsToTime(expect_end_mins)
self._showTrace( self._showTrace(
f"需要满足期望预约持续时间: {expct_duration} 小时, "\ f"需要满足期望预约持续时间: {expct_duration} 小时, "
f"根据开始时间 {actual_begin_time} 计算结束时间: {self.__minsToTime(expect_end_mins)}" f"根据开始时间 {actual_begin_time} 计算结束时间: {expect_end_time}"
) )
# select the end time
# Select end time
if self.__selectNearestTime( if self.__selectNearestTime(
time_id="endTime", time_id="endTime",
time_type="结束时间", time_type="结束时间",
@@ -618,8 +567,7 @@ class LibReserve(LibOperator):
prefer_earlier=end_time["prefer_early"] prefer_earlier=end_time["prefer_early"]
) == -1: ) == -1:
return False return False
else: actual_end_time = self._minsToTime(expect_end_mins)
actual_end_time = self.__minsToTime(expect_end_mins)
self._showTrace( self._showTrace(
f"期望预约时间段: {expect_begin_time} - {expect_end_time}, " f"期望预约时间段: {expect_begin_time} - {expect_end_time}, "
f"实际预约时间段: {actual_begin_time} - {actual_end_time}" f"实际预约时间段: {actual_begin_time} - {actual_end_time}"
@@ -627,6 +575,25 @@ class LibReserve(LibOperator):
return True return True
def validateAndAdjustEndTime(
self,
begin_mins: int,
duration: int
) -> int:
"""
Validate and adjust reserve end time to library closing time if needed.
"""
LIBRARY_CLOSE_TIME = self._timeToMins("23:30")
expect_end_mins = begin_mins + duration * 60
if expect_end_mins > LIBRARY_CLOSE_TIME:
expect_end_mins = LIBRARY_CLOSE_TIME
self._showTrace(
f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
)
return expect_end_mins
def reserve( def reserve(
self, self,
username: str, username: str,
+1
View File
@@ -8,5 +8,6 @@
- LibReserve: Library operator for reserving seat. - LibReserve: Library operator for reserving seat.
- LibCheckin: Library operator for checking in seat. - LibCheckin: Library operator for checking in seat.
- LibCheckout: Library operator for checking out seat. - LibCheckout: Library operator for checking out seat.
- LibChecker: Library operator for checking record status.
- LibRenew: Library operator for renewing seat. - LibRenew: Library operator for renewing seat.
""" """
+244
View File
@@ -0,0 +1,244 @@
# -*- 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 os
import threading
from enum import Enum
from typing import Any, Optional
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
# This config manager class only responsible for global and other
# 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:
"""
Config template class.
"""
def __init__(
self,
config_type: ConfigType
):
self.__config_type = config_type
def template(
self
) -> dict:
"""
Get config template.
Returns:
dict: Config template.
"""
match self.__config_type:
case ConfigType.GLOBAL:
return {
"automation": {
"run_path": {
"current": 0,
"paths": []
},
"user_path": {
"current": 0,
"paths": []
}
}
}
case ConfigType.BULLETIN:
return {
"bulletin": [],
"last_sync_time": None
}
case ConfigType.TIMERTASK:
return {
"timer_tasks": []
}
case _:
return {}
class ConfigManager:
def __init__(
self,
config_dir: str
):
self.__config_dir = os.path.abspath(config_dir)
self.__config_lock = threading.Lock()
self.__config_data = {}
self.initialize()
def initialize(
self
):
for config_type in ConfigType:
self.load(config_type)
def load(
self,
config_type: ConfigType
):
config_path = os.path.join(self.__config_dir, config_type.value)
if os.path.exists(config_path):
try:
config_data = JSONReader(config_path).data()
self.__config_data[config_type.value] = config_data
return
except:
pass
self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
JSONWriter(config_path, self.__config_data[config_type.value])
def get(
self,
config_type: ConfigType,
key: str = "",
default: Optional[Any] = None
) -> Any:
with self.__config_lock:
config_data = self.__config_data[config_type.value]
if key == "":
return config_data
keys = key.split('.')
for k in keys[:-1]:
config_data = config_data.get(k, None)
if config_data is None:
return default
return config_data.get(keys[-1], default)
def set(
self,
config_type: ConfigType,
key: str = "",
value: Any = None
):
with self.__config_lock:
root_data = self.__config_data[config_type.value]
if key == "":
self.__config_data[config_type.value] = value
else:
keys = key.split('.')
config_data = root_data
for k in keys[:-1]:
if k not in config_data:
config_data[k] = {}
config_data = config_data[k]
config_data[keys[-1]] = value
self.save(config_type)
def save(
self,
config_type: ConfigType
):
config_path = os.path.join(self.__config_dir, config_type.value)
JSONWriter(config_path, self.__config_data[config_type.value])
def configDir(
self
) -> str:
return self.__config_dir
# ConfigManager singleton instance.
_config_manager_instance = None
# Utility functions.
#
# Utility function to get validated automation config paths.
def getValidateAutomationConfigPaths(
) -> dict:
"""
Get validated automation config paths from ConfigManager instance.
These function will validate the config paths and return the validated paths in a dict.
Returns:
dict: Validated automation config paths.
"""
config_paths = {"run": "", "user": ""}
auto_config = _config_manager_instance.get(ConfigType.GLOBAL, "automation", {})
for cfg_type in ["run", "user"]:
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
if paths == []:
paths.append(os.path.join(_config_manager_instance.configDir(), f"{cfg_type}.json"))
if index < 0:
index = 0
if index >= len(paths):
index = len(paths) - 1
config_paths[cfg_type] = paths[index]
data = {"current": index, "paths": paths}
auto_config[f"{cfg_type}_path"] = data
_config_manager_instance.set(ConfigType.GLOBAL, "automation", auto_config)
return config_paths
# Utility function to get base config directory.
def getBaseConfigDir(
) -> str:
"""
Get base config directory, on Windows, it is usually at :
'C:\\Users\\<username>\\AppData\\Local\\AutoLibrary\\config'.
Returns:
str: Base config directory.
"""
return _config_manager_instance.configDir()
# Singleton instance of ConfigManager.
_instance_lock = threading.Lock()
def instance(
config_dir: str = ""
) -> ConfigManager:
"""
Initialize ConfigManager singleton instance.
Args:
config_dir (str): Config directory.
"""
global _config_manager_instance
with _instance_lock:
if _config_manager_instance is None:
_config_manager_instance = ConfigManager(config_dir)
else:
if config_dir == "":
return _config_manager_instance
if getBaseConfigDir() != config_dir:
raise ValueError(
"ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance
-89
View File
@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 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 json
class ConfigReader:
def __init__(
self,
config_path: str
):
self._config_path = config_path
self._config_data = {}
if not self.__readConfig():
return None
def __readConfig(
self
) -> bool:
try:
with open(self._config_path, 'r', encoding='utf-8') as file:
self._config_data = json.load(file)
return True
except Exception as e:
print(f"Error reading config file: {e}")
return False
def getConfigs(
self
) -> dict:
return self._config_data.copy()
def getConfig(
self,
key: str
) -> dict:
return self._config_data.get(key, {})
def get(
self,
key: str,
default: any = None
) -> any:
keys = key.split('/')
current = self._config_data
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current
def hasConfig(
self,
key: str
) -> bool:
return self.getConfig(key) != {}
def reReadConfig(
self
) -> bool:
return self.__readConfig()
def configPath(
self
) -> str:
return self._config_path
-87
View File
@@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 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 json
class ConfigWriter:
def __init__(
self,
config_path: str,
config_data: dict
):
self.__config_path = config_path
self.__config_data = config_data if config_data is not None else {}
if config_data is None:
return None
if not self.__writeConfig():
return None
def __writeConfig(
self
) -> bool:
try:
with open(self.__config_path, "w") as f:
json.dump(self.__config_data, f, indent=4, sort_keys=False)
return True
except:
return False
def setConfigs(
self,
configs: dict
) -> bool:
self.__config_data = configs
return self.__writeConfig()
def setConfig(
self,
key: str,
value: dict
) -> bool:
self.__config_data[key] = value
return self.__writeConfig()
def set(
self,
key: str,
value: dict
) -> bool:
keys = key.replace("\\", "/").split("/")
current = self.__config_data
for k in keys[:-1]:
if k not in current or not isinstance(current[k], dict):
current[k] = {}
current = current[k]
current[keys[-1]] = value
return self.__writeConfig()
def reWriteConfig(
self
) -> bool:
return self.__writeConfig()
def configPath(
self
) -> str:
return self.__config_path
+85
View File
@@ -0,0 +1,85 @@
# -*- 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 os
import json
class JSONReader:
"""
JSON reader class.
This class is used to read JSON file.
Args:
json_path (str): The path of JSON file.
Examples:
>>> print(open("config.json", "r", encoding="utf-8").read())
{
"key1": {
"key2": "value1"
}
}
>>> json_reader = JSONReader("config.json")
>>> data = json_reader.data()
>>> data["key1"]["key2"]
"value1"
"""
def __init__(
self,
json_path: str
):
self.__json_path = os.path.abspath(json_path)
self.__json_data = None
self.__read()
def __read(
self
):
try:
with open(self.__json_path, 'r', encoding='utf-8') as file:
self.__json_data = json.load(file)
except FileNotFoundError as e:
raise Exception(f"文件不存在: {self.__json_path}") from e
except PermissionError as e:
raise Exception(f"没有足够的权限读取文件: {self.__json_path}") from e
except json.JSONDecodeError as e:
raise Exception(f"JSON 解析错误: {self.__json_path}") from e
except Exception as e:
raise Exception(f"读取文件时发生未知错误: {e}") from e
def read(
self
) -> bool:
try:
self.__read()
except:
return False
return True
def data(
self
) -> dict:
return self.__json_data.copy()
def path(
self
) -> str:
return self.__json_path
+82
View File
@@ -0,0 +1,82 @@
# -*- 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 os
import json
class JSONWriter:
"""
JSON writer class.
This class is used to write JSON file.
Args:
json_path (str): The path of JSON file.
json_data (dict): The JSON data to be written.
Examples:
>>> json_data = {
... "key1": {
... "key2": "value1"
... }
... }
>>> json_writer = JSONWriter("config.json", json_data)
>>> print(open("config.json", "r", encoding="utf-8").read())
{
"key1": {
"key2": "value1"
}
}
"""
def __init__(
self,
json_path: str,
json_data: dict
):
self.__json_path = os.path.abspath(json_path)
self.__json_data = json_data.copy() if json_data is not None else {}
self.__write()
def __write(
self
):
try:
with open(self.__json_path, "w", encoding="utf-8") as f:
json.dump(self.__json_data, f, indent=4, sort_keys=False)
except PermissionError as e:
raise Exception(f"没有足够的权限写入文件: {self.__json_path}") from e
except IOError as e:
raise Exception(f"写入文件时发生 IO 错误: {self.__json_path}") from e
except TypeError as e:
raise Exception(f"JSON 数据包含无法 JSON 序列化的类型: {e}") from e
except Exception as e:
raise Exception(f"写入文件时发生未知错误: {e}") from e
def write(
self
) -> bool:
try:
self.__write()
except:
return False
return True
def path(
self
) -> str:
return self.__json_path
+50
View File
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 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 datetime, timedelta
def calculateNextRepeatTime(
repeat_days: list,
hour: int,
minute: int,
second: int
) -> datetime:
"""
Calculate the next repeat time based on repeat days and target time.
This function calculates the next execution time for a repeatable task.
If the current day is in repeat_days and the target time has not passed,
it returns today's target time. Otherwise, it finds the next matching day.
Args:
repeat_days (list): List of weekdays to repeat (0=Monday, 6=Sunday).
hour (int): Target hour (0-23).
minute (int): Target minute (0-59).
second (int): Target second (0-59).
Returns:
datetime: The next repeat execution time.
"""
current_time = datetime.now()
current_weekday = current_time.weekday()
target_time = current_time.replace(hour=hour, minute=minute, second=second, microsecond=0)
if current_weekday in repeat_days:
if target_time > current_time:
return target_time
repeat_days_sorted = sorted(repeat_days)
for day in repeat_days_sorted:
if day > current_weekday:
days_until = day - current_weekday
next_time = target_time + timedelta(days=days_until)
return next_time
days_until = 7 - current_weekday + repeat_days_sorted[0]
next_time = target_time + timedelta(days=days_until)
return next_time
+3 -2
View File
@@ -2,6 +2,7 @@
Utils module for the AutoLibrary project. Utils module for the AutoLibrary project.
Here are the classes and modules in this package: Here are the classes and modules in this package:
- ConfigReader: Configuration reader class for the AutoLibrary project. - ConfigManager: Configuration manager class for the AutoLibrary project.
- ConfigWriter: Configuration writer class for the AutoLibrary project. - JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
""" """
+1
View File
@@ -0,0 +1 @@
This folder is used to store the template config files.
+18
View File
@@ -0,0 +1,18 @@
{
"library": {
"host_url": "http://10.1.20.7",
"login_url": "/login"
},
"login": {
"auto_captcha": true,
"max_attempt": 3
},
"web_driver": {
"driver_type": "edge",
"driver_path": "",
"headless": false
},
"mode": {
"run_mode": 1
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"groups": []
}
+6
View File
@@ -0,0 +1,6 @@
This folder is used to store the template files.
Directory structure:
templates
|─── configs // template config files