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

Compare commits

...

22 Commits

Author SHA1 Message Date
github-actions[bot] f984217bda chore(release): v1.2.0 [auto release commit] 2026-03-21 10:55:01 +00:00
KenanZhu 4e7780fe70 docs(readme): 更新自述文件以包含最新功能变化 2026-03-21 18:49:48 +08:00
Kenan Zhu 7149cb2b7d feat(*): 远程签到、定时任务重复执行与浏览器驱动自动管理 (#6)
- 图书馆远程签到
- 定时任务优化
- 浏览器驱动自动管理
2026-03-21 18:34:05 +08:00
KenanZhu 2c90008fcd refactor(WebDriverManager, ALWebDriverDownloadDialog): 重命名驱动状态枚举并完善对话框状态感知 2026-03-21 17:22:25 +08:00
KenanZhu 5c393595d7 fix(ALWebDriverDownloadDialog): 重命名信号避免与 QThread 内置信号冲突并改进线程生命周期管理 2026-03-21 01:53:22 +08:00
KenanZhu 4924f4b031 fix(WebDriverDownloader): 优化下载速度计算逻辑并改用时间间隔触发回调
- 将回调触发条件由进度变化量改为固定时间间隔(0.5s), 避免突发数据导致速度虚高

- 修正 total_size == 0 为 total_size <= 0, 完善边界判断

- 重命名变量提升可读性(last_time/last_size -> last_callback_time/last_callback_size)
2026-03-21 01:52:20 +08:00
KenanZhu 62c1ecdb07 fix(LogManager): 修复 CallerInfoFormatter 中 lineno 类型转换异常 2026-03-21 00:55:17 +08:00
KenanZhu aef28b6d5e feat(ALConfigWidget): 集成浏览器驱动自动下载功能到配置界面 2026-03-21 00:55:09 +08:00
KenanZhu afa1d39051 feat(gui): 新增 ALStatusLabel 状态标签组件和浏览器驱动下载对话框 2026-03-21 00:55:02 +08:00
KenanZhu 84cff6acc3 feat(WebDriverManager): 支持下载取消操作并完善异常处理 2026-03-21 00:54:49 +08:00
KenanZhu e40c7f4f3e chore(*): 降低 ddddocr 版本以避免不必要的打包体积,同时回滚工作流 2026-03-20 20:57:24 +08:00
KenanZhu c8e202dc8c ci(workflows): 修复构建工作流中的模型文件复制问题 2026-03-20 20:09:30 +08:00
KenanZhu 9a3abc365c fix(requirement.txt): 添加缺失的依赖项 pyinstaller 2026-03-20 19:26:17 +08:00
KenanZhu 6b2bf4863e chore(*): 更新项目依赖,并由此修改 CI/CD 工作流配置
- 更新项目依赖
2026-03-20 19:21:56 +08:00
KenanZhu 95aa2bb518 feat(WebDriverManager): 新增浏览器管理类 WebDriverManager
- 新增浏览器管理类,支持下载和管理浏览器驱动
2026-03-20 19:20:43 +08:00
KenanZhu 571af554d2 chore(Main.py): 使用 exec() 替换 exec_() 方法
- chore(Main.py): 使用 exec() 替换 exec_() 方法
2026-03-20 19:20:01 +08:00
KenanZhu 706fc889f9 chore(*): 重构项目结构
- 新增 src/boot 目录,用于存放启动时需要初始化的模块
- 新增 src/managers 目录,用于存放项目中的管理模块
- 新增 src/managers/config 目录,用于存放配置管理模块
- 新增 src/managers/log 目录,用于存放日志管理模块
- 新增 src/managers/driver 目录,用于存放浏览器驱动管理模块
- 修改对应文件中 import 导入路径
2026-03-20 19:19:34 +08:00
KenanZhu bf93cc2cbc style(*): 将中文逗号替换为英文逗号 2026-03-20 08:59:09 +08:00
KenanZhu 1cfe261324 style(ALTimerTaskManageWidget): 优化详细信息的上下文语义
- 使用 “已记录次数” 替代 “已执行次数”,更符合实际含义
2026-03-19 12:23:36 +08:00
KenanZhu e5dea7bcc5 refactor(gui): 统一定时任务字段命名
- 将 task_uuid 字段重命名为 uuid,添加时间字段 add_time 重命名为 added_time
2026-03-19 12:22:32 +08:00
KenanZhu 30b36b68dd refactor(ALTimerTaskManageWidget): 修复重复任务历史记录逻辑
- 修复 onRepeatTimerTaskIs 方法中日期循环索引错误,使用 %7 正确处理跨周星期计算
- 新增 OUTDATED 状态的专属处理逻辑,补全过期任务的历史记录
- 添加函数返回值并统一枚举比较方式为 ==,提高代码一致性
2026-03-19 11:56:44 +08:00
KenanZhu 1cd39ec84c docs(readme): 更新 “后续会有哪些功能?” 部分,可重复性定时任务功能已完成 2026-03-17 16:08:54 +08:00
32 changed files with 2309 additions and 895 deletions
-15
View File
@@ -1,15 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ddddocr = "*"
selenium = "*"
pyinstaller = "*"
pyside6 = "*"
[dev-packages]
[requires]
python_version = "3.13"
Generated
-630
View File
@@ -1,630 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "26dffc26812d5328611959b95713a7ed65e20c08c60089b54283b0f406dd08e4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.13"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"altgraph": {
"hashes": [
"sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406",
"sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"
],
"version": "==0.17.4"
},
"attrs": {
"hashes": [
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
],
"markers": "python_version >= '3.9'",
"version": "==25.4.0"
},
"certifi": {
"hashes": [
"sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de",
"sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"
],
"markers": "python_version >= '3.7'",
"version": "==2025.10.5"
},
"cffi": {
"hashes": [
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
],
"markers": "python_version >= '3.9'",
"version": "==2.0.0"
},
"coloredlogs": {
"hashes": [
"sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
"sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==15.0.1"
},
"ddddocr": {
"hashes": [
"sha256:5991594d481d33ba0b136022e910f578d6d5b0ca536b44886591359622ab0c70",
"sha256:7c44b58ba7d7566d785c65b8526ec5b78efacd121e993dea4fda5f7966897428"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.0.6"
},
"flatbuffers": {
"hashes": [
"sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2",
"sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12"
],
"version": "==25.9.23"
},
"h11": {
"hashes": [
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
],
"markers": "python_version >= '3.8'",
"version": "==0.16.0"
},
"humanfriendly": {
"hashes": [
"sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
"sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==10.0"
},
"idna": {
"hashes": [
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
],
"markers": "python_version >= '3.8'",
"version": "==3.11"
},
"mpmath": {
"hashes": [
"sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f",
"sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"
],
"version": "==1.3.0"
},
"numpy": {
"hashes": [
"sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64",
"sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e",
"sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0",
"sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365",
"sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d",
"sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c",
"sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52",
"sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36",
"sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec",
"sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f",
"sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197",
"sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7",
"sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9",
"sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37",
"sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a",
"sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db",
"sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c",
"sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7",
"sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d",
"sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e",
"sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f",
"sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a",
"sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16",
"sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e",
"sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868",
"sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05",
"sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e",
"sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff",
"sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f",
"sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7",
"sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f",
"sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e",
"sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562",
"sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6",
"sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0",
"sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26",
"sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0",
"sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d",
"sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879",
"sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef",
"sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29",
"sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252",
"sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847",
"sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6",
"sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32",
"sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0",
"sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3",
"sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b",
"sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3",
"sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc",
"sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc",
"sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda",
"sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a",
"sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40",
"sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032",
"sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7",
"sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966",
"sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9",
"sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346",
"sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2",
"sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a",
"sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786",
"sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f",
"sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc",
"sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb",
"sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646",
"sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd",
"sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1",
"sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11",
"sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667",
"sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996",
"sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953",
"sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b",
"sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"
],
"markers": "python_version >= '3.11'",
"version": "==2.3.4"
},
"onnxruntime": {
"hashes": [
"sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc",
"sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77",
"sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95",
"sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088",
"sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b",
"sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435",
"sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f",
"sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b",
"sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7",
"sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c",
"sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c",
"sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612",
"sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872",
"sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2",
"sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e",
"sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466",
"sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3",
"sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36",
"sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321",
"sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6",
"sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e",
"sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145"
],
"markers": "python_version >= '3.10'",
"version": "==1.23.2"
},
"outcome": {
"hashes": [
"sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
"sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.0.post0"
},
"packaging": {
"hashes": [
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
],
"markers": "python_version >= '3.8'",
"version": "==25.0"
},
"pefile": {
"hashes": [
"sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc",
"sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"
],
"markers": "python_full_version >= '3.6.0'",
"version": "==2023.2.7"
},
"pillow": {
"hashes": [
"sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643",
"sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e",
"sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e",
"sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc",
"sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642",
"sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6",
"sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1",
"sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b",
"sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399",
"sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba",
"sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad",
"sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47",
"sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739",
"sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b",
"sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f",
"sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10",
"sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52",
"sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d",
"sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b",
"sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a",
"sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9",
"sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d",
"sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098",
"sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905",
"sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b",
"sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3",
"sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371",
"sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953",
"sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01",
"sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca",
"sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e",
"sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7",
"sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27",
"sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082",
"sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e",
"sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d",
"sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8",
"sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a",
"sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad",
"sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3",
"sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a",
"sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d",
"sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353",
"sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee",
"sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b",
"sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b",
"sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a",
"sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7",
"sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef",
"sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a",
"sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a",
"sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257",
"sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07",
"sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4",
"sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c",
"sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c",
"sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4",
"sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe",
"sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8",
"sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5",
"sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6",
"sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e",
"sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8",
"sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e",
"sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275",
"sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3",
"sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76",
"sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227",
"sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9",
"sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5",
"sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79",
"sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca",
"sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa",
"sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b",
"sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e",
"sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197",
"sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab",
"sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79",
"sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2",
"sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363",
"sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0",
"sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e",
"sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782",
"sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925",
"sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0",
"sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b",
"sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced",
"sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c",
"sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344",
"sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9",
"sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"
],
"markers": "python_version >= '3.10'",
"version": "==12.0.0"
},
"protobuf": {
"hashes": [
"sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954",
"sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995",
"sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef",
"sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455",
"sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee",
"sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9",
"sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3",
"sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035",
"sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90",
"sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"
],
"markers": "python_version >= '3.9'",
"version": "==6.33.0"
},
"pycparser": {
"hashes": [
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
],
"markers": "python_version >= '3.8'",
"version": "==2.23"
},
"pyinstaller": {
"hashes": [
"sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058",
"sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef",
"sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8",
"sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851",
"sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a",
"sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3",
"sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0",
"sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0",
"sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41",
"sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454",
"sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64",
"sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5"
],
"index": "pypi",
"markers": "python_version < '3.15' and python_version >= '3.8'",
"version": "==6.16.0"
},
"pyinstaller-hooks-contrib": {
"hashes": [
"sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6",
"sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038"
],
"markers": "python_version >= '3.8'",
"version": "==2025.9"
},
"pyreadline3": {
"hashes": [
"sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7",
"sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"
],
"markers": "python_version >= '3.8'",
"version": "==3.5.4"
},
"pyside6": {
"hashes": [
"sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339",
"sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f",
"sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820",
"sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01",
"sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"
],
"index": "pypi",
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"pyside6-addons": {
"hashes": [
"sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464",
"sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0",
"sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c",
"sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1",
"sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"pyside6-essentials": {
"hashes": [
"sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a",
"sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8",
"sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1",
"sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998",
"sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"pysocks": {
"hashes": [
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.7.1"
},
"pywin32-ctypes": {
"hashes": [
"sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8",
"sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"
],
"markers": "python_version >= '3.6'",
"version": "==0.2.3"
},
"selenium": {
"hashes": [
"sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c",
"sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==4.38.0"
},
"setuptools": {
"hashes": [
"sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922",
"sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"
],
"markers": "python_version >= '3.9'",
"version": "==80.9.0"
},
"shiboken6": {
"hashes": [
"sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717",
"sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543",
"sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148",
"sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61",
"sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"sniffio": {
"hashes": [
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
},
"sortedcontainers": {
"hashes": [
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
],
"version": "==2.4.0"
},
"sympy": {
"hashes": [
"sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517",
"sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"
],
"markers": "python_version >= '3.9'",
"version": "==1.14.0"
},
"trio": {
"hashes": [
"sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b",
"sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"
],
"markers": "python_version >= '3.10'",
"version": "==0.32.0"
},
"trio-websocket": {
"hashes": [
"sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae",
"sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"
],
"markers": "python_version >= '3.8'",
"version": "==0.12.2"
},
"typing-extensions": {
"hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
],
"markers": "python_version >= '3.9'",
"version": "==4.15.0"
},
"urllib3": {
"extras": [
"socks"
],
"hashes": [
"sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
"sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
],
"markers": "python_version >= '3.9'",
"version": "==2.5.0"
},
"websocket-client": {
"hashes": [
"sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98",
"sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"
],
"markers": "python_version >= '3.9'",
"version": "==1.9.0"
},
"wsproto": {
"hashes": [
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==1.2.0"
}
},
"develop": {}
}
+14 -13
View File
@@ -20,33 +20,34 @@
1. 自动预约 - 支持自动预约 1. 自动预约 - 支持自动预约
2. 自动续约 - 支持自动续约 2. 自动续约 - 支持自动续约
3. 自动签到 - 支持自动签到 3. 自动签到 - 支持自动签到
4. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组 4. 远程签到 - 支持远程签到,无需在图书馆网络环境下即可签到
5. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行 5. 批量操作 - 支持同时预约多个用户,可以指定当前需要跳过的用户,并将用户分成多个组
6. 定时任务 - 使用内置定时任务管理,添加定时任务,指定时间后按当前预约信息自动运行,支持设置重复任务
7. 驱动管理 - 内置浏览器驱动自动管理,支持自动检测浏览器版本并下载对应驱动,无需手动下载
*1,2,3 的具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)* *具体操作方法和注意事项请访问我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals)*
### 如何使用 ### 如何使用
1. 下载最新版本的 [AutoLibrary 压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)。 1. 下载最新版本的 [AutoLibrary 安装程序](https://github.com/KenanZhu/AutoLibrary/releases/latest) 或 [压缩包](https://github.com/KenanZhu/AutoLibrary/releases/latest)
2. 解压下载的文件到任意目录。 2. 双击运行安装程序进行安装,或将压缩包解压到任意目录。
3. 下载对应浏览器类型和版本(具体操作请参考适用软件版本的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals))的驱动文件,并在配置界面的运行配置选项卡对应位置选择你下载好的浏览器驱动 3. 运行 `AutoLibrary`,即可打开主界面
4. 运行 `AutoLibrary-[主版本号].[次版本号].[修订版本号].Z.exe` 文件 (如 `AutoLibrary-1.0.0.exe` 4. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮
5. 点击 [配置] 按钮,在配置界面填写好预约信息和运行配置后,点击 [确认] 按钮 5. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作
6. 点击 [启动脚本] 按钮,即可开始自动预约、续约、签到等操作。
*注意 1*: 关于浏览器驱动的下载和其它相关问题,请参考我们的 [帮助手册](https://www.autolibrary.kenanzhu.com/manuals) 中对应软件版本的内容 *注意 1*: 工具内置浏览器驱动自动管理功能,会自动检测本地浏览器版本并下载对应的驱动文件。如果自动下载失败,也可以手动下载驱动文件并在配置界面的运行配置选项卡对应位置选择驱动文件路径
#### 平台支持 & 编译步骤 #### 平台支持 & 编译步骤
本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤: 本工具目前仅支持 Windows 平台,由于使用 PySide6 库开发,理论上是可以自行编译并在 Linux 和 macOS 上运行,这里提供简单的编译步骤:
1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。 1. 确保系统安装了 Python 3.13 版本 (推荐,过低或高版本会导致兼容问题)。
2. 安装 pyside6 selenium ddddocr 库,命令为 `pip install pyside6 selenium ddddocr` 2. 安装所有依赖库,命令为 `pip install -r requirements.txt` (建议在虚拟环境下操作)
3.`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` 文件即可。
*注意 1*:如果 python 使用的是虚拟环境,请在虚拟环境安装依赖后,在激活的虚拟环境终端中使用 `cd src/gui/batchs` 命令切换到 `batchs` 目录下,再运行编译脚本。否则会提示缺少必要的 Qt PySide 依赖库。 *注意 1*:如果 python 使用的是虚拟环境,请在虚拟环境安装依赖后,在激活的虚拟环境终端中使用 `cd batchs` 命令切换到 `batchs` 目录下,再运行编译脚本。否则会提示缺少必要的 Qt PySide 依赖库。
*注意 2*:由于 ddddocr 的代码版本问题,其中 `__init__.py` 文件中的函数 `def classification(self, img: bytes):` 中的 `image.resize` 方法传入了不符合当前 pillow 版本的 `resample` 参数 `Image.ANTIALIAS`,该重采样常量已经在 10.0.0 版中删除 [1](@ref)。请将 `image.resize` 方法中的参数替换为 `resample=Image.Resampling.LANCZOS`,具体函数如下: *注意 2*:由于 ddddocr 的代码版本问题,其中 `__init__.py` 文件中的函数 `def classification(self, img: bytes):` 中的 `image.resize` 方法传入了不符合当前 pillow 版本的 `resample` 参数 `Image.ANTIALIAS`,该重采样常量已经在 10.0.0 版中删除 [1](@ref)。请将 `image.resize` 方法中的参数替换为 `resample=Image.Resampling.LANCZOS`,具体函数如下:
```python ```python
@@ -103,7 +104,7 @@ def classification(self, img: bytes):
当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想: 当前版本的功能对于正常使用已经足够,不过后续会着重完善预约时的使用体验,暂时有以下构想:
- 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息。 - 引入交互预约面板功能,预约时直接在座位分布图中选择可用座位,并按用户分配,无需事先配置预约信息。
- 优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。 - ~~优化定时任务管理功能,用户可以在定时任务管理界面设置重复运行的定时任务,如每日预约、每周预约等。~~ (已完成)
- 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项。 - 软件的自动更新以及公告栏功能,用户可以自动更新最新版本并获取最新公告事项。
不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。 不过由于本人的时间和能力有限,也需要考虑到图书馆的正常运行,所以后续功能会有所取舍,但也许会进行一些小的功能验证。
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import QApplication
from gui.ALMainWindow import ALMainWindow from gui.ALMainWindow import ALMainWindow
from gui.resources import ALResource from gui.resources import ALResource
from utils.AppInitializer import initializeApp from boot.AppInitializer import initializeApp
def main(): def main():
@@ -30,7 +30,7 @@ def main():
sys.exit(-1) sys.exit(-1)
window = ALMainWindow() window = ALMainWindow()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec())
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -11,7 +11,7 @@ import logging
import queue import queue
import datetime import datetime
from utils.LogManager import getLogger from managers.log.LogManager import getLogger
class MsgBase: class MsgBase:
@@ -11,32 +11,11 @@ import os
from PySide6.QtCore import QStandardPaths, QDir from PySide6.QtCore import QStandardPaths, QDir
from utils.ConfigManager import instance as configInstance from managers.log.LogManager import instance as logInstance
from utils.LogManager import instance as logInstance from managers.config.ConfigManager import instance as configInstance
from managers.driver.WebDriverManager import instance as webdriverInstance
def initializeConfigManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
old_config_dir = os.path.join(app_dir, "config")
new_config_dir = os.path.join(app_dir, "configs")
if QDir(old_config_dir).exists(): # old config dir exists
#we rename it to compatible with new version
logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir)
if not QDir().rename(old_config_dir, new_config_dir):
logger.error("重命名旧配置目录 %s%s 失败", old_config_dir, new_config_dir)
return False
elif not QDir(new_config_dir).exists():
logger.info("初始化配置目录 %s", new_config_dir)
if not QDir().mkpath(new_config_dir):
logger.error("创建配置目录 %s 失败", new_config_dir)
return False
configInstance(new_config_dir)
return True
def initializeLogManager( def initializeLogManager(
) -> bool: ) -> bool:
@@ -48,6 +27,44 @@ def initializeLogManager(
logInstance(log_dir) logInstance(log_dir)
return True return True
def initializeConfigManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
old_config_dir = os.path.join(app_dir, "config")
new_config_dir = os.path.join(app_dir, "configs")
if QDir(old_config_dir).exists(): # old config dir exists
#we rename it to compatible with new version
logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir)
if not QDir().rename(old_config_dir, new_config_dir):
logger.error("重命名旧配置目录 %s%s 失败", old_config_dir, new_config_dir)
return False
elif not QDir(new_config_dir).exists():
logger.info("初始化配置目录 %s", new_config_dir)
if not QDir().mkpath(new_config_dir):
logger.error("创建配置目录 %s 失败", new_config_dir)
return False
configInstance(new_config_dir)
return True
def initializeWebDriverManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
driver_dir = os.path.join(app_dir, "drivers")
logger.info("初始化驱动目录 %s", driver_dir)
if not QDir(driver_dir).exists():
logger.error("创建驱动目录 %s 失败", driver_dir)
if not QDir().mkpath(driver_dir):
logger.error("创建驱动目录 %s 失败", driver_dir)
return False
webdriverInstance(driver_dir)
return True
def initializeApp( def initializeApp(
) -> bool: ) -> bool:
@@ -55,4 +72,6 @@ def initializeApp(
return False return False
if not initializeConfigManager(): if not initializeConfigManager():
return False return False
if not initializeWebDriverManager():
return False
return True return True
+6
View File
@@ -0,0 +1,6 @@
"""
Boot module for the AutoLibrary project.
Here are the classes and modules in this package:
- AppInitializer: Application initializer class.
"""
+18 -1
View File
@@ -20,7 +20,7 @@ from PySide6.QtGui import (
QCloseEvent, QAction QCloseEvent, QAction
) )
import utils.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from utils.JSONReader import JSONReader from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter from utils.JSONWriter import JSONWriter
@@ -29,6 +29,7 @@ from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
from gui.ALSeatMapTable import ALSeatMapTable from gui.ALSeatMapTable import ALSeatMapTable
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
class ALConfigWidget(QWidget, Ui_ALConfigWidget): class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -80,6 +81,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked) self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked) self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked) self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
self.AutoDownloadWebDriverButton.clicked.connect(self.onAutoDownloadWebDriverButtonClicked)
self.BrowseCurrentRunConfigButton.clicked.connect(self.onBrowseCurrentRunConfigButtonClicked) self.BrowseCurrentRunConfigButton.clicked.connect(self.onBrowseCurrentRunConfigButtonClicked)
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked) self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
self.BrowseExportRunConfigButton.clicked.connect(self.onBrowseExportRunConfigButtonClicked) self.BrowseExportRunConfigButton.clicked.connect(self.onBrowseExportRunConfigButtonClicked)
@@ -948,6 +950,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if browser_driver_path: if browser_driver_path:
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path)) self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
@Slot()
def onAutoDownloadWebDriverButtonClicked(
self
):
dialog = ALWebDriverDownloadDialog(self)
dialog.show()
dialog.exec_()
selected_driver_info = dialog.getSelectedDriverInfo()
if selected_driver_info and selected_driver_info.driver_path:
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
@Slot() @Slot()
def onBrowseCurrentRunConfigButtonClicked( def onBrowseCurrentRunConfigButtonClicked(
self self
+3 -3
View File
@@ -19,7 +19,7 @@ from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
) )
import utils.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
from base.MsgBase import MsgBase from base.MsgBase import MsgBase
@@ -302,7 +302,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
self.__alConfigWidget = None self.__alConfigWidget = None
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths() self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
self.setControlButtons(True, None, None) self.setControlButtons(True, None, None)
self._showLog("配置窗口已关闭配置文件路径已更新") self._showLog("配置窗口已关闭,配置文件路径已更新")
@Slot(dict) @Slot(dict)
def onTimerTaskIsReady( def onTimerTaskIsReady(
@@ -334,7 +334,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
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['uuid']}"
) )
if not is_error: if not is_error:
self.timerTaskIsExecuted.emit(timer_task) self.timerTaskIsExecuted.emit(timer_task)
+246
View File
@@ -0,0 +1,246 @@
from enum import Enum
from PySide6.QtWidgets import (
QLabel
)
from PySide6.QtCore import (
Qt, Property, QPropertyAnimation, QEasingCurve
)
from PySide6.QtGui import (
QPainter, QColor, QConicalGradient, QPalette
)
class ALStatusLabel(QLabel):
class Status(Enum):
"""
Enum class for representing the status of ALStatusLabel.
"""
WAITING = 0
RUNNING = 1
SUCCESS = 2
WARNING = 3
FAILURE = 4
def __init__(
self,
parent = None
):
super().__init__(parent)
self.__status = self.Status.WAITING
self.__icon_angle = 0
self.setupUi()
def setupUi(
self
):
self.setFixedSize(36, 36)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
self.RunningAnimation.setDuration(1000)
self.RunningAnimation.setStartValue(0)
self.RunningAnimation.setEndValue(-360)
self.RunningAnimation.setLoopCount(-1)
self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
def isDarkMode(
self
) -> bool:
return self.palette().color(QPalette.ColorRole.Window).value() < 128
def getMarkColor(
self
) -> QColor:
return QColor("#FFFFFF") if self.isDarkMode() else QColor("#454545")
@Property(Status)
def status(
self
) -> Status:
return self.__status
@Property(int)
def iconAngle(
self
) -> int:
return self.__icon_angle
@status.setter
def status(
self,
status: Status
):
if status not in self.Status:
raise ValueError(f"Invalid (class)Status[enum.Enum] value: {status}")
self.__status = status
if self.__status == self.Status.RUNNING:
self.RunningAnimation.start()
else:
self.RunningAnimation.stop()
self.update()
@iconAngle.setter
def iconAngle(
self,
value: int
):
self.__icon_angle = value
self.update()
def paintEvent(
self,
event
):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
center_x = self.width()/2
center_y = self.height()/2
radius = min(center_x, center_y) - 3
match self.__status:
case self.Status.WAITING:
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#969696")) # grey
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.RUNNING:
gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
gradient.setColorAt(1.0, QColor("#2294FF00"))
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(gradient)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.SUCCESS:
# draw the success green circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the success check mark '✓'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
mark_size = radius/2
mark_path = [
(center_x - mark_size, center_y),
(center_x - mark_size/3, center_y + mark_size/2),
(center_x + mark_size, center_y - mark_size/2)
]
painter.drawLine(
int(mark_path[0][0]),int(mark_path[0][1]),
int(mark_path[1][0]),int(mark_path[1][1])
)
painter.drawLine(
int(mark_path[1][0]),int(mark_path[1][1]),
int(mark_path[2][0]),int(mark_path[2][1])
)
case self.Status.WARNING:
# draw the warning orange circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#FF9800")) # orange
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the warning exclamation mark '!'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
painter.drawLine(
int(center_x), int(center_y - radius/2),
int(center_x), int(center_y + radius/6)
)
painter.drawPoint(
int(center_x), int(center_y + radius/2)
)
case self.Status.FAILURE:
# draw the failure red circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#DC0000")) # red
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the failure cross mark '✗'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
mark_size = radius/3
painter.drawLine(
int(center_x - mark_size), int(center_y - mark_size),
int(center_x + mark_size), int(center_y + mark_size)
)
painter.drawLine(
int(center_x + mark_size), int(center_y - mark_size),
int(center_x - mark_size), int(center_y + mark_size)
)
painter.end()
super().paintEvent(event)
+2 -3
View File
@@ -121,11 +121,11 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
) )
task_data = { task_data = {
"name": name, "name": name,
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}", "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, "added_time": added_time,
"status": ALTimerTaskStatus.PENDING, "status": ALTimerTaskStatus.PENDING,
"executed": False, "executed": False,
"repeat": self.RepeatCheckBox.isChecked(), "repeat": self.RepeatCheckBox.isChecked(),
@@ -158,7 +158,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
task_data["repeat_minute"], task_data["repeat_minute"],
task_data["repeat_second"] task_data["repeat_second"]
) )
return task_data return task_data
@Slot(int) @Slot(int)
+1 -1
View File
@@ -49,7 +49,7 @@ class ALTimerTaskHistoryDialog(QDialog):
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}") TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 12px;") TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 12px;")
InfoLayout.addWidget(TaskNameLabel, 0, 0) InfoLayout.addWidget(TaskNameLabel, 0, 0)
TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}") TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('uuid', '未命名')}")
TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;") TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;")
InfoLayout.addWidget(TaskUUIDLabel, 1, 0) InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
InfoLayout.setColumnStretch(0, 1) InfoLayout.setColumnStretch(0, 1)
+43 -23
View File
@@ -25,7 +25,7 @@ from PySide6.QtGui import (
QCloseEvent QCloseEvent
) )
import utils.ConfigManager as ConfigManager import managers.config.ConfigManager as ConfigManager
import utils.TimerUtils as TimerUtils import utils.TimerUtils as TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
@@ -221,7 +221,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK) timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
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["added_time"] = datetime.strptime(task["added_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"] = ALTimerTaskStatus(task["status"]) task["status"] = ALTimerTaskStatus(task["status"])
if "history" in task: if "history" in task:
@@ -245,7 +245,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
try: try:
for task in timer_tasks: for task in timer_tasks:
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S") task["added_time"] = task["added_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
if "history" in task: if "history" in task:
@@ -309,7 +309,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
) )
elif policy == self.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["added_time"],
reverse = order is Qt.SortOrder.DescendingOrder reverse = order is Qt.SortOrder.DescendingOrder
) )
elif policy == self.SortPolicy.BY_EXECUTE_TIME: elif policy == self.SortPolicy.BY_EXECUTE_TIME:
@@ -378,7 +378,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
self.__timer_tasks.append(timer_task) self.__timer_tasks.append(timer_task)
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@staticmethod @staticmethod
def getTimerTaskDetailMessage( def getTimerTaskDetailMessage(
timer_task: dict timer_task: dict
@@ -386,10 +385,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
return ( return (
f"任务名称:{timer_task["name"]}\n" f"任务名称:{timer_task["name"]}\n"
f"添加时间:{timer_task["add_time"]}\n" f"添加时间:{timer_task["added_time"]}\n"
f"当前状态:{timer_task["status"].value}\n" f"当前状态:{timer_task["status"].value}\n"
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\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)}" f"记录次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
) )
@@ -414,10 +413,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
result = msgbox.exec() result = msgbox.exec()
if result != QMessageBox.StandardButton.Yes: if result != QMessageBox.StandardButton.Yes:
return return
task_uuid = timer_task["task_uuid"] task_uuid = timer_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["uuid"] != task_uuid
] ]
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@@ -447,7 +446,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
QMessageBox.warning( QMessageBox.warning(
self, self,
"警告 - AutoLibrary", "警告 - AutoLibrary",
f"存在 {in_queue_count} 个正在执行或已就绪的队列任务无法清除所有定时任务 !" f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !"
) )
return return
# repeat tasks ask before clear # repeat tasks ask before clear
@@ -464,7 +463,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
) )
msgbox.setText( msgbox.setText(
f"存在 {repeat_tasks_count} 个可重复性任务\n" f"存在 {repeat_tasks_count} 个可重复性任务,\n"
"删除可重复性任务将同时删除所有已执行的记录 !\n" "删除可重复性任务将同时删除所有已执行的记录 !\n"
"是否继续 ?" "是否继续 ?"
) )
@@ -563,7 +562,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
): ):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["uuid"] == timer_task["uuid"]:
task["status"] = ALTimerTaskStatus.RUNNING task["status"] = ALTimerTaskStatus.RUNNING
break break
self.timerTasksChanged.emit() self.timerTasksChanged.emit()
@@ -575,17 +574,37 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_task: dict timer_task: dict
) -> dict: ) -> dict:
# only these status are valid
valid_statuses = {ALTimerTaskStatus.EXECUTED, ALTimerTaskStatus.ERROR,
ALTimerTaskStatus.OUTDATED}
if status not in valid_statuses:
return timer_task
if "history" not in timer_task: if "history" not in timer_task:
timer_task["history"] = [] timer_task["history"] = []
executed_time = datetime.now() if status != ALTimerTaskStatus.OUTDATED:
duration = (executed_time - timer_task["execute_time"]).total_seconds() executed_time = datetime.now()
timer_task["history"].append({ duration = (executed_time - timer_task["execute_time"]).total_seconds()
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"), timer_task["history"].append({
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"), "execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
"result": status, "executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
"duration": duration if status is ALTimerTaskStatus.EXECUTED else 0, "result": status,
"uuid": timer_task["task_uuid"] "duration": duration,
}) "uuid": timer_task["uuid"]
})
else:
current_time = datetime.now()
execute_time = timer_task["execute_time"]
execute_weekday = execute_time.weekday()
delta_days = (current_time - execute_time).days
for i in range(delta_days + 1):
if (execute_weekday + i)%7 in timer_task["repeat_days"]:
timer_task["history"].append({
"execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
"executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
"result": status,
"duration": 0,
"uuid": timer_task["uuid"]
})
next_time = TimerUtils.calculateNextRepeatTime( next_time = TimerUtils.calculateNextRepeatTime(
timer_task["repeat_days"], timer_task["repeat_days"],
timer_task["repeat_hour"], timer_task["repeat_hour"],
@@ -598,6 +617,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
timer_task["executed"] = False timer_task["executed"] = False
else: else:
timer_task["status"] = status timer_task["status"] = status
return timer_task
@Slot(dict) @Slot(dict)
def onTimerTaskIsExecuted( def onTimerTaskIsExecuted(
@@ -606,7 +626,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
): ):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["uuid"] == timer_task["uuid"]:
if task.get("repeat", False): if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task) self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task)
else: else:
@@ -621,7 +641,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
): ):
for task in self.__timer_tasks: for task in self.__timer_tasks:
if task["task_uuid"] == timer_task["task_uuid"]: if task["uuid"] == timer_task["uuid"]:
if task.get("repeat", False): if task.get("repeat", False):
self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task) self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task)
else: else:
+3 -3
View File
@@ -5,11 +5,11 @@
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-02-26 15:04:28 UTC Last updated: 2026-03-21 10:54:51 UTC
""" """
AL_VERSION = "1.1.0" AL_VERSION = "1.2.0"
AL_TAG = "v1.1.0" AL_TAG = "v1.2.0"
AL_COMMIT_SHA = "local" AL_COMMIT_SHA = "local"
AL_COMMIT_DATE = "null" # time zone : UTC AL_COMMIT_DATE = "null" # time zone : UTC
AL_BUILD_DATE = "null" # time zone : UTC AL_BUILD_DATE = "null" # time zone : UTC
+576
View File
@@ -0,0 +1,576 @@
# -*- 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 threading
from typing import Optional
from PySide6.QtCore import (
Qt, Slot, QThread, Signal
)
from PySide6.QtWidgets import (
QDialog, QLabel, QComboBox, QProgressBar,
QPushButton, QVBoxLayout, QHBoxLayout,
QMessageBox, QFrame, QLineEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from managers.driver.WebDriverManager import (
instance as webdriver_manager_instance,
WebDriverManager, WebDriverInfo, WebDriverType,
WebDriverStatus
)
from gui.ALStatusLabel import ALStatusLabel
class DownloadWorker(QThread):
"""
Worker thread for downloading web drivers.
"""
progress = Signal(float, int, float, str)
downloadFinished = Signal(object, str)
downloadError = Signal(str)
downloadCancelled = Signal()
def __init__(
self,
driver_manager: WebDriverManager,
driver_info: WebDriverInfo
):
super().__init__()
self.__driver_manager = driver_manager
self.__driver_info = driver_info
self.__driver_path = None
self.__cancelled = False
self.__cancel_event = threading.Event()
def cancel(
self
):
"""
Cancel the download operation.
"""
self.__cancelled = True
self.__cancel_event.set()
def run(
self
):
try:
if self.__cancelled:
self.downloadCancelled.emit()
return
self.__driver_path = self.__driver_manager.installDriver(
self.__driver_info,
progress_callback=self.onProgress,
cancel_event=self.__cancel_event
)
if self.__cancelled:
self.downloadCancelled.emit()
return
if self.__driver_path:
self.downloadFinished.emit(self.__driver_path, "")
else:
self.downloadError.emit("下载失败: 未返回有效路径")
except Exception as e:
if not self.__cancelled:
self.downloadError.emit(f"下载失败: {str(e)}")
def onProgress(
self,
downloaded: float,
total: int,
speed: float,
message: str
):
if self.__cancel_event.is_set():
self.__cancelled = True
if not self.__cancelled:
self.progress.emit(downloaded, total, speed, message)
def stop(
self
):
"""
Cancel and wait for the thread to finish.
Must only be called from the main thread.
"""
self.cancel()
if not self.isFinished():
if not self.wait(5000):
self.terminate()
self.wait()
class ALWebDriverDownloadDialog(QDialog):
def __init__(
self,
parent: Optional[QDialog] = None,
driver_dir: str = ""
):
"""
Web driver download dialog.
Args:
parent: Parent widget.
driver_dir: Driver directory path.
"""
super().__init__(parent)
self.__driver_dir = driver_dir
self.__driver_manager: Optional[WebDriverManager] = None
self.__confirmed = False
self.__selected_driver_info: Optional[WebDriverInfo] = None
self.__driver_infos: list[WebDriverInfo] = []
self.__download_thread: Optional[DownloadWorker] = None
self.setupUi()
self.connectSignals()
self.initializeDriverManager()
self.refreshDriverList()
def showEvent(
self,
event
):
result = super().showEvent(event)
if self.parent():
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 setupUi(
self
):
self.setModal(True)
self.setMaximumHeight(240)
self.setMinimumHeight(240)
self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
self.MainLayout = QVBoxLayout(self)
self.MainLayout.setContentsMargins(5, 5, 5, 5)
self.MainLayout.setSpacing(5)
self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
self.MainLayout.addWidget(self.BrowserCountLabel)
self.DriverInfoLayout = QHBoxLayout()
self.DriverInfoLayout.setSpacing(5)
self.DriverComboBox = QComboBox()
self.DriverInfoLayout.addWidget(self.DriverComboBox)
self.StatusLabel = ALStatusLabel()
self.StatusLabel.setFixedSize(32, 32)
self.DriverInfoLayout.addWidget(self.StatusLabel)
self.MainLayout.addLayout(self.DriverInfoLayout)
self.DetailLayout = QVBoxLayout()
self.DetailLayout.setSpacing(5)
self.DetailLayout.setContentsMargins(5, 5, 5, 5)
self.BrowserTypeLabel = QLabel("类型:")
self.DetailLayout.addWidget(self.BrowserTypeLabel)
self.VersionLabel = QLabel("版本:")
self.DetailLayout.addWidget(self.VersionLabel)
self.PathLabel = QLineEdit()
self.PathLabel.setReadOnly(True)
self.PathLabel.setText("路径:未安装")
self.DetailLayout.addWidget(self.PathLabel)
self.MainLayout.addLayout(self.DetailLayout)
self.Line = QFrame()
self.Line.setFrameShape(QFrame.Shape.HLine)
self.Line.setFrameShadow(QFrame.Shadow.Sunken)
self.MainLayout.addWidget(self.Line)
self.ProgressBar = QProgressBar()
self.ProgressBar.setValue(0)
self.ProgressBar.setTextVisible(False)
self.MainLayout.addWidget(self.ProgressBar)
self.ProgressText = QLabel("")
self.ProgressText.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.MainLayout.addWidget(self.ProgressText)
self.ControlLayout = QHBoxLayout()
self.ControlLayout.setSpacing(8)
self.ControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.RefreshButton = QPushButton("刷新")
self.RefreshButton.setFixedSize(80, 25)
self.DownloadButton = QPushButton("下载驱动")
self.DownloadButton.setFixedSize(80, 25)
self.DeleteButton = QPushButton("删除驱动")
self.DeleteButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setEnabled(False)
self.ControlLayout.addWidget(self.RefreshButton)
self.ControlLayout.addWidget(self.DownloadButton)
self.ControlLayout.addWidget(self.DeleteButton)
self.ControlLayout.addWidget(self.CancelButton)
self.ControlLayout.addWidget(self.ConfirmButton)
self.MainLayout.addLayout(self.ControlLayout)
def connectSignals(
self
):
self.RefreshButton.clicked.connect(self.onRefreshButtonClicked)
self.DownloadButton.clicked.connect(self.onDownloadButtonClicked)
self.DeleteButton.clicked.connect(self.onDeleteButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
def initializeDriverManager(
self
):
try:
self.__driver_manager = webdriver_manager_instance(self.__driver_dir)
except ValueError as e:
QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
self.reject()
def refreshDriverList(
self
):
if not self.__driver_manager:
return
self.__driver_manager.refresh()
self.__driver_infos = self.__driver_manager.getDriverInfos()
self.DriverComboBox.clear()
installed_idx = 0
for i, driver_info in enumerate(self.__driver_infos):
if driver_info.driver_status == WebDriverStatus.INSTALLED:
installed_idx = i # get the installed driver index
display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
self.DriverComboBox.addItem(display_text)
count = len(self.__driver_infos)
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:")
if self.__driver_infos:
self.DriverComboBox.setCurrentIndex(installed_idx)
def onDriverComboBoxChanged(
self,
index: int
):
if not self.__driver_infos or index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info)
@Slot()
def onRefreshButtonClicked(
self
):
self.refreshDriverList()
@Slot()
def onDeleteButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name != "INSTALLED":
QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除")
return
reply = QMessageBox.question(
self,
"确认删除 - AutoLibrary",
f"确定要删除 {driver_info.driver_type.value} 驱动吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
try:
self.__driver_manager.uninstallDriver(driver_info)
self.refreshDriverList()
QMessageBox.information(self, "删除成功 - AutoLibrary", "驱动已成功删除")
except Exception as e:
QMessageBox.critical(self, "删除失败 - AutoLibrary", f"删除驱动时出错:\n{str(e)}")
@Slot()
def onDownloadButtonClicked(
self
):
self.DriverComboBox.setEnabled(False)
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status == WebDriverStatus.INSTALLED:
return
driver_info.driver_status = WebDriverStatus.DOWNLOADING # we set this only to update
# the display, and we will set to not installed in the download thread
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
self.ProgressText.setText("准备开始下载...")
self.updateButtonStates(driver_info)
# set to not installed
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
self.__download_thread = DownloadWorker(self.__driver_manager, driver_info)
self.__download_thread.progress.connect(self.onDownloadProgress)
self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
self.__download_thread.downloadError.connect(self.onDownloadError)
self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
self.__download_thread.finished.connect(self.__onThreadFinished)
self.__download_thread.start()
@Slot()
def onDownloadProgress(
self,
downloaded: float,
total: int,
speed: float,
message: str
):
progress = downloaded
self.ProgressBar.setValue(progress)
if speed >= 1024:
speed_text = f"{speed/1024:.1f} MB/s"
else:
speed_text = f"{speed:.1f} KB/s"
progress_text = f"{message}... {downloaded:.1f}% - {speed_text}"
self.ProgressText.setText(progress_text)
@Slot()
def onDownloadFinished(
self
):
self.DriverComboBox.setEnabled(True)
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
driver_info.driver_status = WebDriverStatus.INSTALLED
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info)
@Slot()
def onDownloadError(
self,
error_message: str
):
self.DriverComboBox.setEnabled(True)
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
driver_info.driver_status = WebDriverStatus.ERROR
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info)
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
@Slot()
def onDownloadCancelled(
self
):
self.DriverComboBox.setEnabled(True)
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
self.__driver_manager.cancelDriverDownload(driver_info)
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
self.updateDriverInfoDisplay(driver_info)
self.updateProgressBarStates(driver_info)
self.updateButtonStates(driver_info)
self.ProgressText.setText("下载已取消")
@Slot()
def onConfirmButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status != WebDriverStatus.INSTALLED:
return
self.__selected_driver_info = driver_info
self.__confirmed = True
self.accept()
@Slot()
def onCancelButtonClicked(
self
):
if self.__download_thread:
reply = QMessageBox.question(
self,
"确认取消 - AutoLibrary",
"正在下载中, 确定要取消下载吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.__download_thread.cancel()
else:
self.__confirmed = False
self.__selected_driver_info = None
self.reject()
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.stop()
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def __onThreadFinished(
self
):
if self.__download_thread:
self.__download_thread.deleteLater()
self.__download_thread = None
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status:
case WebDriverStatus.NOT_INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case WebDriverStatus.INSTALLED:
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case WebDriverStatus.DOWNLOADING:
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case WebDriverStatus.ERROR:
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateProgressBarStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.ProgressBar.setValue(0)
self.ProgressText.setText("未安装")
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.ProgressBar.setValue(100)
self.ProgressText.setText("已安装")
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
pass # update by worker thread
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.ProgressBar.setValue(0)
self.ProgressText.setText("下载失败")
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
self.RefreshButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
self.RefreshButton.setEnabled(False)
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
elif driver_info.driver_status == WebDriverStatus.ERROR:
self.RefreshButton.setEnabled(True)
self.DownloadButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
self.CancelButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
+18
View File
@@ -0,0 +1,18 @@
"""
GUI module for the AutoLibrary project.
Here are the classes and modules in this package:
- ALMainWindow: Main window class.
- ALAboutDialog: About dialog class.
- ALConfigWidget: Configuration widget class.
- ALSeatFrame: Seat frame class.
- ALSeatMapView: Seat map view class.
- ALSeatMapTable: Seat map table class.
- ALSeatMapSelectDialog: Seat map select dialog class.
- ALTimerTaskAddDialog: Timer task add dialog class.
- ALTimerTaskHistoryDialog: Timer task history dialog class.
- ALTimerTaskManageWidget: Timer task manage widget class.
- ALUserTreeWidget: User tree widget class.
- ALMainWorkers: Main workers class.
- ALVersionInfo: Version info class.
"""
+3
View File
@@ -0,0 +1,3 @@
"""
GUI resources module for the AutoLibrary project.
"""
+192 -161
View File
@@ -1233,12 +1233,31 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="2" column="0">
<widget class="QGroupBox" name="BrowserConfigGroupBox"> <widget class="QFrame" name="SystemConfigSpaceFrame">
<property name="title"> <property name="minimumSize">
<string>浏览器设置</string> <size>
<width>0</width>
<height>270</height>
</size>
</property> </property>
<layout class="QVBoxLayout" name="BrowserConfigLayout"> <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 row="1" column="1" colspan="2">
<widget class="QGroupBox" name="RunModeConfigGroupBox">
<property name="title">
<string>运行模式</string>
</property>
<layout class="QVBoxLayout" name="RunModeConfigLayout">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
@@ -1255,162 +1274,59 @@
<number>3</number> <number>3</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="BrowserTypeLabel"> <widget class="QCheckBox" name="AutoReserveCheckBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>100</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>80</width> <width>100</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>浏览器类型:</string> <string>自动预约</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="BrowserTypeComboBox"> <widget class="QCheckBox" name="AutoCheckinCheckBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>80</width> <width>100</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>80</width> <width>100</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;脚本运行使用的浏览器类型&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<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>
</property>
<property name="currentText">
<string>edge</string>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="maxVisibleItems">
<number>3</number>
</property>
<property name="maxCount">
<number>3</number>
</property>
<item>
<property name="text">
<string>edge</string>
</property>
</item>
<item>
<property name="text">
<string>chrome</string>
</property>
</item>
<item>
<property name="text">
<string>firefox</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="BrowserDriverLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>驱动路径:</string> <string>自动签到</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="BrowserDriverLayout"> <widget class="QCheckBox" name="AutoRenewalCheckBox">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
<property name="minimumSize">
<size>
<width>250</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<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 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>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="BrowseBrowserDriverButton">
<property name="minimumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="HeadlessCheckBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>100</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>100</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;运行时不显示浏览器&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<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>
</property>
<property name="text"> <property name="text">
<string>无头模式</string> <string>自动续约</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1529,15 +1445,12 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="1" column="1" colspan="2"> <item row="1" column="0">
<widget class="QGroupBox" name="RunModeConfigGroupBox"> <widget class="QGroupBox" name="BrowserConfigGroupBox">
<property name="title"> <property name="title">
<string>运行模式</string> <string>浏览器设置</string>
</property> </property>
<layout class="QVBoxLayout" name="RunModeConfigLayout"> <layout class="QGridLayout" name="BrowserConfigLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin"> <property name="leftMargin">
<number>3</number> <number>3</number>
</property> </property>
@@ -1550,85 +1463,203 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>3</number> <number>3</number>
</property> </property>
<item> <property name="spacing">
<widget class="QCheckBox" name="AutoReserveCheckBox"> <number>5</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="BrowserTypeLabel">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>自动预约</string> <string>浏览器类型:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="1" column="0">
<widget class="QCheckBox" name="AutoCheckinCheckBox"> <widget class="QComboBox" name="BrowserTypeComboBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>100</width> <width>80</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;脚本运行使用的浏览器类型&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<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>
</property>
<property name="currentText">
<string>edge</string>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="maxVisibleItems">
<number>3</number>
</property>
<property name="maxCount">
<number>3</number>
</property>
<item>
<property name="text">
<string>edge</string>
</property>
</item>
<item>
<property name="text">
<string>chrome</string>
</property>
</item>
<item>
<property name="text">
<string>firefox</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="BrowserDriverLabel">
<property name="minimumSize">
<size>
<width>175</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>175</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text">
<string>自动签到</string> <string>驱动路径:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="AutoRenewalCheckBox"> <layout class="QHBoxLayout" name="BrowserDriverLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
<property name="minimumSize">
<size>
<width>250</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<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 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>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="BrowseBrowserDriverButton">
<property name="minimumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="HeadlessCheckBox">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>100</width> <width>0</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>100</width> <width>16777215</width>
<height>25</height> <height>25</height>
</size> </size>
</property> </property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;运行时不显示浏览器&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<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>
</property>
<property name="text"> <property name="text">
<string>自动续约</string> <string>无头模式</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="AutoDownloadWebDriverButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>25</height>
</size>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>自动下载驱动</string>
</property>
<property name="icon">
<iconset theme="document-properties"/>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QFrame" name="SystemConfigSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>270</height>
</size>
</property>
<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>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="OtherConfigWidget"> <widget class="QWidget" name="OtherConfigWidget">
+8
View File
@@ -0,0 +1,8 @@
"""
Managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
- LogManager: Log manager for logging.
- WebDriverManager: Web driver manager for managing web drivers.
"""
@@ -234,11 +234,12 @@ def instance(
global _config_manager_instance global _config_manager_instance
with _instance_lock: with _instance_lock:
if _config_manager_instance is None: if _config_manager_instance is None:
if not config_dir:
raise ValueError("ConfigManager 需要配置目录参数")
_config_manager_instance = ConfigManager(config_dir) _config_manager_instance = ConfigManager(config_dir)
else: else:
if config_dir == "": if config_dir == "":
return _config_manager_instance return _config_manager_instance
if getBaseConfigDir() != config_dir: if getBaseConfigDir() != config_dir:
raise ValueError( raise ValueError("ConfigManager 的实例已初始化,不能使用不同的配置目录。")
"ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance return _config_manager_instance
+6
View File
@@ -0,0 +1,6 @@
"""
Config managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
"""
+166
View File
@@ -0,0 +1,166 @@
import platform
import installed_browsers
from pathlib import Path
from enum import Enum
from dataclasses import dataclass
class WebBrowserType(Enum):
"""
Web browser type
"""
CHROME = "chrome"
FIREFOX = "firefox"
EDGE = "edge"
class WebBrowserArch(Enum):
"""
Web browser architecture
"""
WINX86_32 = 0
WINX86_64 = 1
WINARM = 2
LINUXX86_32 = 3
LINUXX86_64 = 4
LINUXARM = 5
MACX86_64 = 6
MACARM = 7
@dataclass
class WebBrowserInfo:
"""
Web browser information
Attributes:
browser_arch (WebBrowserArch): Web browser architecture
browser_type (WebBrowserType): Web browser type
browser_version (str): Web browser version
browser_path (Path): Web browser executable file path
"""
browser_arch: WebBrowserArch
browser_type: WebBrowserType
browser_version: str
browser_path: Path
class WebBrowserArchDetector:
"""
Web browser architecture detector
"""
def __init__(
self
):
pass
def detect(
self
) -> WebBrowserArch:
"""
Detect system architecture
Returns:
WebBrowserArch: System architecture
"""
system = platform.system()
machine = platform.machine().lower()
if system == "Windows":
if machine in ["amd64", "x86_64"]:
return WebBrowserArch.WINX86_64
elif machine in ["i386", "i686", "x86"]:
return WebBrowserArch.WINX86_32
elif machine in ["arm64", "aarch64"]:
return WebBrowserArch.WINARM
else:
return WebBrowserArch.WINX86_64
elif system == "Darwin":
if machine in ["arm64", "aarch64"]:
return WebBrowserArch.MACARM
else:
return WebBrowserArch.MACX86_64
elif system == "Linux":
if machine in ["amd64", "x86_64"]:
return WebBrowserArch.LINUXX86_64
elif machine in ["i386", "i686", "x86"]:
return WebBrowserArch.LINUXX86_32
elif machine in ["arm64", "aarch64"]:
return WebBrowserArch.LINUXARM
elif machine.startswith("arm"):
return WebBrowserArch.LINUXARM
else:
return WebBrowserArch.LINUXX86_64
raise ValueError(f"不支持的系统架构 : {system} {machine}")
class WebBrowserDetector:
"""
Web browser detector
"""
def __init__(
self
):
self.browser_arch = WebBrowserArchDetector().detect()
self.browser_infos : list[WebBrowserInfo] = []
def detect(
self
) -> list[WebBrowserInfo]:
"""
Detect installed web browsers on the system.
Returns:
list[WebBrowserInfo]: List of detected browser information objects.
"""
self.browser_infos = []
try:
all_browsers = installed_browsers.browsers()
except Exception as e:
self.browser_infos = []
return self.browser_infos
# Mapping from internal library name to our enum
type_map = {
'chrome': WebBrowserType.CHROME,
'firefox': WebBrowserType.FIREFOX,
'msedge': WebBrowserType.EDGE,
}
for browser in all_browsers:
internal_name = browser.get('name', '').lower()
if internal_name not in type_map:
continue # Not one of the browsers we care about
version = browser.get('version')
if not version:
# Skip browsers with no version info (unlikely, but defensive)
continue
exe_path = browser.get('location')
if not exe_path:
continue
try:
path = Path(exe_path)
if not path.is_file():
continue
except Exception:
continue # Invalid path
info = WebBrowserInfo(
browser_arch=self.browser_arch, # Use system architecture as fallback
browser_type=type_map[internal_name],
browser_version=version,
browser_path=path,
)
self.browser_infos.append(info)
return self.browser_infos
+452
View File
@@ -0,0 +1,452 @@
import os
import time
import shutil
import threading
import requests
import zipfile
import tarfile
from enum import Enum
from pathlib import Path
from typing import Optional, Callable
class WebDriverType(Enum):
"""
Web driver type
"""
CHROME = "chrome"
FIREFOX = "firefox"
EDGE = "edge"
class WebDriverArch(Enum):
"""
Web driver architecture
"""
class Chrome(Enum):
"""
Chrome web driver architecture
"""
WINX86_32 = "win32"
WINX86_64 = "win64"
# LINUX86_32 : no support for linux 32bit
LINUX86_64 = "linux64"
# LINUXARM : no support for linux arm64
MACX86_64 = "mac-x64"
MACARM = "mac-arm64"
class Firefox(Enum):
"""
Firefox web driver architecture
"""
WINX86_32 = "win32"
WINX86_64 = "win64"
WINARM = "win-aarch64"
LINUXX86_32 = "linux32"
LINUXX86_64 = "linux64"
LINUXARM = "linux-aarch64"
MACX86_64 = "macos"
MACARM = "macos-aarch64"
class Edge(Enum):
"""
Edge web driver architecture
"""
WINX86_32 = "win32"
WINX86_64 = "win64"
WINARM = "arm64"
# LINUX86_32 : no support for linux 32bit
LINUXX86_64 = "linux64"
# LINUXARM : no support for linux arm64
MACX86_64 = "mac64"
MACARM = "mac64_m1"
class WebDriverName:
"""
Web driver name
"""
def __init__(
self,
driver_type: WebDriverType
):
self.driver_type = driver_type
def __str__(
self
) -> str:
match self.driver_type:
case WebDriverType.CHROME:
return "chromedriver"
case WebDriverType.FIREFOX:
return "geckodriver"
case WebDriverType.EDGE:
return "msedgedriver"
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverExecName:
"""
Web driver executable file name
"""
def __init__(
self,
driver_type: WebDriverType,
arch: WebDriverArch
):
self.driver_type = driver_type
self.arch = arch
def __str__(
self
) -> str:
is_win = True if self.arch is WebDriverArch.Chrome.WINX86_32 or\
self.arch is WebDriverArch.Chrome.WINX86_64 or\
self.arch is WebDriverArch.Firefox.WINX86_32 or\
self.arch is WebDriverArch.Firefox.WINX86_64 or\
self.arch is WebDriverArch.Edge.WINX86_32 or\
self.arch is WebDriverArch.Edge.WINX86_64 else False
match self.driver_type:
case WebDriverType.CHROME:
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
case WebDriverType.FIREFOX:
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
case WebDriverType.EDGE:
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverFileName:
"""\
Web driver compressed file name
"""
def __init__(
self,
version: str,
driver_type: WebDriverType,
arch: WebDriverArch
):
self.version = version
self.driver_type = driver_type
self.arch = arch
def __str__(
self
) -> str:
match self.driver_type:
case WebDriverType.CHROME:
return f"{WebDriverName(self.driver_type)}-{self.arch.value}.zip"
case WebDriverType.FIREFOX:
if self.arch is WebDriverArch.Firefox.WINX86_32 or\
self.arch is WebDriverArch.Firefox.WINX86_64:
suffix = "zip"
else:
suffix = "tar.gz"
return f"{WebDriverName(self.driver_type)}-v{self.version}-{self.arch.value}.{suffix}"
case WebDriverType.EDGE:
return f"edgedriver_{self.arch.value}.zip" # Edge web driver file name is different
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverURL:
"""
Web driver download URL
"""
def __init__(
self,
version: str,
driver_type: WebDriverType,
arch: WebDriverArch
):
self.version = version
self.driver_type = driver_type
self.arch = arch
self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
def __str__(
self
) -> str:
match self.driver_type:
case WebDriverType.CHROME:
return f"https://storage.googleapis.com/chrome-for-testing-public/"\
f"{self.version}/"\
f"{self.arch.value}/"\
f"{self.file_name}"
case WebDriverType.FIREFOX:
return f"https://github.com/mozilla/geckodriver/releases/download/"\
f"v{self.version}/"\
f"{self.file_name}"
case WebDriverType.EDGE:
return f"https://msedgedriver.microsoft.com/"\
f"{self.version}/"\
f"{self.file_name}"
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverDownloader:
"""
Base class for WebDriver downloaders
Args:
driver_type (WebDriverType): Web driver type
version (str): WebDriver version
arch (WebDriverArch): WebDriver architecture
download_dir (str): Download directory
"""
def __init__(
self,
driver_type: WebDriverType,
driver_version: str,
driver_arch: WebDriverArch,
download_dir: str
):
self.driver_type = driver_type
self.arch = driver_arch
self.version = driver_version
self.download_url = str(WebDriverURL(self.version, self.driver_type, self.arch))
self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
def download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
try:
# downlaod file : 0% - 98%
if not self._download(progress_callback, cancel_event=cancel_event):
return None
# verify file : 98% - 99%
if not self._verify(progress_callback):
progress_callback(0, 100, 0.0, "验证失败")
return None
# extract file : 99% - 100%
driver_path = self._extract(progress_callback)
if not driver_path:
progress_callback(0, 100, 0.0, "解压失败")
return None
return driver_path
except Exception as e:
raise e
def _download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
max_retries: int = 3,
cancel_event: Optional[threading.Event] = None
) -> bool:
CHUNK_SIZE = 8192*8 # 64KB chunk
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Encoding': 'gzip, deflate'
}
for attempt in range(max_retries):
try:
if cancel_event and cancel_event.is_set():
return False
# resume download if file exists
if self.download_path.exists():
downloaded_size = self.download_path.stat().st_size
headers_ = headers.copy()
headers_['Range'] = f"bytes={downloaded_size}-"
mode = 'ab'
else:
downloaded_size = 0
headers_ = headers
mode = 'wb'
# get response
response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=10)
if response.status_code not in [200, 206]:
if self.download_path.exists():
self.download_path.unlink()
downloaded_size = 0
mode = 'wb'
response = requests.get(str(self.download_url), headers=headers, stream=True)
response.raise_for_status()
# get total size
total_size = int(response.headers.get('Content-Length', 0))
if response.status_code == 206: # Partial Content - server supports Range
total_size += downloaded_size
last_callback_time = time.time()
last_callback_size = downloaded_size
callback_interval = 0.1
with open(self.download_path, mode) as f:
for chunk in response.iter_content(CHUNK_SIZE):
current_time = time.time()
if cancel_event and cancel_event.is_set():
response.close()
return False
if not chunk:
continue
f.write(chunk)
downloaded_size += len(chunk)
if not progress_callback or total_size <= 0:
continue
current_progress = (downloaded_size/total_size)*98.0
if current_time - last_callback_time >= callback_interval or current_progress >= 98.0:
elapsed = current_time - last_callback_time
if elapsed > 0:
speed = (downloaded_size - last_callback_size)/(elapsed*1024.0)
else:
speed = 0.0
progress_callback(current_progress, 100, speed, "下载中...")
last_callback_time = current_time
last_callback_size = downloaded_size
if total_size > 0 and self.download_path.stat().st_size < total_size:
raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节")
return True
except Exception as e:
if cancel_event and cancel_event.is_set():
return False
if attempt < max_retries - 1:
progress_callback(0, 100, 0.0, f"{attempt+1} 次重试...")
time.sleep(1)
continue
raise e
def _verify(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
) -> bool:
progress_callback(98, 100, 0.0, "验证完成")
return True
def _extract(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
) -> Optional[Path]:
try:
progress_callback(98, 100, 0.0, "解压中...")
file_path_str = str(self.download_path)
if file_path_str.endswith('.tar.gz'):
with tarfile.open(self.download_path, 'r:gz') as tar_ref:
tar_ref.extractall(self.download_dir)
else:
with zipfile.ZipFile(self.download_path, 'r') as zip_ref:
zip_ref.extractall(self.download_dir)
driver_file = None
for root, _, files in os.walk(self.download_dir):
for file in files:
expected_name = str(WebDriverExecName(self.driver_type, self.arch))
if file == str(expected_name):
src_path = Path(root, file)
dst_path = self.download_dir/file
src_path.rename(dst_path)
driver_file = dst_path
break
if driver_file:
break
if not driver_file:
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
progress_callback(100, 100, 0.0, "解压完成")
self.download_path.unlink()
self._cleanup(driver_file)
return driver_file
except Exception:
return None
def _cleanup(
self,
driver_file: Path
) -> None:
for item in self.download_dir.iterdir():
if item != driver_file:
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
class ChromeDriverDownloader(WebDriverDownloader):
"""
Chrome web driver downloader
Only support version higher than 114
"""
def __init__(
self,
version: str,
arch: WebDriverArch,
download_dir: str
):
super().__init__(WebDriverType.CHROME, version, arch, download_dir)
class FirefoxDriverDownloader(WebDriverDownloader):
"""
Firefox web driver downloader
This class do not resolve version mapping,
only support driver version higher than 0.17.0
"""
def __init__(
self,
version: str,
arch: WebDriverArch,
download_dir: str
):
super().__init__(WebDriverType.FIREFOX, version, arch, download_dir)
class EdgeDriverDownloader(WebDriverDownloader):
"""
Edge web driver downloader
"""
def __init__(
self,
version: str,
arch: WebDriverArch,
download_dir: str
):
super().__init__(WebDriverType.EDGE, version, arch, download_dir)
+471
View File
@@ -0,0 +1,471 @@
# -*- 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
import packaging.version as ver
from enum import Enum
from pathlib import Path
from typing import Optional, Callable
from managers.driver.WebBrowserDetector import (
WebBrowserType, WebBrowserArch, WebBrowserInfo, WebBrowserDetector
)
from managers.driver.WebDriverDownloader import (
WebDriverArch, WebDriverType,
ChromeDriverDownloader, FirefoxDriverDownloader, EdgeDriverDownloader
)
class WebDriverStatus(Enum):
"""
Web driver status.
"""
NOT_INSTALLED = 0
INSTALLED = 1
DOWNLOADING = 2
ERROR = 3
class WebDriverInfo:
"""
Web driver information.
Attributes:
driver_type (WebDriverType): Web driver type
driver_arch (WebDriverArch): Web driver architecture
driver_version (str): Web driver version
browser_version (str): Web browser version
driver_path (Optional[Path]): Web driver executable file path
driver_status (DriverStatus): Web driver status
"""
def __init__(
self
):
self.driver_type = None
self.driver_arch = None
self.driver_version = ""
self.browser_version = ""
self.driver_path: Optional[Path] = None
self.driver_status = WebDriverStatus.NOT_INSTALLED
class WebDriverManager:
"""
Web Driver Manager Singleton Class
Args:
driver_dir (str): The directory to store web drivers.
"""
def __init__(
self,
driver_dir: str
):
self.__driver_dir = os.path.abspath(driver_dir)
self.__browser_detector = WebBrowserDetector()
self.__driver_infos: list[WebDriverInfo] = []
self.__initialized = False
self.__lock = threading.Lock()
self.initialize()
def initialize(
self
):
if self.__initialized:
return
os.makedirs(self.__driver_dir, exist_ok=True)
self._detectBrowsers()
self._checkDriverStatus()
self.__initialized = True
def _detectBrowsers(
self
):
with self.__lock:
browser_infos = self.__browser_detector.detect()
self.__driver_infos = [
self._getDriverInfo(info)
for info in browser_infos
]
def _checkDriverStatus(
self
):
with self.__lock:
for driver_info in self.__driver_infos:
driver_path = self._getDriverPath(driver_info)
if driver_path and driver_path.exists() and driver_path.is_file():
driver_info.driver_path = driver_path
driver_info.driver_status = WebDriverStatus.INSTALLED
def _mapWebBrowserTypeToDriver(
self,
browser_type: WebBrowserType
) -> WebDriverType:
if browser_type == WebBrowserType.CHROME:
return WebDriverType.CHROME
elif browser_type == WebBrowserType.FIREFOX:
return WebDriverType.FIREFOX
elif browser_type == WebBrowserType.EDGE:
return WebDriverType.EDGE
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapWebBrowserArchToDriver(
self,
browser_type: WebBrowserType,
browser_arch: WebBrowserArch
) -> WebDriverArch:
if browser_type == WebBrowserType.CHROME:
if browser_arch == WebBrowserArch.WINX86_32:
return WebDriverArch.Chrome.WINX86_32
elif browser_arch == WebBrowserArch.WINX86_64:
return WebDriverArch.Chrome.WINX86_64
elif browser_arch == WebBrowserArch.WINARM:
raise ValueError("Chrome 不支持 Windows ARM 架构")
elif browser_arch == WebBrowserArch.LINUXX86_32:
raise ValueError("Chrome 不支持 Linux x86_32 架构")
elif browser_arch == WebBrowserArch.LINUXX86_64:
return WebDriverArch.Chrome.LINUXX86_64
elif browser_arch == WebBrowserArch.LINUXARM:
raise ValueError("Chrome 不支持 Linux ARM 架构")
elif browser_arch == WebBrowserArch.MACX86_64:
return WebDriverArch.Chrome.MACX86_64
elif browser_arch == WebBrowserArch.MACARM:
return WebDriverArch.Chrome.MACARM
else:
raise ValueError(f"不支持的 Chrome 浏览器架构 : {browser_arch}")
elif browser_type == WebBrowserType.FIREFOX:
if browser_arch == WebBrowserArch.WINX86_32:
return WebDriverArch.Firefox.WINX86_32
elif browser_arch == WebBrowserArch.WINX86_64:
return WebDriverArch.Firefox.WINX86_64
elif browser_arch == WebBrowserArch.WINARM:
return WebDriverArch.Firefox.WINARM
elif browser_arch == WebBrowserArch.LINUXX86_32:
return WebDriverArch.Firefox.LINUXX86_32
elif browser_arch == WebBrowserArch.LINUXX86_64:
return WebDriverArch.Firefox.LINUXX86_64
elif browser_arch == WebBrowserArch.LINUXARM:
return WebDriverArch.Firefox.LINUXARM
elif browser_arch == WebBrowserArch.MACX86_64:
return WebDriverArch.Firefox.MACX86_64
elif browser_arch == WebBrowserArch.MACARM:
return WebDriverArch.Firefox.MACARM
else:
raise ValueError(f"不支持的 Firefox 浏览器架构 : {browser_arch}")
elif browser_type == WebBrowserType.EDGE:
if browser_arch == WebBrowserArch.WINX86_32:
return WebDriverArch.Edge.WINX86_32
elif browser_arch == WebBrowserArch.WINX86_64:
return WebDriverArch.Edge.WINX86_64
elif browser_arch == WebBrowserArch.WINARM:
return WebDriverArch.Edge.WINARM
elif browser_arch == WebBrowserArch.LINUXX86_32:
raise ValueError("Edge 不支持 Linux x86_32 架构")
elif browser_arch == WebBrowserArch.LINUXX86_64:
return WebDriverArch.Edge.LINUXX86_64
elif browser_arch == WebBrowserArch.LINUXARM:
raise ValueError("Edge 不支持 Linux ARM 架构")
elif browser_arch == WebBrowserArch.MACX86_64:
return WebDriverArch.Edge.MACX86_64
elif browser_arch == WebBrowserArch.MACARM:
return WebDriverArch.Edge.MACARM
else:
raise ValueError(f"不支持的 Edge 浏览器架构 : {browser_arch}")
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapFirefoxDriverVersion(
self,
version: str
) -> str:
version_mapping = [
(ver.Version("128.0"), ver.Version("999.0"), "0.36.0"),
(ver.Version("115.0"), ver.Version("127.0"), "0.35.0"),
(ver.Version("91.0"), ver.Version("114.0"), "0.34.0"),
(ver.Version("91.0"), ver.Version("120.0"), "0.33.0"),
(ver.Version("91.0"), ver.Version("120.0"), "0.32.0"),
(ver.Version("91.0"), ver.Version("120.0"), "0.31.0"),
(ver.Version("78.0"), ver.Version("90.0"), "0.30.0"),
(ver.Version("60.0"), ver.Version("90.0"), "0.29.0"),
(ver.Version("60.0"), ver.Version("90.0"), "0.28.0"),
(ver.Version("60.0"), ver.Version("90.0"), "0.27.0"),
(ver.Version("57.0"), ver.Version("90.0"), "0.26.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.25.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.24.0"),
(ver.Version("57.0"), ver.Version("79.0"), "0.23.0"),
(ver.Version("57.0"), ver.Version("79.0"), "0.22.0"),
(ver.Version("57.0"), ver.Version("79.0"), "0.21.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.20.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.19.0"),
(ver.Version("53.0"), ver.Version("62.0"), "0.18.0"),
(ver.Version("52.0"), ver.Version("62.0"), "0.17.0"),
]
try:
firefox_version = ver.Version(version)
for min_ver, max_ver, gecko_ver in version_mapping:
if min_ver <= firefox_version <= max_ver:
return gecko_ver
raise ValueError(
f"不支持的 Firefox 版本 : {version}"
f"Firefox 版本 52 及以上受支持"
)
except Exception as e:
raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
def _getDriverInfo(
self,
browser_info: WebBrowserInfo
) -> WebDriverInfo:
driver_info = WebDriverInfo()
driver_info.driver_type = self._mapWebBrowserTypeToDriver(browser_info.browser_type)
driver_info.driver_arch = self._mapWebBrowserArchToDriver(browser_info.browser_type, browser_info.browser_arch)
if browser_info.browser_type == WebBrowserType.FIREFOX:
driver_info.driver_version = self._mapFirefoxDriverVersion(browser_info.browser_version)
else:
driver_info.driver_version = browser_info.browser_version
driver_info.browser_version = browser_info.browser_version
return driver_info
def _getDriverPath(
self,
driver_info: WebDriverInfo
) -> Optional[Path]:
driver_type = driver_info.driver_type
driver_arch = driver_info.driver_arch
driver_version = driver_info.driver_version
if driver_type == WebDriverType.CHROME:
driver_name = "chromedriver"
elif driver_type == WebDriverType.FIREFOX:
driver_name = "geckodriver"
elif driver_type == WebDriverType.EDGE:
driver_name = "msedgedriver"
else:
return None
is_win = driver_arch in [
WebDriverArch.Chrome.WINX86_32,
WebDriverArch.Chrome.WINX86_64,
WebDriverArch.Firefox.WINX86_32,
WebDriverArch.Firefox.WINX86_64,
WebDriverArch.Edge.WINX86_32,
WebDriverArch.Edge.WINX86_64,
]
exe_name = f"{driver_name}.exe" if is_win else driver_name
driver_dir = Path(self.__driver_dir)/driver_type.value/driver_version/driver_arch.value
driver_path = driver_dir/exe_name
return driver_path
def refresh(
self
):
self._detectBrowsers()
self._checkDriverStatus()
def getDriverInfos(
self
) -> list[WebDriverInfo]:
with self.__lock:
return self.__driver_infos.copy()
def getDriverInfo(
self,
driver_type: WebDriverType
) -> list[WebDriverInfo]:
with self.__lock:
return [
info
for info in self.__driver_infos
if info.driver_type == driver_type
]
def getDriverPath(
self,
driver_info: WebDriverInfo
) -> Optional[Path]:
if driver_info and driver_info.driver_status == WebDriverStatus.INSTALLED:
return driver_info.driver_path
return None
def installDriver(
self,
driver_info: WebDriverInfo,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
with self.__lock:
if not driver_info:
if progress_callback:
progress_callback(0, 0, 0, "未找到浏览器信息")
else:
raise ValueError("未找到浏览器信息")
if driver_info and driver_info.driver_status == WebDriverStatus.DOWNLOADING:
if progress_callback:
progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动正在下载中")
else:
raise ValueError(f"{driver_info.driver_type} 驱动正在下载中")
try:
if not driver_info:
raise ValueError("未找到浏览器信息")
driver_arch = driver_info.driver_arch
driver_type = driver_info.driver_type
driver_version = driver_info.driver_version
downloader = None
if driver_type == WebDriverType.CHROME:
downloader = ChromeDriverDownloader(
version=driver_version,
arch=driver_arch,
download_dir=self.__driver_dir
)
elif driver_type == WebDriverType.FIREFOX:
downloader = FirefoxDriverDownloader(
version=driver_version,
arch=driver_arch,
download_dir=self.__driver_dir
)
elif driver_type == WebDriverType.EDGE:
downloader = EdgeDriverDownloader(
version=driver_version,
arch=driver_arch,
download_dir=self.__driver_dir
)
if downloader is None:
if progress_callback:
progress_callback(0, 0, 0, f"不支持的 Web Driver 类型")
else:
raise ValueError(f"不支持的 Web Driver 类型")
with self.__lock:
driver_info.driver_status = WebDriverStatus.DOWNLOADING
driver_path = downloader.download(progress_callback=progress_callback, cancel_event=cancel_event)
with self.__lock:
if driver_path:
driver_info.driver_path = driver_path
driver_info.driver_version = driver_version
driver_info.driver_status = WebDriverStatus.INSTALLED
else:
driver_info.driver_status = WebDriverStatus.ERROR
return driver_path
except Exception as e:
with self.__lock:
driver_info.driver_status = WebDriverStatus.ERROR
raise e
def cancelDriverDownload(
self,
driver_info: WebDriverInfo
) -> bool:
import shutil
try:
driver_path = self._getDriverPath(driver_info)
if driver_path:
download_dir = driver_path.parent
if download_dir.exists():
shutil.rmtree(download_dir, ignore_errors=True)
with self.__lock:
driver_info.driver_path = None
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
return True
except Exception:
return False
def uninstallDriver(
self,
driver_info: WebDriverInfo,
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
) -> bool:
with self.__lock:
if not driver_info:
if progress_callback:
progress_callback(0, 0, 0, "未找到浏览器信息")
else:
raise ValueError("未找到浏览器信息")
if driver_info.driver_status != WebDriverStatus.INSTALLED:
if progress_callback:
progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动未安装")
else:
raise ValueError(f"{driver_info.driver_type} 驱动未安装")
try:
driver_path = driver_info.driver_path
driver_path.unlink()
with self.__lock:
driver_info.driver_path = None
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
return True
except Exception:
with self.__lock:
driver_info.driver_status = WebDriverStatus.ERROR
raise
def driverDir(
self
) -> str:
return self.__driver_dir
# WebDriverManager singleton instance.
_webdriver_manager_instance = None
# Singleton instance lock.
_instance_lock = threading.Lock()
def instance(
driver_dir: str = ""
) -> WebDriverManager:
global _webdriver_manager_instance
with _instance_lock:
if _webdriver_manager_instance is None:
if not driver_dir:
raise ValueError("WebDriverManager 需要驱动目录参数")
_webdriver_manager_instance = WebDriverManager(driver_dir)
else:
if driver_dir and _webdriver_manager_instance.driverDir() != os.path.abspath(driver_dir):
raise ValueError("WebDriverManager 的实例已初始化, 不能使用不同的驱动目录")
return _webdriver_manager_instance
+8
View File
@@ -0,0 +1,8 @@
"""
Driver managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- WebBrowserDetector: Web browser detector class.
- WebDriverDownloader: Web driver downloader class.
- WebDriverManager: Web driver manager class.
"""
@@ -55,12 +55,17 @@ class CallerInfoFormatter(logging.Formatter):
if depth < len(record.stack_list): if depth < len(record.stack_list):
frame = record.stack_list[-depth-1] frame = record.stack_list[-depth-1]
record.filename = os.path.basename(frame.filename) record.filename = os.path.basename(frame.filename)
record.lineno = frame.lineno record.lineno = int(frame.lineno)
record.funcName = frame.name record.funcName = frame.name
record.name = record.name[-15:].ljust(15) record.name = record.name[-15:].ljust(15)
record.levelname = record.levelname.ljust(8) record.levelname = record.levelname.ljust(8)
record.filename = record.filename[-20:].ljust(20) record.filename = record.filename[-20:].ljust(20)
record.lineno = f"{record.lineno:04d}" # Ensure lineno is always integer before formatting
try:
lineno_int = int(record.lineno)
except (ValueError, TypeError):
lineno_int = 0
record.lineno = f"{lineno_int:04d}"
return super().format(record) return super().format(record)
@@ -174,11 +179,11 @@ def instance(
with _instance_lock: with _instance_lock:
if _log_manager_instance is None: if _log_manager_instance is None:
if not log_dir: if not log_dir:
raise ValueError("LogManager initialization requires log_dir parameter") raise ValueError("LogManager 需要日志目录参数")
_log_manager_instance = LogManager(log_dir) _log_manager_instance = LogManager(log_dir)
else: else:
if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir): if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir):
raise ValueError("LogManager instance already initialized with a different log directory") raise ValueError("LogManager 的实例已初始化, 不能使用不同的日志目录")
return _log_manager_instance return _log_manager_instance
@@ -187,5 +192,5 @@ def getLogger(
) -> logging.Logger: ) -> logging.Logger:
if _log_manager_instance is None: if _log_manager_instance is None:
raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first") raise RuntimeError("LogManager 未初始化, 请先调用 LogManager.instance(log_dir) 初始化")
return _log_manager_instance.getLogger(name) return _log_manager_instance.getLogger(name)
+6
View File
@@ -0,0 +1,6 @@
"""
Log managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- LogManager: Log manager for logging.
"""
+4 -4
View File
@@ -243,7 +243,7 @@ class AutoLib(MsgBase):
else: else:
result = 1 result = 1
else: else:
self._showTrace(f"用户 {username} 无法预约已跳过") self._showTrace(f"用户 {username} 无法预约, 已跳过")
result = 2 result = 2
# checkin # checkin
@@ -255,7 +255,7 @@ class AutoLib(MsgBase):
else: else:
result = 1 result = 1
else: else:
self._showTrace(f"用户 {username} 无法签到已跳过") self._showTrace(f"用户 {username} 无法签到, 已跳过")
result = 2 result = 2
if last_result == 0: # partly success if last_result == 0: # partly success
result = 0 result = 0
@@ -277,7 +277,7 @@ class AutoLib(MsgBase):
else: else:
result = 1 result = 1
else: else:
self._showTrace(f"用户 {username} 无法续约已跳过") self._showTrace(f"用户 {username} 无法续约, 已跳过")
result = 2 result = 2
if last_result == 0: # partly success if last_result == 0: # partly success
result = 0 result = 0
@@ -322,7 +322,7 @@ class AutoLib(MsgBase):
) )
if r == -1: if r == -1:
self._showTrace( self._showTrace(
f"用户 {user["username"]} 处理过程中页面发生异常无法继续操作, 任务已终止 !", f"用户 {user["username"]} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !",
self.TraceLevel.WARNING self.TraceLevel.WARNING
) )
break break
+1 -1
View File
@@ -370,7 +370,7 @@ class LibChecker(LibOperator):
else: else:
self._showTrace(f"\n"\ self._showTrace(f"\n"\
f" 续约失败 !\n"\ f" 续约失败 !\n"\
f" 续约后结束时间为 {act_record["time"]["end"]}与预期结束时间 {record["time"]["end"]} 不符 !" f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
) )
return False return False
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果") self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
+2 -2
View File
@@ -130,10 +130,10 @@ class LibRenew(LibTimeSelector):
if target_renew_mins > LIBRARY_CLOSE_TIME: if target_renew_mins > LIBRARY_CLOSE_TIME:
actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time) actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time)
if actual_renew_duration <= 0: if actual_renew_duration <= 0:
self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间无法续约 !", self.TraceLevel.ERROR) self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR)
return False return False
self._showTrace( self._showTrace(
f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)}" f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)},"
f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟" f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟"
) )
return True return True
+1 -1
View File
@@ -2,7 +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:
- ConfigManager: Configuration manager class for the AutoLibrary project. - TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project. - JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project. - JSONWriter: JSON writer class for the AutoLibrary project.
""" """