已实现文档浏览、视频播放功能,答题模块中简答题、判断题以及多选题的答案填充待实现;

部分异常捕获需要重做相关逻辑
This commit is contained in:
2025-03-24 13:08:07 +08:00
commit 793ded083b
30 changed files with 1376 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

78
pom.xml Normal file
View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>work.slhaf</groupId>
<artifactId>CxstudyAuto</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.29.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.17</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ocr_api20210707</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.9.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>htmlunit-driver</artifactId>
<version>4.13.0</version>
</dependency>-->
<!--<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.70.0</version>
</dependency>-->
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -0,0 +1,21 @@
package work.slhaf;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.config.Config;
import work.slhaf.task.cxstudy.CxstudyTask;
import work.slhaf.util.AiUtil;
import work.slhaf.util.OcrUtil;
import java.io.IOException;
@Slf4j
public class Main {
public static void main(String[] args) throws InterruptedException, IOException {
Config config = Config.load();
AiUtil.load(config);
OcrUtil.load(config);
CxstudyTask.run(config);
}
}

View File

@@ -0,0 +1,77 @@
package work.slhaf.chat;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import work.slhaf.chat.constant.Constant;
import work.slhaf.chat.pojo.ChatBody;
import work.slhaf.chat.pojo.ChatResponse;
import work.slhaf.chat.pojo.Message;
import work.slhaf.chat.pojo.PrimaryChatResponse;
import java.util.ArrayList;
import java.util.List;
@Data
@NoArgsConstructor
public class ChatClient {
private String clientId;
private String url;
private String apikey;
private String model;
private int top_p;
private int temperature;
private int max_tokens;
public ChatClient(String url,String apikey,String model){
this.url = url;
this.apikey = apikey;
this.model = model;
}
public ChatResponse runChat(List<Message> messages){
HttpRequest request = HttpRequest.post(url);
request.header("Content-Type", "application/json");
request.header("Authorization", "Bearer " + apikey);
ChatBody body;
if (top_p > 0) {
body = ChatBody.builder()
.model(model)
.messages(messages)
.top_p(top_p)
.temperature(temperature)
.max_tokens(max_tokens)
.build();
}else {
body = ChatBody.builder()
.model(model)
.messages(messages)
.build();
}
HttpResponse response = request.body(JSONUtil.toJsonStr(body)).execute();
ChatResponse finalResponse;
try {
PrimaryChatResponse primaryChatResponse = JSONUtil.toBean(response.body(), PrimaryChatResponse.class);
finalResponse = ChatResponse.builder()
.type(Constant.Response.SUCCESS)
.message(primaryChatResponse.getChoices().get(0).getMessage().getContent())
.usageBean(primaryChatResponse.getUsage())
.build();
}catch (Exception e){
finalResponse = ChatResponse.builder()
.type(Constant.Response.ERROR)
.message(response.getStatus()+": "+response.body())
.build();
}
response.close();
return finalResponse;
}
}

View File

@@ -0,0 +1,22 @@
package work.slhaf.chat.constant;
public class Constant {
public static class Character {
public static final String USER = "user";
public static final String SYSTEM = "system";
public static final String ASSISTANT = "assistant";
}
public static class Model {
public static final String DEEP_SEEK_CHAT = "deepseek-chat";
public static final String GLM_4_FLASH = "glm-4_flash";
public static final String GLM_4_PLUS = "glm-4_plus";
public static final String GLM_4_0520 = "glm-4_0520";
}
public static class Response {
public static final String SUCCESS = "success";
public static final String ERROR = "error";
}
}

View File

@@ -0,0 +1,25 @@
package work.slhaf.chat.pojo;
import lombok.*;
import java.util.List;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatBody {
@NonNull
private String model;
@NonNull
private List<Message> messages;
@Builder.Default
private int temperature = 1;
@Builder.Default
private int top_p = 1;
private boolean stream;
@Builder.Default
private int max_tokens = 1024;
private int presence_penalty;
private int frequency_penalty;
}

View File

