Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9bff528
Merge pull request #2 from zhdbk3/dev
zhdbk3 Jul 12, 2025
d13719a
中途换电脑
DingerBtn Jul 12, 2025
6e0657c
可获取答案
DingerBtn Jul 12, 2025
f23955f
获取答案模块完全可用,已移入src
DingerBtn Jul 12, 2025
9ab238c
删除垃圾文件
DingerBtn Jul 13, 2025
d9231b9
更新获取答案方式
DingerBtn Jul 13, 2025
300edea
获取answer的模块深度优化
DingerBtn Jul 13, 2025
be475d0
Update README.md: 补充路径里不要有中文
zhdbk3 Jul 14, 2025
62dc4c5
Update README.md: fix #4 反斜杠转义问题
zhdbk3 Jul 15, 2025
970ddac
获取 id:完成
DingerBtn Jul 15, 2025
d7cd3b2
Merge branch 'main' of https://github.com/zhdbk3/AutoEwt
DingerBtn Jul 15, 2025
7707a5a
Update README.md: Minecraft,启动!
zhdbk3 Jul 17, 2025
704b712
Update README.md: 忘换行了
zhdbk3 Jul 17, 2025
f813244
自动获取答案完成
DingerBtn Jul 21, 2025
165b204
方便用ai,给配置读取加了个样例
DingerBtn Jul 21, 2025
a160d52
Update viewer.py: 删除多余的警告
zhdbk3 Jul 21, 2025
ab73603
重构完成
DingerBtn Jul 22, 2025
bc33b01
重构完成
DingerBtn Jul 22, 2025
2ea51b4
Merge branch 'main' of https://github.com/zhdbk3/AutoEwt
DingerBtn Jul 22, 2025
6206c08
获取token;各种优化
DingerBtn Jul 22, 2025
5287f9b
优化
DingerBtn Jul 22, 2025
9bbba0d
使用 uv
zhdbk3 Jul 23, 2025
5064b42
点击选择题
zhdbk3 Jul 23, 2025
17fad76
Update build.yml: 使用 MinGW 64
zhdbk3 Jul 23, 2025
1c3c2ca
尝试修复跳过视频课的 bug
zhdbk3 Jul 23, 2025
e0dee4f
Merge remote-tracking branch 'origin/main'
zhdbk3 Jul 23, 2025
4974e7b
刚才的写法似乎不行
zhdbk3 Jul 23, 2025
374c2e6
尝试只点击可见元素
zhdbk3 Jul 23, 2025
55affe3
人工重构为面向对象 | 格式化代码
zhdbk3 Jul 25, 2025
0d441c1
修改配置文件模式名称
DingerBtn Jul 26, 2025
f94a596
选择题选正确答案 | 自定义起始天数
zhdbk3 Jul 26, 2025
421a708
Merge remote-tracking branch 'origin/main'
zhdbk3 Jul 26, 2025
f543639
移动启动参数到最下|提示headless参数
DingerBtn Jul 26, 2025
19997c0
Update README.md: 忘改 README 中的配置文件了
zhdbk3 Jul 26, 2025
2fe46f8
Merge branch 'main' of https://github.com/zhdbk3/AutoEwt
DingerBtn Jul 26, 2025
6286cfe
补充跳转页面后等待时间
zhdbk3 Jul 31, 2025
b9a3540
Merge remote-tracking branch 'origin/main'
zhdbk3 Jul 31, 2025
08933c1
修改刷课和刷试卷“去学习”按钮XPath
DingerBtn Feb 6, 2026
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
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ jobs:
file-version: ${{ github.event.inputs.version }}
product-version: ${{ github.event.inputs.version }}
include-data-files: src/config.yml.default=./config.yml.default
mingw64: true

