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

Compare commits

...

5 Commits

41 changed files with 351 additions and 403 deletions
+12 -12
View File
@@ -4,10 +4,6 @@ name: Build Test
# It is triggered when a pull request is opened, synchronized, or reopened against the main branch.
on:
push:
branches:
- main
pull_request:
branches:
- main
@@ -15,7 +11,6 @@ on:
- opened
- synchronize
- reopened
# Allow manual trigger for testing
workflow_dispatch:
#
@@ -49,6 +44,8 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirement.txt
- name: Install dependencies
run: |
@@ -125,7 +122,7 @@ jobs:
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
@@ -153,7 +150,7 @@ jobs:
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
")"
""
"coll = COLLECT("
@@ -169,9 +166,11 @@ jobs:
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Write-Host "`n========================================"
Write-Host "Generated Main.spec"
Write-Host "========================================"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
Write-Host "========================================`n"
shell: pwsh
- name: Build with PyInstaller
@@ -186,7 +185,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
@@ -212,10 +211,11 @@ jobs:
run: |
Write-Host "## Build Test Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Pull request build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Pull Request #${{ github.event.pull_request.number }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Branch: ${{ github.event.pull_request.head.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Pull Request #${{ github.event.pull_request.number || 'N/A' }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Branch: ${{ github.event.pull_request.head.ref || github.ref }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Event: ${{ github.event_name }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
shell: pwsh
+14 -9
View File
@@ -76,15 +76,17 @@ jobs:
run: |
$versionInfoFile = "src/gui/ALVersionInfo.py"
Write-Host "Verifying $versionInfoFile content:"
Write-Host "=================================="
Write-Host "========================================"
Get-Content $versionInfoFile | Write-Host
Write-Host "=================================="
Write-Host "========================================"
shell: pwsh
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: requirement.txt
- name: Install dependencies
run: |
@@ -161,7 +163,7 @@ jobs:
" binaries=[],"
" datas=["
" ('models\\common.onnx', 'ddddocr'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_32x32.ico', 'gui\\resources\\icons'),"
" ('src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico', 'gui\\resources\\icons'),"
" ],"
" hiddenimports=[],"
" hookspath=[],"
@@ -189,7 +191,7 @@ jobs:
" target_arch=None,"
" codesign_identity=None,"
" entitlements_file=None,"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_32x32.ico'],"
" icon=['src\\gui\\resources\\icons\\AutoLibrary_Logo_64.ico'],"
")"
""
"coll = COLLECT("
@@ -205,9 +207,11 @@ jobs:
$specLines | Out-File -FilePath "Main.spec" -Encoding UTF8
Write-Host "✓ Main.spec (non-single file) generated successfully"
Write-Host "`nGenerated Main.spec ============"
Write-Host "`n========================================"
Write-Host "Generated Main.spec"
Write-Host "========================================"
Get-Content "Main.spec" | Write-Host
Write-Host "==================================`n"
Write-Host "========================================`n"
shell: pwsh
- name: Build with PyInstaller
@@ -222,7 +226,7 @@ jobs:
$distDir = "dist/AutoLibrary-$version"
$zipName = "AutoLibrary.$tagName-windows-x86_64.zip"
echo "ZIP_NAME=$zipName" >> $env:GITHUB_OUTPUT
"ZIP_NAME=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Looking for distribution directory: $distDir"
if (Test-Path $distDir) {
@@ -242,13 +246,14 @@ jobs:
name: AutoLibrary.${{ steps.get_version.outputs.TAG_NAME }}-windows-x86_64
path: |
${{ steps.zip_release.outputs.ZIP_NAME }}
retention-days: ${{ github.event_name != 'workflow_call' && 7 || 90 }}
retention-days: ${{ inputs.is_test == 'true' && 7 || 90 }}
- name: Upload build summary
if: ${{ github.event_name != 'workflow_call' }}
if: ${{ inputs.is_test == 'true' }}
run: |
Write-Host "## Build Summary" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8
Write-Host "" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "========================================" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "✓ Build test completed successfully!" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Version: ${{ steps.get_version.outputs.VERSION }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
Write-Host "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
+9 -1
View File
@@ -83,7 +83,9 @@ jobs:
echo "✓ File replaced: $FILE_PATH"
echo ""
echo "Updated file content ==================="
echo "========================================"
echo "Updated file content"
echo "========================================"
cat "$FILE_PATH"
echo "========================================"
@@ -151,3 +153,9 @@ jobs:
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✓ New commit SHA: $COMMIT_SHA"
echo "## Commit Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "- Version: ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- Tag: ${{ inputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
echo "- Commit SHA: $COMMIT_SHA" >> $GITHUB_STEP_SUMMARY
+14 -7
View File
@@ -47,7 +47,7 @@ on:
jobs:
#
# Start :
# virtual job that indacates the start of the release process
# virtual job that indicates the start of the release process
#
start:
@@ -158,7 +158,7 @@ jobs:
needs:
- update-version
- commit-release
if: always() && needs.update-version.result == 'success' && needs.commit-release.result == 'success'
if: always() && needs.update-version.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped')
uses: ./.github/workflows/build.yml
permissions:
contents: write
@@ -205,7 +205,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# End :
# virtual job that indacates the end of the release process
# virtual job that indicates the end of the release process
#
end:
@@ -227,7 +227,7 @@ jobs:
- release
- extract-version
- commit-release
if: ${{ needs.release.result == 'success' && needs.commit-release.result == 'success' }}
if: ${{ needs.release.result == 'success' && (needs.commit-release.result == 'success' || needs.commit-release.result == 'skipped') }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -267,9 +267,13 @@ jobs:
git checkout ${MAIN_BRANCH}
# Show branch status before merge
echo "=== Branch status before merge ==="
echo "========================================"
echo "Branch status before merge"
echo "========================================"
git log --oneline --graph --all -5
echo "=== Diff between ${MAIN_BRANCH} and origin/${BRANCH_NAME} ==="
echo "========================================"
echo "Diff: ${MAIN_BRANCH} vs origin/${BRANCH_NAME}"
echo "========================================"
git diff ${MAIN_BRANCH} origin/${BRANCH_NAME} --stat || echo "No differences found"
# Force create a merge commit even if there are no changes
@@ -279,7 +283,9 @@ jobs:
-m "chore(release): merge ${BRANCH_NAME} to ${MAIN_BRANCH} [auto release commit]"
# Show merge result
echo "=== Merge result ==="
echo "========================================"
echo "Merge result"
echo "========================================"
git log --oneline --graph -3
# Push to main
@@ -310,6 +316,7 @@ jobs:
echo "## Release Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "✓ Release completed successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Actions Performed:" >> $GITHUB_STEP_SUMMARY
+13 -3
View File
@@ -128,10 +128,15 @@ jobs:
echo "Build version file location: $VER_INFO_BUILDFILE"
echo "Commit version file location: $VER_INFO_COMMITFILE"
echo ""
echo "Build version ALVersionInfo.py content ="
echo "========================================"
echo "Build version ALVersionInfo.py"
echo "========================================"
cat "$VER_INFO_BUILDFILE"
echo "========================================"
echo ""
echo "Commit version ALVersionInfo.py content "
echo "========================================"
echo "Commit version ALVersionInfo.py"
echo "========================================"
cat "$VER_INFO_COMMITFILE"
echo "========================================"
@@ -140,11 +145,16 @@ jobs:
run: |
if git diff --quiet src/gui/ALVersionInfo.py; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "! No changes detected in ALVersionInfo.py"
echo " No changes detected in ALVersionInfo.py"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✓ ALVersionInfo.py has been modified"
fi
echo "## Update Version Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "========================================" >> $GITHUB_STEP_SUMMARY
echo "- Version: ${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
echo "- Tag: ${{ steps.get_version.outputs.TAG_NAME }}" >> $GITHUB_STEP_SUMMARY
- name: Upload modified ALVersionInfo.py ready for build
if: steps.check_changes.outputs.has_changes == 'true'
+1 -10
View File
@@ -7,16 +7,7 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from autoscript.ASEngine import ASEngine
__all__ = [
"ASEngine",
"createEngine",
"createMockTargetData",
"createAllVariablesTable",
"createTargetVarDefs",
]
from .ASEngine import ASEngine
_TARGET_VAR_DEFS = [
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Base module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- MsgBase: Base class for messages.
"""
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.
"""
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Boot module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- AppInitializer: Application initializer class.
"""
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.
"""
+9 -2
View File
@@ -1,3 +1,10 @@
from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
__all__ = ["ALAutoScriptOrchDialog"]
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from ._dialog import ALAutoScriptOrchDialog
+7 -17
View File
@@ -1,19 +1,9 @@
# -*- coding: utf-8 -*-
"""
GUI module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
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.
- ALAutoScriptOrchDialog: AutoScript orchestration 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.
"""
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.
"""
+8 -2
View File
@@ -1,3 +1,9 @@
# -*- 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.
"""
GUI resources module for the AutoLibrary project.
"""
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

+6 -8
View File
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""
Interfaces module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Defines abstract interfaces (Protocols) and shared type definitions
used across layers to decouple consumers from concrete implementations.
Key components:
- ConfigProvider: Abstract interface for configuration access.
- ConfigType: Enumeration of configuration file types.
- ConfigKey: Type-safe hierarchical key constants for config lookups.
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.
"""
+7 -6
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""
Managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
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.
"""
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.
"""
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Config managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
"""
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.
"""
+7 -6
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""
Driver managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- WebBrowserDetector: Web browser detector class.
- WebDriverDownloader: Web driver downloader class.
- WebDriverManager: Web driver manager class.
"""
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.
"""
+7 -4
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""
Log managers module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- LogManager: Log manager for logging.
"""
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.
"""
-3
View File
@@ -149,9 +149,6 @@ class AutoLib(MsgBase):
except WebDriverException as e:
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False
except Exception as e:
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
return False
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
return True
+15 -23
View File
@@ -85,9 +85,7 @@ class LoginPage:
EC.presence_of_element_located(self.CAPTCHA_IMG)
)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
def fillCredentials(
@@ -104,17 +102,21 @@ class LoginPage:
el.clear()
el.send_keys(password)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def getCaptchaImageSrc(
self,
) -> str:
) -> str | None:
captcha_el = self._driver.find_element(*self.CAPTCHA_IMG)
return captcha_el.get_attribute("src")
# return 'None' if captcha image element is not found.
# But the 'get_attribute("src")' also return 'None' if there's no attribute with
# that name, which is not what we want.
try:
captcha_el = self._driver.find_element(*self.CAPTCHA_IMG)
return captcha_el.get_attribute("src")
except NoSuchElementException:
return None
def refreshCaptcha(
self,
@@ -123,10 +125,7 @@ class LoginPage:
try:
self._driver.find_element(*self.CAPTCHA_IMG).click()
return True
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def fillCaptcha(
@@ -139,9 +138,7 @@ class LoginPage:
el.clear()
el.send_keys(captcha_text)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def clickLogin(
@@ -151,10 +148,7 @@ class LoginPage:
try:
self._driver.find_element(*self.LOGIN_BUTTON).click()
return True
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def waitLoginSuccess(
@@ -172,9 +166,7 @@ class LoginPage:
EC.presence_of_element_located(self.SUCCESS_INDICATOR_CONTENT)
)
return True
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
def stopPageLoad(
+40 -29
View File
@@ -14,8 +14,10 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
WebDriverException,
)
from pages.ReserveView import ReserveView
@@ -38,6 +40,15 @@ class MainShell:
self._driver = driver
def _clickTab(
self,
locator: tuple,
) -> None:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(locator)
).click()
def gotoReserveView(
self,
) -> ReserveView:
@@ -65,9 +76,7 @@ class MainShell:
try:
self._driver.find_element(*self.TAB_LOGOUT).click()
return True
except NoSuchElementException:
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
def waitCheckinButton(
@@ -81,8 +90,6 @@ class MainShell:
return True
except TimeoutException:
return False
except Exception:
return False
def waitExtendButton(
self,
@@ -95,40 +102,50 @@ class MainShell:
return True
except TimeoutException:
return False
except Exception:
return False
def isCheckinButtonDisabled(
self,
) -> bool:
btn = self._driver.find_element(*self.BTN_CHECKIN)
return "disabled" in btn.get_attribute("class")
try:
btn = self._driver.find_element(*self.BTN_CHECKIN)
return "disabled" in btn.get_attribute("class")
except NoSuchElementException:
return True
def isExtendButtonDisabled(
self,
) -> bool:
btn = self._driver.find_element(*self.BTN_EXTEND)
return "disabled" in btn.get_attribute("class")
try:
btn = self._driver.find_element(*self.BTN_EXTEND)
return "disabled" in btn.get_attribute("class")
except NoSuchElementException:
return True
def clickCheckinButton(
self,
) -> None:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_CHECKIN)
)
btn.click()
try:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_CHECKIN)
)
btn.click()
except (TimeoutException, ElementNotInteractableException):
return
def clickExtendButton(
self,
) -> None:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_EXTEND)
)
btn.click()
try:
btn = WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(self.BTN_EXTEND)
)
btn.click()
except (TimeoutException, ElementNotInteractableException):
return
def enableCheckinButtonByJS(
self,
@@ -154,13 +171,7 @@ class MainShell:
self,
) -> None:
self._driver.refresh()
def _clickTab(
self,
locator: tuple,
) -> None:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(locator)
).click()
try:
self._driver.refresh()
except (TimeoutException, WebDriverException):
return
-6
View File
@@ -44,8 +44,6 @@ class RecordsView:
return self._driver.find_elements(*self.RECORDS_LIST)
except TimeoutException:
return None
except Exception:
return None
def getRecordTimeElement(
self,
@@ -71,8 +69,6 @@ class RecordsView:
)
except TimeoutException:
return False
except Exception:
return False
try:
more_btn = self._driver.find_element(*self.MORE_BTN)
if more_btn.is_displayed() and more_btn.is_enabled():
@@ -82,8 +78,6 @@ class RecordsView:
return False
except (NoSuchElementException, StaleElementReferenceException):
return False
except Exception:
return False
def getRecordText(
self,
+48 -55
View File
@@ -53,6 +53,50 @@ class ReserveView:
self._driver = driver
def _clickOptionByJS(
self,
trigger_id: str,
option_css: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_css}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self._driver.execute_script(script)
time.sleep(0.1)
return result
def _clickOption(
self,
trigger: tuple,
option: tuple,
) -> bool:
try:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(trigger)
).click()
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(option)
).click()
return True
except (TimeoutException, ElementNotInteractableException):
return False
def selectDate(
self,
date_str: str,
@@ -109,21 +153,15 @@ class ReserveView:
).click()
except (TimeoutException, ElementNotInteractableException):
return None
except Exception:
return None
try:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable((By.ID, self.ROOM_BTN_FMT.format(room=room)))
).click()
except (TimeoutException, ElementNotInteractableException):
return None
except Exception:
return None
try:
return SeatMapDialog(self._driver)
except (TimeoutException):
return None
except Exception:
except TimeoutException:
return None
def submitReserve(
@@ -137,57 +175,12 @@ class ReserveView:
return True
except (TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
def refresh(
self,
) -> None:
self._driver.refresh()
def _clickOptionByJS(
self,
trigger_id: str,
option_css: str,
) -> bool:
script = f"""
try {{
var trigger = document.getElementById('{trigger_id}');
if (trigger) {{
trigger.click();
var option = document.querySelector("{option_css}");
if (option) {{
option.click();
return true;
}}
return false;
}}
return false;
}} catch (e) {{
return false;
}}
"""
result = self._driver.execute_script(script)
time.sleep(0.1)
return result
def _clickOption(
self,
trigger: tuple,
option: tuple,
) -> bool:
try:
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(trigger)
).click()
WebDriverWait(self._driver, 2).until(
EC.element_to_be_clickable(option)
).click()
return True
except (TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
self._driver.refresh()
except TimeoutException:
return
+10 -23
View File
@@ -7,26 +7,13 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from pages.AutoLib import AutoLib
from pages.LoginPage import LoginPage
from pages.MainShell import MainShell
from pages.ReserveView import ReserveView
from pages.RecordsView import RecordsView
from pages.components.SeatMapDialog import SeatMapDialog
from pages.components.TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog
__all__ = [
"AutoLib",
"LoginPage",
"MainShell",
"ReserveView",
"RecordsView",
"SeatMapDialog",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]
from .AutoLib import AutoLib
from .LoginPage import LoginPage
from .MainShell import MainShell
from .ReserveView import ReserveView
from .RecordsView import RecordsView
from .components.SeatMapDialog import SeatMapDialog
from .components.TimeSelectDialog import TimeSelectDialog
from .components.ReserveResultDialog import ReserveResultDialog
from .components.CheckinResultDialog import CheckinResultDialog
from .components.RenewDialog import RenewDialog
+3 -8
View File
@@ -45,9 +45,8 @@ class CheckinResultDialog(Dialog):
self._waitPresence(self.RESULT_MSG)
el = self._find(*self.RESULT_MSG)
return el.text
except (TimeoutException, NoSuchElementException, StaleElementReferenceException):
return ""
except Exception:
except (TimeoutException, NoSuchElementException,
StaleElementReferenceException):
return ""
def getDetails(
@@ -59,8 +58,6 @@ class CheckinResultDialog(Dialog):
return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException):
return []
except Exception:
return []
def clickOk(
self,
@@ -69,7 +66,5 @@ class CheckinResultDialog(Dialog):
try:
self._waitClickable(self.OK_BTN).click()
return True
except (NoSuchElementException, TimeoutException, ElementNotInteractableException):
return False
except Exception:
except (TimeoutException, ElementNotInteractableException):
return False
+16 -13
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.common.by import By
@@ -50,9 +51,7 @@ class RenewDialog(Dialog):
self._waitVisible(self.ROOT)
self._waitPresence(self.MESSAGE_HEAD)
self._waitPresence(self.RESULT_MSG)
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
head_msg = self._find(*self.MESSAGE_HEAD).text.strip()
if "警告" in head_msg:
@@ -60,9 +59,7 @@ class RenewDialog(Dialog):
try:
self._waitAllPresence(self.TIME_OPTS)
self._waitPresence(self.OK_BTN)
except (NoSuchElementException, TimeoutException):
return False
except Exception:
except TimeoutException:
return False
return True
@@ -70,13 +67,19 @@ class RenewDialog(Dialog):
self,
) -> str:
return self._find(*self.MESSAGE_HEAD).text.strip()
try:
return self._find(*self.MESSAGE_HEAD).text.strip()
except (NoSuchElementException, StaleElementReferenceException):
return ""
def getResultMessage(
self,
) -> str:
return self._find(*self.RESULT_MSG).text.strip()
try:
return self._find(*self.RESULT_MSG).text.strip()
except (NoSuchElementException, StaleElementReferenceException):
return ""
def getTimeOptions(
self,
@@ -101,7 +104,10 @@ class RenewDialog(Dialog):
prefer_earlier,
)
if result.selected_index >= 0:
all_time_opts[result.selected_index].click()
try:
all_time_opts[result.selected_index].click()
except (ElementNotInteractableException, StaleElementReferenceException):
return TimeSelectionResult(free_times=result.free_times)
return result
def getOkButton(
@@ -117,8 +123,5 @@ class RenewDialog(Dialog):
try:
self._find(*self.OK_BTN).click()
return True
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException):
return False
except Exception:
except (NoSuchElementException, ElementNotInteractableException):
return False
@@ -45,8 +45,6 @@ class ReserveResultDialog(Dialog):
return self._find(*self._titleLocator()).text
except (NoSuchElementException, StaleElementReferenceException):
return ""
except Exception:
return ""
def isSuccess(
self,
@@ -77,5 +75,3 @@ class ReserveResultDialog(Dialog):
return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException):
return []
except Exception:
return []
+1 -7
View File
@@ -43,9 +43,7 @@ class SeatMapDialog(Dialog):
try:
self._waitAllPresence(self.SEAT_ITEMS)
except (NoSuchElementException, TimeoutException):
return None
except Exception:
except TimeoutException:
return None
try:
seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}")
@@ -58,8 +56,6 @@ class SeatMapDialog(Dialog):
except (NoSuchElementException, ValueError, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException):
pass
except Exception:
pass
try:
all_seats = self._findAll(*self.SEAT_ITEMS)
seat_id_upper = seat_id.lstrip('0').upper()
@@ -76,5 +72,3 @@ class SeatMapDialog(Dialog):
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException):
return None
except Exception:
return None
+7 -5
View File
@@ -13,7 +13,8 @@ import logging
from typing import TYPE_CHECKING, Callable, Optional
from selenium.common.exceptions import (
NoSuchElementException,
ElementNotInteractableException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.common.by import By
@@ -106,9 +107,7 @@ class TimeSelectDialog(Dialog):
self._waitAllPresence(
(By.CSS_SELECTOR, f"#{time_id} ul li a")
)
except (NoSuchElementException, TimeoutException):
return []
except Exception:
except TimeoutException:
return []
return self._findAll(
By.CSS_SELECTOR,
@@ -133,7 +132,10 @@ class TimeSelectDialog(Dialog):
prefer_earlier,
)
if result.selected_index >= 0:
all_time_opts[result.selected_index].click()
try:
all_time_opts[result.selected_index].click()
except (ElementNotInteractableException, StaleElementReferenceException):
return TimeSelectionResult(free_times=result.free_times)
return result
def selectTimeRange(
+5 -13
View File
@@ -7,16 +7,8 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from pages.components.SeatMapDialog import SeatMapDialog
from pages.components.TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog
__all__ = [
"SeatMapDialog",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]
from .SeatMapDialog import SeatMapDialog
from .TimeSelectDialog import TimeSelectDialog
from .ReserveResultDialog import ReserveResultDialog
from .CheckinResultDialog import CheckinResultDialog
from .RenewDialog import RenewDialog
+2 -4
View File
@@ -10,6 +10,7 @@ See the LICENSE file for details.
import queue
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
)
@@ -83,9 +84,6 @@ class CheckinFlow(MsgBase):
dialog.clickOk()
self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR)
return False
except (NoSuchElementException, TimeoutException):
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
return False
except Exception:
except (TimeoutException, NoSuchElementException, ElementNotInteractableException):
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
return False
+23 -31
View File
@@ -39,6 +39,28 @@ class RenewFlow(MsgBase):
self._driver: WebDriver = driver
self._shell: MainShell = shell
def _validateRenewTime(
self,
end_time: str,
target_renew_mins: int,
) -> bool:
if target_renew_mins > self.LIBRARY_CLOSE_MINS:
actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time)
if actual_renew_duration <= 0:
self._showTrace(
f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR
)
return False
self._showTrace(
f"续约时间已调整至闭馆时间 "
f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)},"
f"实际续约时长为 "
f"{actual_renew_duration // 60} 小时 "
f"{actual_renew_duration % 60} 分钟"
)
return True
def execute(
self,
username: str,
@@ -106,37 +128,7 @@ class RenewFlow(MsgBase):
self._showTrace(f"当前可供续约的时间有: {result.free_times}")
self._shell.refresh()
return False
except (NoSuchElementException, TimeoutException) as e:
except (NoSuchElementException, TimeoutException, ElementNotInteractableException) as e:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh()
return False
except (ElementNotInteractableException) as e:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh()
return False
except Exception as e:
self._showTrace(f"用户 {username} 续约失败 ! : {e}", self.TraceLevel.ERROR)
self._shell.refresh()
return False
def _validateRenewTime(
self,
end_time: str,
target_renew_mins: int,
) -> bool:
if target_renew_mins > self.LIBRARY_CLOSE_MINS:
actual_renew_duration = self.LIBRARY_CLOSE_MINS - timeStrToMins(end_time)
if actual_renew_duration <= 0:
self._showTrace(
f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR
)
return False
self._showTrace(
f"续约时间已调整至闭馆时间 "
f"{minsToTimeStr(self.LIBRARY_CLOSE_MINS)},"
f"实际续约时长为 "
f"{actual_renew_duration // 60} 小时 "
f"{actual_renew_duration % 60} 分钟"
)
return True
+1 -7
View File
@@ -12,7 +12,6 @@ from dataclasses import dataclass
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
)
from selenium.webdriver.remote.webdriver import WebDriver
@@ -70,10 +69,7 @@ class ReserveFlow(MsgBase):
try:
view = self._shell.gotoReserveView()
except (NoSuchElementException, TimeoutException) as e:
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
return False
except Exception as e:
except (TimeoutException, ElementNotInteractableException) as e:
self._showTrace(f"加载预约选座页面失败 ! : {e}", self.TraceLevel.ERROR)
return False
if not view.selectDate(ctx.date):
@@ -140,8 +136,6 @@ class ReserveFlow(MsgBase):
self._showTrace("预约结果加载失败 !", self.TraceLevel.ERROR)
except (TimeoutException, ElementNotInteractableException):
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
except Exception:
self._showTrace("预约提交失败 !", self.TraceLevel.ERROR)
if not submit_reserve and have_hover_on_page:
view.refresh()
if reserve_success:
+3 -9
View File
@@ -7,12 +7,6 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from pages.flows.ReserveFlow import ReserveFlow
from pages.flows.CheckinFlow import CheckinFlow
from pages.flows.RenewFlow import RenewFlow
__all__ = [
"ReserveFlow",
"CheckinFlow",
"RenewFlow",
]
from .ReserveFlow import ReserveFlow
from .CheckinFlow import CheckinFlow
from .RenewFlow import RenewFlow
+1 -7
View File
@@ -9,11 +9,5 @@ See the LICENSE file for details.
"""
from pages.strategies.TimeSelectMaker import (
minsToTimeStr,
timeStrToMins
timeStrToMins,
)
__all__ = [
"minsToTimeStr",
"timeStrToMins",
]
+11 -26
View File
@@ -11,11 +11,6 @@ import base64
import queue
import ddddocr
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException,
)
from base.MsgBase import MsgBase
from pages.LoginPage import LoginPage
@@ -38,6 +33,9 @@ class CaptchaSolver(MsgBase):
try:
img_src = login_page.getCaptchaImageSrc()
if img_src is None:
self._showTrace("验证码图片元素定位时发生错误 !", self.TraceLevel.ERROR)
return ""
base64_str = img_src.split(',', 1)[1]
captcha_img = base64.b64decode(base64_str)
captcha_text = self._ocr.classification(captcha_img)
@@ -45,15 +43,9 @@ class CaptchaSolver(MsgBase):
self._showTrace(f"识别到验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4:
self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("识别到的验证码长度不等于 4 个字符 !")
return ""
return captcha_text
except (NoSuchElementException, TimeoutException) as e:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
except (ValueError, OSError) as e:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
except Exception as e:
except ValueError as e:
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
return ""
@@ -61,20 +53,13 @@ class CaptchaSolver(MsgBase):
self,
) -> str:
try:
self._showMsg("请输入验证码:")
captcha_text = self._waitMsg(timeout=15)
self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4:
self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
raise Exception("输入的验证码长度不等于 4 个字符 !")
return captcha_text
except ValueError as e:
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
return ""
except Exception as e:
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
self._showMsg("请输入验证码:")
captcha_text = self._waitMsg(timeout=15)
self._showTrace(f"输入的验证码为 : '{captcha_text}'", 20, no_log=True)
if len(captcha_text) != 4:
self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
return ""
return captcha_text
def solveCaptcha(
self,
+9 -5
View File
@@ -112,20 +112,21 @@ class RecordChecker(MsgBase):
try:
time_element = records_view.getRecordTimeElement(reservation)
info_elements = records_view.getRecordInfoElements(reservation)
except (NoSuchElementException, TimeoutException, StaleElementReferenceException):
except (NoSuchElementException, StaleElementReferenceException):
return {
"date": "",
"time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""},
}
except Exception:
try:
time_data = self._decodeReserveTime(time_element)
info_data = self._decodeReserveInfo(info_elements)
except StaleElementReferenceException:
return {
"date": "",
"time": {"begin": "", "end": ""},
"info": {"location": "", "status": ""},
}
time_data = self._decodeReserveTime(time_element)
info_data = self._decodeReserveInfo(info_elements)
return {
"date": time_data["date"],
"time": time_data["time"],
@@ -152,7 +153,10 @@ class RecordChecker(MsgBase):
records_view = shell.gotoRecordsView()
for _ in range(max_check_times):
reservations = records_view.loadRecords()
try:
reservations = records_view.loadRecords()
except TimeoutException:
reservations = None
if reservations is None:
return None
for reservation in reservations[checked_count:]:
+11
View File
@@ -88,11 +88,22 @@ class ReserveChecker(MsgBase):
) -> bool:
cur_time = time.strftime("%H:%M", time.localtime())
cur_date = time.strftime("%Y-%m-%d", time.localtime())
if reserve_info.get("begin_time") is None:
reserve_info["begin_time"] = {}
if "time" not in reserve_info["begin_time"]:
reserve_info["begin_time"]["time"] = cur_time
self._showTrace(f"开始时间未指定, 自动设置为当前时间: {cur_time}")
elif reserve_info.get("date") == cur_date:
begin_mins = timeStrToMins(reserve_info["begin_time"]["time"])
cur_mins = timeStrToMins(cur_time)
if begin_mins < cur_mins:
self._showTrace(
f"开始时间 {reserve_info['begin_time']['time']} 已过当前时间 {cur_time}, "
f"自动调整为当前时间",
self.TraceLevel.WARNING,
)
reserve_info["begin_time"]["time"] = cur_time
if "max_diff" not in reserve_info["begin_time"]:
reserve_info["begin_time"]["max_diff"] = 30
self._showTrace("开始时间最大时间差未指定, 自动设置为 30 分钟")
+3 -9
View File
@@ -7,12 +7,6 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from pages.services.CaptchaSolver import CaptchaSolver
from pages.services.ReserveChecker import ReserveChecker
from pages.services.RecordChecker import RecordChecker
__all__ = [
"CaptchaSolver",
"ReserveChecker",
"RecordChecker",
]
from .CaptchaSolver import CaptchaSolver
from .ReserveChecker import ReserveChecker
from .RecordChecker import RecordChecker
+1 -12
View File
@@ -7,7 +7,7 @@ This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from pages.strategies.TimeSelectMaker import (
from .TimeSelectMaker import (
TimeSelectMaker,
TimeDecisionMaker,
TimeOptionReader,
@@ -17,14 +17,3 @@ from pages.strategies.TimeSelectMaker import (
TimeSelectionResult,
TimeRangeResult,
)
__all__ = [
"TimeSelectMaker",
"TimeDecisionMaker",
"TimeOptionReader",
"ReserveTimeReader",
"RenewTimeReader",
"TimeOption",
"TimeSelectionResult",
"TimeRangeResult",
]
+6 -5
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""
Utils module for the AutoLibrary project.
Copyright (c) 2026 KenanZhu.
All rights reserved.
Here are the classes and modules in this package:
- TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
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.
"""