@@ -0,0 +1,16 @@
package work.slhaf.chat.pojo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatResponse {
private String type;
private String message;
private PrimaryChatResponse.UsageBean usageBean;
}

View File

@@ -0,0 +1,14 @@
package work.slhaf.chat.pojo;
import lombok.*;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
@NonNull
private String role;
@NonNull
private String content;
}

View File

@@ -0,0 +1,111 @@
package work.slhaf.chat.pojo;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class PrimaryChatResponse {
/**
* id
*/
private String id;
/**
* object
*/
private String object;
/**
* created
*/
private int created;
/**
* model
*/
private String model;
/**
* choices
*/
private List<ChoicesBean> choices;
/**
* usage
*/
private UsageBean usage;
/**
* system_fingerprint
*/
private String system_fingerprint;
@Setter
@Getter
public static class UsageBean {
/**
* prompt_tokens
*/
private int prompt_tokens;
/**
* completion_tokens
*/
private int completion_tokens;
/**
* total_tokens
*/
private int total_tokens;
/**
* prompt_cache_hit_tokens
*/
private int prompt_cache_hit_tokens;
/**
* prompt_cache_miss_tokens
*/
private int prompt_cache_miss_tokens;
@Override
public String toString() {
return "UsageBean{" +
"prompt_tokens=" + prompt_tokens +
", completion_tokens=" + completion_tokens +
", total_tokens=" + total_tokens +
", prompt_cache_hit_tokens=" + prompt_cache_hit_tokens +
", prompt_cache_miss_tokens=" + prompt_cache_miss_tokens +
'}';
}
}
@Setter
@Getter
public static class ChoicesBean {
/**
* index
*/
private int index;
/**
* message
*/
private MessageBean message;
/**
* logprobs
*/
private Object logprobs;
/**
* finish_reason
*/
private String finish_reason;
@Setter
@Getter
public static class MessageBean {
/**
* role
*/
private String role;
/**
* content
*/
private String content;
}
}
}

View File

@@ -0,0 +1,21 @@
package work.slhaf.config;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AiConfig {
private String apikey;
private String model;
private String baseUrl;
@Override
public String toString() {
return """
AiConfig:
apikey: '%s'
model: '%s'
""".formatted(apikey, model);
}
}

View File

@@ -0,0 +1,89 @@
package work.slhaf.config;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
@Data
@Slf4j
public class Config {
private CxstudyConfig cxstudyConfig;
private OcrConfig ocrConfig;
private AiConfig aiConfig;
private Config() {
}
public static Config load() throws IOException {
File file = new File("./config.json");
boolean configExists = checkConfigExists(file);
if (!configExists) {
generateConfig(file);
}
return loadFromFile(file);
}
private static void generateConfig(File file) throws IOException {
Scanner sc = new Scanner(System.in);
System.out.println("\r\n---------------\r\n");
AiConfig aiConfig = new AiConfig();
System.out.println("recording ai config:");
System.out.print("apikey: ");
aiConfig.setApikey(sc.nextLine());
System.out.print("baseUrl: ");
aiConfig.setBaseUrl(sc.nextLine());
System.out.print("model: ");
aiConfig.setModel(sc.nextLine());
System.out.println("\r\n---------------\r\n");
OcrConfig ocrConfig = new OcrConfig();
System.out.println("recording ocr config:");
System.out.print("accessKeyId: ");
ocrConfig.setAccessKeyId(sc.nextLine());
System.out.print("accessKeySecret: ");
ocrConfig.setAccessKeySecret(sc.nextLine());
System.out.println("\r\n---------------\r\n");
CxstudyConfig studyConfig = new CxstudyConfig();
System.out.println("recording study config:");
System.out.print("phone: ");
studyConfig.setPhone(sc.nextLine());
System.out.print("password: ");
studyConfig.setPassword(sc.nextLine());
System.out.print("target: ");
studyConfig.setTarget(sc.nextLine());
System.out.println("\r\n---------------\r\n");
Config config = new Config();
config.cxstudyConfig = studyConfig;
config.ocrConfig = ocrConfig;
config.aiConfig = aiConfig;
String configStr = JSONUtil.toJsonPrettyStr(config);
System.out.println("this is your config file: \r\n" + configStr);
System.out.println("press ENTER to generate config: ");
sc.nextLine();
file.createNewFile();
FileUtils.writeStringToFile(file, configStr, StandardCharsets.UTF_8);
System.out.println(" config generated");
}
private static Config loadFromFile(File file) throws IOException {
return JSONUtil.toBean(FileUtils.readFileToString(file, StandardCharsets.UTF_8), Config.class);
}
private static boolean checkConfigExists(File file) {
return file.exists();
}
}

