├── .DS_Store ├── doc ├── env.png ├── 架构.png ├── 演示.png ├── ChatGPT交流.png └── 架构图.drawio ├── src ├── .DS_Store ├── test │ ├── .DS_Store │ └── java │ │ └── com │ │ └── yupi │ │ └── autoreply │ │ ├── MainApplicationTests.java │ │ └── api │ │ ├── openai │ │ └── OpenAiApiTest.java │ │ └── zsxq │ │ └── ZsxqApiTest.java └── main │ ├── resources │ ├── banner.txt │ ├── application.yml │ └── META-INF │ │ └── additional-spring-configuration-metadata.json │ └── java │ └── com │ └── yupi │ └── autoreply │ ├── answerer │ ├── Answerer.java │ ├── DefaultAnswerer.java │ └── OpenAiAnswerer.java │ ├── controller │ └── MainController.java │ ├── api │ ├── openai │ │ ├── model │ │ │ ├── ModelConstant.java │ │ │ ├── CreateCompletionRequest.java │ │ │ └── CreateCompletionResponse.java │ │ └── OpenAiApi.java │ └── zsxq │ │ ├── model │ │ ├── ListTopicsRequest.java │ │ ├── AnswerRequest.java │ │ ├── AnswerResponse.java │ │ └── ListTopicsResponse.java │ │ └── ZsxqApi.java │ ├── model │ └── TaskListItem.java │ ├── monitor │ ├── Monitor.java │ ├── DefaultMonitor.java │ └── ZsxqMonitor.java │ ├── config │ ├── ZsxqConfig.java │ ├── OpenAiConfig.java │ ├── CorsConfig.java │ ├── Knife4jConfig.java │ └── TaskConfig.java │ ├── factory │ ├── AnswererFactory.java │ └── MonitorFactory.java │ ├── common │ ├── BaseResponse.java │ ├── ErrorCode.java │ └── ResultUtils.java │ ├── exception │ ├── BusinessException.java │ ├── GlobalExceptionHandler.java │ └── ThrowUtils.java │ ├── job │ └── JobMediator.java │ ├── MainApplication.java │ └── utils │ └── SpringContextUtils.java ├── .env ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── Dockerfile ├── README.md ├── .gitignore ├── pom.xml ├── mvnw.cmd └── mvnw /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/.DS_Store -------------------------------------------------------------------------------- /doc/env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/env.png -------------------------------------------------------------------------------- /doc/架构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/架构.png -------------------------------------------------------------------------------- /doc/演示.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/演示.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /doc/ChatGPT交流.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/doc/ChatGPT交流.png -------------------------------------------------------------------------------- /src/test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyupi/yu-auto-reply/HEAD/src/test/.DS_Store -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | 🐟 防伪水印哈哈哈哈 3 | by 程序员鱼皮:https://github.com/liyupi 4 | 可能是最好的编程学习圈子:https://yupi.icu 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | OPENAI_MODEL=text-davinci-003 2 | OPENAI_API_KEY=你的API_KEY 3 | ZSXQ_COOKIE=你的星球Cookie 4 | ZSXQ_GROUP_ID=你的星球id 5 | ZSXQ_SILENCED=是否只提醒提问者 -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/answerer/Answerer.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.answerer; 2 | 3 | /** 4 | * 回答者 5 | * 6 | * @author 程序员鱼皮 7 | * @from 编程导航知识星球 8 | */ 9 | public interface Answerer { 10 | 11 | /** 12 | * 回答 13 | * 14 | * @param prompt 提示语 15 | * @return 回答结果 16 | */ 17 | String doAnswer(String prompt); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/answerer/DefaultAnswerer.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.answerer; 2 | 3 | /** 4 | * 默认回答者(降级) 5 | * 6 | * @author 程序员鱼皮 7 | * @from 编程导航知识星球 8 | */ 9 | public class DefaultAnswerer implements Answerer { 10 | 11 | @Override 12 | public String doAnswer(String prompt) { 13 | return "抱歉,我不理解您的问题:" + prompt; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/controller/MainController.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.controller; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | 7 | /** 8 | * 开放接口 9 | * 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | @RestController 14 | @RequestMapping("/") 15 | @Slf4j 16 | public class MainController { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/yupi/autoreply/MainApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | /** 7 | * 主类测试 8 | * 9 | * @author 程序员鱼皮 10 | * @from 编程导航知识星球 11 | */ 12 | @SpringBootTest 13 | class MainApplicationTests { 14 | 15 | @Test 16 | void contextLoads() { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/openai/model/ModelConstant.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.openai.model; 2 | 3 | /** 4 | * 模型常量 5 | * 参考文档 6 | * 7 | * @author 程序员鱼皮 8 | * @from 编程导航知识星球 9 | */ 10 | public interface ModelConstant { 11 | 12 | String GPT_4 = "gpt-4"; 13 | 14 | String GPT_3_5_TURBO = "gpt-3.5-turbo"; 15 | 16 | String TEXT_DAVINCI_003 = "text-davinci-003"; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/model/TaskListItem.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 单个任务配置 7 | */ 8 | @Data 9 | public class TaskListItem { 10 | 11 | /** 12 | * 任务名 13 | */ 14 | private String name = ""; 15 | 16 | /** 17 | * 任务执行周期 18 | */ 19 | private String cron = "0/30 * * * * ?"; 20 | 21 | /** 22 | * 回答者 23 | */ 24 | private String answerer = "openai"; 25 | 26 | /** 27 | * 监控者 28 | */ 29 | private String monitor = "zsxq"; 30 | } -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/zsxq/model/ListTopicsRequest.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.zsxq.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 获取列表请求 7 | * 8 | * @author 程序员鱼皮 9 | * @from 编程导航知识星球 10 | */ 11 | @Data 12 | public class ListTopicsRequest { 13 | 14 | /** 15 | * 星球 id 16 | */ 17 | private String groupId; 18 | 19 | /** 20 | * 主题范围 21 | */ 22 | private String scope; 23 | 24 | /** 25 | * 单页数量 26 | */ 27 | private Integer count; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.1-jdk-8-slim as builder 2 | 3 | MAINTAINER yupi 4 | 5 | # Copy local code to the container image. 6 | WORKDIR /app 7 | COPY pom.xml . 8 | COPY src ./src 9 | 10 | # Build a release artifact. 11 | RUN mvn package -DskipTests 12 | 13 | # 声明环境变量,这样容器就可以在运行时访问它们 14 | ENV OPENAI_MODEL=text-davinci-003 15 | ENV OPENAI_API_KEY=你的API_KEY 16 | ENV ZSXQ_COOKIE=你的星球Cookie 17 | ENV ZSXQ_GROUP_ID=你的星球id 18 | # 是否只提醒提问者 19 | ENV ZSXQ_SILENCED=true 20 | 21 | # Run the web service on container startup. 22 | ENTRYPOINT ["java","-jar","/app/target/yu-auto-reply-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"] -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/zsxq/model/AnswerRequest.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.zsxq.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 回答请求 9 | * 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | @Data 14 | public class AnswerRequest { 15 | 16 | private String topicId; 17 | 18 | private ReqData req_data; 19 | 20 | @Data 21 | public static class ReqData { 22 | 23 | private String text; 24 | 25 | private List image_ids; 26 | 27 | private Boolean silenced; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/monitor/Monitor.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.monitor; 2 | 3 | import com.yupi.autoreply.answerer.Answerer; 4 | import com.yupi.autoreply.model.TaskListItem; 5 | 6 | /** 7 | * 抽象监控者 8 | * 9 | * @author 程序员鱼皮 10 | * @from 编程导航知识星球 11 | */ 12 | public abstract class Monitor { 13 | 14 | TaskListItem taskListItem; 15 | 16 | Monitor(TaskListItem taskListItem) { 17 | this.taskListItem = taskListItem; 18 | } 19 | 20 | /** 21 | * 触发监控 22 | * 23 | * @param answerer 24 | */ 25 | public abstract void onMonitor(Answerer answerer); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/monitor/DefaultMonitor.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.monitor; 2 | 3 | import com.yupi.autoreply.answerer.Answerer; 4 | import com.yupi.autoreply.model.TaskListItem; 5 | 6 | /** 7 | * 默认监控者 8 | * 9 | * @author 程序员鱼皮 10 | * @from 编程导航知识星球 11 | */ 12 | public class DefaultMonitor extends Monitor { 13 | 14 | public DefaultMonitor(TaskListItem taskListItem) { 15 | super(taskListItem); 16 | } 17 | 18 | @Override 19 | public void onMonitor(Answerer answerer) { 20 | String mockMessage = "我是一个新的消息"; 21 | System.out.println(answerer.doAnswer(mockMessage)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/config/ZsxqConfig.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * 知识星球配置 9 | * 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | @Configuration 14 | @ConfigurationProperties(prefix = "zsxq") 15 | @Data 16 | public class ZsxqConfig { 17 | 18 | /** 19 | * 登录 cookie 20 | */ 21 | private String cookie; 22 | 23 | /** 24 | * 星球 id 25 | */ 26 | private String groupId; 27 | 28 | /** 29 | * 是否提醒提问者 30 | */ 31 | private Boolean silenced = true; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/config/OpenAiConfig.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.config; 2 | 3 | import com.yupi.autoreply.api.openai.model.ModelConstant; 4 | import lombok.Data; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * OpenAi 配置 10 | * 11 | * @author 程序员鱼皮 12 | * @from 编程导航知识星球 13 | */ 14 | @Configuration 15 | @ConfigurationProperties(prefix = "openai") 16 | @Data 17 | public class OpenAiConfig { 18 | 19 | /** 20 | * 模型 21 | */ 22 | private String model = ModelConstant.TEXT_DAVINCI_003; 23 | 24 | /** 25 | * apiKey 26 | */ 27 | private String apiKey; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/factory/AnswererFactory.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.factory; 2 | 3 | import com.yupi.autoreply.answerer.Answerer; 4 | import com.yupi.autoreply.answerer.DefaultAnswerer; 5 | import com.yupi.autoreply.answerer.OpenAiAnswerer; 6 | 7 | /** 8 | * 回答者工厂 9 | * 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | public class AnswererFactory { 14 | 15 | /** 16 | * 创建回答者 17 | * 18 | * @param answerer 19 | * @return 20 | */ 21 | public static Answerer createAnswerer(String answerer) { 22 | switch (answerer) { 23 | case "openai": 24 | return new OpenAiAnswerer(); 25 | default: 26 | return new DefaultAnswerer(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/openai/model/CreateCompletionRequest.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.openai.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 创建补全请求 7 | * 参考文档 8 | * 9 | * @author 程序员鱼皮 10 | * @from 编程导航知识星球 11 | */ 12 | @Data 13 | public class CreateCompletionRequest { 14 | 15 | /** 16 | * 模型 17 | */ 18 | private String model; 19 | 20 | /** 21 | * 提示词 22 | */ 23 | private String prompt; 24 | 25 | private Integer max_tokens; 26 | 27 | private Integer temperature; 28 | 29 | private Integer top_p; 30 | 31 | private Integer n; 32 | 33 | private Boolean stream; 34 | 35 | private Integer logprobs; 36 | 37 | private String stop; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/common/BaseResponse.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.common; 2 | 3 | import java.io.Serializable; 4 | import lombok.Data; 5 | 6 | /** 7 | * 通用返回类 8 | * 9 | * @param 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | @Data 14 | public class BaseResponse implements Serializable { 15 | 16 | private int code; 17 | 18 | private T data; 19 | 20 | private String message; 21 | 22 | public BaseResponse(int code, T data, String message) { 23 | this.code = code; 24 | this.data = data; 25 | this.message = message; 26 | } 27 | 28 | public BaseResponse(int code, T data) { 29 | this(code, data, ""); 30 | } 31 | 32 | public BaseResponse(ErrorCode errorCode) { 33 | this(errorCode.getCode(), null, errorCode.getMessage()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/factory/MonitorFactory.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.factory; 2 | 3 | import com.yupi.autoreply.model.TaskListItem; 4 | import com.yupi.autoreply.monitor.DefaultMonitor; 5 | import com.yupi.autoreply.monitor.Monitor; 6 | import com.yupi.autoreply.monitor.ZsxqMonitor; 7 | 8 | /** 9 | * 监视者工厂 10 | * 11 | * @author 程序员鱼皮 12 | * @from 编程导航知识星球 13 | */ 14 | public class MonitorFactory { 15 | 16 | /** 17 | * 创建监视者 18 | * 19 | * @param monitor 20 | * @param taskListItem 21 | * @return 22 | */ 23 | public static Monitor createMonitor(String monitor, TaskListItem taskListItem) { 24 | switch (monitor) { 25 | case "zsxq": 26 | return new ZsxqMonitor(taskListItem); 27 | default: 28 | return new DefaultMonitor(taskListItem); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.exception; 2 | 3 | import com.yupi.autoreply.common.ErrorCode; 4 | 5 | /** 6 | * 自定义异常类 7 | * 8 | * @author 程序员鱼皮 9 | * @from 编程导航知识星球 10 | */ 11 | public class BusinessException extends RuntimeException { 12 | 13 | /** 14 | * 错误码 15 | */ 16 | private final int code; 17 | 18 | public BusinessException(int code, String message) { 19 | super(message); 20 | this.code = code; 21 | } 22 | 23 | public BusinessException(ErrorCode errorCode) { 24 | super(errorCode.getMessage()); 25 | this.code = errorCode.getCode(); 26 | } 27 | 28 | public BusinessException(ErrorCode errorCode, String message) { 29 | super(message); 30 | this.code = errorCode.getCode(); 31 | } 32 | 33 | public int getCode() { 34 | return code; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/common/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.common; 2 | 3 | /** 4 | * 自定义错误码 5 | * 6 | * @author 程序员鱼皮 7 | * @from 编程导航知识星球 8 | */ 9 | public enum ErrorCode { 10 | 11 | SUCCESS(0, "ok"), 12 | PARAMS_ERROR(40000, "请求参数错误"), 13 | NOT_LOGIN_ERROR(40100, "未登录"), 14 | NO_AUTH_ERROR(40101, "无权限"), 15 | NOT_FOUND_ERROR(40400, "请求数据不存在"), 16 | FORBIDDEN_ERROR(40300, "禁止访问"), 17 | SYSTEM_ERROR(50000, "系统内部异常"), 18 | OPERATION_ERROR(50001, "操作失败"); 19 | 20 | /** 21 | * 状态码 22 | */ 23 | private final int code; 24 | 25 | /** 26 | * 信息 27 | */ 28 | private final String message; 29 | 30 | ErrorCode(int code, String message) { 31 | this.code = code; 32 | this.message = message; 33 | } 34 | 35 | public int getCode() { 36 | return code; 37 | } 38 | 39 | public String getMessage() { 40 | return message; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: yu-auto-reply 4 | # 支持 swagger3 5 | mvc: 6 | pathmatch: 7 | matching-strategy: ant_path_matcher 8 | server: 9 | address: 0.0.0.0 10 | port: 8080 11 | servlet: 12 | context-path: /api 13 | # openAI 配置 14 | # https://platform.openai.com/docs/api-reference 15 | openai: 16 | model: ${OPENAI_MODEL:text-davinci-003} 17 | apiKey: ${OPENAI_API_KEY:你的apiKey} 18 | # 知识星球配置 19 | # https://zsxq.com/ 20 | zsxq: 21 | cookie: ${ZSXQ_COOKIE:你的星球cookie} 22 | groupId: ${ZSXQ_GROUP_ID:你的星球id} 23 | # 是否提醒提问者 24 | silenced: ${ZSXQ_SILENCED:true} 25 | # 任务配置 26 | task: 27 | # 并发 28 | concurrent: 29 | # 默认关闭(串行) 30 | enable: false 31 | # 并发大小(不填则等同于任务数,即全并发) 32 | size: 1 33 | # 任务列表,支持配置多个 34 | list: 35 | - name: task1 #任务名 36 | monitor: zsxq #监控者 37 | answerer: openai #回答者 38 | cron: '0/30 * * * * ?' #执行周期 39 | # - name: task2 40 | # monitor: default 41 | # answerer: default 42 | # cron: '0/10 * * * * ?' -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | 7 | /** 8 | * 全局跨域配置 9 | * 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | @Configuration 14 | public class CorsConfig implements WebMvcConfigurer { 15 | 16 | @Override 17 | public void addCorsMappings(CorsRegistry registry) { 18 | // 覆盖所有请求 19 | registry.addMapping("/**") 20 | // 允许发送 Cookie 21 | .allowCredentials(true) 22 | // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突) 23 | .allowedOriginPatterns("*") 24 | .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") 25 | .allowedHeaders("*") 26 | .exposedHeaders("*"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/job/JobMediator.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.job; 2 | 3 | import com.yupi.autoreply.answerer.Answerer; 4 | import com.yupi.autoreply.factory.AnswererFactory; 5 | import com.yupi.autoreply.factory.MonitorFactory; 6 | import com.yupi.autoreply.model.TaskListItem; 7 | import com.yupi.autoreply.monitor.Monitor; 8 | 9 | /** 10 | * 任务中介(负责协调监控者和回答者,把参数传给他们) 11 | * 12 | * @author 程序员鱼皮 13 | * @from 编程导航知识星球 14 | */ 15 | public class JobMediator implements Runnable { 16 | 17 | private final TaskListItem taskListItem; 18 | 19 | public JobMediator(TaskListItem taskListItem) { 20 | this.taskListItem = taskListItem; 21 | } 22 | 23 | @Override 24 | public void run() { 25 | // 根据配置选择 monitor 和 answerer 26 | Monitor monitor = MonitorFactory.createMonitor(taskListItem.getMonitor(), taskListItem); 27 | Answerer answerer = AnswererFactory.createAnswerer(taskListItem.getAnswerer()); 28 | // 监控并回答 29 | monitor.onMonitor(answerer); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.exception; 2 | 3 | import com.yupi.autoreply.common.BaseResponse; 4 | import com.yupi.autoreply.common.ErrorCode; 5 | import com.yupi.autoreply.common.ResultUtils; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.RestControllerAdvice; 9 | 10 | /** 11 | * 全局异常处理器 12 | * 13 | * @author 程序员鱼皮 14 | * @from 编程导航知识星球 15 | */ 16 | @RestControllerAdvice 17 | @Slf4j 18 | public class GlobalExceptionHandler { 19 | 20 | @ExceptionHandler(BusinessException.class) 21 | public BaseResponse businessExceptionHandler(BusinessException e) { 22 | log.error("BusinessException", e); 23 | return ResultUtils.error(e.getCode(), e.getMessage()); 24 | } 25 | 26 | @ExceptionHandler(RuntimeException.class) 27 | public BaseResponse runtimeExceptionHandler(RuntimeException e) { 28 | log.error("RuntimeException", e); 29 | return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/openai/model/CreateCompletionResponse.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.openai.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 创建补全响应 9 | * 参考文档 10 | * 11 | * @author 程序员鱼皮 12 | * @from 编程导航知识星球 13 | */ 14 | @Data 15 | public class CreateCompletionResponse { 16 | 17 | private Integer created; 18 | 19 | private Usage usage; 20 | 21 | private String model; 22 | 23 | private String id; 24 | 25 | /** 26 | * 回答列表 27 | */ 28 | private List choices; 29 | 30 | private String object; 31 | 32 | @Data 33 | public static class ChoicesItem { 34 | 35 | private String finishReason; 36 | 37 | private Integer index; 38 | 39 | private String text; 40 | 41 | private Integer logprobs; 42 | } 43 | 44 | @Data 45 | public static class Usage { 46 | 47 | private Integer completionTokens; 48 | 49 | private Integer promptTokens; 50 | 51 | private Integer totalTokens; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/yupi/autoreply/api/openai/OpenAiApiTest.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.openai; 2 | 3 | import com.yupi.autoreply.api.openai.model.CreateCompletionRequest; 4 | import com.yupi.autoreply.api.openai.model.CreateCompletionResponse; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | 9 | import javax.annotation.Resource; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | /** 14 | * OpenAiApi 测试 15 | * 16 | * @author 程序员鱼皮 17 | * @from 编程导航知识星球 18 | **/ 19 | @SpringBootTest 20 | class OpenAiApiTest { 21 | 22 | @Resource 23 | private OpenAiApi openAiApi; 24 | 25 | private static final String OPENAI_API_KEY = "你的 OPENAI_API_KEY"; 26 | 27 | @Test 28 | void createCompletion() { 29 | CreateCompletionRequest request = new CreateCompletionRequest(); 30 | request.setModel("text-davinci-003"); 31 | request.setPrompt("我的提问"); 32 | CreateCompletionResponse response = openAiApi.createCompletion(request, OPENAI_API_KEY); 33 | Assertions.assertNotNull(response); 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/exception/ThrowUtils.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.exception; 2 | 3 | import com.yupi.autoreply.common.ErrorCode; 4 | 5 | /** 6 | * 抛异常工具类 7 | * 8 | * @author 程序员鱼皮 9 | * @from 编程导航知识星球 10 | */ 11 | public class ThrowUtils { 12 | 13 | /** 14 | * 条件成立则抛异常 15 | * 16 | * @param condition 17 | * @param runtimeException 18 | */ 19 | public static void throwIf(boolean condition, RuntimeException runtimeException) { 20 | if (condition) { 21 | throw runtimeException; 22 | } 23 | } 24 | 25 | /** 26 | * 条件成立则抛异常 27 | * 28 | * @param condition 29 | * @param errorCode 30 | */ 31 | public static void throwIf(boolean condition, ErrorCode errorCode) { 32 | throwIf(condition, new BusinessException(errorCode)); 33 | } 34 | 35 | /** 36 | * 条件成立则抛异常 37 | * 38 | * @param condition 39 | * @param errorCode 40 | * @param message 41 | */ 42 | public static void throwIf(boolean condition, ErrorCode errorCode, String message) { 43 | throwIf(condition, new BusinessException(errorCode, message)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/common/ResultUtils.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.common; 2 | 3 | /** 4 | * 返回工具类 5 | * 6 | * @author 程序员鱼皮 7 | * @from 编程导航知识星球 8 | */ 9 | public class ResultUtils { 10 | 11 | /** 12 | * 成功 13 | * 14 | * @param data 15 | * @param 16 | * @return 17 | */ 18 | public static BaseResponse success(T data) { 19 | return new BaseResponse<>(0, data, "ok"); 20 | } 21 | 22 | /** 23 | * 失败 24 | * 25 | * @param errorCode 26 | * @return 27 | */ 28 | public static BaseResponse error(ErrorCode errorCode) { 29 | return new BaseResponse<>(errorCode); 30 | } 31 | 32 | /** 33 | * 失败 34 | * 35 | * @param code 36 | * @param message 37 | * @return 38 | */ 39 | public static BaseResponse error(int code, String message) { 40 | return new BaseResponse(code, null, message); 41 | } 42 | 43 | /** 44 | * 失败 45 | * 46 | * @param errorCode 47 | * @return 48 | */ 49 | public static BaseResponse error(ErrorCode errorCode, String message) { 50 | return new BaseResponse(errorCode.getCode(), null, message); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply; 2 | 3 | import com.yupi.autoreply.config.OpenAiConfig; 4 | import com.yupi.autoreply.config.ZsxqConfig; 5 | import com.yupi.autoreply.utils.SpringContextUtils; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.CommandLineRunner; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 11 | import org.springframework.scheduling.annotation.EnableScheduling; 12 | 13 | /** 14 | * 主类(项目启动入口) 15 | * 16 | * @author 程序员鱼皮 17 | * @from 编程导航知识星球 18 | */ 19 | @SpringBootApplication 20 | @EnableScheduling 21 | @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) 22 | @Slf4j 23 | public class MainApplication implements CommandLineRunner { 24 | 25 | public static void main(String[] args) { 26 | SpringApplication.run(MainApplication.class, args); 27 | } 28 | 29 | @Override 30 | public void run(String... args) throws Exception { 31 | OpenAiConfig openAiConfig = SpringContextUtils.getBean(OpenAiConfig.class); 32 | ZsxqConfig zsxqConfig = SpringContextUtils.getBean(ZsxqConfig.class); 33 | log.info("OpenAi 配置 {}", zsxqConfig); 34 | log.info("知识星球配置 {}", openAiConfig); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "openai.apiKey", 5 | "type": "java.lang.String", 6 | "description": "Description for openai.apiKey." 7 | }, 8 | { 9 | "name": "zsxq.cookie", 10 | "type": "java.lang.String", 11 | "description": "Description for zsxq.cookie." 12 | }, 13 | { 14 | "name": "openai.model", 15 | "type": "java.lang.String", 16 | "description": "Description for openai.model." 17 | }, 18 | { 19 | "name": "zsxq.groupId", 20 | "type": "java.lang.String", 21 | "description": "Description for zsxq.groupId." 22 | }, 23 | { 24 | "name": "zsxq.silenced", 25 | "type": "java.lang.String", 26 | "description": "Description for zsxq.silenced." 27 | }, 28 | { 29 | "name": "task.cron", 30 | "type": "java.lang.String", 31 | "description": "Description for task.cron." 32 | }, 33 | { 34 | "name": "task.list", 35 | "type": "java.lang.String", 36 | "description": "Description for task.list." 37 | }, 38 | { 39 | "name": "task.concurrent.enable", 40 | "type": "java.lang.String", 41 | "description": "Description for task.concurrent.enable." 42 | }, 43 | { 44 | "name": "task.concurrent.size", 45 | "type": "java.lang.String", 46 | "description": "Description for task.concurrent.size." 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /doc/架构图.drawio: -------------------------------------------------------------------------------- 1 | 7Zpbk5owFMc/TR7bAbkYHsFLu53ttFMf2n1MIQItEotx1f30PQlBReLOal2R6c7suJyTC+F/zi8XFFmD2fpDQebJZxbRDPWMaI2sIer1sOfCp3BsSodrmKUjLtKodO05JukTVU5DeZdpRBe1ipyxjKfzujNkeU5DXvORomCrerUpy+p3nZOYNhyTkGRN7/c04ol6rF5/5/9I0zip7my6XlkyI1Vl9SSLhERsteeyRsgaFIzx8mq2HtBMaFfpUrYbHyndDqygOX9Jg/HDwCfOn09+8rSkm693s1UevhMNRDePJFuqJ1aj5ZtKgoIt84iKXgxkBask5XQyJ6EoXUHMwZfwWQaWCZcRWSSyrjBU37TgdH101OZWC8ghymaUFxuoohrYWMmn8sf0lL3aRcO0lS/Zi4RVVSQqA+Jt3zuR4ELpdIpm5o1rZho3p1mF81GJWMETFrOcZPeMzZUWvyjnGzUnkCVnddnoOuU/9q4fRFfvHWUN16pnaWwqI4en+bFv7LUS5q6ZtKp25ehp1JgsDmICT8iWRUifk0LNYqSIKX+mnqePcUEzwtPH+jguH69mio/6yBsj30EjjPwxwi4aucjDwimKDIQt4cFjcT3ykNdH/kgWBcgzZdEI+f1/Q+USdJh1OrYT9D4dPQ0d7qvBYTZEEZk2UWbOcvgX/K+89LrAi2YZBQSCQAAieIHMtzvAgmW2zoJ9HRbOzOtzGLogC1YXWLA0LDgIOyiAJcOWULjCEwAgliwao8DoAB220Tod7lVXCuOclcJsbaWwu0CH3aDjy5zm4PHv4EPgICiQGyd/iLzmdHhtCmzv5vZLzanhVdeIEzP6HHouSIHTBQqcF+2Xbp6FG9gveW8sHFfH7QILroYFjDDkvC8QABY8X26cPIQ9DRSClz7CkhcM5/FmRrSNia17AXVdTPDbxum4Ov0uYNLXYKI7VhzSUZ4mHHXQ8K3bo6P9Y4VzVTo69gIKd4EOfNoi0jhr394i0jh9t76IeBqNQVEsZppyBsKBVN0Q01Lb+lkH3wJZTtv6VTc7SFLIO5iVS918V5+bpaSl2uDUboL0soOAvK7tghfsNx2wjBW7yW2aZtmBi2RpnIMZguAU/IEIRxqSzFcFszSKxG20wayH+yLxdOo82Jp4mpp4Wq8WT92bvuPROxKZU8OR0Sl/LhgLiEOax/ey2tDeeb4pSYSLQfNpJr+OT6AhhR6COUtzLjVyAvgD1QZinXFgrAOwzZ0Nf6J6wQcsh+GTVEaQkgVf0QWXweeEk5/ySdv4Atiuo+9qdhj4qpnSfOt1BsNvmXLxTHF6V8sUMHc/dpFle78YskZ/AQ== -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/config/Knife4jConfig.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import springfox.documentation.builders.ApiInfoBuilder; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.spi.DocumentationType; 10 | import springfox.documentation.spring.web.plugins.Docket; 11 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 12 | 13 | /** 14 | * Knife4j 接口文档配置 15 | * 官方文档 16 | * 17 | * @author 程序员鱼皮 18 | * @from 编程导航知识星球 19 | */ 20 | @Configuration 21 | @EnableSwagger2 22 | @Profile({"dev", "test"}) 23 | public class Knife4jConfig { 24 | 25 | @Bean 26 | public Docket defaultApi2() { 27 | return new Docket(DocumentationType.SWAGGER_2) 28 | .apiInfo(new ApiInfoBuilder() 29 | .title("接口文档") 30 | .description("yu-auto-reply") 31 | .version("1.0") 32 | .build()) 33 | .select() 34 | // 指定 Controller 扫描包路径 35 | .apis(RequestHandlerSelectors.basePackage("com.yupi.autoreply.controller")) 36 | .paths(PathSelectors.any()) 37 | .build(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/openai/OpenAiApi.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.openai; 2 | 3 | import cn.hutool.http.HttpRequest; 4 | import cn.hutool.json.JSONUtil; 5 | import com.yupi.autoreply.api.openai.model.CreateCompletionRequest; 6 | import com.yupi.autoreply.api.openai.model.CreateCompletionResponse; 7 | import com.yupi.autoreply.common.ErrorCode; 8 | import com.yupi.autoreply.exception.BusinessException; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.stereotype.Service; 11 | 12 | /** 13 | * OpenAi 接口 14 | * 参考文档 15 | * 16 | * @author 程序员鱼皮 17 | * @from 编程导航知识星球 18 | **/ 19 | @Service 20 | public class OpenAiApi { 21 | 22 | /** 23 | * 补全 24 | * 25 | * @param request 26 | * @param openAiApiKey 27 | * @return 28 | */ 29 | public CreateCompletionResponse createCompletion(CreateCompletionRequest request, String openAiApiKey) { 30 | if (StringUtils.isBlank(openAiApiKey)) { 31 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 openAiApiKey"); 32 | } 33 | String url = "https://api.openai.com/v1/completions"; 34 | String json = JSONUtil.toJsonStr(request); 35 | String result = HttpRequest.post(url) 36 | .header("Authorization", "Bearer " + openAiApiKey) 37 | .body(json) 38 | .execute() 39 | .body(); 40 | return JSONUtil.toBean(result, CreateCompletionResponse.class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/yupi/autoreply/api/zsxq/ZsxqApiTest.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.zsxq; 2 | 3 | import com.yupi.autoreply.api.zsxq.model.AnswerRequest; 4 | import com.yupi.autoreply.api.zsxq.model.AnswerResponse; 5 | import com.yupi.autoreply.api.zsxq.model.ListTopicsRequest; 6 | import com.yupi.autoreply.api.zsxq.model.ListTopicsResponse; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | 11 | import javax.annotation.Resource; 12 | 13 | /** 14 | * ZsxqApi 测试 15 | * 16 | * @author 程序员鱼皮 17 | * @from 编程导航知识星球 18 | **/ 19 | @SpringBootTest 20 | class ZsxqApiTest { 21 | 22 | @Resource 23 | private ZsxqApi zsxqApi; 24 | 25 | private static final String COOKIE = "你的 COOKIE"; 26 | 27 | @Test 28 | void listTopics() { 29 | ListTopicsRequest request = new ListTopicsRequest(); 30 | request.setScope("unanswered_questions"); 31 | request.setCount(30); 32 | request.setGroupId("知识星球id"); 33 | ListTopicsResponse listTopicsResponse = zsxqApi.listTopics(request, COOKIE); 34 | Assertions.assertNotNull(listTopicsResponse); 35 | } 36 | 37 | @Test 38 | void answer() { 39 | AnswerRequest request = new AnswerRequest(); 40 | request.setTopicId("问题id"); 41 | AnswerRequest.ReqData reqData = new AnswerRequest.ReqData(); 42 | reqData.setText("我的回答"); 43 | reqData.setSilenced(true); 44 | request.setReq_data(reqData); 45 | AnswerResponse answerResponse = zsxqApi.answer(request, COOKIE); 46 | Assertions.assertNotNull(answerResponse); 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/utils/SpringContextUtils.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.utils; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.validation.constraints.NotNull; 9 | 10 | /** 11 | * Spring 上下文获取工具 12 | * 13 | * @author 程序员鱼皮 14 | * @from 编程导航知识星球 15 | */ 16 | @Component 17 | public class SpringContextUtils implements ApplicationContextAware { 18 | 19 | private static ApplicationContext applicationContext; 20 | 21 | @Override 22 | public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException { 23 | SpringContextUtils.applicationContext = applicationContext; 24 | } 25 | 26 | /** 27 | * 通过名称获取 Bean 28 | * 29 | * @param beanName 30 | * @return 31 | */ 32 | public static Object getBean(String beanName) { 33 | return applicationContext.getBean(beanName); 34 | } 35 | 36 | /** 37 | * 通过 class 获取 Bean 38 | * 39 | * @param beanClass 40 | * @param 41 | * @return 42 | */ 43 | public static T getBean(Class beanClass) { 44 | return applicationContext.getBean(beanClass); 45 | } 46 | 47 | /** 48 | * 通过名称和类型获取 Bean 49 | * 50 | * @param beanName 51 | * @param beanClass 52 | * @param 53 | * @return 54 | */ 55 | public static T getBean(String beanName, Class beanClass) { 56 | return applicationContext.getBean(beanName, beanClass); 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/answerer/OpenAiAnswerer.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.answerer; 2 | 3 | import com.yupi.autoreply.api.openai.OpenAiApi; 4 | import com.yupi.autoreply.api.openai.model.CreateCompletionRequest; 5 | import com.yupi.autoreply.api.openai.model.CreateCompletionResponse; 6 | import com.yupi.autoreply.config.OpenAiConfig; 7 | import com.yupi.autoreply.config.ZsxqConfig; 8 | import com.yupi.autoreply.utils.SpringContextUtils; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import javax.annotation.Resource; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * OpenAi 回答者 17 | * 18 | * @author 程序员鱼皮 19 | * @from 编程导航知识星球 20 | */ 21 | @Slf4j 22 | public class OpenAiAnswerer implements Answerer { 23 | 24 | private final OpenAiApi openAiApi = SpringContextUtils.getBean(OpenAiApi.class); 25 | 26 | private final OpenAiConfig openAiConfig = SpringContextUtils.getBean(OpenAiConfig.class); 27 | 28 | @Override 29 | public String doAnswer(String prompt) { 30 | CreateCompletionRequest request = new CreateCompletionRequest(); 31 | request.setPrompt(prompt); 32 | request.setModel(openAiConfig.getModel()); 33 | request.setTemperature(0); 34 | request.setMax_tokens(1024); 35 | CreateCompletionResponse response = openAiApi.createCompletion(request, openAiConfig.getApiKey()); 36 | List choicesItemList = response.getChoices(); 37 | String answer = choicesItemList.stream() 38 | .map(CreateCompletionResponse.ChoicesItem::getText) 39 | .collect(Collectors.joining()); 40 | log.info("OpenAiAnswerer 回答成功 \n 答案:{}", answer); 41 | return answer; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/config/TaskConfig.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.config; 2 | 3 | import com.yupi.autoreply.job.JobMediator; 4 | import com.yupi.autoreply.model.TaskListItem; 5 | import lombok.Data; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang3.BooleanUtils; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.springframework.boot.context.properties.ConfigurationProperties; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.scheduling.annotation.SchedulingConfigurer; 12 | import org.springframework.scheduling.config.ScheduledTaskRegistrar; 13 | 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.concurrent.Executors; 17 | 18 | /** 19 | * 任务配置 20 | * 21 | * @author 程序员鱼皮 22 | * @from 编程导航知识星球 23 | */ 24 | @Configuration 25 | @ConfigurationProperties(prefix = "task") 26 | @Data 27 | @Slf4j 28 | public class TaskConfig implements SchedulingConfigurer { 29 | 30 | /** 31 | * 并发配置 32 | */ 33 | private ConcurrentConfig concurrent = new ConcurrentConfig(); 34 | 35 | /** 36 | * 任务列表 37 | */ 38 | private List list; 39 | 40 | @Override 41 | public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { 42 | if (BooleanUtils.isTrue(concurrent.getEnable())) { 43 | // 如果开启并发,默认并发度为任务数,即全并发,可通过配置更改 44 | int size = Optional.ofNullable(concurrent.getSize()).orElse(list.size()); 45 | taskRegistrar.setScheduler(Executors.newScheduledThreadPool(size)); 46 | } 47 | log.info("--- 任务注册开始 ---"); 48 | for (int i = 0; i < list.size(); i++) { 49 | TaskListItem taskListItem = list.get(i); 50 | if (StringUtils.isBlank(taskListItem.getName())) { 51 | taskListItem.setName("task" + (i + 1)); 52 | } 53 | taskRegistrar.addCronTask(new JobMediator(taskListItem), taskListItem.getCron()); 54 | log.info("任务注册成功 {}", taskListItem); 55 | } 56 | log.info("--- 任务注册结果 ---"); 57 | } 58 | 59 | @Data 60 | public static class ConcurrentConfig { 61 | private Boolean enable = false; 62 | 63 | private Integer size; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/zsxq/model/AnswerResponse.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.zsxq.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 回答响应 7 | * 8 | * @author 程序员鱼皮 9 | * @from 编程导航知识星球 10 | */ 11 | @Data 12 | public class AnswerResponse { 13 | 14 | private RespData respData; 15 | 16 | private boolean succeeded; 17 | 18 | @Data 19 | public static class Group { 20 | private long groupId; 21 | private String name; 22 | private String type; 23 | } 24 | 25 | @Data 26 | public static class Owner { 27 | private String avatarUrl; 28 | private long userId; 29 | private String name; 30 | private String location; 31 | } 32 | 33 | @Data 34 | public static class OwnerDetail { 35 | private int questionsCount; 36 | private String joinTime; 37 | } 38 | 39 | @Data 40 | public static class Question { 41 | private Owner owner; 42 | private boolean expired; 43 | private Questionee questionee; 44 | private boolean anonymous; 45 | private OwnerDetail ownerDetail; 46 | private String ownerLocation; 47 | private String text; 48 | } 49 | 50 | @Data 51 | public static class Questionee { 52 | private String avatarUrl; 53 | private long userId; 54 | private String name; 55 | private String alias; 56 | private String description; 57 | private String location; 58 | } 59 | 60 | @Data 61 | public static class RespData { 62 | private TopicsItem topics; 63 | } 64 | 65 | @Data 66 | public static class TopicsItem { 67 | private int readingCount; 68 | private Question question; 69 | private boolean answered; 70 | private String createTime; 71 | private UserSpecific userSpecific; 72 | private int rewardsCount; 73 | private String type; 74 | private boolean digested; 75 | private int likesCount; 76 | private int commentsCount; 77 | private boolean sticky; 78 | private long topicId; 79 | private int readersCount; 80 | private Group group; 81 | } 82 | 83 | @Data 84 | public static class UserSpecific { 85 | private boolean subscribed; 86 | private boolean liked; 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/zsxq/model/ListTopicsResponse.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.zsxq.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 获取列表响应 9 | * 10 | * @author 程序员鱼皮 11 | * @from 编程导航知识星球 12 | */ 13 | @Data 14 | public class ListTopicsResponse { 15 | 16 | private RespData respData; 17 | 18 | private boolean succeeded; 19 | 20 | @Data 21 | public static class Group { 22 | private long groupId; 23 | private String name; 24 | private String type; 25 | } 26 | 27 | @Data 28 | public static class Owner { 29 | private String avatarUrl; 30 | private long userId; 31 | private String name; 32 | private String location; 33 | } 34 | 35 | @Data 36 | public static class OwnerDetail { 37 | private int questionsCount; 38 | private String joinTime; 39 | } 40 | 41 | @Data 42 | public static class Question { 43 | private Owner owner; 44 | private boolean expired; 45 | private Questionee questionee; 46 | private boolean anonymous; 47 | private OwnerDetail ownerDetail; 48 | private String ownerLocation; 49 | private String text; 50 | } 51 | 52 | @Data 53 | public static class Questionee { 54 | private String avatarUrl; 55 | private long userId; 56 | private String name; 57 | private String alias; 58 | private String description; 59 | private String location; 60 | } 61 | 62 | @Data 63 | public static class RespData { 64 | private List topics; 65 | } 66 | 67 | @Data 68 | public static class TopicsItem { 69 | private int readingCount; 70 | private Question question; 71 | private boolean answered; 72 | private String createTime; 73 | private UserSpecific userSpecific; 74 | private int rewardsCount; 75 | private String type; 76 | private boolean digested; 77 | private int likesCount; 78 | private int commentsCount; 79 | private boolean sticky; 80 | private String topicId; 81 | private int readersCount; 82 | private Group group; 83 | } 84 | 85 | @Data 86 | public static class UserSpecific { 87 | private boolean subscribed; 88 | private boolean liked; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yu-auto-reply 自动回复 2 | 3 | > 作者:[程序员鱼皮](https://github.com/liyupi) 4 | > 5 | > 编程学习圈:[编程导航知识星球](https://yupi.icu) 6 | 7 | [toc] 8 | 9 | 基于 Java Spring Boot 的平台监控及自动回复工具,支持灵活地配置多个监控任务,支持一键部署! 10 | 11 | 演示视频:https://www.bilibili.com/video/BV1WX4y1o7aL 12 | 13 | ![](doc/演示.png) 14 | 15 | 本项目采用多种设计模式,解耦监控者及回答者,可以灵活配置多个不同平台的监控,并绑定不同类型的自动回复。 16 | 17 | 🙏🏻 大家喜欢这个项目的话,感谢动手点点 star,后面作者可能会官方提供更多的平台监控支持。 18 | 19 | ## 功能特性 20 | 21 | ### 监控能力 22 | 23 | - 知识星球提问监控 24 | - 默认监控(模拟数据) 25 | 26 | ### 回复能力 27 | 28 | - OpenAI 回答(支持自选模型,比如 gpt-4) 29 | - 默认监控(模拟数据) 30 | 31 | ### 配置能力 32 | 33 | - 支持配置多个任务 34 | - 每个任务可以灵活指定监控和回答方式 35 | 36 | ### 部署能力 37 | 38 | - 支持 Docker 容器化部署 39 | - 支持 Railway 一键部署 40 | - 支持动态指定环境变量来改变配置 41 | 42 | ## 快速启动 43 | 44 | 1)修改 `application.yml` 配置,主要包含 3 部分: 45 | 46 | - openAI 配置(需要有一个 API Key) 47 | - 知识星球配置(需要自行获取 cookie) 48 | - 任务配置 49 | 50 | 详细配置如下: 51 | 52 | ```yml 53 | # openAI 配置 54 | # https://platform.openai.com/docs/api-reference 55 | openai: 56 | model: ${OPENAI_MODEL:text-davinci-003} 57 | apiKey: ${OPENAI_API_KEY:你的apiKey} 58 | # 知识星球配置 59 | # https://zsxq.com/ 60 | zsxq: 61 | cookie: ${ZSXQ_COOKIE:你的星球cookie} 62 | groupId: ${ZSXQ_GROUP_ID:你的星球id} 63 | # 是否提醒提问者 64 | silenced: ${ZSXQ_SILENCED:true} 65 | # 任务配置 66 | task: 67 | # 任务列表,支持配置多个 68 | list: 69 | - name: task1 #任务名 70 | monitor: zsxq #监控者 71 | answerer: openai #回答者 72 | cron: '0/30 * * * * ?' #执行周期 73 | ``` 74 | 75 | 2)直接运行主类 `MainApplication` 即可 76 | 77 | ## 一键部署 78 | 79 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/BMJMMm?referralCode=tKgj86) 80 | 81 | 点击上述部署按钮后,会自动识别环境变量,改成自己的就可以了: 82 | 83 | ![](doc/env.png) 84 | 85 | ## 架构设计 86 | 87 | 一图胜千言: 88 | 89 | ![](doc/架构.png) 90 | 91 | 本项目最关键的设计就是在于 **解耦监控者与回答者** ,你可以监控任何平台,并且给每个平台绑定不同的自动回答(比如 OpenAI)。 92 | 93 | 实现关键: 94 | 95 | 1. 定义 Answerer 回答者接口,统一回答的方法 96 | 2. 定义 Monitor 监控者接口,统一监控的方法,通过 Answerer 回调参数实现对监控到的消息进行自动回复 97 | 3. 使用中介者模式,用 JobMediator 类组合 Monitor 和 Answerer,而不是把回答者和监控者强绑定 98 | 4. 使用工厂模式,根据配置生成监控者和回答者 99 | 5. 使用 Spring Scheduler,读取 yml 配置来自动创建多任务 100 | 101 | ## 开发 102 | 103 | ### 自定义监控 104 | 105 | 1)编写一个类,实现 `monitor/Monitor` 抽象类 106 | 107 | 2)修改 `factory/MonitorFactory` 的 `createMonitor` 方法,补充创建你自己的监控者 108 | 109 | ### 自定义回答 110 | 111 | 1)编写一个类,实现 `answerer/Answerer` 接口 112 | 113 | 2)修改 `factory/AnswererFactory` 的 `createAnswerer` 方法,补充创建你自己的回答者 114 | 115 | 116 | ## 免费 ChatGPT 交流群 117 | 118 | ![](doc/ChatGPT交流.png) 119 | 120 | 121 | ## 欢迎贡献 122 | 123 | **作者平时非常忙** ,本项目也是仅用了几个小时抽空做的,开源出来给大家参考,但是 PR 和 Issues 响应不会很及时,感谢理解! 124 | 125 | 如有项目本身的问题,欢迎提 issues 和 PR; 126 | 127 | 如有编程方面的问题、或者需要项目教学,请看 [编程导航知识星球](https://yupi.icu) 128 | 129 | 130 | ## 问答 131 | 132 | 1)问:为什么先支持知识星球? 133 | 134 | 答:因为 OpenAI 的 API 不是免费的,星球可以限制提问次数,防止刷接口 135 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/api/zsxq/ZsxqApi.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.api.zsxq; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.core.util.URLUtil; 5 | import cn.hutool.http.HttpRequest; 6 | import cn.hutool.json.JSONUtil; 7 | import com.yupi.autoreply.api.zsxq.model.AnswerRequest; 8 | import com.yupi.autoreply.api.zsxq.model.AnswerResponse; 9 | import com.yupi.autoreply.api.zsxq.model.ListTopicsRequest; 10 | import com.yupi.autoreply.api.zsxq.model.ListTopicsResponse; 11 | import com.yupi.autoreply.common.ErrorCode; 12 | import com.yupi.autoreply.exception.BusinessException; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.Map; 18 | 19 | /** 20 | * 知识星球接口 21 | * 22 | * @author 程序员鱼皮 23 | * @from 编程导航知识星球 24 | */ 25 | @Service 26 | public class ZsxqApi { 27 | 28 | private static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0"; 29 | 30 | /** 31 | * 获取主题列表 32 | * 33 | * @param request 34 | * @param cookie 35 | * @return 36 | */ 37 | public ListTopicsResponse listTopics(ListTopicsRequest request, String cookie) { 38 | String groupId = request.getGroupId(); 39 | if (StringUtils.isBlank(groupId)) { 40 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 groupId"); 41 | } 42 | if (StringUtils.isBlank(cookie)) { 43 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 cookie"); 44 | } 45 | String url = String.format("https://api.zsxq.com/v2/groups/%s/topics", groupId); 46 | Map stringObjectMap = BeanUtil.beanToMap(request); 47 | String query = URLUtil.buildQuery(stringObjectMap, StandardCharsets.UTF_8); 48 | String result = HttpRequest.get(url) 49 | .header("cookie", cookie) 50 | .header("user-agent", USER_AGENT) 51 | .body(query) 52 | .execute() 53 | .body(); 54 | return JSONUtil.toBean(result, ListTopicsResponse.class); 55 | } 56 | 57 | /** 58 | * 回答问题 59 | * 60 | * @param request 61 | * @param cookie 62 | * @return 63 | */ 64 | public AnswerResponse answer(AnswerRequest request, String cookie) { 65 | String topicId = request.getTopicId(); 66 | if (StringUtils.isBlank(topicId)) { 67 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 topicId"); 68 | } 69 | if (StringUtils.isBlank(cookie)) { 70 | throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 cookie"); 71 | } 72 | String url = String.format("https://api.zsxq.com/v2/topics/%s/answer", topicId); 73 | String json = JSONUtil.toJsonStr(request); 74 | String result = HttpRequest.post(url) 75 | .header("cookie", cookie) 76 | .header("user-agent", USER_AGENT) 77 | .body(json) 78 | .execute() 79 | .body(); 80 | return JSONUtil.toBean(result, AnswerResponse.class); 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | ### Java template 35 | # Compiled class file 36 | *.class 37 | 38 | # Log file 39 | *.log 40 | 41 | # BlueJ files 42 | *.ctxt 43 | 44 | # Mobile Tools for Java (J2ME) 45 | .mtj.tmp/ 46 | 47 | # Package Files # 48 | *.jar 49 | *.war 50 | *.nar 51 | *.ear 52 | *.zip 53 | *.tar.gz 54 | *.rar 55 | 56 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 57 | hs_err_pid* 58 | 59 | ### Maven template 60 | target/ 61 | pom.xml.tag 62 | pom.xml.releaseBackup 63 | pom.xml.versionsBackup 64 | pom.xml.next 65 | release.properties 66 | dependency-reduced-pom.xml 67 | buildNumber.properties 68 | .mvn/timing.properties 69 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 70 | .mvn/wrapper/maven-wrapper.jar 71 | 72 | ### JetBrains template 73 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 74 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 75 | 76 | # User-specific stuff 77 | .idea/**/workspace.xml 78 | .idea/**/tasks.xml 79 | .idea/**/usage.statistics.xml 80 | .idea/**/dictionaries 81 | .idea/**/shelf 82 | 83 | # Generated files 84 | .idea/**/contentModel.xml 85 | 86 | # Sensitive or high-churn files 87 | .idea/**/dataSources/ 88 | .idea/**/dataSources.ids 89 | .idea/**/dataSources.local.xml 90 | .idea/**/sqlDataSources.xml 91 | .idea/**/dynamic.xml 92 | .idea/**/uiDesigner.xml 93 | .idea/**/dbnavigator.xml 94 | 95 | # Gradle 96 | .idea/**/gradle.xml 97 | .idea/**/libraries 98 | 99 | # Gradle and Maven with auto-import 100 | # When using Gradle or Maven with auto-import, you should exclude module files, 101 | # since they will be recreated, and may cause churn. Uncomment if using 102 | # auto-import. 103 | # .idea/artifacts 104 | # .idea/compiler.xml 105 | # .idea/jarRepositories.xml 106 | # .idea/modules.xml 107 | # .idea/*.iml 108 | # .idea/modules 109 | # *.iml 110 | # *.ipr 111 | 112 | # CMake 113 | cmake-build-*/ 114 | 115 | # Mongo Explorer plugin 116 | .idea/**/mongoSettings.xml 117 | 118 | # File-based project format 119 | *.iws 120 | 121 | # IntelliJ 122 | out/ 123 | 124 | # mpeltonen/sbt-idea plugin 125 | .idea_modules/ 126 | 127 | # JIRA plugin 128 | atlassian-ide-plugin.xml 129 | 130 | # Cursive Clojure plugin 131 | .idea/replstate.xml 132 | 133 | # Crashlytics plugin (for Android Studio and IntelliJ) 134 | com_crashlytics_export_strings.xml 135 | crashlytics.properties 136 | crashlytics-build.properties 137 | fabric.properties 138 | 139 | # Editor-based Rest Client 140 | .idea/httpRequests 141 | 142 | # Android studio 3.1+ serialized cache file 143 | .idea/caches/build_file_checksums.ser 144 | 145 | -------------------------------------------------------------------------------- /src/main/java/com/yupi/autoreply/monitor/ZsxqMonitor.java: -------------------------------------------------------------------------------- 1 | package com.yupi.autoreply.monitor; 2 | 3 | import cn.hutool.core.util.RandomUtil; 4 | import com.github.xiaoymin.knife4j.core.util.CollectionUtils; 5 | import com.yupi.autoreply.answerer.Answerer; 6 | import com.yupi.autoreply.api.zsxq.ZsxqApi; 7 | import com.yupi.autoreply.api.zsxq.model.AnswerRequest; 8 | import com.yupi.autoreply.api.zsxq.model.AnswerResponse; 9 | import com.yupi.autoreply.api.zsxq.model.ListTopicsRequest; 10 | import com.yupi.autoreply.api.zsxq.model.ListTopicsResponse; 11 | import com.yupi.autoreply.common.ErrorCode; 12 | import com.yupi.autoreply.config.ZsxqConfig; 13 | import com.yupi.autoreply.exception.BusinessException; 14 | import com.yupi.autoreply.model.TaskListItem; 15 | import com.yupi.autoreply.utils.SpringContextUtils; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * 知识星球监控者 23 | * 24 | * @author 程序员鱼皮 25 | * @from 编程导航知识星球 26 | */ 27 | @Slf4j 28 | public class ZsxqMonitor extends Monitor { 29 | 30 | private final ZsxqApi zsxqApi = SpringContextUtils.getBean(ZsxqApi.class); 31 | 32 | private final ZsxqConfig zsxqConfig = SpringContextUtils.getBean(ZsxqConfig.class); 33 | 34 | public ZsxqMonitor(TaskListItem taskListItem) { 35 | super(taskListItem); 36 | } 37 | 38 | @Override 39 | public void onMonitor(Answerer answerer) { 40 | String taskName = taskListItem.getName(); 41 | log.info("任务 {} 监控开始", taskName); 42 | String cookie = zsxqConfig.getCookie(); 43 | // 1. 获取未回答的问题列表 44 | ListTopicsRequest listTopicsRequest = new ListTopicsRequest(); 45 | listTopicsRequest.setCount(20); 46 | listTopicsRequest.setGroupId(zsxqConfig.getGroupId()); 47 | listTopicsRequest.setScope("unanswered_questions"); 48 | ListTopicsResponse listTopicsResponse = zsxqApi.listTopics(listTopicsRequest, zsxqConfig.getCookie()); 49 | List topics = listTopicsResponse.getRespData().getTopics(); 50 | if (CollectionUtils.isEmpty(topics)) { 51 | log.info("暂无新提问"); 52 | return; 53 | } 54 | for (ListTopicsResponse.TopicsItem topic : topics) { 55 | String question = topic.getQuestion().getText().trim(); 56 | log.info("{} 收到新提问 \n 问题:{}", taskName, question); 57 | // 2. 获取回答 58 | String answer = answerer.doAnswer(question); 59 | // 3. 回复 60 | AnswerRequest answerRequest = new AnswerRequest(); 61 | answerRequest.setTopicId(topic.getTopicId()); 62 | AnswerRequest.ReqData reqData = new AnswerRequest.ReqData(); 63 | reqData.setSilenced(zsxqConfig.getSilenced()); 64 | reqData.setText(answer); 65 | reqData.setImage_ids(new ArrayList<>()); 66 | answerRequest.setReq_data(reqData); 67 | AnswerResponse answerResponse = zsxqApi.answer(answerRequest, cookie); 68 | if (answerResponse.isSucceeded()) { 69 | log.info("{} 回答成功 \n 问题:{} \n 回答:{}", taskName, question, answer); 70 | } else { 71 | log.error("{} 回答失败 \n 问题:{}", taskName, question); 72 | } 73 | // 随机缓冲 74 | try { 75 | Thread.sleep(1000 + RandomUtil.randomInt(0, 2000)); 76 | } catch (InterruptedException e) { 77 | throw new BusinessException(ErrorCode.SYSTEM_ERROR, e.getMessage()); 78 | } 79 | } 80 | log.info("任务 {} 监控结束", taskName); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.2 10 | 11 | 12 | com.yupi 13 | yu-auto-reply 14 | 0.0.1-SNAPSHOT 15 | yu-auto-reply 16 | 17 | 1.8 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-aop 27 | 28 | 29 | 30 | com.github.xiaoymin 31 | knife4j-spring-boot-starter 32 | 3.0.3 33 | 34 | 35 | 36 | org.apache.commons 37 | commons-lang3 38 | 39 | 40 | 41 | com.google.code.gson 42 | gson 43 | 2.9.1 44 | 45 | 46 | 47 | cn.hutool 48 | hutool-all 49 | 5.8.8 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-devtools 54 | runtime 55 | true 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-configuration-processor 60 | true 61 | 62 | 63 | org.projectlombok 64 | lombok 65 | true 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-test 70 | test 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-maven-plugin 79 | 80 | 81 | 82 | org.projectlombok 83 | lombok 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | --------------------------------------------------------------------------------