zhs 0.1.1__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.
- zhs/__init__.py +3 -0
- zhs/__main__.py +309 -0
- zhs/ai/__init__.py +0 -0
- zhs/ai/course.py +425 -0
- zhs/ai/exam.py +303 -0
- zhs/ai/exam_base.py +426 -0
- zhs/ai/homework.py +207 -0
- zhs/ai/models.py +145 -0
- zhs/ai/ppt.py +87 -0
- zhs/ai/video.py +130 -0
- zhs/api/__init__.py +25 -0
- zhs/api/ai_analysis_api.py +123 -0
- zhs/api/encrypted_query.py +208 -0
- zhs/api/http_client.py +216 -0
- zhs/api/sso.py +49 -0
- zhs/api/zhidao_homework_api.py +161 -0
- zhs/cache/__init__.py +8 -0
- zhs/cache/ai_cache.py +69 -0
- zhs/cache/base.py +100 -0
- zhs/cache/zhidao_cache.py +174 -0
- zhs/cli/__init__.py +9 -0
- zhs/cli/bootstrap.py +183 -0
- zhs/cli/course_type.py +99 -0
- zhs/cli/services/__init__.py +8 -0
- zhs/cli/services/exam_service.py +104 -0
- zhs/cli/services/fetch_service.py +60 -0
- zhs/cli/services/homework_service.py +255 -0
- zhs/cli/services/play_service.py +191 -0
- zhs/config.py +351 -0
- zhs/crypto.py +134 -0
- zhs/exceptions.py +38 -0
- zhs/hike/__init__.py +0 -0
- zhs/hike/course.py +53 -0
- zhs/hike/models.py +34 -0
- zhs/hike/video.py +222 -0
- zhs/llm/__init__.py +0 -0
- zhs/llm/base.py +83 -0
- zhs/llm/factory.py +57 -0
- zhs/llm/openai.py +94 -0
- zhs/llm/prompts.py +186 -0
- zhs/llm/zhidao.py +287 -0
- zhs/logger.py +118 -0
- zhs/login.py +182 -0
- zhs/reporter.py +76 -0
- zhs/session.py +312 -0
- zhs/utils/__init__.py +0 -0
- zhs/utils/cookie.py +33 -0
- zhs/utils/display.py +188 -0
- zhs/utils/path.py +15 -0
- zhs/zhidao/__init__.py +1 -0
- zhs/zhidao/course.py +170 -0
- zhs/zhidao/homework/__init__.py +31 -0
- zhs/zhidao/homework/analyzer.py +259 -0
- zhs/zhidao/homework/cache.py +19 -0
- zhs/zhidao/homework/models.py +182 -0
- zhs/zhidao/homework/scanner.py +148 -0
- zhs/zhidao/homework/worker.py +710 -0
- zhs/zhidao/models.py +112 -0
- zhs/zhidao/quiz.py +99 -0
- zhs/zhidao/video.py +455 -0
- zhs-0.1.1.dist-info/METADATA +301 -0
- zhs-0.1.1.dist-info/RECORD +65 -0
- zhs-0.1.1.dist-info/WHEEL +4 -0
- zhs-0.1.1.dist-info/entry_points.txt +2 -0
- zhs-0.1.1.dist-info/licenses/LICENSE +674 -0
zhs/__init__.py
ADDED
zhs/__main__.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""ZHS CLI 入口
|
|
2
|
+
|
|
3
|
+
智慧树自动刷课工具,支持知到/Hike/AI 课程。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
zhs init 初始化配置
|
|
7
|
+
zhs login 扫码登录
|
|
8
|
+
zhs play 刷视频
|
|
9
|
+
zhs exam 考试
|
|
10
|
+
zhs homework 写作业
|
|
11
|
+
zhs fetch 获取课程列表
|
|
12
|
+
|
|
13
|
+
本模块仅保留 typer 命令声明与参数解析,业务逻辑全部委托给 zhs.cli 子包。
|
|
14
|
+
为兼容现有测试(tests/cli/test_main.py 通过 patch("zhs.__main__._run_*") 注入 mock),
|
|
15
|
+
所有从 cli 子包导入的函数均以带下划线前缀的别名暴露在模块命名空间。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from loguru import logger
|
|
20
|
+
|
|
21
|
+
from zhs.cli.bootstrap import do_login as _do_login
|
|
22
|
+
from zhs.cli.bootstrap import load_config_and_session as _load_config_and_session
|
|
23
|
+
from zhs.cli.bootstrap import parse_proxy as _parse_proxy
|
|
24
|
+
from zhs.cli.bootstrap import setup_logger as _setup_logger
|
|
25
|
+
from zhs.cli.bootstrap import try_restore_cookies as _try_restore_cookies # noqa: F401
|
|
26
|
+
from zhs.cli.course_type import detect_course_type as _detect_course_type
|
|
27
|
+
from zhs.cli.course_type import validate_course_type as _validate_course_type
|
|
28
|
+
from zhs.cli.services.exam_service import run_ai_exam as _run_ai_exam
|
|
29
|
+
from zhs.cli.services.fetch_service import fetch_course_list as _fetch_course_list
|
|
30
|
+
from zhs.cli.services.homework_service import run_ai_homework as _run_ai_homework
|
|
31
|
+
from zhs.cli.services.homework_service import run_ai_homework_by_str as _run_ai_homework_by_str
|
|
32
|
+
from zhs.cli.services.homework_service import run_all_homework as _run_all_homework
|
|
33
|
+
from zhs.cli.services.homework_service import run_homework_from_url as _run_homework_from_url
|
|
34
|
+
from zhs.cli.services.homework_service import run_zhidao_homework_by_course as _run_zhidao_homework_by_course
|
|
35
|
+
from zhs.cli.services.play_service import run_ai as _run_ai
|
|
36
|
+
from zhs.cli.services.play_service import run_ai_by_str as _run_ai_by_str
|
|
37
|
+
from zhs.cli.services.play_service import run_all as _run_all
|
|
38
|
+
from zhs.cli.services.play_service import run_hike as _run_hike
|
|
39
|
+
from zhs.cli.services.play_service import run_zhidao as _run_zhidao
|
|
40
|
+
from zhs.config import AppConfig, ConfigManager
|
|
41
|
+
from zhs.login import LoginManager
|
|
42
|
+
from zhs.session import ZhsSession
|
|
43
|
+
|
|
44
|
+
# 显式导出供 tests/cli/test_main.py 导入的别名
|
|
45
|
+
__all__ = [
|
|
46
|
+
"_detect_course_type",
|
|
47
|
+
"_validate_course_type",
|
|
48
|
+
"app",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
app = typer.Typer(name="zhs", help="智慧树自动刷课工具", no_args_is_help=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# zhs init
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command()
|
|
60
|
+
def init() -> None:
|
|
61
|
+
"""初始化 .zhs/ 目录及默认配置"""
|
|
62
|
+
from zhs.utils.path import get_data_dir
|
|
63
|
+
|
|
64
|
+
data_dir = get_data_dir()
|
|
65
|
+
config_mgr = ConfigManager()
|
|
66
|
+
|
|
67
|
+
# 创建目录结构
|
|
68
|
+
(data_dir / "cache").mkdir(parents=True, exist_ok=True)
|
|
69
|
+
(data_dir / "logs").mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
# 保存默认配置
|
|
72
|
+
config_path = config_mgr.config_path
|
|
73
|
+
if not config_path.exists():
|
|
74
|
+
config_mgr.save(AppConfig())
|
|
75
|
+
print(f"配置文件已创建: {config_path}")
|
|
76
|
+
else:
|
|
77
|
+
print(f"配置文件已存在: {config_path}")
|
|
78
|
+
|
|
79
|
+
print(f"数据目录: {data_dir}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# zhs login
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def login(
|
|
89
|
+
show_in_terminal: bool = typer.Option(False, "--show-in-terminal", help="终端显示二维码"), # noqa: B008
|
|
90
|
+
image_path: str | None = typer.Option(None, "--image-path", help="二维码保存路径"), # noqa: B008
|
|
91
|
+
proxy: str | None = typer.Option(None, "--proxy", help="代理"), # noqa: B008
|
|
92
|
+
debug: bool = typer.Option(False, "-d", "--debug", help="调试模式"), # noqa: B008
|
|
93
|
+
console_log: bool = typer.Option(False, "--console-log", help="日志输出到控制台"), # noqa: B008
|
|
94
|
+
) -> None:
|
|
95
|
+
"""扫码登录智慧树"""
|
|
96
|
+
from zhs.utils.display import msg_done, msg_info, msg_warn
|
|
97
|
+
|
|
98
|
+
config_mgr = ConfigManager()
|
|
99
|
+
config = config_mgr.load()
|
|
100
|
+
|
|
101
|
+
if image_path:
|
|
102
|
+
config.qr.image_path = image_path
|
|
103
|
+
if not config.qr.image_path:
|
|
104
|
+
from zhs.utils.path import get_data_dir
|
|
105
|
+
|
|
106
|
+
config.qr.image_path = str(get_data_dir() / "qrcode.png")
|
|
107
|
+
if proxy:
|
|
108
|
+
_parse_proxy(config, proxy)
|
|
109
|
+
|
|
110
|
+
_setup_logger(config, debug, console_log)
|
|
111
|
+
|
|
112
|
+
session = ZhsSession(config)
|
|
113
|
+
login_mgr = LoginManager(session, config)
|
|
114
|
+
|
|
115
|
+
print(msg_info("请使用智慧树 APP 扫描二维码登录"))
|
|
116
|
+
print(msg_warn(f"二维码已保存到: {config.qr.image_path}"))
|
|
117
|
+
_do_login(login_mgr, config, show_in_terminal)
|
|
118
|
+
print(msg_done("登录成功!Cookie 已保存,现在可以运行 zhs play 刷课了"))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# zhs play
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command()
|
|
127
|
+
def play(
|
|
128
|
+
course: list[str] | None = typer.Option(None, "-c", "--course", help="课程 ID"), # noqa: B008
|
|
129
|
+
course_type: str | None = typer.Option(None, "--type", help="课程类型: zhidao/hike/ai/auto"), # noqa: B008
|
|
130
|
+
ai_course: int | None = typer.Option(None, "--ai-course", help="AI 课程 courseId"), # noqa: B008
|
|
131
|
+
ai_class: int | None = typer.Option(None, "--ai-class", help="AI 课程 classId"), # noqa: B008
|
|
132
|
+
speed: float | None = typer.Option(None, "-s", "--speed", help="播放速度"), # noqa: B008
|
|
133
|
+
limit: int = typer.Option(0, "-l", "--limit", help="每门课程时间限制(分钟)", min=0), # noqa: B008
|
|
134
|
+
learn_optional: bool = typer.Option(False, "--learn-optional", help="AI 课程学习选学资源"), # noqa: B008
|
|
135
|
+
proxy: str | None = typer.Option(None, "--proxy", help="代理"), # noqa: B008
|
|
136
|
+
debug: bool = typer.Option(False, "-d", "--debug", help="调试模式"), # noqa: B008
|
|
137
|
+
console_log: bool = typer.Option(False, "--console-log", help="日志输出到控制台"), # noqa: B008
|
|
138
|
+
) -> None:
|
|
139
|
+
"""刷视频"""
|
|
140
|
+
result = _load_config_and_session(debug, console_log, proxy)
|
|
141
|
+
if result is None:
|
|
142
|
+
raise typer.Exit(1)
|
|
143
|
+
config, session = result
|
|
144
|
+
|
|
145
|
+
# 校验 --type
|
|
146
|
+
validated_type = _validate_course_type(course_type)
|
|
147
|
+
if course_type is not None and validated_type is None:
|
|
148
|
+
# 无效的 --type,不继续运行
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
|
|
151
|
+
# CLI 参数覆盖配置
|
|
152
|
+
if speed is not None:
|
|
153
|
+
config.video.zhidao_speed = speed
|
|
154
|
+
config.video.hike_speed = speed
|
|
155
|
+
config.video.ai_speed = speed
|
|
156
|
+
if limit:
|
|
157
|
+
config.limit = limit
|
|
158
|
+
if learn_optional:
|
|
159
|
+
config.video.ai_learn_optional = True
|
|
160
|
+
|
|
161
|
+
# AI 课程走 --ai-course + --ai-class
|
|
162
|
+
if ai_course is not None and ai_class is not None:
|
|
163
|
+
_run_ai(session, config, ai_course, ai_class)
|
|
164
|
+
elif course:
|
|
165
|
+
# 内联路由循环,确保 _run_zhidao/_run_hike/_run_ai_by_str 从 __main__ 命名空间查找
|
|
166
|
+
# (兼容 tests/cli/test_main.py 中 @patch("zhs.__main__._run_*") 的注入)
|
|
167
|
+
for c in course:
|
|
168
|
+
detected_type = _detect_course_type(c, validated_type)
|
|
169
|
+
try:
|
|
170
|
+
if detected_type == "zhidao":
|
|
171
|
+
_run_zhidao(session, config, c)
|
|
172
|
+
elif detected_type == "hike":
|
|
173
|
+
_run_hike(session, config, c)
|
|
174
|
+
elif detected_type == "ai":
|
|
175
|
+
_run_ai_by_str(session, config, c)
|
|
176
|
+
else:
|
|
177
|
+
print(f"未知的课程类型: {detected_type},跳过课程 {c}")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"课程 {c} 处理失败: {e}")
|
|
180
|
+
print(f"课程 {c} 处理失败: {e}")
|
|
181
|
+
else:
|
|
182
|
+
_run_all(session, config, validated_type)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# zhs homework
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command()
|
|
191
|
+
def homework(
|
|
192
|
+
course: list[str] | None = typer.Option(None, "-c", "--course", help="课程 ID"), # noqa: B008
|
|
193
|
+
course_type: str | None = typer.Option(None, "--type", help="课程类型: zhidao/ai/auto"), # noqa: B008
|
|
194
|
+
url: str | None = typer.Option(None, "--url", help="作业 URL(从浏览器复制)"), # noqa: B008
|
|
195
|
+
ai_course: int | None = typer.Option(None, "--ai-course", help="AI 课程 courseId"), # noqa: B008
|
|
196
|
+
ai_class: int | None = typer.Option(None, "--ai-class", help="AI 课程 classId"), # noqa: B008
|
|
197
|
+
no_ai: bool = typer.Option(False, "--no-ai", help="不使用 AI 模型(随机生成)"), # noqa: B008
|
|
198
|
+
homework_threshold: int | None = typer.Option(None, "--homework-threshold", help="满分阈值百分比(0-100)"), # noqa: B008
|
|
199
|
+
max_submit: int | None = typer.Option(None, "--max-submit", help="最大提交次数"), # noqa: B008
|
|
200
|
+
proxy: str | None = typer.Option(None, "--proxy", help="代理"), # noqa: B008
|
|
201
|
+
debug: bool = typer.Option(False, "-d", "--debug", help="调试模式"), # noqa: B008
|
|
202
|
+
console_log: bool = typer.Option(False, "--console-log", help="日志输出到控制台"), # noqa: B008
|
|
203
|
+
) -> None:
|
|
204
|
+
"""写作业"""
|
|
205
|
+
result = _load_config_and_session(debug, console_log, proxy)
|
|
206
|
+
if result is None:
|
|
207
|
+
raise typer.Exit(1)
|
|
208
|
+
config, session = result
|
|
209
|
+
|
|
210
|
+
# 校验 --type
|
|
211
|
+
validated_type = _validate_course_type(course_type)
|
|
212
|
+
if course_type is not None and validated_type is None:
|
|
213
|
+
raise typer.Exit(1)
|
|
214
|
+
|
|
215
|
+
# CLI 参数覆盖配置
|
|
216
|
+
if no_ai:
|
|
217
|
+
config.ai.enabled = False
|
|
218
|
+
if homework_threshold is not None:
|
|
219
|
+
config.homework.threshold = homework_threshold
|
|
220
|
+
if max_submit is not None:
|
|
221
|
+
config.homework.max_submit = max_submit
|
|
222
|
+
|
|
223
|
+
# --url 模式:直接指定作业
|
|
224
|
+
if url:
|
|
225
|
+
try:
|
|
226
|
+
_run_homework_from_url(session, config, url)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"URL 作业处理失败: {e}")
|
|
229
|
+
print(f"URL 作业处理失败: {e}")
|
|
230
|
+
raise typer.Exit(1) from e
|
|
231
|
+
# AI 课程走 --ai-course + --ai-class
|
|
232
|
+
elif ai_course is not None and ai_class is not None:
|
|
233
|
+
try:
|
|
234
|
+
_run_ai_homework(session, config, ai_course, ai_class)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"AI 课程 {ai_course} 作业处理失败: {e}")
|
|
237
|
+
print(f"AI 课程 {ai_course} 作业处理失败: {e}")
|
|
238
|
+
elif course:
|
|
239
|
+
for c in course:
|
|
240
|
+
detected_type = _detect_course_type(c, validated_type)
|
|
241
|
+
try:
|
|
242
|
+
if detected_type == "ai":
|
|
243
|
+
_run_ai_homework_by_str(session, config, c)
|
|
244
|
+
elif detected_type == "zhidao":
|
|
245
|
+
_run_zhidao_homework_by_course(session, config, c)
|
|
246
|
+
else:
|
|
247
|
+
logger.warning(f"暂不支持 {detected_type} 课程的作业功能")
|
|
248
|
+
print(f"暂不支持 {detected_type} 课程的作业功能")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.error(f"课程 {c} 作业处理失败: {e}")
|
|
251
|
+
print(f"课程 {c} 作业处理失败: {e}")
|
|
252
|
+
else:
|
|
253
|
+
_run_all_homework(session, config, validated_type)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
# zhs exam
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command()
|
|
262
|
+
def exam(
|
|
263
|
+
course: list[str] | None = typer.Option(None, "-c", "--course", help="课程 ID"), # noqa: B008
|
|
264
|
+
course_type: str | None = typer.Option(None, "--type", help="课程类型: zhidao/ai/auto"), # noqa: B008
|
|
265
|
+
ai_course: int | None = typer.Option(None, "--ai-course", help="AI 课程 courseId"), # noqa: B008
|
|
266
|
+
ai_class: int | None = typer.Option(None, "--ai-class", help="AI 课程 classId"), # noqa: B008
|
|
267
|
+
submit: bool = typer.Option(False, "--submit", help="答题后提交考试(默认不提交)"), # noqa: B008
|
|
268
|
+
proxy: str | None = typer.Option(None, "--proxy", help="代理"), # noqa: B008
|
|
269
|
+
debug: bool = typer.Option(False, "-d", "--debug", help="调试模式"), # noqa: B008
|
|
270
|
+
console_log: bool = typer.Option(False, "--console-log", help="日志输出到控制台"), # noqa: B008
|
|
271
|
+
) -> None:
|
|
272
|
+
"""AI 课程考试"""
|
|
273
|
+
result = _load_config_and_session(debug, console_log, proxy)
|
|
274
|
+
if result is None:
|
|
275
|
+
raise typer.Exit(1)
|
|
276
|
+
config, session = result
|
|
277
|
+
|
|
278
|
+
from zhs.utils.display import msg_warn
|
|
279
|
+
|
|
280
|
+
if course_type != "ai" and not ai_course:
|
|
281
|
+
print(msg_warn("目前仅支持 AI 课程考试,请使用 --type ai 或 --ai-course 指定"))
|
|
282
|
+
raise typer.Exit(1)
|
|
283
|
+
|
|
284
|
+
_run_ai_exam(session, config, ai_course, ai_class, submit)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# zhs fetch
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.command()
|
|
293
|
+
def fetch(
|
|
294
|
+
fetch_type: str = typer.Option("all", "--type", help="数据类型: all/course/homework"), # noqa: B008
|
|
295
|
+
proxy: str | None = typer.Option(None, "--proxy", help="代理"), # noqa: B008
|
|
296
|
+
debug: bool = typer.Option(False, "-d", "--debug", help="调试模式"), # noqa: B008
|
|
297
|
+
console_log: bool = typer.Option(False, "--console-log", help="日志输出到控制台"), # noqa: B008
|
|
298
|
+
) -> None:
|
|
299
|
+
"""获取课程数据"""
|
|
300
|
+
result = _load_config_and_session(debug, console_log, proxy)
|
|
301
|
+
if result is None:
|
|
302
|
+
raise typer.Exit(1)
|
|
303
|
+
config, session = result
|
|
304
|
+
|
|
305
|
+
_fetch_course_list(session, fetch_type)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
app()
|
zhs/ai/__init__.py
ADDED
|
File without changes
|