View File

@@ -0,0 +1,22 @@
package work.slhaf.config;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CxstudyConfig {
private String phone;
private String password;
private String target;
@Override
public String toString() {
return """
CxstudyConfig:
phone: '%s'
password: '%s'
target: '%s'
""".formatted(phone, password, target);
}
}

View File

@@ -0,0 +1,20 @@
package work.slhaf.config;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OcrConfig {
private String accessKeyId;
private String accessKeySecret;
@Override
public String toString() {
return """
OcrConfig:
accessKeyId: '%s'
accessKeySecret: '%s'
""".formatted(accessKeyId, accessKeySecret);
}
}

View File

@@ -0,0 +1,186 @@
package work.slhaf.pojo;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Setter
@Getter
public class OCRDataInfo {
/**
* algo_version
*/
private String algo_version;
/**
* 文字内容-段落
*/
private String content;
/**
* height
*/
private int height;
/**
* orgHeight
*/
private int orgHeight;
/**
* orgWidth
*/
private int orgWidth;
/**
* prism_version
*/
private String prism_version;
/**
* prism_wnum
*/
private int prism_wnum;
/**
* prism_wordsInfo
*/
private List<PrismWordsInfoBean> prism_wordsInfo;
/**
* width
*/
private int width;
public static class PrismWordsInfoBean {
/**
* angle
*/
private int angle;
/**
* direction
*/
private int direction;
/**
* height
*/
private int height;
/**
* pos
*/
private List<PosBean> pos;
/**
* prob
*/
private int prob;
/**
* width
*/
private int width;
/**
* 文字内容-行
*/
private String word;
/**
* x
*/
private int x;
/**
* y
*/
private int y;
public int getAngle() {
return angle;
}
public void setAngle(int angle) {
this.angle = angle;
}
public int getDirection() {
return direction;
}
public void setDirection(int direction) {
this.direction = direction;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public List<PosBean> getPos() {
return pos;
}
public void setPos(List<PosBean> pos) {
this.pos = pos;
}
public int getProb() {
return prob;
}
public void setProb(int prob) {
this.prob = prob;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public static class PosBean {
/**
* x
*/
private int x;
/**
* y
*/
private int y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
}
}

View File

@@ -0,0 +1,334 @@
package work.slhaf.task.cxstudy;
import cn.hutool.json.JSONUtil;
import io.github.bonigarcia.wdm.WebDriverManager;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
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.task.cxstudy.pojo.answer.Answer;
import work.slhaf.task.cxstudy.pojo.answer.MultiChoiceAnswer;
import work.slhaf.task.cxstudy.pojo.answer.SingleChoiceAnswer;
import work.slhaf.task.cxstudy.pojo.answer.TextAnswer;
import work.slhaf.task.cxstudy.pojo.question.MultiChoiceQuestion;
import work.slhaf.task.cxstudy.pojo.question.SingleChoiceQuestion;
import work.slhaf.task.cxstudy.pojo.question.Question;
import work.slhaf.task.cxstudy.pojo.question.TextQuestion;
import work.slhaf.util.AiUtil;
import work.slhaf.util.OcrUtil;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class CxstudyTask {
static WebDriver driver;
static Actions actions;
static WebDriverWait wait;
public static void run(Config config) throws InterruptedException {
String phone = config.getCxstudyConfig().getPhone();
String passwd = config.getCxstudyConfig().getPassword();
String targetClassName = config.getCxstudyConfig().getTarget();
while (true) {
try {
//注册driver
initialize();
//登录、跳转页面
loginAndSwitch(phone, passwd, targetClassName);
Thread.sleep(2000);
List<WebElement> toBeFinished = getTodoTasks();
int chapterCount = 0;
while (!toBeFinished.isEmpty()) {
//记录元素对应文本
toBeFinished.getFirst().click();
chapterCount++;
log.info("处理第{}节", chapterCount);
// toBeFinished.get(14).click();
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe"));
int subTasksNum = driver.findElements(By.xpath(".//div[@class='ans-attach-ct']")).size();
for (int i = 0; i < subTasksNum; i++) {
//检测frame内部元素
//frame分为三层:
// 第一层index: 6 保存所有含有内容的frame
// 第二层index: i 视频层 i->prev_video_left
// 第三层index: 0->newTestTitle, 2->fileBox
// int taskContainersNum = driver.findElements(By.xpath(".//div[@class='ans-attach-ct']")).size();
// log.info("发现{}个任务", taskContainersNum);
// for (int t = 0; t < taskContainersNum; t++) {
// handleVideoTask(i);
handleTestTask(i);
// handleDocTask(i);
// }
}
driver.get(driver.getCurrentUrl());
driver.switchTo().defaultContent();
toBeFinished = getTodoTasks();
}
driver.quit();
break;
} catch (Exception e) {
log.error("\r\n---------------\r\n");
log.error(e.getLocalizedMessage());
log.error("\r\n---------------\r\n");
driver.quit();
log.error("Something went wrong, will retry later..");
Thread.sleep(5000);
}
}
}
private static void handleDocTask(int i) {
try {
driver.switchTo().frame(i).switchTo().frame(2);
log.info("开始处理文档浏览...");
JavascriptExecutor js = (JavascriptExecutor) driver;
long lastHeight = (long) js.executeScript("return document.body.scrollHeight;");
long currentHeight = 0;
int step = 2000;
while (currentHeight < lastHeight) {
js.executeScript("window.scrollBy(0, arguments[0]);", step);
Thread.sleep(600);
currentHeight = (long) js.executeScript("return window.scrollY + window.innerHeight;");
lastHeight = (long) js.executeScript("return document.body.scrollHeight;");
}
log.info("文档浏览任务完成!");
} catch (Exception ignored) {
} finally {
driver.switchTo().defaultContent();
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe"));
}
}
private static void handleTestTask(int i) {
try {
driver.switchTo().frame(i).switchTo().frame(0);
wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@class='singleQuesId']")));
log.info("开始处理测验...");
//所有题目元素
List<WebElement> questions = driver.findElements(By.xpath(".//div[@class='singleQuesId']"));
log.info("发现{}个题目...", questions.size());
for (WebElement question : questions) {
log.info("处理第{}个题目...", questions.indexOf(question));
//获取题目截图通过OCR获取题目信息
WebElement questionTitleElement = question.findElement(By.xpath(".//div[@class='TiMu newTiMu']"));
File screenshot = questionTitleElement.getScreenshotAs(OutputType.FILE);
String questionTitle = OcrUtil.getContentOfImage(screenshot);
log.info("题目信息: {}", questionTitle);
try {
//尝试获取单选选项
//li role='radio'
question.findElement(By.xpath(".//li[@role='radio']"));
List<WebElement> choiceElements = question.findElements(By.xpath(".//li[@role='radio']"));
SingleChoiceQuestion singleChoiceQuestion = new SingleChoiceQuestion();
singleChoiceQuestion.setTitle(questionTitle);
List<String> choices = getChoices(choiceElements);
singleChoiceQuestion.setChoices(choices);
Answer primaryAnswer = getAnswer(singleChoiceQuestion);
SingleChoiceAnswer answer = (SingleChoiceAnswer) primaryAnswer;
String answerContent = answer.getAnswer();
log.info("答案: {}", answerContent);
String xpath = ".//span[@data='" + answerContent + "']";
question.findElement(By.xpath(xpath)).findElement(By.xpath("..")).findElement(By.xpath("..")).click();
continue;
} catch (Exception ignored) {
}
try {
//尝试获取复选框
//li role='checkbox'
question.findElement(By.xpath(".//li[@role='checkbox']"));
List<WebElement> choiceElements = question.findElements(By.xpath(".//li[@role='checkbox']"));
MultiChoiceQuestion multiChoiceQuestion = new MultiChoiceQuestion();
multiChoiceQuestion.setTitle(questionTitle);
List<String> choices = getChoices(choiceElements);
multiChoiceQuestion.setChoices(choices);
continue;
} catch (Exception ignored) {
}
try {
//尝试跳转到简答题输入框
driver.switchTo().frame("ueditor_0");
driver.findElement(By.tagName("p")).sendKeys("answer");
driver.switchTo().parentFrame();
} catch (Exception ignored) {
}
}
log.info("答题任务完成!");
} catch (Exception ignored) {
} finally {
driver.switchTo().defaultContent();
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe"));
}
}
private static List<String> getChoices(List<WebElement> choiceElements) {
List<String> choices = new ArrayList<>();
for (WebElement choiceElement : choiceElements) {
String label = choiceElement.findElement(By.tagName("label")).getText();
String content = choiceElement.findElement(By.tagName("a")).getText();
choices.add(String.format("%s. %s", label, content));
}
return choices;
}
private 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 Exception("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 (Exception e) {
log.error(e.getLocalizedMessage());
log.error("3s后重试...");
Thread.sleep(3000);
tryTimes++;
}
}
return answer;
}
private static void handleVideoTask(int i) {
try {
driver.switchTo().frame(i);
wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@class='prev_video_left']")));
log.info("开始处理视频播放...");
WebElement play = wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//button[@class='vjs-big-play-button']")));
Thread.sleep(2000);
play.click();
WebElement video = driver.findElement(By.id("video_html5_api"));
//获取总时长
String totalTime = driver.findElement(By.xpath("//span[@class='vjs-duration-display']")).getText();
// 获取视频的位置信息
Point location = video.getLocation();
Dimension size = video.getSize();
int videoX = location.getX();
int videoY = location.getY();
int videoWidth = size.getWidth();
int videoHeight = size.getHeight();
// 确定鼠标起始位置:进入视频区域
int startX = videoX + videoWidth / 4;
int startY = videoY + videoHeight / 4;
actions.moveToElement(video, startX, startY).perform();
Thread.sleep(500);
int step = 10; // 每次移动步长
while (true) {
for (int j = 0; j < 20; j++) {
int offsetX = (int) (Math.random() * step) - step / 2; // 随机左右移动
int offsetY = (int) (Math.random() * step) - step / 2; // 随机上下移动
actions.moveByOffset(offsetX, offsetY).perform();
Thread.sleep(200);
}
String currentTime = driver.findElement(By.xpath(".//span[@class='vjs-current-time-display']")).getText();
if (totalTime.equals("0:00") || totalTime.isEmpty()) {
totalTime = driver.findElement(By.xpath("//span[@class='vjs-duration-display']")).getText();
} else if (currentTime.equals(totalTime)) {
log.info(totalTime);
break;
}
log.info(currentTime + "/" + totalTime);
//恢复暂停
try {
WebElement playingButton = driver.findElement(By.xpath(".//button[@class='vjs-play-control vjs-control vjs-button vjs-paused']"));
playingButton.click();
} catch (Exception ignored) {
}
}
log.info("视频播放任务完成!");
} catch (Exception ignored) {
} finally {
driver.switchTo().defaultContent();
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt("iframe"));
}
}
private static List<WebElement> getTodoTasks() {
driver.switchTo().defaultContent();
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='posCatalog_select']")));
List<WebElement> toBeFinished = new ArrayList<>();
for (WebElement webElement : driver.findElements(By.xpath("//div[@class='posCatalog_select']"))) {
try {
webElement.findElement(By.xpath(".//span[@class='orangeNew']"));
toBeFinished.add(webElement);
} catch (Exception ignored) {
}
}
log.info("获取到{}个任务", toBeFinished.size());
return toBeFinished;
}
private static void loginAndSwitch(String phone, String passwd, String targetClassName) {
log.info("登录中...");
driver.get("https://i.chaoxing.com/");
WebElement phoneInput = driver.findElement(By.xpath("//input[@id='phone']"));
phoneInput.sendKeys(phone);
WebElement passwdInput = driver.findElement(By.xpath("//input[@id='pwd']"));
passwdInput.sendKeys(passwd);
WebElement submitInput = driver.findElement(By.xpath("//button[@id='loginBtn']"));
submitInput.click();
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@name='课程']"))).click();
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(0));
String xpath = ".//span[@title='" + targetClassName + "']";
String targetClassHref = wait.until(ExpectedConditions.elementToBeClickable(By.xpath(xpath))).findElement(By.xpath("..")).getDomAttribute("href");
driver.get(targetClassHref);
driver.findElement(By.xpath("//a[@title='章节']")).click();
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(0));
wait.until(ExpectedConditions.elementToBeClickable(By.xpath(".//div[@role='link']"))).findElement(By.xpath("..")).click();
log.info("登录成功!");
}
private static void initialize() {
WebDriverManager.firefoxdriver().setup();
driver = new FirefoxDriver();
actions = new Actions(driver);
wait = new WebDriverWait(driver, Duration.ofSeconds(5));
log.info("webdriver创建完毕!");
}
}

