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.
Files changed (65) hide show
  1. zhs/__init__.py +3 -0
  2. zhs/__main__.py +309 -0
  3. zhs/ai/__init__.py +0 -0
  4. zhs/ai/course.py +425 -0
  5. zhs/ai/exam.py +303 -0
  6. zhs/ai/exam_base.py +426 -0
  7. zhs/ai/homework.py +207 -0
  8. zhs/ai/models.py +145 -0
  9. zhs/ai/ppt.py +87 -0
  10. zhs/ai/video.py +130 -0
  11. zhs/api/__init__.py +25 -0
  12. zhs/api/ai_analysis_api.py +123 -0
  13. zhs/api/encrypted_query.py +208 -0
  14. zhs/api/http_client.py +216 -0
  15. zhs/api/sso.py +49 -0
  16. zhs/api/zhidao_homework_api.py +161 -0
  17. zhs/cache/__init__.py +8 -0
  18. zhs/cache/ai_cache.py +69 -0
  19. zhs/cache/base.py +100 -0
  20. zhs/cache/zhidao_cache.py +174 -0
  21. zhs/cli/__init__.py +9 -0
  22. zhs/cli/bootstrap.py +183 -0
  23. zhs/cli/course_type.py +99 -0
  24. zhs/cli/services/__init__.py +8 -0
  25. zhs/cli/services/exam_service.py +104 -0
  26. zhs/cli/services/fetch_service.py +60 -0
  27. zhs/cli/services/homework_service.py +255 -0
  28. zhs/cli/services/play_service.py +191 -0
  29. zhs/config.py +351 -0
  30. zhs/crypto.py +134 -0
  31. zhs/exceptions.py +38 -0
  32. zhs/hike/__init__.py +0 -0
  33. zhs/hike/course.py +53 -0
  34. zhs/hike/models.py +34 -0
  35. zhs/hike/video.py +222 -0
  36. zhs/llm/__init__.py +0 -0
  37. zhs/llm/base.py +83 -0
  38. zhs/llm/factory.py +57 -0
  39. zhs/llm/openai.py +94 -0
  40. zhs/llm/prompts.py +186 -0
  41. zhs/llm/zhidao.py +287 -0
  42. zhs/logger.py +118 -0
  43. zhs/login.py +182 -0
  44. zhs/reporter.py +76 -0
  45. zhs/session.py +312 -0
  46. zhs/utils/__init__.py +0 -0
  47. zhs/utils/cookie.py +33 -0
  48. zhs/utils/display.py +188 -0
  49. zhs/utils/path.py +15 -0
  50. zhs/zhidao/__init__.py +1 -0
  51. zhs/zhidao/course.py +170 -0
  52. zhs/zhidao/homework/__init__.py +31 -0
  53. zhs/zhidao/homework/analyzer.py +259 -0
  54. zhs/zhidao/homework/cache.py +19 -0
  55. zhs/zhidao/homework/models.py +182 -0
  56. zhs/zhidao/homework/scanner.py +148 -0
  57. zhs/zhidao/homework/worker.py +710 -0
  58. zhs/zhidao/models.py +112 -0
  59. zhs/zhidao/quiz.py +99 -0
  60. zhs/zhidao/video.py +455 -0
  61. zhs-0.1.1.dist-info/METADATA +301 -0
  62. zhs-0.1.1.dist-info/RECORD +65 -0
  63. zhs-0.1.1.dist-info/WHEEL +4 -0
  64. zhs-0.1.1.dist-info/entry_points.txt +2 -0
  65. zhs-0.1.1.dist-info/licenses/LICENSE +674 -0
zhs/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ZHS - 智慧树自动刷课工具"""
2
+
3
+ __version__ = "0.1.0"
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