新增了"智慧树"平台的AI答题功能;

抽取了部分任务共用逻辑至TaskUtil中;
This commit is contained in:
2025-04-07 22:05:20 +08:00
parent 9670e6863c
commit acc90b6bf3
4 changed files with 243 additions and 68 deletions

View File

@@ -12,4 +12,7 @@
- 视频播放
- 文档浏览
- 网页浏览
- AI答题
> 登录需确保智慧树账号绑定QQ号使用时需确保QQ在线通过QQ进行自动登录
> AI答题部分简答题模块目前只遇到且实现了单空题的作答不清楚是否存在多个输入框的题目

View File

@@ -172,7 +172,7 @@ public class CxstudyTask {
TextQuestion textQuestion = new TextQuestion();
textQuestion.setTitle(questionTitle);
Answer answer = getAnswer(textQuestion);
Answer answer = TaskUtil.getAnswer(textQuestion);
TextAnswer textAnswer = (TextAnswer) answer;
WebElement inputArea = driver.findElement(By.xpath(".//body[@contenteditable='true']"));
//输入答案,并检测是否正确输入
@@ -207,7 +207,7 @@ public class CxstudyTask {
multiChoiceQuestion.setTitle(questionTitle);
List<String> choices = getChoices(choiceElements);
multiChoiceQuestion.setChoices(choices);
Answer answer = getAnswer(multiChoiceQuestion);
Answer answer = TaskUtil.getAnswer(multiChoiceQuestion);
MultiChoiceAnswer multiChoiceAnswer = (MultiChoiceAnswer) answer;
List<String> answers = multiChoiceAnswer.getAnswers();
//输入答案,并检测是否正确输入
@@ -233,7 +233,7 @@ public class CxstudyTask {
singleChoiceQuestion.setTitle(questionTitle);
List<String> choices = getChoices(choiceElements);
singleChoiceQuestion.setChoices(choices);
Answer primaryAnswer = getAnswer(singleChoiceQuestion);
Answer primaryAnswer = TaskUtil.getAnswer(singleChoiceQuestion);
SingleChoiceAnswer answer = (SingleChoiceAnswer) primaryAnswer;
String answerContent = answer.getAnswer();
log.info("答案: {}", answerContent);
@@ -264,42 +264,6 @@ public class CxstudyTask {
return choices;
}
private Answer getAnswer(Question question) throws InterruptedException {
Answer answer = null;
int tryTimes = 0;
while (tryTimes < 3) {
try {
String questionStr = JSONUtil.toJsonPrettyStr(question);
String responseStr = AiUtil.runChat(questionStr);
// 使用正则表达式提取Markdown代码块中的JSON内容
String jsonPattern = "```[\\s\\S]*?\\n([\\s\\S]*?)\\n```";
Pattern pattern = Pattern.compile(jsonPattern);
Matcher matcher = pattern.matcher(responseStr);
String jsonStr = "";
if (matcher.find()) {
jsonStr = matcher.group(1).trim();
}
if (jsonStr.isEmpty()) {
throw new AiResponseFormatException("AI 回答格式不匹配: " + jsonStr);
}
if (question instanceof SingleChoiceQuestion) {
answer = JSONUtil.toBean(jsonStr, SingleChoiceAnswer.class);
} else if (question instanceof MultiChoiceQuestion) {
answer = JSONUtil.toBean(jsonStr, MultiChoiceAnswer.class);
} else if (question instanceof TextQuestion) {
answer = JSONUtil.toBean(jsonStr, TextAnswer.class);
}
break;
} catch (AiResponseFormatException e) {
log.warn(e.getLocalizedMessage());
log.warn("3s后重试...");
Thread.sleep(3000);
tryTimes++;
}
}
return answer;
}
private void handleVideoTask(int i) throws InterruptedException {
try {

View File

@@ -3,12 +3,19 @@ package work.slhaf.task;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.*;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import work.slhaf.config.Config;
import work.slhaf.exception.TargetClassNotFoundException;
import work.slhaf.exception.UnknownTaskPointException;
import work.slhaf.pojo.answer.MultiChoiceAnswer;
import work.slhaf.pojo.answer.SingleChoiceAnswer;
import work.slhaf.pojo.answer.TextAnswer;
import work.slhaf.pojo.question.MultiChoiceQuestion;
import work.slhaf.pojo.question.SingleChoiceQuestion;
import work.slhaf.pojo.question.TextQuestion;
import work.slhaf.util.TaskUtil;
import java.time.Duration;
@@ -32,32 +39,13 @@ public class ZhsTask {
while (true) {
try {
switchPage();
List<WebElement> todoTasks = getTodoTasks();
while (!todoTasks.isEmpty()) {
todoTasks.getFirst().click();
System.out.println();
try {
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='resources-list']")));
} catch (Exception e) {
driver.get(driver.getCurrentUrl());
}
HashMap<WebElement, String> todoTaskPoints = getTodoTaskPoints();
log.info("发现{}个任务点", todoTaskPoints.size());
AtomicInteger count = new AtomicInteger(0);
for (Map.Entry<WebElement, String> entry : todoTaskPoints.entrySet()) {
WebElement k = entry.getKey();
String v = entry.getValue();
count.incrementAndGet();
log.info("处理第{}个任务点", count.get());
switch (v) {
case "icon-box video" -> handleVideoTask(k);
case "icon-box baidu", "icon-box bzhan" -> handleWebTask(k);
case "icon-box book", "icon-box other", "icon-box img" -> handleDocTask(k);
default -> throw new UnknownTaskPointException("未设置处理策略的任务点类型: " + v);
}
}
driver.findElement(By.xpath("//img[@alt='back']")).click();
todoTasks = getTodoTasks();
List<WebElement> todoLearningTasks = getTodoLearningTasks();
handleTodoLearningTasks(todoLearningTasks);
if (aiEnable) {
List<WebElement> todoTestTasks = getTodoTestTasks();
handleTodoTestTasks(todoTestTasks);
}else {
log.info("AI答题未开启, 不处理测验任务");
}
log.info("任务完成!");
driver.quit();
@@ -77,6 +65,172 @@ public class ZhsTask {
}
}
private void handleTodoTestTasks(List<WebElement> todoTestTasks) throws InterruptedException {
log.info("开始处理测验任务...");
while (!todoTestTasks.isEmpty()){
todoTestTasks.getFirst().click();
waitingTaskPageLoading();
//跳转答题页面,并等待题目加载
driver.findElement(By.xpath("//div[@class='practice-handle ZHIHUISHU_QZMD']")).click();
while (true) {
WebElement questionContent = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='questionContent']")));
Thread.sleep(3000);
//读取题目信息并获取答案
String questionType = null;
while (questionType == null) {
try {
questionType = questionContent.findElement(By.xpath(".//div[@class='questionTitle']")).getText().split(" ")[1];
}catch (ArrayIndexOutOfBoundsException ignored){
Thread.sleep(200);
}
}
String questionTitle = questionContent.findElement(By.xpath(".//div[@class='centent-pre']")).getText();
switch (questionType) {
case "单选题", "判断题" -> {
SingleChoiceQuestion singleChoiceQuestion = new SingleChoiceQuestion();
singleChoiceQuestion.setTitle(questionTitle);
List<String> choices = new ArrayList<>();
List<WebElement> choiceElements = questionContent.findElements(By.xpath(".//li[@class='clearfix']"));
for (WebElement choiceElement : choiceElements) {
choices.add(choiceElement.getText());
}
singleChoiceQuestion.setChoices(choices);
handleSingleChoiceQuestion(singleChoiceQuestion, choiceElements);
}
case "多选题" -> {
MultiChoiceQuestion multiChoiceQuestion = new MultiChoiceQuestion();
multiChoiceQuestion.setTitle(questionTitle);
List<WebElement> choiceElements = questionContent.findElements(By.xpath(".//label[@class='el-checkbox']"));
List<String> choices = new ArrayList<>();
HashMap<String, WebElement> choiceElementsMap = new HashMap<>();
for (WebElement choiceElement : choiceElements) {
String choice = choiceElement.getText();
choices.add(choiceElement.getText());
choiceElementsMap.put(choice.substring(0, 1), choiceElement);
}
multiChoiceQuestion.setChoices(choices);
handleMultiChoiceQuestion(multiChoiceQuestion, choiceElementsMap);
}
default -> {
TextQuestion textQuestion = new TextQuestion();
textQuestion.setTitle(questionTitle);
handleTextQuestion(textQuestion);
}
}
//下一题或提交
WebElement nextButton = driver.findElement(By.xpath("//span[contains(@class,'next-topic')]"));
boolean hasNoNext = nextButton.getDomAttribute("class").contains("noNext");
if (hasNoNext) {
driver.findElement(By.xpath("//span[contains(@class,'reviewDone')]")).click();
break;
}else {
nextButton.click();
}
}
//回到任务列表页面
log.info("作答完毕");
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='backup-icon']"))).click();
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//img[@alt='back']"))).click();
//重新获取测验任务列表
todoTestTasks = getTodoTestTasks();
}
}
private void handleTextQuestion(TextQuestion textQuestion) throws InterruptedException {
TextAnswer textAnswer = (TextAnswer) TaskUtil.getAnswer(textQuestion);
String answer = textAnswer.getAnswer();
driver.findElement(By.xpath("//input[@class='el-input__inner']")).sendKeys(answer);
}
private void handleMultiChoiceQuestion(MultiChoiceQuestion multiChoiceQuestion, HashMap<String,WebElement> choiceElementsMap) throws InterruptedException {
MultiChoiceAnswer multiChoiceAnswer = (MultiChoiceAnswer) TaskUtil.getAnswer(multiChoiceQuestion);
List<String> answers = multiChoiceAnswer.getAnswers();
for (String answer : answers) {
WebElement answerElement = choiceElementsMap.get(answer);
while (answerElement.getDomAttribute("class").equals("el-checkbox")) {
answerElement.click();
Thread.sleep(300);
}
}
}
private void handleSingleChoiceQuestion(SingleChoiceQuestion singleChoiceQuestion, List<WebElement> choiceElements) throws InterruptedException {
SingleChoiceAnswer singleChoiceAnswer = (SingleChoiceAnswer) TaskUtil.getAnswer(singleChoiceQuestion);
String answer = singleChoiceAnswer.getAnswer();
for (WebElement choiceElement : choiceElements) {
if (choiceElement.getText().startsWith(answer)) {
String checkedAttribute = choiceElement.findElement(By.xpath(".//i[contains(@class,'iconfont checkIcon fl')]")).getDomAttribute("class");
while ("iconfont checkIcon fl".equals(checkedAttribute)) {
choiceElement.click();
Thread.sleep(300);
checkedAttribute = choiceElement.findElement(By.xpath(".//i[contains(@class,'iconfont checkIcon fl')]")).getDomAttribute("class");
}
break;
}
}
}
private void handleTodoLearningTasks(List<WebElement> todoLearningTasks) throws InterruptedException {
log.info("开始处理学习任务...");
while (!todoLearningTasks.isEmpty()) {
todoLearningTasks.getFirst().click();
waitingTaskPageLoading();
HashMap<WebElement, String> todoTaskPoints = getTodoTaskPoints();
log.info("发现{}个任务点", todoTaskPoints.size());
AtomicInteger count = new AtomicInteger(0);
for (Map.Entry<WebElement, String> entry : todoTaskPoints.entrySet()) {
WebElement k = entry.getKey();
String v = entry.getValue();
count.incrementAndGet();
log.info("处理第{}个任务点", count.get());
switch (v) {
case "icon-box video" -> handleVideoTask(k);
case "icon-box baidu", "icon-box bzhan" -> handleWebTask(k);
case "icon-box book", "icon-box other", "icon-box img" -> handleDocTask(k);
default -> throw new UnknownTaskPointException("未设置处理策略的任务点类型: " + v);
}
}
driver.findElement(By.xpath("//img[@alt='back']")).click();
todoLearningTasks = getTodoLearningTasks();
}
}
private void waitingTaskPageLoading() {
while (true) {
try {
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='resources-list']")));
break;
} catch (Exception e) {
driver.get(driver.getCurrentUrl());
}
}
}
private List<WebElement> getTodoTestTasks() {
log.info("获取测验任务...");
//必须通过item-content定位元素, 但页面在默认情况下都是item-content
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='item-content']")));
List<WebElement> totalTasks = driver.findElements(By.xpath("//div[@class='item-content']"));
List<WebElement> todoTasks = new ArrayList<>();
for (WebElement task : totalTasks) {
try{
String progress = task.findElement(By.xpath(".//div[@class='el-progress__text']")).getText();
if (progress.equals("0 %")){
todoTasks.add(task);
}
} catch (NoSuchElementException ignored) {
}
}
log.info("获取到{}个测验任务", todoTasks.size());
return todoTasks;
}
private void handleWebTask(WebElement task) throws InterruptedException {
//记录当前句柄
String primaryHandle = driver.getWindowHandle();
@@ -158,7 +312,8 @@ public class ZhsTask {
return todoTaskPoints;
}
private List<WebElement> getTodoTasks() {
private List<WebElement> getTodoLearningTasks() {
log.info("获取学习任务...");
List<WebElement> totalTasks;
List<WebElement> todoTasks = new ArrayList<>();
boolean pct = false;
@@ -186,7 +341,7 @@ public class ZhsTask {
}
todoTasks.add(task);
}
log.info("获取到{}个任务", todoTasks.size());
log.info("获取到{}个学习任务", todoTasks.size());
return todoTasks;
}
@@ -207,7 +362,7 @@ public class ZhsTask {
throw new TargetClassNotFoundException("未找到课程: " + targetClassName);
}
targetClass.click();
System.out.println();
}
private void login() {

View File

@@ -1,6 +1,8 @@
package work.slhaf.util;
import cn.hutool.json.JSONUtil;
import io.github.bonigarcia.wdm.WebDriverManager;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
@@ -9,7 +11,20 @@ import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import work.slhaf.config.Config;
import work.slhaf.exception.AiResponseFormatException;
import work.slhaf.pojo.answer.Answer;
import work.slhaf.pojo.answer.MultiChoiceAnswer;
import work.slhaf.pojo.answer.SingleChoiceAnswer;
import work.slhaf.pojo.answer.TextAnswer;
import work.slhaf.pojo.question.MultiChoiceQuestion;
import work.slhaf.pojo.question.Question;
import work.slhaf.pojo.question.SingleChoiceQuestion;
import work.slhaf.pojo.question.TextQuestion;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class TaskUtil {
private TaskUtil() {}
@@ -30,4 +45,42 @@ public class TaskUtil {
default -> throw new IllegalStateException("不支持的浏览器驱动!: " + config.getBrowser());
};
}
public static Answer getAnswer(Question question) throws InterruptedException {
Answer answer = null;
int tryTimes = 0;
while (tryTimes < 3) {
try {
String questionStr = JSONUtil.toJsonPrettyStr(question);
String responseStr = AiUtil.runChat(questionStr);
// 使用正则表达式提取Markdown代码块中的JSON内容
String jsonPattern = "```[\\s\\S]*?\\n([\\s\\S]*?)\\n```";
Pattern pattern = Pattern.compile(jsonPattern);
Matcher matcher = pattern.matcher(responseStr);
String jsonStr = "";
if (matcher.find()) {
jsonStr = matcher.group(1).trim();
}
if (jsonStr.isEmpty()) {
throw new AiResponseFormatException("AI 回答格式不匹配: " + jsonStr);
}
if (question instanceof SingleChoiceQuestion) {
answer = JSONUtil.toBean(jsonStr, SingleChoiceAnswer.class);
} else if (question instanceof MultiChoiceQuestion) {
answer = JSONUtil.toBean(jsonStr, MultiChoiceAnswer.class);
} else if (question instanceof TextQuestion) {
answer = JSONUtil.toBean(jsonStr, TextAnswer.class);
}
break;
} catch (AiResponseFormatException e) {
log.warn(e.getLocalizedMessage());
log.warn("3s后重试...");
Thread.sleep(3000);
tryTimes++;
}
}
return answer;
}
}