Initial commit: EF course autopilot tool
Auto-complete EF English courses with JWT token authentication. Supports multiple task types: multiple-choice, gapfill, matching, flashcards, speaking-practice, text-highlights, sequencing, media-with-time-markers, language-focus. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Token
|
||||
token.txt
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@@ -0,0 +1,75 @@
|
||||
# EF Course Autopilot
|
||||
|
||||
自动完成 EF (English First) 企业英语课程的工具。
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `ef_course_autopilot.py` | 核心脚本 — 自动完成单门课程 |
|
||||
| `ef_course_loop.py` | 交互式循环脚本 — 批量完成多门课程(**推荐使用**)|
|
||||
| `token.txt` | JWT token 文件(需自行创建) |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 获取 JWT Token
|
||||
|
||||
1. 用 Chrome 打开 `https://learn.corporate.ef.com.cn` 并登录
|
||||
2. 按 F12 打开开发者工具 → 切换到 **Network(网络)** 标签
|
||||
3. 找一个请求,复制其请求头中 `ef_access_token` 的值(一长串 JWT 字符串)
|
||||
4. 将复制的 token 粘贴到 `token.txt` 文件,**仅保留一行内容,不要有多余换行或空格**
|
||||
|
||||
### 2. 运行
|
||||
|
||||
```bash
|
||||
python3 ef_course_loop.py
|
||||
```
|
||||
|
||||
脚本会自动读取运行目录下的 `token.txt`,然后交互式询问课程数量,完成后询问是否继续。
|
||||
|
||||
如需指定自定义 token 文件路径:
|
||||
|
||||
```bash
|
||||
python3 ef_course_loop.py --token-file /path/to/token.txt
|
||||
```
|
||||
|
||||
SSL 验证默认禁用(macOS Python 证书问题),如需启用:
|
||||
|
||||
```bash
|
||||
python3 ef_course_loop.py --verify-ssl
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
脚本模拟浏览器请求,按以下步骤自动完成课程:
|
||||
|
||||
1. **GET focus** — 获取当前课程的 courseId / nodeId
|
||||
2. **POST open-lesson** — 打开课程,获取 lessonId
|
||||
3. **POST lesson/command (open-lesson)** — 初始化课程,获取全部 Activity/Task
|
||||
4. **POST lesson/command (submit-task-response) ×N** — 逐个提交任务答案
|
||||
|
||||
### 支持的任务类型
|
||||
|
||||
- `multiple-choice` — 选择题(随机选一个选项)
|
||||
- `language-focus` — 语言知识点(标记已查看)
|
||||
- `media-with-time-markers` — 音视频(标记已播放)
|
||||
- `gapfill` — 填空(保留 expectedResponse)
|
||||
- `matching` — 匹配题(保留 expectedResponse)
|
||||
- `flashcards` — 闪卡(所有卡片标记已翻转)
|
||||
- `text-highlights` — 文本高亮(标记已查看)
|
||||
- `sequencing` — 排序(保留 expectedResponse)
|
||||
- `speaking-practice` — 口语练习(可跳过)
|
||||
|
||||
## SSL 证书问题
|
||||
|
||||
macOS 上 Python 3.12 可能出现 `SSL: CERTIFICATE_VERIFY_FAILED` 错误。
|
||||
|
||||
**解决办法一:安装证书**
|
||||
```bash
|
||||
/Applications/Python\ 3.12/Install\ Certificates.command
|
||||
```
|
||||
|
||||
**解决办法二:启用验证**(如需)
|
||||
```bash
|
||||
python3 ef_course_loop.py --verify-ssl
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
打包脚本 — 使用 PyInstaller 将 ef_course_loop.py 打包为单文件可执行程序。
|
||||
|
||||
用法:
|
||||
python3 build.py
|
||||
|
||||
依赖:
|
||||
pip3 install pyinstaller
|
||||
|
||||
注意:需要在目标平台上分别运行打包(PyInstaller 不支持交叉编译)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
system = platform.system()
|
||||
print(f"当前平台: {system}")
|
||||
|
||||
# 检查 PyInstaller
|
||||
if not shutil.which("pyinstaller"):
|
||||
print("❌ 未找到 PyInstaller,请先安装: pip3 install pyinstaller")
|
||||
sys.exit(1)
|
||||
|
||||
# 清理上次打包产物
|
||||
for p in ["build", "dist", "ef_course_autopilot.spec"]:
|
||||
if os.path.exists(p):
|
||||
shutil.rmtree(p) if os.path.isdir(p) else os.remove(p)
|
||||
print(f" 清理: {p}")
|
||||
|
||||
# 根据平台设置输出名称
|
||||
output_name = "ef_course_autopilot.exe" if system == "Windows" else "ef_course_autopilot"
|
||||
|
||||
print("\n开始打包...")
|
||||
cmd = [
|
||||
"pyinstaller",
|
||||
"--onefile",
|
||||
"--name", output_name,
|
||||
"--distpath", "dist",
|
||||
"--workpath", "build",
|
||||
"--specpath", ".",
|
||||
"ef_course_loop.py",
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd)
|
||||
if result.returncode != 0:
|
||||
print(f"\n❌ 打包失败 (exit code: {result.returncode})")
|
||||
sys.exit(1)
|
||||
|
||||
# 打包完成后附带 token.txt.example 到 dist 目录
|
||||
example_src = "token.txt.example"
|
||||
example_dst = os.path.join("dist", example_src)
|
||||
if os.path.exists(example_src):
|
||||
shutil.copy2(example_src, example_dst)
|
||||
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f" ✅ 打包成功!")
|
||||
print(f" 输出: dist/{output_name}")
|
||||
print(f" 提示: 将 token.txt 放在可执行文件同目录下即可运行")
|
||||
print(f"{'=' * 50}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+1111
File diff suppressed because it is too large
Load Diff
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
EF Course Autopilot — 交互式循环运行脚本
|
||||
|
||||
从 token.txt 文件读取 JWT token(默认读取运行目录下的 token.txt)。
|
||||
token 需从浏览器开发者工具中获取,详见 README.md。
|
||||
|
||||
用法示例:
|
||||
python3 ef_course_loop.py # 默认读取 ./token.txt
|
||||
python3 ef_course_loop.py --token-file /path/to/token.txt
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ef_course_autopilot import EFCourseAutopilot
|
||||
|
||||
|
||||
def read_token(token_file: str) -> str:
|
||||
"""从指定文件读取 token(仅支持单行内容)"""
|
||||
try:
|
||||
with open(token_file, "r") as f:
|
||||
token = f.read().strip()
|
||||
if not token:
|
||||
print(f"❌ token 文件为空: {token_file}")
|
||||
sys.exit(1)
|
||||
print(f" 📄 从文件读取 token ({token_file})")
|
||||
return token
|
||||
except FileNotFoundError:
|
||||
print(f"❌ token 文件不存在: {token_file}")
|
||||
print(f" 请创建 {token_file} 并将 ef_access_token 粘贴到第一行")
|
||||
sys.exit(1)
|
||||
except IOError as e:
|
||||
print(f"❌ 读取文件失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="EF Course Autopilot — 交互式循环运行",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""\
|
||||
使用示例:
|
||||
python3 ef_course_loop.py # 默认读取 ./token.txt
|
||||
python3 ef_course_loop.py --token-file /path/to/token.txt
|
||||
|
||||
token.txt 说明:
|
||||
- 仅能包含一行,即 ef_access_token 的值
|
||||
- 从浏览器开发者工具中取请求携带的 ef_access_token 内容,手动粘贴
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token-file",
|
||||
default="token.txt",
|
||||
help="从文件读取 JWT token(默认: ./token.txt)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify-ssl",
|
||||
action="store_true",
|
||||
help="启用SSL证书验证(默认禁用)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print(" EF Course Autopilot — 交互式循环运行")
|
||||
print("=" * 60)
|
||||
|
||||
while True:
|
||||
# 1. 从文件获取 token
|
||||
token = read_token(args.token_file)
|
||||
|
||||
# 2. 输入课程数量
|
||||
while True:
|
||||
try:
|
||||
count_str = input("请输入要完成的课程数量: ").strip()
|
||||
count = int(count_str)
|
||||
if count <= 0:
|
||||
print("❌ 数量必须大于 0")
|
||||
continue
|
||||
break
|
||||
except ValueError:
|
||||
print("❌ 请输入有效数字")
|
||||
|
||||
# 3. 循环完成课程
|
||||
total_success = 0
|
||||
total_fail = 0
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(count):
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" 第 {i + 1}/{count} 门课程")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
autopilot = EFCourseAutopilot(
|
||||
token=token,
|
||||
skip_on_fail=True,
|
||||
verify_ssl=args.verify_ssl,
|
||||
)
|
||||
|
||||
try:
|
||||
success = autopilot.run()
|
||||
if success:
|
||||
total_success += 1
|
||||
else:
|
||||
total_fail += 1
|
||||
except Exception as e:
|
||||
print(f"\n❌ 课程执行异常: {e}")
|
||||
total_fail += 1
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n{'#' * 60}")
|
||||
print(f" 本轮完成!")
|
||||
print(f" 计划: {count} 门")
|
||||
print(f" ✅ 成功: {total_success}")
|
||||
print(f" ❌ 失败: {total_fail}")
|
||||
print(f" ⏱️ 耗时: {elapsed:.1f}秒")
|
||||
print(f"{'#' * 60}")
|
||||
|
||||
# 4. 询问是否继续
|
||||
again = input("\n是否继续?(y/n): ").strip().lower()
|
||||
if again != "y":
|
||||
print("\n程序结束。")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
eyJraWQiOiJ...(将你的 ef_access_token 粘贴在此,仅一行,不要换行)
|
||||
Reference in New Issue
Block a user