Compare commits
9 Commits
eedf48d6df
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c446bb6fd3 | |||
| 48e9d0d4b2 | |||
| 229369c308 | |||
| e0a01af7cd | |||
| 703e3956f4 | |||
| be2a1fb1bb | |||
| 6601213068 | |||
| 03e2366c38 | |||
| 4afa7c51f8 |
+109
-1
@@ -162,7 +162,14 @@ def build_response_gapfill(task: TaskInfo) -> dict:
|
|||||||
"""gapfill: 填空题 — 优先使用expectedResponse中的正确答案"""
|
"""gapfill: 填空题 — 优先使用expectedResponse中的正确答案"""
|
||||||
expected = task.expected_response.get("contents", {}).get("gapfill", {})
|
expected = task.expected_response.get("contents", {}).get("gapfill", {})
|
||||||
if expected:
|
if expected:
|
||||||
return {"gapfill": expected}
|
result = {}
|
||||||
|
for gid, gdata in expected.items():
|
||||||
|
user_input = gdata.get("userInput", "")
|
||||||
|
# 处理 "friendlier | more friendly" 多答案格式,取第一个
|
||||||
|
if " | " in user_input:
|
||||||
|
user_input = user_input.split(" | ")[0].strip()
|
||||||
|
result[gid] = {"id": gid, "userInput": user_input}
|
||||||
|
return {"gapfill": result}
|
||||||
|
|
||||||
# 回退:从task.detail获取gap列表, 填入空字符串
|
# 回退:从task.detail获取gap列表, 填入空字符串
|
||||||
gf = task.task_detail.get("gapfill", {})
|
gf = task.task_detail.get("gapfill", {})
|
||||||
@@ -310,6 +317,38 @@ def build_response_categorisation(task: TaskInfo) -> dict:
|
|||||||
return {"categorisation": result}
|
return {"categorisation": result}
|
||||||
|
|
||||||
|
|
||||||
|
def build_response_writing_challenge(task: TaskInfo) -> dict:
|
||||||
|
"""writing-challenge: 写作挑战 — 优先使用expectedResponse,否则生成足够字数的占位文本"""
|
||||||
|
expected = task.expected_response.get("contents", {}).get("writingChallenge", {})
|
||||||
|
if expected and expected.get("userInput", "").strip():
|
||||||
|
return {"writingChallenge": expected}
|
||||||
|
|
||||||
|
# 从task_detail获取字数要求
|
||||||
|
min_words = task.task_detail.get("writingChallenge", {}).get("minWordCount", 20)
|
||||||
|
|
||||||
|
# 生成满足最低字数要求的占位文本
|
||||||
|
paragraph = (
|
||||||
|
"I think this is a very interesting topic. There are many things "
|
||||||
|
"to consider when writing about this subject. First of all, it is "
|
||||||
|
"important to understand the main ideas and think carefully about "
|
||||||
|
"the best way to express your thoughts. In my opinion, practice "
|
||||||
|
"is the key to success and everyone should try their best to "
|
||||||
|
"improve their skills every day. This is why I believe that "
|
||||||
|
"learning English is very useful for communication with people "
|
||||||
|
"from different countries and cultures around the world."
|
||||||
|
)
|
||||||
|
word_count = len(paragraph.split())
|
||||||
|
if word_count < min_words:
|
||||||
|
repeat = (min_words // word_count) + 1
|
||||||
|
paragraph = " ".join([paragraph] * repeat)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"writingChallenge": {
|
||||||
|
"userInput": paragraph,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 任务类型 → 答题策略映射
|
# 任务类型 → 答题策略映射
|
||||||
RESPONSE_BUILDERS = {
|
RESPONSE_BUILDERS = {
|
||||||
"media-with-time-markers": build_response_media_with_time_markers,
|
"media-with-time-markers": build_response_media_with_time_markers,
|
||||||
@@ -322,6 +361,7 @@ RESPONSE_BUILDERS = {
|
|||||||
"multiple-choice": build_response_multiple_choice,
|
"multiple-choice": build_response_multiple_choice,
|
||||||
"text-highlights": build_response_text_highlights,
|
"text-highlights": build_response_text_highlights,
|
||||||
"categorisation": build_response_categorisation,
|
"categorisation": build_response_categorisation,
|
||||||
|
"writing-challenge": build_response_writing_challenge,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -337,6 +377,9 @@ class EFCourseAutopilot:
|
|||||||
- 步骤②-④ (api.ef.studio域): 需要 Authorization: Bearer JWT token
|
- 步骤②-④ (api.ef.studio域): 需要 Authorization: Bearer JWT token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 类级别:本次运行中已处理过的课程 (course_id:node_id)
|
||||||
|
_processed_lessons: set = set()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
token: str = "",
|
token: str = "",
|
||||||
@@ -376,6 +419,7 @@ class EFCourseAutopilot:
|
|||||||
self.lesson = LessonSession()
|
self.lesson = LessonSession()
|
||||||
self.activities: list[ActivityInfo] = []
|
self.activities: list[ActivityInfo] = []
|
||||||
self.results: list[SubmitResult] = []
|
self.results: list[SubmitResult] = []
|
||||||
|
self.repeat_detected = False
|
||||||
|
|
||||||
def _setup_session(self):
|
def _setup_session(self):
|
||||||
"""配置HTTP会话的通用headers和SSL设置"""
|
"""配置HTTP会话的通用headers和SSL设置"""
|
||||||
@@ -846,8 +890,63 @@ class EFCourseAutopilot:
|
|||||||
self._log(f"下一单元已激活 (status={put_resp.status_code})")
|
self._log(f"下一单元已激活 (status={put_resp.status_code})")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if is_completed and not next_unit_id:
|
||||||
|
self._log("当前等级所有单元已完成,尝试切换至下一等级")
|
||||||
|
try:
|
||||||
|
levels_url = f"{self.base_url}/wl/api/change-level/levels"
|
||||||
|
levels_resp = self.session.get(
|
||||||
|
levels_url, params={"locale": self.locale},
|
||||||
|
headers=headers, timeout=REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
levels_resp.raise_for_status()
|
||||||
|
levels_data = levels_resp.json()
|
||||||
|
|
||||||
|
current_level_id = levels_data.get("currentLevelId")
|
||||||
|
levels = levels_data.get("levels", [])
|
||||||
|
if not current_level_id or not levels:
|
||||||
|
self._log("无法获取等级信息,跳过等级切换")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 找到 currentLevelId 之后的第一个未开始的等级
|
||||||
|
found_current = False
|
||||||
|
next_level = None
|
||||||
|
for level in levels:
|
||||||
|
lid = level.get("id") or level.get("levelId")
|
||||||
|
if not found_current:
|
||||||
|
if lid == current_level_id:
|
||||||
|
found_current = True
|
||||||
|
continue
|
||||||
|
status = level.get("progress", {}).get("status", "")
|
||||||
|
if status == "not_started":
|
||||||
|
next_level = level
|
||||||
|
break
|
||||||
|
if status == "started":
|
||||||
|
next_level = level
|
||||||
|
break
|
||||||
|
|
||||||
|
if next_level:
|
||||||
|
next_level_id = next_level.get("id") or next_level.get("levelId")
|
||||||
|
self._log(f"切换至下一等级 (levelId={next_level_id})")
|
||||||
|
study_plan_url = f"{self.base_url}/wl/api/study-plan/study-plan"
|
||||||
|
put_resp = self.session.put(
|
||||||
|
study_plan_url,
|
||||||
|
params={"locale": self.locale},
|
||||||
|
json={"courseId": self.ctx.course_id, "levelId": next_level_id},
|
||||||
|
headers=headers, timeout=REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
put_resp.raise_for_status()
|
||||||
|
self._log(f"等级切换成功 (status={put_resp.status_code})")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self._log("未找到可切换的下一等级")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"等级切换失败: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
# 主流程
|
# 主流程
|
||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@@ -887,6 +986,12 @@ class EFCourseAutopilot:
|
|||||||
else:
|
else:
|
||||||
self.step_get_focus()
|
self.step_get_focus()
|
||||||
|
|
||||||
|
# 检测是否重复处理同一课程
|
||||||
|
lesson_key = f"{self.ctx.course_id}:{self.ctx.node_id}"
|
||||||
|
if lesson_key in self._processed_lessons:
|
||||||
|
self.repeat_detected = True
|
||||||
|
return False
|
||||||
|
|
||||||
# 步骤②: 注册课程会话
|
# 步骤②: 注册课程会话
|
||||||
self.step_open_lesson_enrollment()
|
self.step_open_lesson_enrollment()
|
||||||
|
|
||||||
@@ -953,6 +1058,9 @@ class EFCourseAutopilot:
|
|||||||
# 步骤⑤: 检查并激活下一单元
|
# 步骤⑤: 检查并激活下一单元
|
||||||
self.step_activate_next_unit()
|
self.step_activate_next_unit()
|
||||||
|
|
||||||
|
# 标记该课程已处理(避免下次循环重复提交)
|
||||||
|
self._processed_lessons.add(f"{self.ctx.course_id}:{self.ctx.node_id}")
|
||||||
|
|
||||||
return all(r.success for r in self.results)
|
return all(r.success for r in self.results)
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
|
|||||||
+45
-3
@@ -11,6 +11,7 @@ token 需从浏览器开发者工具中获取,详见 README.md。
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import random
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -36,6 +37,19 @@ def read_token(token_file: str) -> str:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
QUOTES = [
|
||||||
|
"我的答案是乱编的,但我的坚持是真的",
|
||||||
|
"孩子不会了,所以孩子放弃了",
|
||||||
|
"代码替我上课,良心替我请假",
|
||||||
|
"EF 没教会我英语,但我教会了 EF 怎么跑路",
|
||||||
|
"我提交的不是答案,是对教育系统的一封情书",
|
||||||
|
"这门课过了,但我的英语水平还卡在加载中",
|
||||||
|
"别人是来学习的,我是来给服务器刷数据的",
|
||||||
|
"如果努力有用的话,我还要写脚本干嘛",
|
||||||
|
"我和这门课,总得先疯一个",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="EF Course Autopilot — 交互式循环运行",
|
description="EF Course Autopilot — 交互式循环运行",
|
||||||
@@ -99,15 +113,43 @@ token.txt 说明:
|
|||||||
verify_ssl=args.verify_ssl,
|
verify_ssl=args.verify_ssl,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
try:
|
try:
|
||||||
success = autopilot.run()
|
success = autopilot.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 课程执行异常: {e}")
|
||||||
|
if attempt < max_retries:
|
||||||
|
print(f" 第 {attempt + 1}/{max_retries} 次重试...\n")
|
||||||
|
autopilot = EFCourseAutopilot(
|
||||||
|
token=token,
|
||||||
|
skip_on_fail=True,
|
||||||
|
verify_ssl=args.verify_ssl,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
total_fail += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
if autopilot.repeat_detected:
|
||||||
|
if attempt < max_retries:
|
||||||
|
print(f"\n⚠️ 检测到重复,第 {attempt + 1}/{max_retries} 次重试...\n")
|
||||||
|
autopilot = EFCourseAutopilot(
|
||||||
|
token=token,
|
||||||
|
skip_on_fail=True,
|
||||||
|
verify_ssl=args.verify_ssl,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
print("\n⚠️ 检测到重复课程,自动循环终止。")
|
||||||
|
print(f" 「{random.choice(QUOTES)}」 —— 凯特莎")
|
||||||
|
print(" 请手动完成此课程,完成后重新运行本程序继续后续课程。")
|
||||||
|
total_fail += 1
|
||||||
|
break
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
total_success += 1
|
total_success += 1
|
||||||
else:
|
else:
|
||||||
total_fail += 1
|
total_fail += 1
|
||||||
except Exception as e:
|
break
|
||||||
print(f"\n❌ 课程执行异常: {e}")
|
|
||||||
total_fail += 1
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
print(f"\n{'#' * 60}")
|
print(f"\n{'#' * 60}")
|
||||||
|
|||||||
Reference in New Issue
Block a user