From acc90b6bf3fe18f4f44877b8a87ccda694ad0add Mon Sep 17 00:00:00 2001 From: slhaf Date: Mon, 7 Apr 2025 22:05:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86"=E6=99=BA=E6=85=A7?= =?UTF-8?q?=E6=A0=91"=E5=B9=B3=E5=8F=B0=E7=9A=84AI=E7=AD=94=E9=A2=98?= =?UTF-8?q?=E5=8A=9F=E8=83=BD;=20=E6=8A=BD=E5=8F=96=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BB=BB=E5=8A=A1=E5=85=B1=E7=94=A8=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E8=87=B3TaskUtil=E4=B8=AD;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + .../java/work/slhaf/task/CxstudyTask.java | 42 +--- src/main/java/work/slhaf/task/ZhsTask.java | 213 +++++++++++++++--- src/main/java/work/slhaf/util/TaskUtil.java | 53 +++++ 4 files changed, 243 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index d6a9022..f085998 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,7 @@ - 视频播放 - 文档浏览 - 网页浏览 +- AI答题 > 登录需确保智慧树账号绑定QQ号,使用时需确保QQ在线,通过QQ进行自动登录 + +> AI答题部分简答题模块目前只遇到且实现了单空题的作答,不清楚是否存在多个输入框的题目 diff --git a/src/main/java/work/slhaf/task/CxstudyTask.java b/src/main/java/work/slhaf/task/CxstudyTask.java index 2e2466a..dfcb6dc 100644 --- a/src/main/java/work/slhaf/task/CxstudyTask.java +++ b/src/main/java/work/slhaf/task/CxstudyTask.java @@ -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 choices = getChoices(choiceElements); multiChoiceQuestion.setChoices(choices); - Answer answer = getAnswer(multiChoiceQuestion); + Answer answer = TaskUtil.getAnswer(multiChoiceQuestion); MultiChoiceAnswer multiChoiceAnswer = (MultiChoiceAnswer) answer; List answers = multiChoiceAnswer.getAnswers(); //输入答案,并检测是否正确输入 @@ -233,7 +233,7 @@ public class CxstudyTask { singleChoiceQuestion.setTitle(questionTitle); List 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 { diff --git a/src/main/java/work/slhaf/task/ZhsTask.java b/src/main/java/work/slhaf/task/ZhsTask.java index fdc6ee8..7240c27 100644 --- a/src/main/java/work/slhaf/task/ZhsTask.java +++ b/src/main/java/work/slhaf/task/ZhsTask.java @@ -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 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 todoTaskPoints = getTodoTaskPoints(); - log.info("发现{}个任务点", todoTaskPoints.size()); - AtomicInteger count = new AtomicInteger(0); - for (Map.Entry 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 todoLearningTasks = getTodoLearningTasks(); + handleTodoLearningTasks(todoLearningTasks); + if (aiEnable) { + List todoTestTasks = getTodoTestTasks(); + handleTodoTestTasks(todoTestTasks); + }else { + log.info("AI答题未开启, 不处理测验任务"); } log.info("任务完成!"); driver.quit(); @@ -77,6 +65,172 @@ public class ZhsTask { } } + private void handleTodoTestTasks(List 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 choices = new ArrayList<>(); + List 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 choiceElements = questionContent.findElements(By.xpath(".//label[@class='el-checkbox']")); + List choices = new ArrayList<>(); + HashMap 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 choiceElementsMap) throws InterruptedException { + MultiChoiceAnswer multiChoiceAnswer = (MultiChoiceAnswer) TaskUtil.getAnswer(multiChoiceQuestion); + List 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 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 todoLearningTasks) throws InterruptedException { + log.info("开始处理学习任务..."); + while (!todoLearningTasks.isEmpty()) { + todoLearningTasks.getFirst().click(); + waitingTaskPageLoading(); + + HashMap todoTaskPoints = getTodoTaskPoints(); + log.info("发现{}个任务点", todoTaskPoints.size()); + AtomicInteger count = new AtomicInteger(0); + for (Map.Entry 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 getTodoTestTasks() { + log.info("获取测验任务..."); + //必须通过item-content定位元素, 但页面在默认情况下都是item-content + wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='item-content']"))); + List totalTasks = driver.findElements(By.xpath("//div[@class='item-content']")); + List 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 getTodoTasks() { + private List getTodoLearningTasks() { + log.info("获取学习任务..."); List totalTasks; List 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() { diff --git a/src/main/java/work/slhaf/util/TaskUtil.java b/src/main/java/work/slhaf/util/TaskUtil.java index 3191e90..dfac15b 100644 --- a/src/main/java/work/slhaf/util/TaskUtil.java +++ b/src/main/java/work/slhaf/util/TaskUtil.java @@ -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; + } + }