- name: 上传工件
uses: actions/upload-artifact@v4
with:
name: AutoEwt-v${{ github.event.inputs.version }}-${{ runner.os }}
path: |
build/main.dist/*
build/main.dist/*
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

## 如何使用

> [!NOTE]
> **不要把本软件或浏览器驱动放在包含中文或其他特殊字符的路径下!**

### 1. 下载浏览器驱动

请打开您的浏览器,查看它的版本。
Expand Down Expand Up @@ -47,14 +50,31 @@ password: 密码
list_url: 课程列表页面的链接
# 浏览器启动时的参数,这里给了个静音
options: --mute-audio
# AutoEwt 模式,选填 watch(看课)/ test(做试卷)
mode: watch
# AutoEwt 模式,选填 video(看课)/ paper(做试卷)
mode: video
# 是否 (true / false) 给选择题选择正确答案
choose_correctly: true
# 如果 choose_correctly 为 true,请填写 report_id 字段,否则可以不动
# 一张**已经完成**的试卷的 reportId(请到浏览器上方链接中获取该参数的值)
report_id: reportId
# 从第几天开始扫描(用于开发者调试程序方便,当然用户也可以设置,每次启动能省一丢丢时间)
day_to_start_on: 1
```

> [!NOTE]
> 填写 `driver_path` 字段时,您可能会使用 Windows 11 右键菜单的“复制文件地址”,这样得到的路径是带引号的。请**删除引号**~~或在左引号前加上 `r` 或把每个 `\` 都换成 `\\`~~。

### 3. 启动!

双击运行 `AutoEwt`,然后用您的双手去做更有意义的事!

> [!NOTE]
> 在该程序运行时启动 Minecraft 可能会导致视频被暂停,建议 Minecraft 启动完成之后检查一下视频播放情况,以免造成不必要的损失。
>
> 该 bug 作用机理尚不明确,难以稳定复现,且不存在任何日志记录,无法修复,见谅。
>
> 如果您找到了稳定复现的方法,欢迎提 issue 报告。

## 开发环境

- Python 3.12
Expand Down
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[project]
name = "autoewt"
version = "0.6.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"jsons~=1.1.1",
"pyyaml~=6.0.2",
"requests~=2.32.4",
"selenium~=4.34.2",
]

[[tool.uv.index]]
url = "https://mirrors.aliyun.com/pypi/simple"
default = true
6 changes: 4 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
selenium
pyyaml
selenium~=4.34.2
pyyaml~=6.0.2
requests~=2.32.4
jsons~=1.1.1
38 changes: 0 additions & 38 deletions rubbish/tmp.py

This file was deleted.

129 changes: 129 additions & 0 deletions src/auto_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#
# 此模块用于存储通用的工具函数
#

import os
import logging
import time
from abc import ABC, abstractmethod
from functools import cache

import yaml
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.by import By


@cache
def read_config() -> dict:
"""
配置文件样例:
# 修改时不要删掉冒号后的空格
browser: 浏览器名称(首字母大写),如 Chrome, Edge 等
driver_path: 浏览器驱动路径
username: 用户名
password: 密码
list_url: 课程列表页面的链接
# 浏览器启动时的参数,这里给了个静音
options: --mute-audio
# AutoEwt 模式,选填 watch(看课)/ test(做试卷)
mode: watch
"""
if not os.path.exists('config.yml'):
logging.error('配置文件 config.yml 不存在,请检查!')
exit(1)
with open('config.yml', encoding='utf-8') as f:
config = yaml.load(f, yaml.FullLoader)
# 密码可能是纯数字
config['password'] = str(config['password'])
logging.info('成功读取到配置文件')
return config


class AutoBase(ABC):
def __init__(self):
self.config = read_config()
self.mode = self.config['mode']
self.driver = self.init_driver()

self.token = self.login()
self.finish_days_list()

def init_driver(self) -> webdriver.Edge:
browser = self.config['browser']

options = getattr(webdriver, browser.lower()).options.Options()
options.add_argument(self.config['options'])
driver = getattr(webdriver, browser)(
service=getattr(webdriver, browser.lower()).service.Service(self.config['driver_path']),
options=options
)
driver.maximize_window()
driver.get(self.config['list_url'])
driver.implicitly_wait(3)
return driver

def login(self) -> str:
"""
登录
:return: token
"""
logging.info('登录账号……')
self.driver.find_element(By.ID, 'login__password_userName').send_keys(self.config['username'])
self.driver.find_element(By.ID, 'login__password_password').send_keys(self.config['password'])
self.driver.find_element(By.CLASS_NAME, 'ant-btn-block').submit()
# 等待一段时间,确保页面加载完成并生成 token
time.sleep(3)
# 获取所有 cookie
cookies = self.driver.get_cookies()
token = None
for cookie in cookies:
if cookie['name'] == 'token':
token = cookie['value']
break
return token

def click(self, btn: WebElement) -> None:
"""
通过调用 .click(); 事件点击一个按钮,这比直接在 Python 中 .click() 更稳定
对于可能被遮挡而无法点击的按钮,应使用此方法
:param btn: 要点击的按钮
:return: None
"""
self.driver.execute_script('arguments[0].click();', btn)

def click_and_switch(self, btn: WebElement) -> None:
"""
点击按钮并切换到新页面
:param btn: 要点击的按钮
"""
self.click(btn)
time.sleep(1) # 给新页面反应一会
# 切换到当前页面
handles = self.driver.window_handles
self.driver.switch_to.window(handles[1])
time.sleep(3)

def close_and_switch(self) -> None:
"""关闭当前页面并返回到首页"""
handles = self.driver.window_handles
self.driver.close()
self.driver.switch_to.window(handles[0])
time.sleep(1)

def finish_days_list(self) -> None:
"""完成所有天"""
time.sleep(5)
days = self.driver.find_elements(By.CSS_SELECTOR, 'li[data-active="true"], li[data-active="false"]')
logging.info(f'一共有 {len(days)} 天的任务')
for i in range(self.config['day_to_start_on'] - 1, len(days)):
logging.info(f'================ 第 {i + 1} / {len(days)} 天 ================')
self.finish_a_day(days[i])

@abstractmethod
def finish_a_day(self, day: WebElement) -> None:
"""
完成一天的任务
:param day: 该天在网页上的标签
"""
...
Empty file added src/auto_paper/__init__.py
Empty file.
93 changes: 93 additions & 0 deletions src/auto_paper/auto_paper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#
# Created by 着火的冰块nya (zhdbk3) on 2025/1/23
#

import time
import logging
import traceback
import random

from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement

from auto_base import AutoBase
from .get_corret_answer_v2 import get_answer
from .get_info_by_current_url import get_info_from_url


class AutoPaper(AutoBase):
def finish_a_day(self, day: WebElement) -> None:
self.click(day)
time.sleep(2)
btns = self.driver.find_elements(
By.XPATH,
"//div[contains(@class, 'btn-AoqsA')][contains(., '测一测') or contains(., '继续答')]")
unit = '张试卷'
logging.info(f'该天还剩 {len(btns)} {unit}')
for i in range(len(btns)):
logging.info(f'第 {i + 1} / {len(btns)} {unit}')
try:
self.finish_a_test(btns[i])
except:
logging.error(traceback.format_exc())
logging.warning('该试卷已跳过')
# 关闭页面,返回首页
self.close_and_switch()

def finish_a_test(self, btn: WebElement):
"""
完成一张试卷
选择题随机选一个,大题**并不**传一个占位图片文件[doge],并自批为满分
"""
self.click_and_switch(btn)

# 获取当前 URL 各参数
params = get_info_from_url(self.driver.current_url)
biz_code = params['bizCode']
paper_id = params['paperId']

# 统一获取答案
# API 无法直接获取母题的子题的答案,故在此统一获取
answers: dict[str, list[str]] = {}
questions = self.driver.find_elements(By.CSS_SELECTOR, '.pm-question > .pm-question-content')
for q in questions:
q_id = q.get_attribute('id')[13:] # 截断前面的 ewt-question-
answers |= get_answer(biz_code, paper_id, str(self.config['report_id']), q_id, self.token)
time.sleep(0.2)

# 完成选择题
options_uls = self.driver.find_elements(By.CLASS_NAME, 'pm-question-options')
for ul in options_uls:
if not self.config['choose_correctly']:
# 随机选一个选项
items = ul.find_elements(By.CSS_SELECTOR, 'li')
self.click(random.choice(items))
else:
# 选择正确答案
q_id = ul.find_element(By.XPATH, './ancestor::*[2]').get_attribute('id')[13:]
for letter in answers[q_id]:
self.click(ul.find_element(
By.XPATH, f".//span[contains(@class, 'pm-tag-letter') and text()='{letter}']"
))
time.sleep(0.2)

time.sleep(1)

# 提交
self.click(self.driver.find_element(By.CLASS_NAME, 'ant-btn-primary'))
time.sleep(1)
self.click(self.driver.find_element(By.CLASS_NAME, 'confirm-right'))
time.sleep(5)

# 自批满分(不一定有此环节)
n = len(self.driver.find_elements(By.CSS_SELECTOR, '.content-left-scroll > ul > li'))
if n != 0:
for i in range(n):
self.click(self.driver.find_element(By.CSS_SELECTOR, '.content-right-scroll > ul > li:last-child'))
time.sleep(2)
self.click(self.driver.find_element(By.CLASS_NAME, 'content-main-footer_submit_btn_full'))
time.sleep(1)
self.click(self.driver.find_element(By.CLASS_NAME, 'confirm-right'))
time.sleep(3)

self.close_and_switch()
Loading
Loading