Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ uv run --python 3.13 main.py -c config.ini
python main.py -a ask # 使用询问模式
```

### 章节学习次数配置说明

在配置文件的 `[common]` 部分,可以通过下面两个选项控制章节学习次数功能:

- `add_learning_count = false`:是否在完成刷课任务后,继续对课程章节执行学习次数增加
- `target_count = 100`:章节学习次数的目标总次数,程序会轮询课程章节直到达到该次数

当前实现会先完成所选课程的任务点,再统一执行章节学习次数增加流程。如果开启了 `add_learning_count`,它会作为刷课完成后的追加步骤执行,而不是独立模式。

**外部通知配置说明**

这功能会在所有课程学习任务结束后,或是程序出现错误时,使用外部通知服务推送消息告知你(~~有用但不多~~)
Expand Down
142 changes: 142 additions & 0 deletions api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,42 @@ def study_read(self, _course, _job, _job_info) -> StudyResult:
logger.info(f"阅读任务学习 -> {_resp_json['msg']}")
return StudyResult.SUCCESS

def _send_monitor_heartbeat(self, course, point):
"""
发送章节监控心跳包到 detect.chaoxing.com。
模拟真实浏览器的 JSONP 打点请求,佐证访问行为的真人属性。

Args:
course: 课程信息字典
point: 当前章节信息字典
"""
version = get_timestamp()
callback = f"jsonp{random.randint(10**20, 10**21 - 1)}"
params = {
"version": version,
"refer": "http://i.mooc.chaoxing.com",
"from": "",
"fid": self.get_fid(),
"jsoncallback": callback,
"t": get_timestamp(),
}
referer_url = (
f"https://mooc1.chaoxing.com/mycourse/studentstudy?"
f"chapterId={point['id']}&courseId={course['courseId']}"
f"&clazzid={course['clazzId']}&cpi={course['cpi']}&mooc2=1"
)
try:
session = SessionManager.get_session()
resp = session.get(
"https://detect.chaoxing.com/api/monitor",
params=params,
headers={"Referer": referer_url},
timeout=5,
)
logger.trace(f"Monitor heartbeat sent -> {resp.status_code}")
except Exception as e:
logger.trace(f"Monitor heartbeat failed (non-critical): {e}")

def study_emptypage(self, _course, point):
_session = SessionManager.get_session()
# &cpi=0&verificationcode=&mooc2=1&microTopicId=0&editorPreview=0
Expand All @@ -1079,3 +1115,109 @@ def study_emptypage(self, _course, point):
else:
logger.info(f"空页面任务完成 -> {point['title']}")
return StudyResult.SUCCESS

def _access_chapter_for_count(self, _course, point):
_session = SessionManager.get_session()
# &cpi=0&verificationcode=&mooc2=1&microTopicId=0&editorPreview=0
_resp = _session.get(
url="https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudyAjax",
params={
"courseId": _course["courseId"],
"clazzid": _course["clazzId"],
"chapterId": point["id"],
"cpi": _course["cpi"],
"verificationcode": "",
"mooc2": 1,
"microTopicId": 0,
"editorPreview": 0,
},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if _resp.status_code != 200:
logger.error(f"章节访问失败 -> [{_resp.status_code}]{point['title']}")
return None
else:
logger.info(f"章节访问成功 -> {point['title']}")
return _resp.text

def _extract_and_send_setlog(self, html_text):
"""
从 studentstudyAjax 返回的 HTML 中提取 setlog URL 并执行。
该 URL 包含服务端生成的 encode 参数,是记录章节学习次数的关键 API。

Args:
html_text: studentstudyAjax 返回的 HTML 内容
"""
match = re.search(
r'<script[^>]+src="(https://fystat-ans\.chaoxing\.com/log/setlog[^"]+)"',
html_text
)
if not match:
logger.trace("未在响应中找到 setlog URL")
return

setlog_url = match.group(1)
try:
session = SessionManager.get_session()
resp = session.get(setlog_url, timeout=5)
logger.trace(f"Setlog sent -> {resp.status_code}")
except Exception as e:
logger.trace(f"Setlog failed (non-critical): {e}")

def increase_chapter_learning_count(self, course, points, target_count):
"""
增加课程章节学习次数。

循环遍历课程的所有章节,每访问一个章节页面:
1. 调 studentstudyAjax 获取页面 HTML(含服务端生成的 setlog URL)
2. 提取并执行 setlog URL(记录学习次数)
3. 立即发送 monitor 心跳包(模拟 fn() 首次心跳)
4. 停留 30 秒(模拟前端 setInterval(fn, 30000) 的间隔)
5. 再次发送 monitor 心跳包(模拟 30s 后的第二次心跳)
6. 计数器 +1,继续下一个章节

Args:
course: 课程信息字典
points: 课程所有章节列表
target_count: 目标总次数

Returns:
StudyResult: 操作结果
"""
total = 0
consecutive_failures = 0
max_consecutive_failures = 10
logger.info(f"开始增加章节学习次数, 目标总次数: {target_count}, 章节数: {len(points)}")
while total < target_count:
for point in points:
if total >= target_count:
break
Comment thread
coderabbitai[bot] marked this conversation as resolved.
self.rate_limiter.limit_rate(random_time=True, random_min=0, random_max=0.2)
html_text = self._access_chapter_for_count(course, point)
if not html_text:
logger.error(f"章节学习次数增加失败, 当前章节: {point['title']}")
consecutive_failures += 1
if consecutive_failures >= max_consecutive_failures:
logger.error(
f"章节学习次数增加连续失败 {consecutive_failures} 次, 终止任务"
)
return StudyResult.ERROR
continue

consecutive_failures = 0

# 第 1 步:从 HTML 中提取 setlog URL 并执行(真正的计次 API)
self._extract_and_send_setlog(html_text)

# 第 2 步:立即发送 monitor 心跳包(模拟 fn())
self._send_monitor_heartbeat(course, point)

# 第 3 步:停留 30 秒(模拟前端 setInterval 间隔)
time.sleep(30)

# 第 4 步:再次发送 monitor 心跳包(模拟 setInterval 触发的第二次心跳)
self._send_monitor_heartbeat(course, point)

total += 1
logger.info(f"章节学习次数进度: {total}/{target_count}")
logger.info(f"章节学习次数增加完成, 共完成: {total} 次")
return StudyResult.SUCCESS
33 changes: 33 additions & 0 deletions api/process.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time

from api.logger import logger
from api.config import GlobalConst as gc


Expand Down Expand Up @@ -60,3 +61,35 @@ def show_progress(task_name: str, start_position: int, duration: int,

print(progress_text, end="", flush=True)
time.sleep(gc.THRESHOLD)


def increase_learning_count_for_course(chaoxing, course, config):
"""
为单个课程增加章节学习次数。
遍历课程的所有章节,轮询调用 studentstudyAjax,直到总次数达到 target_count。

Args:
chaoxing: Chaoxing 实例
course: 课程信息字典
config: 配置字典,需包含 target_count 字段
"""
target_count = config.get("target_count", 100)
logger.info(f"开始为课程 [{course['title']}] 增加章节学习次数, 目标总次数: {target_count}")

# 获取课程所有章节
point_list = chaoxing.get_course_point(
course["courseId"], course["clazzId"], course["cpi"]
)
points = point_list.get("points", [])
if not points:
logger.warning(f"课程 [{course['title']}] 没有章节, 跳过")
return

logger.info(f"课程 [{course['title']}] 共有 {len(points)} 个章节")

# 调用核心方法
result = chaoxing.increase_chapter_learning_count(course, points, target_count)
if result.is_success():
logger.info(f"课程 [{course['title']}] 章节学习次数增加完成")
else:
logger.error(f"课程 [{course['title']}] 章节学习次数增加失败")
7 changes: 7 additions & 0 deletions config_template.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ jobs = 4

; 遇到关闭任务点时的行为: retry-重试(默认), continue-继续
notopen_action = retry

; 是否在刷完课程任务点后增加章节学习次数
add_learning_count = false

; 章节学习次数的目标总次数(所有章节轮询访问,每访问一次计1次)
target_count = 100

[tiku]
; 可选项 :
; 1. TikuYanxi(言溪题库 https://tk.enncy.cn/)
Expand Down
40 changes: 37 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from api.notification import Notification
from api.live import Live
from api.live_process import LiveProcessor
from api.process import increase_learning_count_for_course

class ChapterResult(enum.Enum):
SUCCESS=0,
Expand Down Expand Up @@ -86,6 +87,20 @@ def parse_args():

parser.add_argument("--auto-sign", action="store_true", help="自动签到")

parser.add_argument(
"-lc",
"--add-learning-count",
action="store_true",
help="开启章节学习次数增加模式",
)
parser.add_argument(
"-tc",
"--target-count",
type=int,
default=100,
help="章节学习次数目标总次数 (默认100)",
)

# 在解析之前捕获 -h 的行为
if len(sys.argv) == 2 and sys.argv[1] in {"-h", "--help"}:
parser.print_help()
Expand Down Expand Up @@ -119,6 +134,10 @@ def load_config_from_file(config_path):
common_config["notopen_action"] = "retry"
if "use_cookies" in common_config:
common_config["use_cookies"] = str_to_bool(common_config["use_cookies"])
if "add_learning_count" in common_config:
common_config["add_learning_count"] = str_to_bool(common_config["add_learning_count"])
if "target_count" in common_config:
common_config["target_count"] = int(common_config["target_count"])
if "username" in common_config and common_config["username"] is not None:
common_config["username"] = common_config["username"].strip()
if "password" in common_config and common_config["password"] is not None:
Expand Down Expand Up @@ -148,7 +167,9 @@ def build_config_from_args(args):
"course_list": [item.strip() for item in args.list.split(",") if item.strip()] if args.list else None,
"speed": args.speed if args.speed else 1.0,
"jobs": args.jobs,
"notopen_action": args.notopen_action if args.notopen_action else "retry"
"notopen_action": args.notopen_action if args.notopen_action else "retry",
"add_learning_count": args.add_learning_count,
"target_count": args.target_count,
}
return common_config, {}, {}

Expand Down Expand Up @@ -496,6 +517,10 @@ def main():
common_config["speed"] = min(2.0, max(1.0, common_config.get("speed", 1.0)))
common_config["notopen_action"] = common_config.get("notopen_action", "retry")

# 初始化增加章节学习次数配置
add_learning_count = str_to_bool(common_config.get("add_learning_count", False))
target_count = int(common_config.get("target_count", 100))

# 初始化超星实例
chaoxing = init_chaoxing(common_config, tiku_config)

Expand All @@ -515,14 +540,23 @@ def main():

# 过滤要学习的课程
course_task = filter_courses(all_course, common_config.get("course_list"))

# 开始学习
logger.info(f"课程列表过滤完毕, 当前课程任务数量: {len(course_task)}")

# 开始学习
for course in course_task:
process_course(chaoxing, course, common_config)

logger.info("所有课程学习任务已完成")
notification.send("chaoxing : 所有课程学习任务已完成")

# 刷课完成后,如果开启了增加章节学习次数,则执行
if add_learning_count:
logger.info("刷课完成,开始增加章节学习次数...")
for course in course_task:
common_config["target_count"] = target_count
increase_learning_count_for_course(chaoxing, course, common_config)
logger.info("所有课程章节学习次数增加完成")
notification.send("chaoxing : 所有课程章节学习次数增加完成")

except SystemExit as e:
if e.code != 0:
Expand Down