Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 631785122b | |||
| 82ea40d3dc | |||
| 1244084c75 | |||
| 7a599c4f63 | |||
| d2cef258aa | |||
| a4c5ee299e | |||
| 7d92717136 | |||
| 3ef301f199 | |||
| b10d333eb2 | |||
| d5dc012ade | |||
| dac87068ef | |||
| f13d983124 | |||
| 30db2fbf9a | |||
| 11bcd77208 | |||
| 833e5576da | |||
| efa211761d | |||
| 269eed1cac | |||
| 99975ebeee | |||
| a8789ad743 | |||
| bf106ea234 |
@@ -1,14 +1,17 @@
|
||||
**/build
|
||||
**/dist
|
||||
**/models/*.onnx
|
||||
**/driver/*.exe
|
||||
**/.git
|
||||
**/.vscode
|
||||
**/.stfolder
|
||||
**/.stignore
|
||||
**/gui/AutoLibraryResources.py
|
||||
**/__pycache__
|
||||
**/gui/AutoLibraryResource.py
|
||||
**/gui/Ui_ALMainWindow.py
|
||||
**/gui/Ui_ALConfigWidget.py
|
||||
**/gui/translators/qtbase_zh_CN.qm
|
||||
.vscode/
|
||||
|
||||
.stfolder
|
||||
.stignore
|
||||
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
model/*.onnx
|
||||
driver/*.exe
|
||||
gui/configs/*.json
|
||||
gui/translators/qtbase_zh_CN.qm
|
||||
gui/AutoLibraryResources.py
|
||||
gui/AutoLibraryResource.py
|
||||
gui/Ui_ALMainWindow.py
|
||||
gui/Ui_ALConfigWidget.py
|
||||
Main.spec
|
||||
|
||||
@@ -17,6 +17,7 @@ from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.edge.service import Service
|
||||
|
||||
from MsgBase import MsgBase
|
||||
from LibChecker import LibChecker
|
||||
from LibLogin import LibLogin
|
||||
from LibLogout import LibLogout
|
||||
from LibReserve import LibReserve
|
||||
@@ -29,7 +30,7 @@ class AutoLib(MsgBase):
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
@@ -51,7 +52,8 @@ class AutoLib(MsgBase):
|
||||
edge_options.add_argument("--no-sandbox")
|
||||
edge_options.add_argument("--disable-dev-shm-usage")
|
||||
|
||||
edge_options.add_argument("--window-size=1280,720")
|
||||
# must be 1920x1080, otherwise the page will cause some elements not accessible
|
||||
edge_options.add_argument("--window-size=1920,1080")
|
||||
edge_options.add_argument("--remote-allow-origins=*")
|
||||
|
||||
# omit ssl errors and verbose log level
|
||||
@@ -95,16 +97,25 @@ class AutoLib(MsgBase):
|
||||
except Exception as e:
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}")
|
||||
return False
|
||||
# init library operators
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
|
||||
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
|
||||
return True
|
||||
|
||||
|
||||
def __initLibOperators(
|
||||
self
|
||||
):
|
||||
|
||||
if not self.__driver:
|
||||
self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !")
|
||||
return
|
||||
self.__lib_checker = LibChecker(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_logout = LibLogout(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_reserve = LibReserve(self._input_queue, self._output_queue, self.__driver)
|
||||
|
||||
|
||||
def __waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# wait for page load
|
||||
@@ -144,10 +155,11 @@ class AutoLib(MsgBase):
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
reserve_info: dict,
|
||||
) -> bool:
|
||||
reserve_info: dict
|
||||
) -> int:
|
||||
|
||||
success = False
|
||||
# result : 0 - success, 1 - failed, 2 - passed
|
||||
result = 1
|
||||
|
||||
# login
|
||||
if not self.__lib_login.login(
|
||||
@@ -156,37 +168,42 @@ class AutoLib(MsgBase):
|
||||
self.__system_config_reader.get("login/max_attempt", 5),
|
||||
self.__system_config_reader.get("login/auto_captcha", True),
|
||||
):
|
||||
return False
|
||||
run_mode = self.__system_config_reader.get("run/mode", 1)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
"auto_renewal": run_mode&0x4,
|
||||
}
|
||||
# reserve or checkin or renewal
|
||||
return 1
|
||||
"""
|
||||
Here, we collect the run mode from the config file.
|
||||
"""
|
||||
if self.__lib_reserve.canReserve(reserve_info.get("date")) and run_mode["auto_reserve"]:
|
||||
if self.__lib_reserve.reserve(reserve_info):
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
success = True
|
||||
run_mode = self.__system_config_reader.get("mode/run_mode", 0)
|
||||
run_mode = {
|
||||
"auto_reserve": run_mode&0x1,
|
||||
"auto_checkin": run_mode&0x2,
|
||||
"auto_renewal": run_mode&0x4,
|
||||
}
|
||||
# reserve
|
||||
if run_mode["auto_reserve"]:
|
||||
if self.__lib_checker.canReserve(reserve_info.get("date")):
|
||||
if self.__lib_reserve.reserve(reserve_info):
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
result = 0
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
success = False
|
||||
result = 2
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
username,
|
||||
):
|
||||
# if logout is failed, we must make sure the host to be reloaded
|
||||
# otherwise, the next login may fail
|
||||
self.__driver.get(self.__system_config_reader.get("library/host_url"))
|
||||
return False
|
||||
return success
|
||||
return 1
|
||||
return result
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
system_config_reader: ConfigReader,
|
||||
users_config_reader: ConfigReader,
|
||||
users_config_reader: ConfigReader
|
||||
):
|
||||
|
||||
self.__system_config_reader = system_config_reader
|
||||
@@ -196,30 +213,40 @@ class AutoLib(MsgBase):
|
||||
else:
|
||||
if not self.__initDriverUrl():
|
||||
return
|
||||
self.__initLibOperators()
|
||||
|
||||
user_counter = {"current": 0, "success": 0, "failed": 0}
|
||||
user_counter = {"current": 0, "success": 0, "failed": 0, "passed": 0}
|
||||
users = self.__users_config_reader.get("users")
|
||||
self._showTrace(f"共发现 {len(users)} 个用户, "\
|
||||
f"用户配置文件路径: {self.__users_config_reader.configPath()}")
|
||||
|
||||
self._showTrace(
|
||||
f"共发现 {len(users)} 个用户, "\
|
||||
f"用户配置文件路径: {self.__users_config_reader.configPath()}"
|
||||
)
|
||||
for user in users:
|
||||
self._showTrace(f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......")
|
||||
if self.__run(
|
||||
user_counter["current"] += 1
|
||||
self._showTrace(
|
||||
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user['username']}......"
|
||||
)
|
||||
r = self.__run(
|
||||
username=user["username"],
|
||||
password=user["password"],
|
||||
reserve_info=user["reserve_info"],
|
||||
):
|
||||
)
|
||||
if r == 0:
|
||||
user_counter["success"] += 1
|
||||
else:
|
||||
elif r == 1:
|
||||
user_counter["failed"] += 1
|
||||
elif r == 2:
|
||||
user_counter["passed"] += 1
|
||||
self._showTrace(f"处理完成, 共计 {user_counter["current"]} 个用户, "\
|
||||
f"成功 {user_counter["success"]} 个用户, "\
|
||||
f"失败 {user_counter["failed"]} 个用户")
|
||||
f"失败 {user_counter["failed"]} 个用户, "\
|
||||
f"跳过 {user_counter["passed"]} 个用户"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def close(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
if self.__driver:
|
||||
@@ -228,5 +255,5 @@ class AutoLib(MsgBase):
|
||||
self._showTrace(f"浏览器驱动已关闭")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"浏览器驱动未初始化,无需关闭")
|
||||
self._showTrace(f"浏览器驱动未初始化, 无需关闭")
|
||||
return False
|
||||
@@ -0,0 +1,264 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import queue
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from LibOperator import LibOperator
|
||||
|
||||
|
||||
class LibChecker(LibOperator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
self.__driver = driver
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def __navigateToReserveRecordPage(
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def __decodeReserveInfo(
|
||||
self,
|
||||
info_elements
|
||||
) -> str:
|
||||
|
||||
location = ""
|
||||
status = ""
|
||||
for info in info_elements:
|
||||
if "已预约" in info.text:
|
||||
status = "已预约"
|
||||
elif "使用中" in info.text:
|
||||
status = "使用中"
|
||||
elif "已完成" in info.text:
|
||||
status = "已完成"
|
||||
elif "已结束使用" in info.text:
|
||||
status = "已结束使用"
|
||||
elif "已取消" in info.text:
|
||||
status = "已取消"
|
||||
elif "失约" in info.text:
|
||||
status = "失约"
|
||||
elif "图书馆" in info.text:
|
||||
location = info.text.strip()
|
||||
return {
|
||||
"location": location,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def __decodeReserveRecord(
|
||||
self,
|
||||
reservation
|
||||
) -> dict:
|
||||
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
info_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
except:
|
||||
return None
|
||||
# process time element to get the date string
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r"(\d{4}-\d{1,2}-\d{1,2})", time_str)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
else:
|
||||
date_str = ""
|
||||
time_match = re.search(r"(\d{1,2}:\d{2}) -- (\d{1,2}:\d{2})", time_str)
|
||||
if time_match:
|
||||
begin_time = time_match.group(1)
|
||||
end_time = time_match.group(2)
|
||||
else:
|
||||
time_str = ""
|
||||
info = self.__decodeReserveInfo(info_elements)
|
||||
return {
|
||||
"date": date_str,
|
||||
"time": {
|
||||
"begin": begin_time,
|
||||
"end": end_time,
|
||||
},
|
||||
"info": info
|
||||
}
|
||||
|
||||
|
||||
def __getReserveRecord(
|
||||
self,
|
||||
wanted_date: str,
|
||||
wanted_status: str
|
||||
) -> dict:
|
||||
|
||||
if wanted_date is None:
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
date_obj = datetime.strptime(wanted_date, "%Y-%m-%d").date()
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
|
||||
|
||||
if not self.__navigateToReserveRecordPage():
|
||||
return None
|
||||
for _ in range(max_check_times):
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList > dl"))
|
||||
)
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList > dl:not(#moreBlock)"
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return None
|
||||
for i in range(checked_count, len(reservations)): # the last one is load button
|
||||
reservation = reservations[i]
|
||||
record = self.__decodeReserveRecord(reservation)
|
||||
print(record)
|
||||
if record is None:
|
||||
continue
|
||||
record_date = record["date"]
|
||||
record_time = record["time"]
|
||||
status = record["info"]["status"]
|
||||
location = record["info"]["location"]
|
||||
if record_date == "" or record_time == {"begin": "", "end": ""}:
|
||||
continue
|
||||
is_wanted = (status == wanted_status)
|
||||
# reservation is later than the given date, check the next one
|
||||
if datetime.strptime(record_date, "%Y-%m-%d").date() > date_obj:
|
||||
continue
|
||||
# reservation is earlier than the given date, can reserve
|
||||
if datetime.strptime(record_date, "%Y-%m-%d").date() < date_obj:
|
||||
return None
|
||||
# query the wanted status
|
||||
if is_wanted:
|
||||
self._showTrace(
|
||||
f"寻找到用户第 {i + 1} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record_date} {record_time['begin']} - {record_time['end']} {location}"
|
||||
)
|
||||
return {
|
||||
"index": i,
|
||||
"date": record_date,
|
||||
"time": record_time,
|
||||
"status": wanted_status
|
||||
}
|
||||
checked_count = len(reservations)
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
else:
|
||||
self._showTrace("用户无法加载更多预约记录")
|
||||
break
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# no reserved or using record in the given date
|
||||
# then can reserve
|
||||
if self.__getReserveRecord(date, "已预约") is None:
|
||||
if self.__getReserveRecord(date, "使用中") is None:
|
||||
self._showTrace(f"用户在 {date} 可以预约")
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 有使用中的预约, 无法预约")
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
|
||||
|
||||
def canCheckin(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
# have a reserved record in the given date
|
||||
record = self.__getReserveRecord(date, "已预约")
|
||||
if record is not None:
|
||||
begin_time = record["time"]["begin"]
|
||||
begin_time = datetime.strptime(f"{date} {begin_time}", "%Y-%m-%d %H:%M")
|
||||
time_diff = datetime.now() - begin_time
|
||||
time_diff_seconds = time_diff.total_seconds()
|
||||
# before 30 minutes, cant checkin
|
||||
if time_diff_seconds < -30*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"距离当前时间还有 {abs(time_diff_seconds)/60:.2f} 分钟, 无法签到"
|
||||
)
|
||||
return False
|
||||
# before in 30 minutes, can checkin
|
||||
elif -30*60 <= time_diff_seconds < 0:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"距离当前时间还有 {abs(time_diff_seconds)/60:.2f} 分钟, 可以签到"
|
||||
)
|
||||
return True
|
||||
# past less than 30 minutes, can checkin
|
||||
elif 0 <= time_diff_seconds < 30*60:
|
||||
self._showTrace(
|
||||
f"用户在 {date} 的预约开始时间为 {begin_time}, "
|
||||
f"当前时间已经 {abs(time_diff_seconds)/60:.2f} 分钟, 可以签到"
|
||||
)
|
||||
return True
|
||||
self._showTrace(f"用户在 {date} 有没有有效预约记录, 无法签到")
|
||||
return False
|
||||
@@ -34,7 +34,7 @@ class LibCheckout(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -36,7 +36,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
# wait to verify login success
|
||||
@@ -59,7 +59,7 @@ class LibLogin(LibOperator):
|
||||
def __fillLogInElements(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
password: str
|
||||
) -> bool:
|
||||
|
||||
# ensure elements presence and fill them
|
||||
@@ -77,7 +77,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __autoRecognizeCaptcha(
|
||||
self,
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# auto recognize captcha
|
||||
@@ -99,7 +99,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __manualRecognizeCaptcha(
|
||||
self,
|
||||
self
|
||||
) -> str:
|
||||
|
||||
# manual recognize captcha
|
||||
@@ -117,7 +117,7 @@ class LibLogin(LibOperator):
|
||||
|
||||
|
||||
def __refreshCaptcha(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
# refresh captcha
|
||||
@@ -134,12 +134,47 @@ class LibLogin(LibOperator):
|
||||
return False
|
||||
|
||||
|
||||
def __solveCaptcha(
|
||||
self,
|
||||
auto_captcha: bool = True
|
||||
) -> str:
|
||||
|
||||
max_attempts = 5
|
||||
|
||||
for _ in range(max_attempts):
|
||||
if auto_captcha:
|
||||
captcha_text = self.__autoRecognizeCaptcha()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
return captcha_text
|
||||
self._showTrace(f"验证码识别失败 {max_attempts} 次, 请检查验证码是否正确 !")
|
||||
return ""
|
||||
|
||||
|
||||
def __fillCaptchaElement(
|
||||
self,
|
||||
captcha_text: str
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
captcha_element = self.__driver.find_element(By.NAME, "answer")
|
||||
captcha_element.clear()
|
||||
captcha_element.send_keys(captcha_text)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"验证码填写失败 ! : {e}")
|
||||
self.__refreshCaptcha()
|
||||
return False
|
||||
|
||||
|
||||
def login(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
max_attempts: int = 5,
|
||||
auto_captcha: bool = True,
|
||||
auto_captcha: bool = True
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
@@ -153,17 +188,11 @@ class LibLogin(LibOperator):
|
||||
password,
|
||||
):
|
||||
continue
|
||||
while True:
|
||||
if auto_captcha:
|
||||
captcha_text = self.__autoRecognizeCaptcha()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
break
|
||||
captcha_element = self.__driver.find_element(By.NAME, "answer")
|
||||
captcha_element.clear()
|
||||
captcha_element.send_keys(captcha_text)
|
||||
captcha_text = self.__solveCaptcha(auto_captcha)
|
||||
if not captcha_text:
|
||||
continue
|
||||
if not self.__fillCaptchaElement(captcha_text):
|
||||
continue
|
||||
self._showTrace("尝试登录...")
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
|
||||
@@ -22,7 +22,7 @@ class LibLogout(LibOperator):
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
driver,
|
||||
driver
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
@@ -31,7 +31,7 @@ class LibLogout(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
return True
|
||||
@@ -39,24 +39,18 @@ class LibLogout(LibOperator):
|
||||
|
||||
def logout(
|
||||
self,
|
||||
username: str,
|
||||
username: str
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
|
||||
return False
|
||||
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
By.XPATH, "//a[@href='/logout']"
|
||||
).click()
|
||||
|
||||
self._showTrace(f"用户 {username} 注销成功 !")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._showTrace(f"用户 {username} 注销失败 ! : {e}")
|
||||
|
||||
return False
|
||||
|
||||
@@ -17,14 +17,14 @@ class LibOperator(MsgBase):
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
|
||||
@@ -28,7 +28,7 @@ class LibRenew(LibOperator):
|
||||
|
||||
|
||||
def _waitResponseLoad(
|
||||
self,
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
pass
|
||||
@@ -83,9 +83,9 @@ class LibReserve(LibOperator):
|
||||
raise
|
||||
if "预定好了" in title or "预约成功" in title or "操作成功" in title:
|
||||
if len(contents) >= 6:
|
||||
date_val = contents[1].split(" : ")[1].strip() if " : " in contents[1] else contents[1].strip()
|
||||
time_val = contents[2].split(" : ")[1].strip() if " : " in contents[2] else contents[2].strip()
|
||||
seat_val = contents[3].split(" : ")[1].strip() if " : " in contents[3] else contents[3].strip()
|
||||
date_val = contents[1].split(" : ")[1].strip() if " : " in contents[1] else contents[1].strip()
|
||||
time_val = contents[2].split(" : ")[1].strip() if " : " in contents[2] else contents[2].strip()
|
||||
seat_val = contents[3].split(" : ")[1].strip() if " : " in contents[3] else contents[3].strip()
|
||||
checkin_val = contents[5].strip()
|
||||
self._showTrace(f"\n"\
|
||||
f" 预约成功 !\n"\
|
||||
@@ -137,10 +137,12 @@ class LibReserve(LibOperator):
|
||||
raise ValueError(f"房间 '{reserve_info['room']}' 不存在")
|
||||
if reserve_info.get("seat_id") is None:
|
||||
raise ValueError("未指定座位")
|
||||
if reserve_info["seat_id"] == "":
|
||||
raise ValueError("未指定座位号")
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -224,7 +226,7 @@ class LibReserve(LibOperator):
|
||||
reserve_info["begin_time"]["time"], reserve_info["end_time"]["time"] = end_time_str, begin_time_str
|
||||
reserve_info["begin_time"]["prefer_early"], reserve_info["end_time"]["prefer_early"] = \
|
||||
reserve_info["end_time"]["prefer_early"], reserve_info["begin_time"]["prefer_early"]
|
||||
self._showTrace("预约开始时间晚于预约结束时间,自动调换开始时间和结束时间")
|
||||
self._showTrace("预约开始时间晚于预约结束时间, 自动调换开始时间和结束时间")
|
||||
|
||||
# update the begin_mins and end_mins after swap
|
||||
begin_time_str, end_time_str = end_time_str, begin_time_str
|
||||
@@ -246,7 +248,7 @@ class LibReserve(LibOperator):
|
||||
reserve_info["end_time"]["time"] = self.__minsToTime(new_end_mins)
|
||||
self._showTrace("预约持续时间超过8小时, 自动设置为 8 小时")
|
||||
self._showTrace(
|
||||
f"预约信息检查完成,准备预约 "
|
||||
f"预约信息检查完成, 准备预约 "
|
||||
f"{reserve_info['date']} "
|
||||
f"{reserve_info['begin_time']["time"]} - "
|
||||
f"{reserve_info['end_time']["time"]} "
|
||||
@@ -263,7 +265,7 @@ class LibReserve(LibOperator):
|
||||
trigger_locator: tuple,
|
||||
fail_msg: str,
|
||||
success_msg: str,
|
||||
option_locator: tuple = None,
|
||||
option_locator: tuple = None
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
@@ -403,9 +405,9 @@ class LibReserve(LibOperator):
|
||||
if abs_diff < best_time_diff or (
|
||||
abs_diff == best_time_diff and (
|
||||
# prefer earlier time
|
||||
(prefer_earlier and actual_diff < 0) or
|
||||
(prefer_earlier and actual_diff <= 0) or
|
||||
# prefer later time
|
||||
(not prefer_earlier and actual_diff > 0)
|
||||
(not prefer_earlier and actual_diff >= 0)
|
||||
)
|
||||
):
|
||||
best_time_diff = abs_diff
|
||||
@@ -489,100 +491,6 @@ class LibReserve(LibOperator):
|
||||
return True
|
||||
|
||||
|
||||
def canReserve(
|
||||
self,
|
||||
date: str
|
||||
) -> bool:
|
||||
|
||||
if date is None:
|
||||
self._showTrace("日期未指定, 无法检查预约状态")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"正在检查用户在日期 {date} 是否可预约......")
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
try:
|
||||
# we need to navigate to the history page to check if we can reserve
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/history?type=SEAT']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, ".myReserveList dl"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
return False
|
||||
checked_count = 0
|
||||
max_attemots = 3 # we only check (3*4=)12 reservations
|
||||
|
||||
for _ in range(max_attemots):
|
||||
try:
|
||||
# check if there's any reservation on the date
|
||||
reservations = self.__driver.find_elements(
|
||||
By.CSS_SELECTOR, ".myReserveList dl"
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
return False
|
||||
for i in range(checked_count, len(reservations) - 1): # the last one is load button
|
||||
reservation = reservations[i]
|
||||
try:
|
||||
time_element = reservation.find_element(
|
||||
By.CSS_SELECTOR, "dt"
|
||||
)
|
||||
status_elements = reservation.find_elements(
|
||||
By.CSS_SELECTOR, "a"
|
||||
)
|
||||
is_reserved = any("已预约" in status.text for status in status_elements)
|
||||
# process time element to get the date string
|
||||
time_str = time_element.text.strip()
|
||||
today = datetime.now().date()
|
||||
if "明天" in time_str:
|
||||
target_date = today + timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "今天" in time_str:
|
||||
target_date = today
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
elif "昨天" in time_str:
|
||||
target_date = today - timedelta(days=1)
|
||||
date_str = target_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_match = re.search(r'(\d{4}-\d{1,2}-\d{1,2})', time_str)
|
||||
if date_match:
|
||||
date_str = date_match.group(1)
|
||||
else:
|
||||
continue
|
||||
# reservation is earlier than the given date, can reserve
|
||||
if datetime.strptime(date_str, "%Y-%m-%d").date() < date_obj:
|
||||
self._showTrace(f"用户在 {date} 可预约")
|
||||
return True
|
||||
# reservation is later than the given date, check the next one
|
||||
elif datetime.strptime(date_str, "%Y-%m-%d").date() > date_obj:
|
||||
continue
|
||||
# compare with the given date
|
||||
if date_str == date and is_reserved:
|
||||
self._showTrace(f"用户在 {date} 已存在有效预约, 无法预约")
|
||||
return False
|
||||
except:
|
||||
self._showTrace(f"解析第 {i + 1} 条预约记录时发生未知错误 !")
|
||||
continue
|
||||
checked_count = len(reservations) - 1
|
||||
# load new reservations if still not sure
|
||||
try:
|
||||
more_btn = self.__driver.find_element(By.ID, "moreBtn")
|
||||
if more_btn.is_displayed() and more_btn.is_enabled():
|
||||
self.__driver.execute_script("arguments[0].scrollIntoView(true);", more_btn)
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
else:
|
||||
break
|
||||
except:
|
||||
break
|
||||
self._showTrace(f"用户在 {date} 可预约")
|
||||
return True
|
||||
|
||||
|
||||
def reserve(
|
||||
self,
|
||||
reserve_info: dict
|
||||
@@ -600,7 +508,7 @@ class LibReserve(LibOperator):
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@href='/map']"))
|
||||
).click()
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
except:
|
||||
@@ -615,7 +523,7 @@ class LibReserve(LibOperator):
|
||||
return False
|
||||
# room find
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "findRoom"))
|
||||
).click()
|
||||
except:
|
||||
@@ -638,7 +546,7 @@ class LibReserve(LibOperator):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
WebDriverWait(self.__driver, 5).until(
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "reserveBtn"))
|
||||
).click()
|
||||
submit_reserve = True
|
||||
|
||||
@@ -16,7 +16,7 @@ class MsgBase:
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
output_queue: queue.Queue,
|
||||
output_queue: queue.Queue
|
||||
):
|
||||
|
||||
self._class_name = self.__class__.__name__
|
||||
@@ -43,7 +43,7 @@ class MsgBase:
|
||||
|
||||
def _waitMsg(
|
||||
self,
|
||||
timeout: float = 1.0,
|
||||
timeout: float = 1.0
|
||||
) -> str:
|
||||
|
||||
try:
|
||||
@@ -55,7 +55,7 @@ class MsgBase:
|
||||
|
||||
def _inputMsg(
|
||||
self,
|
||||
timeout: float = 1.0,
|
||||
timeout: float = 1.0
|
||||
) -> bool:
|
||||
|
||||
try:
|
||||
|
||||
|
After Width: | Height: | Size: 785 KiB |
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="./AutoLibrary.ico" type="image/x-icon">
|
||||
<title>AutoLibrary 操作手册</title>
|
||||
<style>
|
||||
* {
|
||||
@@ -46,31 +47,25 @@
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-header:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
@@ -79,7 +74,6 @@
|
||||
border-radius: 5px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover,
|
||||
.sidebar-nav a.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
@@ -112,14 +106,6 @@
|
||||
margin: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
.intro-box {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
@@ -149,7 +135,6 @@
|
||||
flex-shrink: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.step-number::before {
|
||||
content: counter(step-counter);
|
||||
}
|
||||
@@ -157,27 +142,56 @@
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-image {
|
||||
background: #e9ecef;
|
||||
border-radius: 5px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--gray);
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
.step-content ol {
|
||||
padding-left: 1em;
|
||||
}
|
||||
.step-content ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
.step-content li {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.note {
|
||||
.step-image {
|
||||
background: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin: 1rem 0;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.step-image img {
|
||||
max-width: 60%;
|
||||
height: auto;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.intro-box {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid var(--secondary);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.info {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #0783ff;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.important {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #f8d7da;
|
||||
border-left: 4px solid #dc3545;
|
||||
@@ -185,6 +199,14 @@
|
||||
margin: 1rem 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
.highlight {
|
||||
background: #e3f2fd;
|
||||
color: #3498db;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
font-family: 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #2d2d2d;
|
||||
@@ -195,18 +217,18 @@
|
||||
margin: 1rem 0;
|
||||
font-family: 'Consolas', monospace;
|
||||
white-space: pre-wrap;
|
||||
border-radius: 0 5px 5px 0;
|
||||
border-radius: 15px 15px 5px 5px;
|
||||
font-size: 0.9rem;
|
||||
overflow-x: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.code-block .key { color: #2b997f; }
|
||||
.code-block .string { color: #9acf65; }
|
||||
.code-block .number { color: #3dd942; }
|
||||
.code-block .boolean { color: #24e9ff; }
|
||||
.code-block .null { color: #ff79c6; }
|
||||
.code-block .property { color: #8be9fd; }
|
||||
.code-block .punctuation { color: #f8f8f2; }
|
||||
.code-block .bool { color: #569CD6; }
|
||||
.code-block .string { color: #CE9178; }
|
||||
.code-block .number { color: #B5CEA8; }
|
||||
.code-block .boolean { color: #569CD6; }
|
||||
.code-block .null { color: #569CD6; }
|
||||
.code-block .property { color: #9CDCFE; }
|
||||
.code-block .punctuation { color: #D4D4D4; }
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
@@ -214,7 +236,6 @@
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
@@ -222,25 +243,34 @@
|
||||
padding: 1.5rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
.download-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.config-tab {
|
||||
.tabs-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tab-button {
|
||||
padding: 0.8rem 1.5rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
@@ -250,34 +280,36 @@
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.config-tab.active {
|
||||
.tab-button.active {
|
||||
color: var(--secondary);
|
||||
border-bottom: 3px solid var(--secondary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: white;
|
||||
border-radius: 0 5px 5px 5px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* align-items: center;
|
||||
justify-content: center; */
|
||||
}
|
||||
|
||||
.download-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
margin-top: 2rem;
|
||||
.tab-pane img {
|
||||
max-width: 60%;
|
||||
max-height: 60%;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -292,7 +324,6 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #2980b9;
|
||||
transform: translateY(-2px);
|
||||
@@ -304,7 +335,6 @@
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--light);
|
||||
@@ -314,23 +344,22 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
padding: 1rem 1.5rem;
|
||||
background: rgb(220, 220, 220);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.faq-item.active .faq-answer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.browser-drivers {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.browser-card {
|
||||
flex: 1;
|
||||
background: white;
|
||||
@@ -340,12 +369,10 @@
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.browser-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.browser-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
@@ -356,17 +383,14 @@
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.browser-logo img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.browser-card h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.browser-card .btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -397,15 +421,13 @@
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>AutoLibrary</h1>
|
||||
<p>操作手册 v0.01</p>
|
||||
<p>操作手册 alpha-v0.03</p>
|
||||
</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="#intro" class="active">工具简介</a></li>
|
||||
<li><a href="#preparation">准备工作</a></li>
|
||||
<li><a href="#usage">使用步骤</a></li>
|
||||
<li><a href="#configuration">配置说明</a></li>
|
||||
<li><a href="#features">功能详解</a></li>
|
||||
<li><a href="#interface">用户界面</a></li>
|
||||
<li><a href="#features">功能介绍</a></li>
|
||||
<li><a href="#troubleshooting">故障排除</a></li>
|
||||
<li><a href="#faq">常见问题</a></li>
|
||||
<li><a href="#download">下载安装</a></li>
|
||||
@@ -413,7 +435,8 @@
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<section id="name" class="section">
|
||||
<section id="name" class="section" style="display: flex; align-items: center; gap: 10px;">
|
||||
<img src="./AutoLibrary.ico" alt="AutoLibrary" style="width: 80px; height: 80px;">
|
||||
<h1>AutoLibrary</h1>
|
||||
</section>
|
||||
|
||||
@@ -424,7 +447,7 @@
|
||||
<div class="intro-box">
|
||||
<p>AutoLibrary 是一款专为北京建筑大学图书馆设计的自动化工具,旨在帮助学生简化图书馆座位操作流程,节省宝贵时间。</p>
|
||||
</div>
|
||||
<p>本工具模拟人工操作,通过简单的界面交互使用。</p>
|
||||
<p>本工具模拟人工操作,通过简单的界面配置并交互使用。</p>
|
||||
|
||||
<h3>工具特点</h3>
|
||||
<ul>
|
||||
@@ -475,11 +498,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>注意:</strong> 浏览器驱动版本必须与您的浏览器版本兼容,否则工具无法正常工作。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
浏览器驱动下载页面截图区域
|
||||
<div class="info">
|
||||
<strong>提示:</strong> 浏览器驱动版本必须与您的浏览器版本兼容,否则本工具将无法正常工作。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,9 +508,11 @@
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>确认驱动路径</h3>
|
||||
<p>下载驱动后,将浏览器驱动程序的路径(如'C:\Users\Administrator\Downloads\msedgedriver.exe')输入到AutoLibrary配置界面中。</p>
|
||||
<p>下载驱动后,将浏览器驱动程序的路径通过配置窗口加载到AutoLibrary中。</p>
|
||||
|
||||
<p>例如:<span class="highlight">C:\Users\Administrator\Downloads\msedgedriver.exe</span></p>
|
||||
<div class="step-image">
|
||||
驱动文件放置位置截图区域
|
||||
<img src="./配置窗口-系统配置-浏览器路径选择.png" alt="浏览器驱动路径示意图">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -503,13 +525,13 @@
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>编辑配置文件</h3>
|
||||
<p>使用文本编辑器(如记事本、Visual Studio Code等)打开config.json和users.json文件,按照您的需求修改参数。</p>
|
||||
<div class="warning">
|
||||
<strong>重要:</strong> 请勿使用Microsoft Word等富文本编辑器,这可能导致文件格式错误。
|
||||
<h3>启动工具</h3>
|
||||
<p>双击运行AutoLibrary.exe文件,工具将启动主界面。</p>
|
||||
<div class="info">
|
||||
<strong>提示:</strong>软件首次启动,未初始化配置文件,直接运行脚本会提示失败。
|
||||
</div>
|
||||
<div class="step-image">
|
||||
配置文件编辑截图区域
|
||||
<img src="./运行主界面.png" alt="运行主界面">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,10 +539,180 @@
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>运行工具</h3>
|
||||
<p>双击运行main.exe文件,工具将自动开始预约流程。</p>
|
||||
<div class="step-image">
|
||||
工具运行界面截图区域
|
||||
<h3>配置工具</h3>
|
||||
<p>对于不同用户的需求,你可以使用两种不同的方式来配置工具</p>
|
||||
<p>1. 使用界面配置:点击主界面窗口右上角的配置按钮,打开配置窗口。</p>
|
||||
<div id="use-ui" class="tabs-container">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-button active" data-tab="user-config">用户配置</button>
|
||||
<button class="tab-button" data-tab="system-config">系统配置</button>
|
||||
<button class="tab-button" data-tab="other-config">其它</button>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div id="user-config" class="tab-pane active">
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-用户配置.png" alt="配置窗口-用户配置">
|
||||
</div>
|
||||
<div class="info">
|
||||
<strong>提示:</strong>初次运行软件时,用户配置默认为空,需要手动添加。
|
||||
</div>
|
||||
<h4>用户列表</h4>
|
||||
<p>用户列表显示当前配置文件中的所有用户,你可以添加、删除用户。选中用户项以进行详细的配置。</p>
|
||||
<h4>用户信息</h4>
|
||||
<ol>
|
||||
<p><i>-学号:</i>用户的学号。</p>
|
||||
<p><i>-密码:</i>用户的密码,用户默认密码为000000。</p>
|
||||
</ol>
|
||||
<h4>预约信息</h4>
|
||||
<ol>
|
||||
<p><i>-日期(YYYY-MM-DD):</i>座位预约日期,默认显示当前日期,无法更改(图书馆19:00-23:00可以预约第二天座位,软件将在18:00-23:00允许用户选择第二天的日期)。</p>
|
||||
<p><i>-地点:</i>预约座位的地点,默认值为“图书馆”。</p>
|
||||
<p><i>-楼层:</i>预约座位的楼层,默认值为“二层”。</p>
|
||||
<p><i>-区域:</i>预约座位的区域,默认值为“二层内环”。</p>
|
||||
<p><i>-座位号:</i>预约座位的座位号。</p>
|
||||
<p><i>-开始时间(HH:mm):</i>预约座位的开始时间,默认值为当前时间,可选时间范围为7:30-23:30。</p>
|
||||
<p><i>-结束时间(HH:mm):</i>预约座位的结束时间,默认值为当前时间加上两个小时,可选时间范围与开始时间相同。</p>
|
||||
<p><i>-最大时间偏差(分钟):</i>选择的开始/结束时间不可用时,会按照该时间偏差范围寻找最近的可用时间。选择0则表示严格按照选择的时间预约,可选范围为0-120分钟。</p>
|
||||
<p><i>-优先选择最早/晚:</i>当预约时间列表中存在多个相距最近的可用时间时,选择最早(开始时间)/最晚(结束时间)的时间,不勾选将会按照脚本默认行为选择。</p>
|
||||
<p><i>-期望时长(小时):</i>预约座位的期望时长,默认值为“2小时”,可选范围为0-8小时。</p>
|
||||
<p><i>-优先满足期望时长:</i>勾选此项,会优先满足预约时长限制,当座位紧张时可能会导致预约失败。</p>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="system-config" class="tab-pane">
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-系统配置.png" alt="配置窗口-系统配置">
|
||||
</div>
|
||||
<h4>图书馆设置</h4>
|
||||
<p>这里主要包含了关于图书馆的访问网址设置,不需要更改。</p>
|
||||
<h4>浏览器设置</h4>
|
||||
<p>主要包含浏览器类别选择(当前支持Edge Chromium和Mozilla Firefox),浏览器驱动路径选择以及无头模式设置。</p>
|
||||
<ol>
|
||||
<p><i>-浏览器类别:</i>选择您使用的浏览器类别(Edge Chromium或Mozilla Firefox)。</p>
|
||||
<p><i>-浏览器驱动路径:</i>点击浏览按钮选择对应浏览器类型和版本的浏览器驱动程序的路径。</p>
|
||||
<p><i>-无头模式:</i>如果您不希望看到浏览器窗口自动操作,可将无头模式设置为true。</p>
|
||||
</ol>
|
||||
<h4>登录设置</h4>
|
||||
<ol>
|
||||
<p><i>-自动识别验证码:</i>默认勾选。</p>
|
||||
<p><i>-登录尝试次数:</i>设置登录尝试的最大次数,默认值为3次。</p>
|
||||
</ol>
|
||||
<h4>运行模式</h4>
|
||||
<ol>
|
||||
<p><i>-自动预约:</i>脚本按照配置中起始时间和预期时长进行预约,用户如果当天存在有效预约,将自动跳过预约步骤。</p>
|
||||
<p><i>-自动签到:</i>如果用户在脚本启动时满足图书馆预约条件,将自动签到,如果用户当天无有效预约或不在可签到时间内,则自动跳过。</p>
|
||||
<p><i>-自动续约:</i>如果用户在脚本启动时满足图书馆预约条件,将自动续约,如果用户当天无有效预约或不在可续约时间内,则自动跳过。</p>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="other-config" class="tab-pane">
|
||||
<div class="step-image">
|
||||
<img src="./配置窗口-其它.png" alt="配置窗口-其它">
|
||||
</div>
|
||||
<h4>当前配置:</h4>
|
||||
<p>这里主要显示脚本当前使用的系统配置文件和用户配置文件的路径。你可以使用右侧浏览按钮选择新的配置文件路径。</p>
|
||||
<h4>导出配置:</h4>
|
||||
<p>选择导出配置文件的目标路径和文件名,点击‘导出配置文件’按钮,将当前的配置项导出。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>2. 使用配置文件:在脚本可执行文件的根目录创建系统配置文件system.json和用户配置文件users.json。</p>
|
||||
<div id="use-file" class="tabs-container">
|
||||
<div class="tab-buttons">
|
||||
<div class="tab-button active" data-tab="system.config">系统配置文件</div>
|
||||
<div class="tab-button" data-tab="users.config">用户配置文件</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div id="system.config" class="tab-pane active">
|
||||
<p>system.json文件控制工具的基本运行参数:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"library"</span>: {
|
||||
<span class="property">"host_url"</span>: <span class="string">"http://10.1.20.7"</span>,
|
||||
<span class="property">"login_url"</span>: <span class="string">"/login"</span>
|
||||
},
|
||||
<span class="property">"mode"</span>: {
|
||||
<span class="property">"run_mode"</span>: <span class="number">1</span>
|
||||
},
|
||||
<span class="property">"login"</span>: {
|
||||
<span class="property">"auto_captcha"</span>: <span class="bool">true</span>,
|
||||
<span class="property">"max_attempt"</span>: <span class="number">3</span>
|
||||
},
|
||||
<span class="property">"web_driver"</span>: {
|
||||
<span class="property">"driver_type"</span>: <span class="string">"edge"</span>,
|
||||
<span class="property">"driver_path"</span>: <span class="string">"msedgedriver.exe"</span>,
|
||||
<span class="property">"headless"</span>: <span class="bool">false</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ol>
|
||||
<p><strong>library/host_url</strong>: 图书馆主机URL,无需更改。</p>
|
||||
<p><strong>library/login_url</strong>: 登录页面URL,无需更改。</p>
|
||||
<p><strong>mode/run_mode</strong>: 运行模式,可组合使用(+1:自动预约/+2:自动签到/+4:自动续约)</p>
|
||||
<p><strong>login/auto_captcha</strong>: 自动验证码识别,建议保持true</p>
|
||||
<p><strong>login/max_attempt</strong>: 登录尝试次数,默认3次</p>
|
||||
<p><strong>web_driver/driver_type</strong>: 浏览器类型(edge/chrome/firefox)</p>
|
||||
<p><strong>web_driver/driver_path</strong>: 驱动文件路径</p>
|
||||
<p><strong>web_driver/headless</strong>: 无头模式,默认false运行时显示浏览器窗口</p>
|
||||
</ol>
|
||||
</div>
|
||||
<div id="users.config" class="tab-pane">
|
||||
<p>users.json文件控制用户的预约和签到参数:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"users"</span>: [
|
||||
{
|
||||
<span class="property">"username"</span>: <span class="string">"您的学号"</span>,
|
||||
<span class="property">"password"</span>: <span class="string">"您的密码"</span>,
|
||||
<span class="property">"reserve_info"</span>: {
|
||||
<span class="property">"date"</span>: <span class="string">"2025-10-30"</span>,
|
||||
<span class="property">"place"</span>: <span class="string">"1"</span>,
|
||||
<span class="property">"floor"</span>: <span class="string">"4"</span>,
|
||||
<span class="property">"room"</span>: <span class="string">"5"</span>,
|
||||
<span class="property">"begin_time"</span>: {
|
||||
<span class="property">"time"</span>: <span class="string">"09:30"</span>,
|
||||
<span class="property">"max_diff"</span>: <span class="number">30</span>,
|
||||
<span class="property">"prefer_early"</span>: <span class="bool">true</span>
|
||||
},
|
||||
<span class="property">"end_time"</span>: {
|
||||
<span class="property">"time"</span>: <span class="string">"21:23"</span>,
|
||||
<span class="property">"max_diff"</span>: <span class="number">30</span>,
|
||||
<span class="property">"prefer_early"</span>: <span class="bool">false</span>
|
||||
},
|
||||
<span class="property">"seat_id"</span>: <span class="string">"31A"</span>,
|
||||
<span class="property">"expect_duration"</span>: <span class="number">6</span>
|
||||
<span class="property">"satisfy_duration"</span>: <span class="bool">true</span>
|
||||
}
|
||||
},
|
||||
/* 可以添加多个上述的配置块,每个用户预约信息独立配置 */
|
||||
]
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ol>
|
||||
<p><strong>username</strong>: 学号</p>
|
||||
<p><strong>password</strong>: 密码</p>
|
||||
<p><strong>reserve_info/date</strong>: 预约日期(格式:YYYY-MM-DD)</p>
|
||||
<p><strong>reserve_info/place</strong>: 图书馆或者字符“1”</p>
|
||||
<p><strong>reserve_info/floor</strong>: 预约楼层(“2”:二层,“3”:三层,“4”:四层,“5”:五层)</p>
|
||||
<p><strong>reserve_info/room</strong>: 预约房间()</p>
|
||||
<p><strong>reserve_info/seat_id</strong>: 座位编号(例如:“12A/12a/012A/012a”)</p>
|
||||
<p><strong>reserve_info/begin_time</strong>: 预约开始时间(格式:HH:mm)</p>
|
||||
<p><strong>reserve_info/begin_time/max_diff</strong>: 最大时间差(分钟)</p>
|
||||
<p><strong>reserve_info/begin_time/prefer_early</strong>: 是否优先预约较早时间(默认true)</p>
|
||||
<p><strong>reserve_info/end_time</strong>: 预约结束时间(格式:HH:mm)</p>
|
||||
<p><strong>reserve_info/end_time/max_diff</strong>: 最大时间差(分钟)</p>
|
||||
<p><strong>reserve_info/end_time/prefer_early</strong>: 是否优先预约较早时间(默认true)</p>
|
||||
<p><strong>reserve_info/expect_duration</strong>: 期望使用时长(小时)</p>
|
||||
<p><strong>reserve_info/satisfy_duration</strong>: 是否满足期望时长(默认true)</p>
|
||||
</ol>
|
||||
<div class="info">
|
||||
<strong>提示:</strong> 可以添加多个用户,工具会按顺序处理每个用户的预约请求。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -529,9 +721,10 @@
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>监控运行状态</h3>
|
||||
<p>如果headless模式设置为false,您将看到浏览器窗口自动操作。请勿手动干预浏览器窗口。</p>
|
||||
<p>如果系统设置中没有勾选浏览器无头模式运行,工具会在运行过程中打开浏览器窗口,显示自动运行过程。</p>
|
||||
<p>除此之外,你还可以通过软件的运行日志输出区域查看详细的运行状态和错误信息。</p>
|
||||
<div class="step-image">
|
||||
浏览器自动操作截图区域
|
||||
<img src="./监控运行状态-运行图.png" alt="监控运行状态">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -539,139 +732,54 @@
|
||||
<div class="step">
|
||||
<div class="step-number"></div>
|
||||
<div class="step-content">
|
||||
<h3>查看结果</h3>
|
||||
<p>工具运行完成后,查看生成的日志文件确认预约结果。</p>
|
||||
<h3>查看运行结果</h3>
|
||||
<p>软件运行结束后日志会显示本次运行结果:“处理完成, 共计 n 个用户, 成功 n 个用户, 失败 m 个用户”。</p>
|
||||
<div class="step-image">
|
||||
运行日志截图区域
|
||||
<img src="./监控运行状态-运行结果.png" alt="查看运行结果">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="configuration" class="section">
|
||||
<h2>配置说明</h2>
|
||||
<p>AutoLibrary通过两个配置文件来控制工具行为:config.json(工具设置)和users.json(用户信息)。</p>
|
||||
|
||||
<div class="config-tabs">
|
||||
<button class="config-tab active" data-tab="config-tab">config.json</button>
|
||||
<button class="config-tab" data-tab="users-tab">users.json</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="config-tab" class="tab-pane active">
|
||||
<h3>工具配置文件</h3>
|
||||
<p>config.json文件控制工具的基本运行参数:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"library"</span>: {
|
||||
<span class="property">"lib_host_url"</span>: <span class="string">"http://10.1.20.7"</span>,
|
||||
<span class="property">"lib_login_url"</span>: <span class="string">"/login"</span>
|
||||
},
|
||||
<span class="property">"mode"</span>: {
|
||||
<span class="property">"run_mode"</span>: <span class="number">1</span>
|
||||
},
|
||||
<span class="property">"login"</span>: {
|
||||
<span class="property">"auto_captcha"</span>: <span class="key">true</span>,
|
||||
<span class="property">"login_attempt"</span>: <span class="number">3</span>
|
||||
},
|
||||
<span class="property">"web_driver"</span>: {
|
||||
<span class="property">"driver_type"</span>: <span class="string">"edge"</span>,
|
||||
<span class="property">"driver_path"</span>: <span class="string">"msedgedriver.exe"</span>,
|
||||
<span class="property">"headless"</span>: <span class="key">false</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ul>
|
||||
<li><strong>run_mode</strong>: 运行模式,可组合使用(1+4+8=13)</li>
|
||||
<li><strong>auto_captcha</strong>: 自动验证码识别,建议保持true</li>
|
||||
<li><strong>login_attempt</strong>: 登录尝试次数,默认3次</li>
|
||||
<li><strong>driver_type</strong>: 浏览器类型(edge/chrome/firefox)</li>
|
||||
<li><strong>driver_path</strong>: 驱动文件路径</li>
|
||||
<li><strong>headless</strong>: 无头模式,false会显示浏览器窗口</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="users-tab" class="tab-pane">
|
||||
<h3>用户配置文件</h3>
|
||||
<p>users.json文件包含用户账号和预约信息:</p>
|
||||
<div class="code-block">
|
||||
{
|
||||
<span class="property">"users"</span>: [
|
||||
{
|
||||
<span class="property">"username"</span>: <span class="string">"您的学号"</span>,
|
||||
<span class="property">"password"</span>: <span class="string">"您的密码"</span>,
|
||||
<span class="property">"reserve_info"</span>: {
|
||||
<span class="property">"date"</span>: <span class="string">"2025-10-30"</span>,
|
||||
<span class="property">"start_time"</span>: <span class="string">"09:30"</span>,
|
||||
<span class="property">"end_time"</span>: <span class="string">"17:00"</span>,
|
||||
<span class="property">"place"</span>: <span class="string">"1"</span>,
|
||||
<span class="property">"floor"</span>: <span class="string">"4"</span>,
|
||||
<span class="property">"room"</span>: <span class="string">"5"</span>,
|
||||
<span class="property">"seat_id"</span>: <span class="string">"31A"</span>,
|
||||
<span class="property">"expect_duration"</span>: <span class="number">6</span>
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>参数说明</h4>
|
||||
<ul>
|
||||
<li><strong>username</strong>: 学号</li>
|
||||
<li><strong>password</strong>: 密码</li>
|
||||
<li><strong>date</strong>: 预约日期(格式:YYYY-MM-DD)</li>
|
||||
<li><strong>start_time/end_time</strong>: 预约时间段</li>
|
||||
<li><strong>place/floor/room</strong>: 图书馆位置信息</li>
|
||||
<li><strong>seat_id</strong>: 座位编号(重要)</li>
|
||||
<li><strong>expect_duration</strong>: 期望使用时长(小时)</li>
|
||||
</ul>
|
||||
<div class="note">
|
||||
<strong>提示:</strong> 可以添加多个用户,工具会按顺序处理每个用户的预约请求。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" class="section">
|
||||
<h2>功能详解</h2>
|
||||
<h2>功能介绍</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⏰</div>
|
||||
<h3>自动预约(模式 +1)</h3>
|
||||
<p>当您当前没有有效预约时,工具会自动为您预约指定座位。</p>
|
||||
<div class="note">
|
||||
<h3>自动预约</h3>
|
||||
<p>如果用户当前没有有效预约时,工具会自动为您预约指定座位。</p>
|
||||
<div class="info">
|
||||
<strong>适用场景:</strong> 提前预约第二天的座位
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">✅</div>
|
||||
<h3>自动签到(模式 +4)</h3>
|
||||
<p>如果您已有预约,且在可签到时间范围内,工具会自动完成签到。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 避免因忘记签到而失去座位
|
||||
<h3>自动签到</h3>
|
||||
<p>如果用户当前已有预约,且在可签到时间范围(开始时间的前后30分钟)内,工具会自动完成签到。</p>
|
||||
<div class="info">
|
||||
<strong>适用场景:</strong> 因忘记签到而导致失约,影响正常使用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>自动续约(模式 +8)</h3>
|
||||
<p>当您正在使用座位且到达可续约时间时,工具会自动延长使用时间。</p>
|
||||
<div class="note">
|
||||
<strong>适用场景:</strong> 需要长时间使用座位的情况
|
||||
<h3>自动续约</h3>
|
||||
<p>如果用户当前正在使用座位,且到达可续约时间(结束时间前的120分钟),工具会自动延长使用时间。</p>
|
||||
<div class="info">
|
||||
<strong>适用场景:</strong> 需要长时间使用座位
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>模式组合使用</h3>
|
||||
<p>运行模式可以组合使用,只需将对应模式的数值相加:</p>
|
||||
<p>运行模式可以组合使用,只需在配置窗口中勾选对应模式即可:</p>
|
||||
<ul>
|
||||
<li>自动预约 + 自动签到 + 自动续约 = 13(推荐)</li>
|
||||
<li>自动预约 = 1</li>
|
||||
<li>自动预约 + 自动签到 = 5</li>
|
||||
<li>
|
||||
<ol><strong>自动预约 + 自动签到 + 自动续约(推荐)</strong></ol>
|
||||
<ol>自动预约</ol>
|
||||
<ol>自动预约 + 自动签到</ol>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -680,13 +788,13 @@
|
||||
<h3>常见问题及解决方法</h3>
|
||||
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">工具启动时报错"无法找到驱动"</div>
|
||||
<div class="faq-question">工具启动时报错"无法找到驱动/Unable to obtain driver"等类似报错信息</div>
|
||||
<div class="faq-answer">
|
||||
<p>这是因为浏览器驱动未正确安装或版本不匹配。</p>
|
||||
<p>这是大概率是因为浏览器驱动未正确安装或版本不匹配。</p>
|
||||
<ul>
|
||||
<li>检查驱动文件是否放置在正确位置</li>
|
||||
<li>确认驱动版本与浏览器版本完全匹配</li>
|
||||
<li>尝试重新下载并安装驱动</li>
|
||||
<ol>1,检查驱动文件是否放置在正确位置</ol>
|
||||
<ol>2,确认驱动版本与浏览器版本完全匹配,例如:Chrome浏览器需要对应版本的chromedriver.exe,切勿混用</ol>
|
||||
<ol>3,尝试重新下载并安装驱动</ol>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -694,11 +802,11 @@
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">登录失败,提示账号密码错误</div>
|
||||
<div class="faq-answer">
|
||||
<p>请检查users.json文件中的账号密码是否正确。</p>
|
||||
<p>请检查配置界面中的账号密码是否正确。</p>
|
||||
<ul>
|
||||
<li>确认学号和密码无误</li>
|
||||
<li>检查是否有特殊字符需要转义</li>
|
||||
<li>尝试手动登录图书馆系统确认账号可用</li>
|
||||
<ol>1,确认学号和密码无误</ol>
|
||||
<ol>2,检查是否有不支持的特殊字符需要转义</ol>
|
||||
<ol>3,尝试手动登录图书馆系统确认账号可用</ol>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -708,9 +816,8 @@
|
||||
<div class="faq-answer">
|
||||
<p>目标座位可能已被他人预约或不在可预约时间。</p>
|
||||
<ul>
|
||||
<li>确认座位编号是否正确</li>
|
||||
<li>检查预约时间是否符合图书馆规定</li>
|
||||
<li>尝试预约其他座位或调整预约时间</li>
|
||||
<ol>1,确认座位编号是否正确,是否在该楼层指定区域</ol>
|
||||
<ol>2,尝试预约其它座位或调整预约时间,例如调整允许的开始或结束时间的最大偏差,位置紧张情况下可以让脚本根据允许的时间范围选择最佳起始时间</ol>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -729,7 +836,10 @@
|
||||
<div class="faq-item">
|
||||
<div class="faq-question">可以同时预约多个座位吗?</div>
|
||||
<div class="faq-answer">
|
||||
<p>根据图书馆规定,每个账号同一时间段只能预约一个座位。但您可以在users.json中添加多个账号,工具会依次处理每个账号的预约请求。</p>
|
||||
<p>根据图书馆规定,每个账号同一时间段只能预约一个座位。但您可以在配置界面中添加多个账号,工具会依次处理每个账号的预约请求。</p>
|
||||
<div class="important">
|
||||
<p><strong>重要:</strong>本工具软件旨在简化并辅助用户正常使用时的图书馆服务流程,请勿滥用影响他人及图书馆正常运行。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -745,21 +855,20 @@
|
||||
<h2>下载安装</h2>
|
||||
<div class="download-section">
|
||||
<h3>获取AutoLibrary</h3>
|
||||
<p>点击下方按钮下载最新版本的AutoLibrary工具包:</p>
|
||||
<a href="#" class="btn">下载 AutoLibrary v0.01</a>
|
||||
<div class="note" style="margin-top: 1.5rem;">
|
||||
<p>文件大小:约15MB</p>
|
||||
<p>点击下方按钮下载最新版本的AutoLibrary压缩包:</p>
|
||||
<a href="#" class="btn">下载 AutoLibrary alpha-v0.03</a>
|
||||
<div class="info" style="margin-top: 1.5rem;">
|
||||
<p>文件大小:约98MB</p>
|
||||
<p>系统要求:Windows 10/11,支持Edge/Chrome/Firefox浏览器</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>安装步骤</h3>
|
||||
<ol>
|
||||
<li>下载压缩包并解压到任意文件夹</li>
|
||||
<li>根据您使用的浏览器下载对应版本的驱动</li>
|
||||
<li>将驱动文件放置到工具文件夹中</li>
|
||||
<li>按照本手册说明编辑配置文件</li>
|
||||
<li>双击main.exe运行工具</li>
|
||||
<ol>下载压缩包并解压到任意文件夹</ol>
|
||||
<ol>根据您使用的浏览器下载对应版本的驱动</ol>
|
||||
<ol>按照本手册说明配置账号密码等参数</ol>
|
||||
<ol>点击启动脚本,即可开始自动预约和使用座位</ol>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
@@ -788,20 +897,28 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.config-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
document.querySelectorAll('.tabs-container').forEach(container => {
|
||||
const tabButtons = container.querySelectorAll('.tab-button');
|
||||
const tabPanes = container.querySelectorAll('.tab-pane');
|
||||
|
||||
document.querySelectorAll('.config-tab').forEach(t => {
|
||||
t.classList.remove('active');
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const containerButtons = this.closest('.tabs-container').querySelectorAll('.tab-button');
|
||||
const containerPanes = this.closest('.tabs-container').querySelectorAll('.tab-pane');
|
||||
|
||||
containerButtons.forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
|
||||
containerPanes.forEach(pane => {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
this.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -817,8 +934,8 @@
|
||||
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '0px 0px -50% 0px',
|
||||
threshold: 0.2
|
||||
rootMargin: '-45% 0px -45% 0px',
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
|
||||
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -1 +1 @@
|
||||
This folder is used to store the browser driver by selenium.
|
||||
This folder is used to store the browser driver using by selenium.
|
||||
@@ -569,7 +569,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
|
||||
|
||||
@Slot()
|
||||
def onFloorComboBoxCurrentIndexChanged(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
floor = self.FloorComboBox.currentText()
|
||||
|
||||
@@ -1127,13 +1127,13 @@
|
||||
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>265</width>
|
||||
<width>250</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>260</width>
|
||||
<width>300</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
|
||||
@@ -70,7 +70,7 @@ class AutoLibWorker(QThread):
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self.showTraceSignal.emit(
|
||||
"配置文件路径不存在,请检查配置文件路径是否正确。"
|
||||
"配置文件路径不存在, 请检查配置文件路径是否正确。"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
@@ -102,7 +102,7 @@ class AutoLibWorker(QThread):
|
||||
self.showTraceSignal.emit("AutoLibrary 运行结束")
|
||||
except Exception as e:
|
||||
self.showTraceSignal.emit(
|
||||
f"AutoLibrary 运行时发生异常:{e}"
|
||||
f"AutoLibrary 运行时发生异常 : {e}"
|
||||
)
|
||||
finally:
|
||||
self.finishedSignal.emit()
|
||||
@@ -127,10 +127,6 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
self.setupUi(self)
|
||||
self.__input_queue = queue.Queue()
|
||||
self.__output_queue = queue.Queue()
|
||||
self.__auto_lib = AutoLib(
|
||||
self.__input_queue,
|
||||
self.__output_queue,
|
||||
)
|
||||
self.__config_paths = {
|
||||
"system":
|
||||
f"{QDir.toNativeSeparators(QFileInfo(sys.executable).absoluteDir().absoluteFilePath("system.json"))}",
|
||||
@@ -167,13 +163,11 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
def closeEvent(
|
||||
self,
|
||||
event: QCloseEvent,
|
||||
event: QCloseEvent
|
||||
):
|
||||
|
||||
if self.__timer and self.__timer.isActive():
|
||||
self.__timer.stop()
|
||||
if self.__auto_lib:
|
||||
self.__auto_lib.close()
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
super().closeEvent(event)
|
||||
@@ -181,7 +175,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
def appendToTextEdit(
|
||||
self,
|
||||
text: str,
|
||||
text: str
|
||||
):
|
||||
|
||||
cursor = self.MessageIOTextEdit.textCursor()
|
||||
@@ -206,7 +200,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
self,
|
||||
config_button_enabled: bool,
|
||||
start_button_enabled: bool,
|
||||
stop_button_enabled: bool,
|
||||
stop_button_enabled: bool
|
||||
):
|
||||
|
||||
self.ConfigButton.setEnabled(config_button_enabled)
|
||||
@@ -216,7 +210,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
@Slot()
|
||||
def showMsg(
|
||||
self,
|
||||
msg: str,
|
||||
msg: str
|
||||
):
|
||||
|
||||
self.appendToTextEdit(f"[{self.__class_name:<12}] >>> : {msg}")
|
||||
@@ -224,7 +218,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
@Slot()
|
||||
def showTrace(
|
||||
self,
|
||||
msg: str,
|
||||
msg: str
|
||||
):
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
@@ -232,7 +226,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
@Slot()
|
||||
def pollMsgQueue(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
try:
|
||||
@@ -245,7 +239,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
@Slot(dict)
|
||||
def onConfigWidgetClosed(
|
||||
self,
|
||||
config_paths: dict,
|
||||
config_paths: dict
|
||||
):
|
||||
|
||||
self.__alConfigWidget = None
|
||||
@@ -256,7 +250,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
@Slot()
|
||||
def onConfigButtonClicked(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
if self.__alConfigWidget is None:
|
||||
@@ -274,7 +268,7 @@ class ALMainWindow(QMainWindow, Ui_ALMainWindow):
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
self,
|
||||
self
|
||||
):
|
||||
|
||||
self.setControlButtons(False, False, True)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
this folder is used to store the config files.
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2025 KenanZhu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
This folder is used to store the model using by ddddocr.
|
||||
@@ -1 +0,0 @@
|
||||
This folder is used to store the models by ddddocr.
|
||||