buaalogin-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- buaalogin_cli/__init__.py +1 -0
- buaalogin_cli/__main__.py +12 -0
- buaalogin_cli/cli.py +227 -0
- buaalogin_cli/config.py +47 -0
- buaalogin_cli/constants.py +15 -0
- buaalogin_cli/log.py +67 -0
- buaalogin_cli/service.py +203 -0
- buaalogin_cli-0.1.0.dist-info/METADATA +11 -0
- buaalogin_cli-0.1.0.dist-info/RECORD +11 -0
- buaalogin_cli-0.1.0.dist-info/WHEEL +4 -0
- buaalogin_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""北航校园网自动登录工具"""
|
buaalogin_cli/cli.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""BUAA 校园网自动登录 CLI 工具"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from . import service
|
|
6
|
+
from .config import config
|
|
7
|
+
from .constants import CONFIG_FILE, LOG_FILE
|
|
8
|
+
from .log import setup_console
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
help="BUAA 校园网自动登录工具",
|
|
12
|
+
add_completion=False,
|
|
13
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.callback(invoke_without_command=True)
|
|
18
|
+
def callback(ctx: typer.Context):
|
|
19
|
+
"""BUAA 校园网登录工具。
|
|
20
|
+
|
|
21
|
+
在任何子命令执行前调用,加载配置文件并设置默认参数值。
|
|
22
|
+
如果未指定子命令则显示帮助信息。
|
|
23
|
+
"""
|
|
24
|
+
# 这些值会覆盖子命令中的参数默认值
|
|
25
|
+
file_config = config.to_dict()
|
|
26
|
+
|
|
27
|
+
# 子命令会继承配置文件的值(如果命令行未显式指定参数)
|
|
28
|
+
ctx.default_map = {
|
|
29
|
+
"login": file_config,
|
|
30
|
+
"run": file_config,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# 若未指定子命令,显示帮助信息后退出
|
|
34
|
+
if ctx.invoked_subcommand is None:
|
|
35
|
+
typer.echo(ctx.get_help())
|
|
36
|
+
raise typer.Exit()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("login")
|
|
40
|
+
def login_cmd(
|
|
41
|
+
username: str | None = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--user",
|
|
44
|
+
"-u",
|
|
45
|
+
envvar="BUAA_USERNAME",
|
|
46
|
+
metavar="学号",
|
|
47
|
+
help="校园网账号",
|
|
48
|
+
show_default=False,
|
|
49
|
+
),
|
|
50
|
+
password: str | None = typer.Option(
|
|
51
|
+
None,
|
|
52
|
+
"--pass",
|
|
53
|
+
"-p",
|
|
54
|
+
envvar="BUAA_PASSWORD",
|
|
55
|
+
metavar="密码",
|
|
56
|
+
help="校园网密码",
|
|
57
|
+
show_default=False,
|
|
58
|
+
),
|
|
59
|
+
headless: bool = typer.Option(
|
|
60
|
+
True,
|
|
61
|
+
"--headless/--headed",
|
|
62
|
+
help="是否使用无头模式运行浏览器",
|
|
63
|
+
),
|
|
64
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="显示详细调试信息"),
|
|
65
|
+
):
|
|
66
|
+
"""执行单次登录。"""
|
|
67
|
+
setup_console(verbose=verbose)
|
|
68
|
+
_do_login_cmd(username, password, headless)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("run")
|
|
72
|
+
def run_cmd(
|
|
73
|
+
username: str | None = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--user",
|
|
76
|
+
"-u",
|
|
77
|
+
envvar="BUAA_USERNAME",
|
|
78
|
+
metavar="学号",
|
|
79
|
+
help="校园网账号",
|
|
80
|
+
show_default=False,
|
|
81
|
+
),
|
|
82
|
+
password: str | None = typer.Option(
|
|
83
|
+
None,
|
|
84
|
+
"--pass",
|
|
85
|
+
"-p",
|
|
86
|
+
envvar="BUAA_PASSWORD",
|
|
87
|
+
metavar="密码",
|
|
88
|
+
help="校园网密码",
|
|
89
|
+
show_default=False,
|
|
90
|
+
),
|
|
91
|
+
interval: int = typer.Option(
|
|
92
|
+
5,
|
|
93
|
+
"--interval",
|
|
94
|
+
"-i",
|
|
95
|
+
envvar="BUAA_CHECK_INTERVAL",
|
|
96
|
+
metavar="分钟",
|
|
97
|
+
min=1,
|
|
98
|
+
help="检测间隔",
|
|
99
|
+
),
|
|
100
|
+
headless: bool = typer.Option(
|
|
101
|
+
True,
|
|
102
|
+
"--headless/--headed",
|
|
103
|
+
help="是否使用无头模式运行浏览器",
|
|
104
|
+
),
|
|
105
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="显示详细调试信息"),
|
|
106
|
+
):
|
|
107
|
+
"""持续保持在线,定期检测并自动重连。"""
|
|
108
|
+
setup_console(verbose=verbose)
|
|
109
|
+
|
|
110
|
+
passwd = password
|
|
111
|
+
|
|
112
|
+
if username is None or passwd is None:
|
|
113
|
+
typer.secho("❌ 缺少账号或密码", fg=typer.colors.RED)
|
|
114
|
+
typer.echo("\n请通过以下方式之一提供凭据:")
|
|
115
|
+
typer.echo(" 1. 运行 `buaalogin config` 配置")
|
|
116
|
+
typer.echo(" 2. 使用命令行参数: --user <账号> --pass <密码>")
|
|
117
|
+
typer.echo(" 3. 设置环境变量: BUAA_USERNAME, BUAA_PASSWORD")
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
service.keep_alive(username, passwd, interval, headless=headless)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command("config")
|
|
124
|
+
def config_cmd(
|
|
125
|
+
username: str | None = typer.Option(
|
|
126
|
+
None, "--user", "-u", metavar="学号", help="校园网账号"
|
|
127
|
+
),
|
|
128
|
+
password: str | None = typer.Option(
|
|
129
|
+
None, "--pass", "-p", metavar="密码", help="校园网密码"
|
|
130
|
+
),
|
|
131
|
+
interval: int | None = typer.Option(
|
|
132
|
+
None, "--interval", "-i", metavar="分钟", min=1, help="保活检测间隔"
|
|
133
|
+
),
|
|
134
|
+
show: bool = typer.Option(
|
|
135
|
+
False, "--show", "-s", is_flag=True, help="仅显示当前配置", show_default=True
|
|
136
|
+
),
|
|
137
|
+
):
|
|
138
|
+
"""配置账户信息。"""
|
|
139
|
+
if show:
|
|
140
|
+
typer.secho(f"配置文件: {CONFIG_FILE}", fg=typer.colors.CYAN)
|
|
141
|
+
saved = config.to_dict()
|
|
142
|
+
if saved:
|
|
143
|
+
for key, value in saved.items():
|
|
144
|
+
typer.echo(f" {key} = {value}")
|
|
145
|
+
else:
|
|
146
|
+
typer.echo(" (尚未配置)")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# 交互式输入
|
|
150
|
+
if not username:
|
|
151
|
+
username = typer.prompt("请输入 BUAA 学号")
|
|
152
|
+
while not username:
|
|
153
|
+
typer.secho("学号不能为空", fg=typer.colors.RED)
|
|
154
|
+
username = typer.prompt("请输入 BUAA 学号")
|
|
155
|
+
if not password:
|
|
156
|
+
password = typer.prompt("请输入密码")
|
|
157
|
+
while not password:
|
|
158
|
+
typer.secho("密码不能为空", fg=typer.colors.RED)
|
|
159
|
+
password = typer.prompt("请输入密码")
|
|
160
|
+
|
|
161
|
+
# 更新配置并保存
|
|
162
|
+
config.username = username
|
|
163
|
+
config.password = password
|
|
164
|
+
if interval is not None:
|
|
165
|
+
config.interval = interval
|
|
166
|
+
config.save_to_json(CONFIG_FILE)
|
|
167
|
+
typer.secho("✅ 配置已保存!", fg=typer.colors.GREEN)
|
|
168
|
+
typer.echo(f" 位置: {CONFIG_FILE}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("status")
|
|
172
|
+
def status_cmd():
|
|
173
|
+
"""检查当前网络连接状态。退出码:已登录=0,未登录=1。"""
|
|
174
|
+
if service.get_status() == service.NetworkStatus.LOGGED_IN:
|
|
175
|
+
typer.secho("✅ 网络正常", fg=typer.colors.GREEN)
|
|
176
|
+
raise typer.Exit(0)
|
|
177
|
+
else:
|
|
178
|
+
typer.secho("❌ 未登录或无法访问外网", fg=typer.colors.RED)
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command("info")
|
|
183
|
+
def info_cmd():
|
|
184
|
+
"""显示配置文件和日志文件的存储位置。"""
|
|
185
|
+
config_file = CONFIG_FILE
|
|
186
|
+
|
|
187
|
+
typer.echo("配置文件:")
|
|
188
|
+
typer.echo(f" {config_file}")
|
|
189
|
+
if config_file.exists():
|
|
190
|
+
typer.secho(" ✅ 已存在", fg=typer.colors.GREEN)
|
|
191
|
+
else:
|
|
192
|
+
typer.secho(" ⚠️ 未配置", fg=typer.colors.YELLOW)
|
|
193
|
+
|
|
194
|
+
typer.echo()
|
|
195
|
+
typer.echo("日志文件:")
|
|
196
|
+
typer.echo(f" {LOG_FILE}")
|
|
197
|
+
if LOG_FILE.exists():
|
|
198
|
+
size = LOG_FILE.stat().st_size
|
|
199
|
+
typer.secho(f" ✅ 文件大小: {size / 1024:.1f} KB", fg=typer.colors.GREEN)
|
|
200
|
+
else:
|
|
201
|
+
typer.secho(" 📝 尚未生成", fg=typer.colors.BLUE)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _do_login_cmd(
|
|
205
|
+
cli_username: str | None,
|
|
206
|
+
cli_pass: str | None,
|
|
207
|
+
headless: bool = True,
|
|
208
|
+
):
|
|
209
|
+
"""执行单次登录的 CLI 逻辑(含错误提示)。"""
|
|
210
|
+
if cli_username is None or cli_pass is None:
|
|
211
|
+
typer.secho("❌ 缺少账号或密码", fg=typer.colors.RED)
|
|
212
|
+
typer.echo("\n请通过以下方式之一提供凭据:")
|
|
213
|
+
typer.echo(" 1. 运行 `buaalogin config` 配置")
|
|
214
|
+
typer.echo(" 2. 使用命令行参数: --user <账号> --pass <密码>")
|
|
215
|
+
typer.echo(" 3. 设置环境变量: BUAA_USERNAME, BUAA_PASSWORD")
|
|
216
|
+
raise typer.Exit(1)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
service.login(cli_username, cli_pass, headless=headless)
|
|
220
|
+
typer.secho("✅ 登录成功", fg=typer.colors.GREEN)
|
|
221
|
+
except service.LoginError as e:
|
|
222
|
+
typer.secho(f"❌ 登录失败: {e}", fg=typer.colors.RED)
|
|
223
|
+
raise typer.Exit(1) from None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
app()
|
buaalogin_cli/config.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""配置管理模块:配置加载/保存、优先级合并"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from msgspec import UNSET, Struct, UnsetType, structs
|
|
9
|
+
from msgspec import json as msgjson
|
|
10
|
+
|
|
11
|
+
from .constants import CONFIG_FILE
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Config(Struct, omit_defaults=True):
|
|
15
|
+
"""用户配置,保存到配置文件。
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
username: 校园网账号。
|
|
19
|
+
password: 校园网密码。
|
|
20
|
+
interval: 检测间隔(分钟)。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
username: str | UnsetType = UNSET
|
|
24
|
+
password: str | UnsetType = UNSET
|
|
25
|
+
interval: int | UnsetType = UNSET
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def load_from_json(cls, file_path: str | Path) -> Config:
|
|
29
|
+
"""从 JSON 文件加载配置,文件不存在时返回空配置。"""
|
|
30
|
+
try:
|
|
31
|
+
bytes = Path(file_path).read_bytes()
|
|
32
|
+
return msgjson.decode(bytes, type=Config)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return cls()
|
|
35
|
+
|
|
36
|
+
def save_to_json(self, file_path: Path | str) -> int:
|
|
37
|
+
"""保存当前配置到 JSON 文件。"""
|
|
38
|
+
bytes = msgjson.encode(self) + b"\n"
|
|
39
|
+
return Path(file_path).write_bytes(bytes)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
"""导出为字典(过滤 UNSET 值)。"""
|
|
43
|
+
return {k: v for k, v in structs.asdict(self).items() if v is not UNSET}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 全局配置实例
|
|
47
|
+
config = Config.load_from_json(CONFIG_FILE)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""常量模块:路径、URL"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from platformdirs import user_config_dir, user_log_dir
|
|
6
|
+
|
|
7
|
+
APP_NAME = "buaalogin-cli"
|
|
8
|
+
|
|
9
|
+
# 文件路径
|
|
10
|
+
CONFIG_FILE = Path(user_config_dir(APP_NAME, ensure_exists=True)) / "config.json"
|
|
11
|
+
LOG_FILE = Path(user_log_dir(APP_NAME, ensure_exists=True)) / f"{APP_NAME}.log"
|
|
12
|
+
|
|
13
|
+
# URL
|
|
14
|
+
GATEWAY_URL = "https://gw.buaa.edu.cn"
|
|
15
|
+
LOGIN_URL = GATEWAY_URL
|
buaalogin_cli/log.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""日志配置模块:集中管理日志设置,供所有模块导入使用"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from .constants import LOG_FILE
|
|
8
|
+
|
|
9
|
+
# 移除 loguru 默认的 stderr handler,稍后重新配置
|
|
10
|
+
logger.remove()
|
|
11
|
+
|
|
12
|
+
# 日志格式
|
|
13
|
+
LOG_FORMAT_CONSOLE = (
|
|
14
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
15
|
+
"<level>{level:<8}</level> | "
|
|
16
|
+
"<cyan>[{extra[trigger]}]</cyan> "
|
|
17
|
+
"<level>{message}</level>"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
LOG_FORMAT_FILE = (
|
|
21
|
+
"{time:YYYY-MM-DD HH:mm:ss} | {level:<8} | [{extra[trigger]:<8}] | "
|
|
22
|
+
"{name}:{function}:{line} - {message}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# 配置默认 trigger(防止 KeyError)
|
|
26
|
+
logger.configure(extra={"trigger": "unknown"})
|
|
27
|
+
|
|
28
|
+
# 文件输出:永远记录 DEBUG 级别(带轮转和压缩)
|
|
29
|
+
logger.add(
|
|
30
|
+
LOG_FILE,
|
|
31
|
+
format=LOG_FORMAT_FILE,
|
|
32
|
+
level="DEBUG", # 文件永远记录全量日志
|
|
33
|
+
encoding="utf-8",
|
|
34
|
+
rotation="10 MB",
|
|
35
|
+
retention="7 days",
|
|
36
|
+
compression="zip",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 控制台 handler ID,用于动态切换级别
|
|
40
|
+
_console_handler_id: int | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def setup_console(verbose: bool = False) -> None:
|
|
44
|
+
"""配置输出级别
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
verbose: True 时输出 DEBUG 级别,否则输出 INFO 级别
|
|
48
|
+
"""
|
|
49
|
+
global _console_handler_id
|
|
50
|
+
|
|
51
|
+
# 移除旧的控制台 handler
|
|
52
|
+
if _console_handler_id is not None:
|
|
53
|
+
logger.remove(_console_handler_id)
|
|
54
|
+
|
|
55
|
+
level = "DEBUG" if verbose else "INFO"
|
|
56
|
+
_console_handler_id = logger.add(
|
|
57
|
+
sys.stderr,
|
|
58
|
+
format=LOG_FORMAT_CONSOLE,
|
|
59
|
+
level=level,
|
|
60
|
+
colorize=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# 默认 INFO 级别控制台输出
|
|
65
|
+
setup_console(verbose=False)
|
|
66
|
+
|
|
67
|
+
__all__ = ["logger", "setup_console"]
|
buaalogin_cli/service.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""网络状态检测、登录、持续保活"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from playwright.sync_api import TimeoutError as PlaywrightTimeout
|
|
9
|
+
from playwright.sync_api import sync_playwright
|
|
10
|
+
|
|
11
|
+
from .constants import GATEWAY_URL, LOG_FILE, LOGIN_URL
|
|
12
|
+
from .log import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LoginError(Exception):
|
|
16
|
+
"""登录失败异常。"""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NetworkStatus(Enum):
|
|
22
|
+
"""网络状态枚举。"""
|
|
23
|
+
|
|
24
|
+
UNKNOWN_NETWORK = auto() # 非校园网环境(DNS 解析失败或超时)
|
|
25
|
+
LOGGED_OUT = auto() # 校园网环境,未登录
|
|
26
|
+
LOGGED_IN = auto() # 校园网环境,已登录
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# region 网络状态检测
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_status() -> NetworkStatus:
|
|
33
|
+
"""获取当前网络状态。
|
|
34
|
+
|
|
35
|
+
通过访问校园网网关 (https://gw.buaa.edu.cn/) 检测:
|
|
36
|
+
- 请求失败(DNS/超时)→ UNKNOWN_NETWORK
|
|
37
|
+
- URL 包含 "success" → LOGGED_IN
|
|
38
|
+
- 其他情况 → LOGGED_OUT
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
NetworkStatus 枚举值。
|
|
42
|
+
"""
|
|
43
|
+
log = logger.bind(trigger="status")
|
|
44
|
+
log.debug("正在检测网络状态...")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
response = requests.get(
|
|
48
|
+
GATEWAY_URL,
|
|
49
|
+
headers={"User-Agent": "Mozilla/5.0"},
|
|
50
|
+
timeout=5,
|
|
51
|
+
allow_redirects=True,
|
|
52
|
+
)
|
|
53
|
+
final_url = response.url
|
|
54
|
+
|
|
55
|
+
if "success" in final_url.lower():
|
|
56
|
+
log.debug(f"网络状态: 已登录 (URL: {final_url})")
|
|
57
|
+
return NetworkStatus.LOGGED_IN
|
|
58
|
+
else:
|
|
59
|
+
log.debug(f"网络状态: 未登录 (URL: {final_url})")
|
|
60
|
+
return NetworkStatus.LOGGED_OUT
|
|
61
|
+
|
|
62
|
+
except requests.RequestException as e:
|
|
63
|
+
log.debug(f"网络状态: 非校园网环境 ({e})")
|
|
64
|
+
return NetworkStatus.UNKNOWN_NETWORK
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# endregion
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# region 登录
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def login(username: str, password: str, *, headless: bool = True) -> None:
|
|
74
|
+
"""使用 Playwright 模拟浏览器登录校园网。
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
username: 用户名。
|
|
78
|
+
password: 密码。
|
|
79
|
+
headless: 是否使用无头模式。
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
LoginError: 登录失败时抛出,包含错误信息。
|
|
83
|
+
"""
|
|
84
|
+
log = logger.bind(trigger="login")
|
|
85
|
+
|
|
86
|
+
# 先快速检查状态,避免不必要地启动浏览器
|
|
87
|
+
status = get_status()
|
|
88
|
+
if status == NetworkStatus.LOGGED_IN:
|
|
89
|
+
log.info("已经处于登录状态")
|
|
90
|
+
return
|
|
91
|
+
if status == NetworkStatus.UNKNOWN_NETWORK:
|
|
92
|
+
raise LoginError("未检测到校园网环境")
|
|
93
|
+
|
|
94
|
+
# 只有 LOGGED_OUT 时才启动浏览器
|
|
95
|
+
with sync_playwright() as p:
|
|
96
|
+
browser_path = p.chromium.executable_path
|
|
97
|
+
log.debug(f"使用浏览器: {browser_path}")
|
|
98
|
+
|
|
99
|
+
browser = p.chromium.launch(headless=headless, executable_path=browser_path)
|
|
100
|
+
context = browser.new_context()
|
|
101
|
+
page = context.new_page()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
log.info("正在打开登录页面...")
|
|
105
|
+
page.goto(LOGIN_URL, timeout=30000)
|
|
106
|
+
page.wait_for_load_state("networkidle", timeout=10000)
|
|
107
|
+
|
|
108
|
+
# 填写用户名和密码
|
|
109
|
+
log.debug("正在填写登录信息...")
|
|
110
|
+
page.locator("#username:visible").fill(username)
|
|
111
|
+
page.locator("#password:visible").fill(password)
|
|
112
|
+
|
|
113
|
+
# 点击登录按钮
|
|
114
|
+
log.info("正在提交登录...")
|
|
115
|
+
page.locator("#login").click()
|
|
116
|
+
|
|
117
|
+
page.wait_for_timeout(3000)
|
|
118
|
+
|
|
119
|
+
# 检查登录结果
|
|
120
|
+
if "success" in page.url.lower():
|
|
121
|
+
log.success("登录成功!")
|
|
122
|
+
return
|
|
123
|
+
else:
|
|
124
|
+
error_msg = _get_error_message(page)
|
|
125
|
+
log.warning(f"登录失败:{error_msg}")
|
|
126
|
+
raise LoginError(error_msg)
|
|
127
|
+
|
|
128
|
+
except PlaywrightTimeout as e:
|
|
129
|
+
log.error(f"页面加载超时:{e}")
|
|
130
|
+
raise LoginError(f"页面加载超时:{e}") from e
|
|
131
|
+
except LoginError:
|
|
132
|
+
raise
|
|
133
|
+
except Exception as e:
|
|
134
|
+
log.error(f"登录过程出错:{e}")
|
|
135
|
+
raise LoginError(f"{e}") from e
|
|
136
|
+
finally:
|
|
137
|
+
log.debug("正在关闭浏览器...")
|
|
138
|
+
browser.close()
|
|
139
|
+
log.debug("浏览器已关闭")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_error_message(page) -> str:
|
|
143
|
+
"""获取登录错误信息。"""
|
|
144
|
+
try:
|
|
145
|
+
# layer.js 弹窗错误信息
|
|
146
|
+
elem = page.locator(".layui-layer-content").first
|
|
147
|
+
if elem.is_visible(timeout=1000):
|
|
148
|
+
return elem.text_content() or "未知错误"
|
|
149
|
+
return "未知错误"
|
|
150
|
+
except Exception:
|
|
151
|
+
return "无法获取错误信息"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# endregion
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# region 持续保活
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def keep_alive(
|
|
161
|
+
username: str, password: str, check_interval_min: int, *, headless: bool = True
|
|
162
|
+
):
|
|
163
|
+
"""持续保持在线,检查登录状态并自动重连。
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
username: 校园网用户名。
|
|
167
|
+
password: 校园网密码。
|
|
168
|
+
check_interval_min: 检查间隔(分钟)。
|
|
169
|
+
headless: 是否使用无头模式运行浏览器。
|
|
170
|
+
"""
|
|
171
|
+
log = logger.bind(trigger="run")
|
|
172
|
+
check_interval_sec = check_interval_min * 60
|
|
173
|
+
|
|
174
|
+
log.info(f"保活服务已启动,检查间隔: {check_interval_min} 分钟")
|
|
175
|
+
log.info(f"使用账户: {username}")
|
|
176
|
+
log.info(f"日志文件: {LOG_FILE}")
|
|
177
|
+
|
|
178
|
+
while True:
|
|
179
|
+
try:
|
|
180
|
+
status = get_status()
|
|
181
|
+
|
|
182
|
+
if status == NetworkStatus.UNKNOWN_NETWORK:
|
|
183
|
+
log.warning("未检测到校园网环境,等待下次检查...")
|
|
184
|
+
elif status == NetworkStatus.LOGGED_IN:
|
|
185
|
+
log.info("已登录,无需操作")
|
|
186
|
+
else: # LOGGED_OUT
|
|
187
|
+
log.warning("未登录,正在启动浏览器登录...")
|
|
188
|
+
try:
|
|
189
|
+
login(username, password, headless=headless)
|
|
190
|
+
log.success("登录成功")
|
|
191
|
+
except LoginError as e:
|
|
192
|
+
log.warning(f"登录未成功: {e}")
|
|
193
|
+
|
|
194
|
+
time.sleep(check_interval_sec)
|
|
195
|
+
except KeyboardInterrupt:
|
|
196
|
+
log.info("User Exit.")
|
|
197
|
+
sys.exit(0)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
log.error(f"发生错误: {e}")
|
|
200
|
+
time.sleep(10)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# endregion
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: buaalogin-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Dist: playwright>=1.40.0
|
|
5
|
+
Requires-Dist: typer>=0.9.0
|
|
6
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
7
|
+
Requires-Dist: loguru>=0.7.0
|
|
8
|
+
Requires-Dist: msgspec>=0.18.0
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Project-URL: Homepage, https://github.com/Misty02600/buaalogin-cli
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
buaalogin_cli/__init__.py,sha256=xJgs3LxemEaP9JsJ-BA9OhDJ78LozOw8i3HmHjDM9b0,40
|
|
2
|
+
buaalogin_cli/__main__.py,sha256=tlTuBxuEzJKIPa5Js84z86QLffGdkt0gI7ZG9SzbpTI,192
|
|
3
|
+
buaalogin_cli/cli.py,sha256=2pYLu7-vu5cyOpsXNiApxuHO5YLt8JN6Co40rsw2A9s,7019
|
|
4
|
+
buaalogin_cli/config.py,sha256=9c4ATq7qTuOMnNbt9JkvlSrRvOLWxKvRHK7ghmzBuJs,1414
|
|
5
|
+
buaalogin_cli/constants.py,sha256=Z7X0gF92Vr9lbCU3vMoFBU2S1OgHzuUg3rD5BNqQES4,392
|
|
6
|
+
buaalogin_cli/log.py,sha256=PkaBWkYbikqraOAxN5JHHuSi-M7_qI55oQCX84qGm5Q,1632
|
|
7
|
+
buaalogin_cli/service.py,sha256=PTQM-vKjXcxTyTmahnpTm9-kuDRt9OTJ-g7MW4aKmcU,6083
|
|
8
|
+
buaalogin_cli-0.1.0.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
|
|
9
|
+
buaalogin_cli-0.1.0.dist-info/entry_points.txt,sha256=saYSPX9fiGodsJR_0BNpG3SYj7fH5iEYF92172dHTjA,59
|
|
10
|
+
buaalogin_cli-0.1.0.dist-info/METADATA,sha256=hKFeoNe9xeSyfqDIF3ui7xgmu5loXW7zyjl8eF9sRtM,337
|
|
11
|
+
buaalogin_cli-0.1.0.dist-info/RECORD,,
|