View File

@@ -0,0 +1,4 @@
package work.slhaf.task.cxstudy.pojo.answer;
public class Answer {
}

View File

@@ -0,0 +1,12 @@
package work.slhaf.task.cxstudy.pojo.answer;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class MultiChoiceAnswer extends Answer {
private List<String> answers;
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.task.cxstudy.pojo.answer;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class SingleChoiceAnswer extends Answer {
private String answer;
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.task.cxstudy.pojo.answer;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class TextAnswer extends Answer {
private String answer;
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.task.cxstudy.pojo.question;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class MultiChoiceQuestion extends Question {
private String type = "多选题";
private List<String> choices;
}

View File

@@ -0,0 +1,8 @@
package work.slhaf.task.cxstudy.pojo.question;
import lombok.Data;
@Data
public class Question {
private String title;
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.task.cxstudy.pojo.question;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class SingleChoiceQuestion extends Question {
private String type = "单选题题";
private List<String> choices;
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.task.cxstudy.pojo.question;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class TextQuestion extends Question{
private String type = "简答题";
}

View File

@@ -0,0 +1,90 @@
package work.slhaf.util;
import work.slhaf.chat.ChatClient;
import work.slhaf.chat.constant.Constant;
import work.slhaf.chat.pojo.ChatResponse;
import work.slhaf.chat.pojo.Message;
import work.slhaf.config.Config;
import java.util.ArrayList;
import java.util.List;
public class AiUtil {
private static ChatClient chatClient;
private static final String PROMPT = """
你是一个智能问卷系统助手。你将接收一个JSON格式的Question对象需要根据Question的类型和内容生成对应的Answer JSON并将结果放入Markdown代码块中。
Question JSON可能包含以下类型
1. 简答题TextQuestion包含title字段和type字段
2. 单选题SingleChoiceQuestion包含title字段、type字段和choices字段
3. 多选题MultiChoiceQuestion包含title字段、type字段和choices字段
请根据以下规则生成Answer JSON
1. 如果Question类型是"简答题"生成TextAnswer对象包含answer字段
2. 如果Question类型是"单选题"生成SingleChoiceAnswer对象包含answer字段且answer必须是choices中的一个选项
3. 如果Question类型是"多选题"生成MultiChoiceAnswer对象包含answers字段且answers必须是choices中的多个选项至少选择一个。注意choices的格式为"A. 答案1""B. 答案2"等,你只需要返回选项字母(如"A""B"),不需要返回选项内容
示例1
输入:
{
"title": "请描述你的工作经历",
"type": "简答题"
}
输出:
```json
{
"answer": "我有5年Java开发经验曾参与多个大型项目..."
}
```
示例2
输入:
{
"title": "你最喜欢的编程语言是?",
"type": "单选题",
"choices": ["A. Java", "B. Python", "C. C++"]
}
输出:
```json
{
"answer": "B"
}
```
示例3
输入:
{
"title": "你使用过哪些数据库?",
"type": "多选题",
"choices": ["A. MySQL", "B. MongoDB", "C. Redis"]
}
输出:
```json
{
"answers": ["A", "C"]
}
```
注意两种选择题的选项内容不包含序号可能存在乱码当输入的Question JSON为选择题时你需要同时根据choices和title中的内容来确定正确选项。
请根据接收到的Question JSON生成对应的Answer JSON并将结果放入Markdown代码块中。确保答案内容合理且符合问题要求。
""";
private AiUtil() {}
public static void load(Config config) {
String apikey = config.getAiConfig().getApikey();
String model = config.getAiConfig().getModel();
String baseUrl = config.getAiConfig().getBaseUrl();
chatClient = new ChatClient(baseUrl, apikey, model);
}
public static String runChat(String message) {
List<Message> messages = new ArrayList<>();
Message system = new Message(Constant.Character.SYSTEM,PROMPT);
messages.add(system);
messages.add(new Message(Constant.Character.USER,message));
ChatResponse chatResponse = chatClient.runChat(messages);
return chatResponse.getMessage();
}
}

View File

@@ -0,0 +1,84 @@
package work.slhaf.util;
import cn.hutool.json.JSONUtil;
import com.aliyun.ocr_api20210707.Client;
import com.aliyun.ocr_api20210707.models.RecognizeAdvancedRequest;
import com.aliyun.ocr_api20210707.models.RecognizeAdvancedResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.config.Config;
import work.slhaf.pojo.OCRDataInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@Slf4j
public class OcrUtil {
private static Client client;
private OcrUtil() {}
public static void load(Config basicConfig) {
//读取密钥
String accessKeyId = basicConfig.getOcrConfig().getAccessKeyId();
String accessKeySecret = basicConfig.getOcrConfig().getAccessKeySecret();
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
try {
client = new com.aliyun.ocr_api20210707.Client(config);
} catch (Exception e) {
log.error("创建client出错");
e.printStackTrace();
}
log.info("OCR client已创建");
}
public static String getContentOfImage(File file) throws FileNotFoundException {
//设置请求信息
RecognizeAdvancedRequest recognizeAdvancedRequest = new RecognizeAdvancedRequest()
.setBody(new FileInputStream(file))
.setNeedRotate(Boolean.TRUE)
.setNeedRotate(Boolean.TRUE);
try {
//发送请求并处理回应
RecognizeAdvancedResponse response = client.recognizeAdvancedWithOptions(recognizeAdvancedRequest, new RuntimeOptions());
OCRDataInfo ocrDataInfo = JSONUtil.toBean(response.getBody().getData(), OCRDataInfo.class);
if (!ocrDataInfo.getContent().isEmpty()) {
StringBuilder str = new StringBuilder();
ocrDataInfo.getPrism_wordsInfo().forEach(prismWordsInfoBean -> {
str.append(prismWordsInfoBean.getWord());
if (!ocrDataInfo.getPrism_wordsInfo().get(ocrDataInfo.getPrism_wordsInfo().size() - 1).equals(prismWordsInfoBean)) {
str.append("\r\n");
}
});
return str.toString();
} else {
return null;
}
} catch (TeaException error) {
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
log.error(error.getMessage());
// 诊断地址
log.error(error.getData().get("Recommend").toString());
com.aliyun.teautil.Common.assertAsString(error.message);
return "ERROR";
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
log.error(error.getMessage());
// 诊断地址
log.error(error.getData().get("Recommend").toString());
com.aliyun.teautil.Common.assertAsString(error.message);
return "ERROR";
}
}
}

View File

@@ -0,0 +1,13 @@
import org.junit.Test;
import work.slhaf.config.Config;
import java.io.IOException;
public class ProjectTest {
@Test
public void configTest() throws IOException {
Config config = Config.load();
System.out.println(config.toString());
}
}