├── README.md ├── accounts.yaml ├── application.yaml ├── docs └── feishu.md ├── pom.xml ├── remote_deploy.sh ├── src ├── main │ └── java │ │ └── com │ │ └── rawchen │ │ └── feishubot │ │ ├── FeishuBotApplication.java │ │ ├── config │ │ └── FeiShuConfig.java │ │ ├── controller │ │ └── EventController.java │ │ ├── entity │ │ ├── Account.java │ │ ├── AccountList.java │ │ ├── Conversation.java │ │ ├── Mode.java │ │ ├── Status.java │ │ ├── gpt │ │ │ ├── Answer.java │ │ │ ├── Author.java │ │ │ ├── Content.java │ │ │ ├── ErrorCode.java │ │ │ ├── Message.java │ │ │ ├── Model.java │ │ │ └── Models.java │ │ └── gptRequestBody │ │ │ └── CreateConversationBody.java │ │ ├── handler │ │ ├── EventHandler.java │ │ └── MessageHandler.java │ │ ├── scheduling │ │ └── ScheduledTask.java │ │ ├── service │ │ ├── AccountService.java │ │ ├── MessageService.java │ │ └── UserService.java │ │ └── util │ │ ├── MessageCard.java │ │ ├── MessageContent.java │ │ ├── StringUtil.java │ │ ├── Task.java │ │ ├── TaskPool.java │ │ └── chatgpt │ │ ├── AccountPool.java │ │ ├── AccountUtil.java │ │ ├── AnswerProcess.java │ │ ├── ChatService.java │ │ ├── ConversationPool.java │ │ └── RequestIdSet.java └── test │ └── java │ └── com │ └── rawchen │ └── feishubot │ └── FeishuBotApplicationTests.java ├── start.sh └── stop.sh /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 集成ChatGPT的飞书机器人。使用方式:创建群后添加机器人到群中,@机器人发送内容,也可以私聊机器人发送内容。 4 | 5 | 该项目通过chatgpt账号,与[自建ChatGPT接口代理](https://github.com/linweiyuan/go-chatgpt-api)方式无需科学上网也能使用chatgpt服务。 6 | 7 | 该项目使用accesstoken方式,无需花费apikey额度,只要有chatgpt账号就能使用。 8 | 9 | 默认使用3.5模型,plus账户也可以接入4.0模型。 10 | 11 | 一个账号同一时间只能一次对话,因此对于多人同时使用时,通过配置多账号,自动切换空闲账号服务。 12 | 13 | ## 效果 14 | ![](https://s2.loli.net/2023/07/26/dM8BKgDjuNkZGCb.png) 15 | ![](https://s2.loli.net/2023/07/26/nAaQrmKplcjSENw.png) 16 | 17 | ## 功能支持 18 | 19 | - [x] 飞书集成 20 | - [x] 流式显示 21 | - [x] 支持配置多账户 22 | - [x] 多人使用自动切换账号服务 23 | 24 | ## 运行环境 25 | 26 | JDK1.8 27 | 28 | ## 飞书配置 29 | 30 | [飞书机器人配置和部署](docs/feishu.md) -------------------------------------------------------------------------------- /accounts.yaml: -------------------------------------------------------------------------------- 1 | accounts: 2 | - account: 3 | token: sk-mNI2dbRxxxxxxxxxxxxxxxxxxxxx 4 | -------------------------------------------------------------------------------- /application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9001 3 | 4 | feishu: 5 | appId: cli_a44axxxxxxxxxxx 6 | appSecret: 9GLUNqVTusxAxxxxxxxxxxxxxxxxxxxxx 7 | encryptKey: 8 | verificationToken: 9 | 10 | proxy: 11 | url: https://api.baizhi.ai 12 | 13 | logging: 14 | level: 15 | root: info 16 | com.rawchen: debug 17 | com.lark.oapi.event.EventDispatcher: off 18 | org.apache.coyote.http11.Http11Processor: warn 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/feishu.md: -------------------------------------------------------------------------------- 1 | 1. 飞书创建自建应用,添加机器人能力 2 | 2. 添加4个权限:以应用身份读取通讯录/接收群聊中@机器人消息事件/读取用户发给机器人的单聊消息/以应用的身份发消息 3 | 3. 权限管理中“通讯录权限范围”选为全部成员 4 | 4. 添加1个事件:接收消息v2.0 5 | 5. 配置accounts.yaml添加至少一个chatgpt账号,配置application.yaml添加应用凭证和事件凭证4条 6 | 6. mvn构建源代码为jar包,上传至服务器。上传两配置文件至同级目录。 7 | 7. 执行'nohup java -jar xxx.jar >> app.log &' 8 | 8. 跑成功后配置自建应用的事件订阅请求地址:http://服务器ip:9001/chatEvent 9 | 9. 自建应用版本发布,可用范围选为“所有员工” -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.12 9 | 10 | 11 | com.rawchen 12 | FeishuBot 13 | 0.0.1-SNAPSHOT 14 | FeishuBot 15 | FeishuBot 16 | 17 | 1.8 18 | 19 | 20 | 21 | 22 | Central Repository 23 | https://repo1.maven.org/maven2/ 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-test 36 | test 37 | 38 | 39 | 40 | com.larksuite.oapi 41 | oapi-sdk 42 | 2.3.5 43 | 44 | 45 | 46 | 47 | oapi-sdk-servlet-ext 48 | com.larksuite.oapi 49 | 1.0.0-rc3 50 | 51 | 52 | oapi-sdk 53 | com.larksuite.oapi 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | 67 | 68 | 69 | cn.hutool 70 | hutool-all 71 | 5.8.19 72 | 73 | 74 | 75 | com.alibaba 76 | fastjson 77 | 1.2.61 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-maven-plugin 87 | 88 | true 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /remote_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo '---------------开始自动部署---------------' 4 | 5 | # 项目根目录 6 | ROOT_PATH=$(cd $(dirname $0);cd .; pwd) 7 | 8 | # 项目名称 9 | PROJECT_NAME=FeishuBot 10 | 11 | # LINUX部署目录 12 | DEPLOY_PATH=root/FeishuBot 13 | 14 | # 服务器地址 15 | SERVER_IP=xxx.xx.xx.xx 16 | 17 | echo '---------------项目本地路径---------------' 18 | 19 | echo ${ROOT_PATH} && cd ${ROOT_PATH} 20 | 21 | echo '---------------开始项目打包---------------' 22 | 23 | mvn clean package -Dmaven.test.skip=true 24 | 25 | mv target/${PROJECT_NAME}-0.0.1-SNAPSHOT.jar target/${PROJECT_NAME}.jar 26 | 27 | echo '---------------开始上传项目---------------' 28 | 29 | scp -r -i id_rsa.pem -P 22 target/${PROJECT_NAME}.jar root@${SERVER_IP}:/${DEPLOY_PATH} 30 | 31 | echo '---------------kill源进程---------------' 32 | 33 | ssh -i id_rsa.pem -p 22 root@${SERVER_IP} << EOF 34 | 35 | cd /${DEPLOY_PATH}; 36 | 37 | ps -ef | grep ${PROJECT_NAME}.jar | grep -v 'grep' | cut -c 9-15 | xargs kill -s 9 38 | 39 | echo '---------------启动项目中---------------' 40 | 41 | ./start.sh 42 | 43 | tail -f -n 50 app.log 44 | 45 | EOF -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/FeishuBotApplication.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot; 2 | 3 | import cn.hutool.extra.spring.SpringUtil; 4 | import com.lark.oapi.ws.Client; 5 | import com.rawchen.feishubot.handler.EventHandler; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @SpringBootApplication 11 | @EnableScheduling 12 | public class FeishuBotApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(FeishuBotApplication.class, args); 16 | 17 | Client cli = new Client.Builder("cli_a44axxxxxxxxx", "9GLUNqVTusxxxxxxxxxxxxxxxxxxx") 18 | .eventHandler(EventHandler.EVENT_HANDLER) 19 | .build(); 20 | cli.start(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/config/FeiShuConfig.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.config; 2 | 3 | import com.lark.oapi.Client; 4 | import com.lark.oapi.sdk.servlet.ext.ServletAdapter; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class FeiShuConfig { 11 | 12 | @Value("${feishu.appId}") 13 | private String appId; 14 | 15 | @Value("${feishu.appSecret}") 16 | private String appSecret; 17 | 18 | @Bean 19 | public ServletAdapter getServletAdapter() { 20 | return new ServletAdapter(); 21 | } 22 | 23 | @Bean 24 | public Client getClient() { 25 | return Client.newBuilder(appId, appSecret).build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/controller/EventController.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.controller; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.lark.oapi.event.EventDispatcher; 5 | import com.lark.oapi.sdk.servlet.ext.ServletAdapter; 6 | import com.rawchen.feishubot.entity.Conversation; 7 | import com.rawchen.feishubot.handler.MessageHandler; 8 | import com.rawchen.feishubot.util.chatgpt.ConversationPool; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | 17 | @RestController 18 | @RequiredArgsConstructor 19 | @Slf4j 20 | public class EventController { 21 | 22 | // protected final MessageHandler messageHandler; 23 | 24 | @Value("${feishu.verificationToken}") 25 | private String verificationToken; 26 | 27 | @Value("${feishu.encryptKey}") 28 | private String encryptionKey; 29 | 30 | private EventDispatcher EVENT_DISPATCHER; 31 | 32 | protected final ConversationPool conversationPool; 33 | 34 | protected final ServletAdapter servletAdapter; 35 | 36 | //处理事件回调 37 | @RequestMapping("/chatEvent") 38 | public void event(HttpServletRequest request, HttpServletResponse response) 39 | throws Throwable { 40 | if (EVENT_DISPATCHER == null) { 41 | // init(); 42 | } 43 | 44 | servletAdapter.handleEvent(request, response, EVENT_DISPATCHER); 45 | } 46 | 47 | 48 | /** 49 | * 处理消息卡片事件回调 50 | * 51 | * @param body 52 | * @return 53 | */ 54 | @PostMapping("/cardEvent") 55 | @ResponseBody 56 | public String event(@RequestBody String body) { 57 | // log.debug("收到消息卡片事件: {}", body); 58 | // JSONObject payload = new JSONObject(body); 59 | // if (payload.opt("challenge") != null) { 60 | // JSONObject res = new JSONObject(); 61 | // res.put("challenge", payload.get("challenge")); 62 | // return res.toString(); 63 | // } 64 | // 65 | // String chatId = String.valueOf(payload.get("open_chat_id")); 66 | // 67 | // JSONObject action = (JSONObject) payload.get("action"); 68 | // String option = (String) action.get("option"); 69 | // 70 | // Conversation bean = JSONUtil.toBean(option, Conversation.class); 71 | // log.debug("收到模型选择: {}", bean); 72 | // if (bean.getParentMessageId() == null) { 73 | // bean.setParentMessageId(""); 74 | // } 75 | // if (bean.getConversationId() == null) { 76 | // bean.setConversationId(""); 77 | // } 78 | // bean.setChatId(chatId); 79 | // conversationPool.addConversation(chatId, bean); 80 | return ""; 81 | } 82 | 83 | @GetMapping("/ping") 84 | @ResponseBody 85 | public String ping() { 86 | return "pong"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/Account.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Account { 7 | private String account; 8 | private String password; 9 | private String token; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/AccountList.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class AccountList { 9 | private List accounts; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/Conversation.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Conversation { 7 | /** 8 | * gpt的会话id 9 | */ 10 | public String conversationId; 11 | /** 12 | * 对话id,用于区分不同用户 13 | */ 14 | public String chatId; 15 | /** 16 | * 服务该会话的gpt账号 17 | */ 18 | public String account; 19 | /** 20 | * 服务该会话的gpt模型 21 | */ 22 | public String model; 23 | /** 24 | * gpt中的上下文消息id 25 | */ 26 | public String parentMessageId; 27 | public volatile Status status; 28 | /** 29 | * 消息中的标题 30 | */ 31 | public String title; 32 | 33 | /** 34 | * 回复模式 35 | */ 36 | public Mode mode; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/Mode.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity; 2 | 3 | public enum Mode { 4 | /** 5 | * 快速回复模式,只要问就找可用的账号服务 6 | */ 7 | FAST, 8 | /** 9 | * 保持会话模式,尽可能使用同一个账号服务 10 | */ 11 | KEEP 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/Status.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity; 2 | 3 | public enum Status { 4 | RUNNING, 5 | FINISHED, 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/Answer.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Answer { 7 | 8 | private int seq; 9 | 10 | /** 11 | * 是否正常返回GPT对话结果 12 | */ 13 | private boolean success; 14 | private Message message; 15 | private String conversationId; 16 | private Object error; 17 | /** 18 | * 对话响应是否结束 19 | */ 20 | private boolean finished; 21 | /** 22 | * 对话响应的文本 23 | */ 24 | private String answer; 25 | 26 | /** 27 | * 错误响应码 28 | */ 29 | private int errorCode; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/Author.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Author { 7 | private String role; 8 | private String name; 9 | private String metadata; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/Content.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class Content { 9 | private String content_type; 10 | private List parts; 11 | private String text; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class ErrorCode { 7 | 8 | public static final int INVALID_JWT = 1; 9 | 10 | /** 11 | * 账号繁忙 12 | */ 13 | public static final int BUSY = 2; 14 | public static final int INVALID_API_KEY = 3; 15 | 16 | /** 17 | * 4.0 3小时25次的对话限制 18 | */ 19 | public static final int CHAT_LIMIT = 4; 20 | 21 | 22 | public static final int RESPONSE_ERROR = 5; 23 | 24 | public static final int ACCOUNT_DEACTIVATED = 6; 25 | 26 | public static final Map map = new HashMap<>(); 27 | 28 | static { 29 | map.put(INVALID_JWT, "无效的access token"); 30 | map.put(BUSY, "账号繁忙中"); 31 | map.put(INVALID_API_KEY, "无效的api key"); 32 | map.put(CHAT_LIMIT, "4.0接口被限制了"); 33 | map.put(RESPONSE_ERROR, "响应错误"); 34 | map.put(ACCOUNT_DEACTIVATED, "账号被停用"); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/Message.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * gpt对话响应对象 7 | */ 8 | @Data 9 | public class Message { 10 | private String id; 11 | private Author author; 12 | private Double createTime; 13 | private Object updateTime; 14 | private Content content; 15 | private String status; 16 | private Boolean endTurn; 17 | private Double weight; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/Model.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | /** 9 | * openai 查询模型接口返回的模型信息 10 | */ 11 | public class Model { 12 | private String slug; 13 | private String max_tokens; 14 | private String title; 15 | private String description; 16 | private List tags; 17 | private String capabilities; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gpt/Models.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gpt; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | public class Models { 9 | 10 | /** 11 | * plus用户默认模型 12 | */ 13 | // public static final String PLUS_DEFAULT_MODEL = "Default (GPT-3.5)"; 14 | public static final String PLUS_DEFAULT_MODEL = "gpt-3.5-turbo"; 15 | 16 | /** 17 | * 普通用户默认模型 18 | */ 19 | public static String NORMAL_DEFAULT_MODEL; 20 | 21 | /** 22 | * 空模型 23 | */ 24 | public static final String EMPTY_MODEL = "Empty"; 25 | 26 | /** 27 | * 模型title和对应模型 28 | */ 29 | public static Map modelMap = new HashMap<>(); 30 | 31 | /** 32 | * plus模型title池 用于判断请求是否是plus模型 33 | */ 34 | public static Set plusModelTitle = new HashSet<>(); 35 | 36 | /** 37 | * normal模型title池 38 | */ 39 | public static Set normalModelTitle = new HashSet<>(); 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/entity/gptRequestBody/CreateConversationBody.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.entity.gptRequestBody; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 构造ChatGpt对话api请求体 12 | */ 13 | public class CreateConversationBody { 14 | public static String of(String messageId, String text, String parentMessageId, String conversationId, String model) { 15 | JSONObject param = new JSONObject(); 16 | // body.put("action", "next"); 17 | // JSONArray messages = new JSONArray(); 18 | // JSONObject message = new JSONObject(); 19 | // message.put("id", messageId); 20 | // JSONObject author = new JSONObject(); 21 | // author.put("role", "user"); 22 | // message.put("author", author); 23 | // JSONObject content = new JSONObject(); 24 | // content.put("content_type", "text"); 25 | // JSONArray parts = new JSONArray(); 26 | // parts.put(text); 27 | // content.put("parts", parts); 28 | // message.put("content", content); 29 | // messages.put(message); 30 | // body.put("messages", messages); 31 | // body.put("parent_message_id", parentMessageId); 32 | // body.put("conversation_id", conversationId); 33 | // body.put("model", model); 34 | // body.put("timezone_offset_min", -480); 35 | // body.put("history_and_training_disabled", false); 36 | 37 | 38 | JSONObject message = new JSONObject(); 39 | message.put("content", text); 40 | message.put("role", "user"); 41 | // Message message = new Message().setRole("user").setContent(text); 42 | JSONArray messages = new JSONArray(); 43 | messages.add(message); 44 | param.put("messages", messages); 45 | param.put("model", model); 46 | // param.put("model", "gpt-4-turbo"); 47 | param.put("stream", false); 48 | 49 | return param.toJSONString(); 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/handler/EventHandler.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.handler; 2 | 3 | import cn.hutool.core.date.DateUtil; 4 | import cn.hutool.extra.spring.SpringUtil; 5 | import cn.hutool.json.JSONUtil; 6 | import com.lark.oapi.core.request.EventReq; 7 | import com.lark.oapi.event.CustomEventHandler; 8 | import com.lark.oapi.event.EventDispatcher; 9 | import com.lark.oapi.service.im.ImService; 10 | import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1; 11 | import lombok.Data; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.nio.charset.StandardCharsets; 16 | 17 | /** 18 | * 事件处理器 19 | * 20 | * @author shuangquan.chen 21 | * @date 2024-04-29 16:15 22 | */ 23 | @Slf4j 24 | @Component 25 | @Data 26 | public class EventHandler { 27 | 28 | public static final EventDispatcher EVENT_HANDLER = EventDispatcher.newBuilder("", "") 29 | .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() { 30 | @Override 31 | public void handle(P2MessageReceiveV1 event) { 32 | //处理消息事件 33 | try { 34 | // log.info("收到消息: {}", Jsons.DEFAULT.toJson(event)); 35 | MessageHandler messageHandler = SpringUtil.getBean(MessageHandler.class); 36 | // log.info("event: {}", JSONUtil.toJsonStr(event)); 37 | messageHandler.process(event); 38 | } catch (Exception e) { 39 | log.error("处理消息失败", e); 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | }) 44 | .build(); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/handler/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.handler; 2 | 3 | import cn.hutool.core.date.DateUtil; 4 | import cn.hutool.json.JSONUtil; 5 | import com.alibaba.fastjson.JSONObject; 6 | import com.lark.oapi.Client; 7 | import com.lark.oapi.service.contact.v3.model.User; 8 | import com.lark.oapi.service.im.v1.model.*; 9 | import com.rawchen.feishubot.entity.Conversation; 10 | import com.rawchen.feishubot.entity.Mode; 11 | import com.rawchen.feishubot.entity.Status; 12 | import com.rawchen.feishubot.entity.gpt.Answer; 13 | import com.rawchen.feishubot.entity.gpt.ErrorCode; 14 | import com.rawchen.feishubot.entity.gpt.Models; 15 | import com.rawchen.feishubot.service.MessageService; 16 | import com.rawchen.feishubot.service.UserService; 17 | import com.rawchen.feishubot.util.chatgpt.AccountPool; 18 | import com.rawchen.feishubot.util.chatgpt.ChatService; 19 | import com.rawchen.feishubot.util.chatgpt.ConversationPool; 20 | import com.rawchen.feishubot.util.chatgpt.RequestIdSet; 21 | import lombok.RequiredArgsConstructor; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.springframework.stereotype.Component; 24 | import org.springframework.util.StringUtils; 25 | 26 | import java.util.HashMap; 27 | import java.util.Map; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | @Component 31 | @Slf4j 32 | @RequiredArgsConstructor 33 | public class MessageHandler { 34 | 35 | 36 | protected final Client client; 37 | protected final AccountPool accountPool; 38 | protected final UserService userService; 39 | protected final MessageService messageService; 40 | protected final ConversationPool conversationPool; 41 | 42 | /** 43 | * 飞书推送事件会重复,用于去重 44 | * 45 | * @param event 46 | * @return 47 | */ 48 | private boolean checkInvalidEvent(P2MessageReceiveV1 event) { 49 | // String requestId = event.getRequestId(); 50 | String requestId = event.getHeader().getEventId(); 51 | // 根据内存记录的消息事件id去重 52 | if (RequestIdSet.requestIdSet.contains(requestId)) { 53 | //log.warn("重复请求,requestId:{}", requestId); 54 | return false; 55 | } 56 | RequestIdSet.requestIdSet.add(requestId); 57 | 58 | String createTime = event.getEvent().getMessage().getCreateTime(); 59 | Long createTimeLong = Long.valueOf(createTime); 60 | Long now = System.currentTimeMillis(); 61 | if (now - createTimeLong > 1000 * 10) { 62 | // 根据消息事件的创建时间去重(如果现在的时间比创建时间大10秒就说明是重复的,正常服务器时间同步正确应该只有1s的差别) 63 | //log.warn("消息过期,requestId:{}", requestId); 64 | return false; 65 | } 66 | 67 | P2MessageReceiveV1Data messageEvent = event.getEvent(); 68 | EventMessage message = messageEvent.getMessage(); 69 | 70 | String chatType = message.getChatType(); 71 | String msgType = message.getMessageType(); 72 | 73 | log.info("对话类型: {},消息类型: {}", chatType, msgType); 74 | 75 | 76 | // 只处理私聊文本消息 77 | // if (!chatType.equals("p2p") || !msgType.equals("text")) { 78 | // log.warn("不支持的ChatType或MsgType,ChatGpt不处理"); 79 | // return false; 80 | // } 81 | 82 | // 不是group群组@机器人的消息和机器人私聊消息,或者不是文本就不处理 83 | if ((!chatType.equals("group") && !chatType.equals("p2p")) || !msgType.equals("text")) { 84 | log.warn("不支持的ChatType或MsgType,ChatGpt不处理"); 85 | return false; 86 | } 87 | return true; 88 | } 89 | 90 | 91 | public void process(P2MessageReceiveV1 event) throws Exception { 92 | P2MessageReceiveV1Data messageEvent = event.getEvent(); 93 | EventMessage message = messageEvent.getMessage(); 94 | // 如果是重复消息不处理 95 | if (!checkInvalidEvent(event)) { 96 | return; 97 | } 98 | log.info("事件时间: {}", DateUtil.formatDateTime(DateUtil.date(Long.parseLong(event.getEvent().getMessage().getCreateTime())))); 99 | JSONObject jsonObject = JSONObject.parseObject(message.getContent()); 100 | String text = jsonObject.getString("text"); 101 | 102 | // 去掉事件获取开头的"@_user_1" 103 | if (text.startsWith("@_user_1")) { 104 | text = text.substring(9); 105 | } 106 | 107 | try { 108 | User user = userService.getUserByOpenId(event.getEvent().getSender().getSenderId().getOpenId()); 109 | String name = user.getName(); 110 | log.info("{}: {}", name, text); 111 | } catch (Exception e) { 112 | log.error("获取用户信息失败", e); 113 | } 114 | 115 | if (text == null || text.equals("")) { 116 | return; 117 | } 118 | String chatId = message.getChatId(); 119 | //尝试从会话池中获取会话,从而保持上下文 120 | Conversation conversation = conversationPool.getConversation(chatId); 121 | String model; 122 | 123 | String account = null; 124 | ChatService chatService; 125 | 126 | if (conversation == null) { 127 | //如果没有会话,新建会话,采用默认3.5的模型 128 | conversation = new Conversation(); 129 | chatService = accountPool.getFreeChatService(Models.EMPTY_MODEL); 130 | if (chatService != null) { 131 | // if (chatService.getLevel() == 4) { 132 | if (chatService.getLevel() == 3) { 133 | model = Models.PLUS_DEFAULT_MODEL; 134 | } else { 135 | model = Models.NORMAL_DEFAULT_MODEL; 136 | } 137 | 138 | conversation.setChatId(chatId); 139 | conversation.setModel(model); 140 | //默认使用keep模式 141 | conversation.setMode(Mode.FAST); 142 | 143 | conversationPool.addConversation(chatId, conversation); 144 | } else { 145 | model = null; 146 | } 147 | 148 | 149 | } else { 150 | 151 | //新会话意图 152 | model = conversation.getModel(); 153 | chatService = accountPool.getFreeChatService(model); 154 | } 155 | 156 | 157 | if (chatService == null) { 158 | if (accountPool.getSize() == 0) { 159 | messageService.sendTextMessageByChatId(chatId, "服务器未配置可用账户"); 160 | return; 161 | } 162 | 163 | //如果是fast模式,则需要切换账号服务 164 | // if (conversation.getMode() == Mode.FAST) { 165 | // chatService = accountPool.getFreeChatService(Models.EMPTY_MODEL); 166 | // if (chatService == null) { 167 | // messageService.sendTextMessageByChatId(chatId, "目前无空闲该模型,请稍后再试,或者更换模型"); 168 | // return; 169 | // } else { 170 | // conversation.setAccount(chatService.getAccount()); 171 | // 172 | // if (chatService.getLevel() == 4) { 173 | // conversation.setModel(Models.PLUS_DEFAULT_MODEL); 174 | // } else { 175 | // conversation.setModel(Models.NORMAL_DEFAULT_MODEL); 176 | // } 177 | // } 178 | // } 179 | } 180 | 181 | //账号池里所有的账号都在运行 182 | if (chatService == null) { 183 | messageService.sendTextMessageByChatId(chatId, "目前无空闲账号,请稍后再试"); 184 | return; 185 | } 186 | 187 | String firstText = "正在生成中,请稍后..."; 188 | 189 | if (chatService.getStatus() == Status.RUNNING) { 190 | //fast模式,切换账号,创建新会话 191 | chatService = accountPool.getFreeChatService(Models.EMPTY_MODEL); 192 | if (chatService.getStatus() == Status.RUNNING) { 193 | firstText = "目前该账号正在运行,请稍等..."; 194 | } else { 195 | conversation.setAccount(chatService.getAccount()); 196 | //fast模式下,切换账号和原账号模型保持一致 197 | //如果原账号是plus模型,则切换到的是normal账号,则用normal的模型 198 | if (chatService.getLevel() == 4) { 199 | if (Models.plusModelTitle.contains(conversation.getModel())) { 200 | conversation.setModel(conversation.getModel()); 201 | } else { 202 | conversation.setModel(Models.PLUS_DEFAULT_MODEL); 203 | } 204 | } else { 205 | conversation.setModel(Models.NORMAL_DEFAULT_MODEL); 206 | } 207 | } 208 | } 209 | conversation.setAccount(chatService.getAccount()); 210 | chatService.setStatus(Status.RUNNING); 211 | conversation.setStatus(Status.RUNNING); 212 | 213 | String title; 214 | if (!StringUtils.hasLength(chatService.getAccount())) { 215 | title = model; 216 | } else { 217 | title = model + " : " + chatService.getAccount(); 218 | } 219 | 220 | if (chatService.getLevel() == 4) { 221 | title += " [plus] "; 222 | } 223 | 224 | String modelParam = Models.PLUS_DEFAULT_MODEL; 225 | title = modelParam; 226 | // model = modelParam; 227 | 228 | // title += "「" + conversation.getMode() + "」"; 229 | 230 | 231 | CreateMessageResp resp = messageService.sendGptAnswerMessage(chatId, title, firstText); 232 | String messageId = resp.getData().getMessageId(); 233 | 234 | // Map selections = createSelection(conversation); 235 | 236 | // String modelParam = Models.modelMap.get(conversation.getModel()).getSlug(); 237 | 238 | // log.info("新建会话"); 239 | conversation.setTitle(title); 240 | String finalTitle = title; 241 | ChatService finalChatService = chatService; 242 | chatService.newChat(text, modelParam, answer -> { 243 | processAnswer(answer, finalTitle, chatId, finalChatService, messageId, event, model); 244 | }); 245 | 246 | 247 | // log.info("服务完成: {}|{}|{}|{}", chatService.getAccount(), model, chatId, messageId); 248 | log.info("服务完成: {}|{}|{}|{}\n", chatService.getAccessToken(), model, chatId, messageId); 249 | } 250 | 251 | private Map createSelection(Conversation conversation) { 252 | //将curConversation转换成json格式 253 | String conversationStr = JSONUtil.toJsonStr(conversation); 254 | Map selections = new HashMap<>(); 255 | selections.put("使用当前上下文", conversationStr); 256 | 257 | for (String s : Models.normalModelTitle) { 258 | Conversation normalModelConversation = new Conversation(); 259 | normalModelConversation.setModel(s); 260 | selections.put(s, JSONUtil.toJsonStr(normalModelConversation)); 261 | } 262 | 263 | if (AccountPool.plusPool.isEmpty()) { 264 | return selections; 265 | } 266 | 267 | for (String modelTitle : Models.plusModelTitle) { 268 | Conversation plusModelConversation = new Conversation(); 269 | plusModelConversation.setModel(modelTitle); 270 | selections.put(modelTitle, JSONUtil.toJsonStr(plusModelConversation)); 271 | } 272 | return selections; 273 | } 274 | 275 | private void processAnswer(Answer answer, String title, String chatId, ChatService chatService, String messageId, P2MessageReceiveV1 event, String model) throws Exception { 276 | if (!answer.isSuccess()) { 277 | // gpt请求失败 278 | if (answer.getErrorCode() == ErrorCode.INVALID_JWT || answer.getErrorCode() == ErrorCode.INVALID_API_KEY) { 279 | 280 | messageService.modifyGptAnswerMessageCard(messageId, title, (String) answer.getError()); 281 | boolean build = chatService.build(); 282 | if (build) { 283 | accountPool.modifyChatService(chatService); 284 | RequestIdSet.requestIdSet.remove(event.getRequestId()); 285 | process(event); 286 | } else { 287 | messageService.modifyGptAnswerMessageCard(messageId, title, "账号重新登录失败"); 288 | } 289 | return; 290 | } else if (answer.getErrorCode() == ErrorCode.ACCOUNT_DEACTIVATED) { 291 | answer.setError(ErrorCode.map.get(ErrorCode.ACCOUNT_DEACTIVATED)); 292 | log.error("账号{} {}", chatService.getAccount(), ErrorCode.map.get(ErrorCode.ACCOUNT_DEACTIVATED)); 293 | AccountPool.removeAccount(chatService.getAccount()); 294 | } 295 | messageService.modifyGptAnswerMessageCard(messageId, title, (String) answer.getError()); 296 | chatService.setStatus(Status.FINISHED); 297 | conversationPool.getConversation(chatId).setStatus(Status.FINISHED); 298 | return; 299 | } 300 | Conversation conversation = conversationPool.getConversation(chatId); 301 | // conversation.setParentMessageId(answer.getMessage().getId()); 302 | // conversation.setConversationId(answer.getConversationId()); 303 | 304 | if (!answer.isFinished()) { 305 | chatService.setStatus(Status.RUNNING); 306 | conversation.setStatus(Status.RUNNING); 307 | } else { 308 | chatService.setStatus(Status.FINISHED); 309 | conversation.setStatus(Status.FINISHED); 310 | // conversation.setChatId(chatId); 311 | // conversation.setParentMessageId(answer.getMessage().getId()); 312 | // conversation.setConversationId(answer.getConversationId()); 313 | // conversation.setAccount(chatService.getAccount()); 314 | // conversation.setModel(model); 315 | // conversation.setTitle(title); 316 | } 317 | 318 | // selections.put("使用当前上下文", JSONUtil.toJsonStr(conversation)); 319 | 320 | String content = answer.getAnswer(); 321 | if (content == null || content.equals("")) { 322 | return; 323 | } 324 | PatchMessageResp resp1 = messageService.modifyGptAnswerMessageCard(messageId, title, content); 325 | if (resp1.getCode() != 0) { 326 | log.error("code: {}", resp1.getCode()); 327 | log.error("msg: {}", resp1.getMsg()); 328 | log.error("error msg: {}", resp1.getError().getMessage()); 329 | } 330 | 331 | if (answer.isFinished() && resp1.getCode() == 230020) { 332 | //保证最后完整的gpt响应 不会被飞书消息频率限制 333 | while (answer.isFinished() && resp1.getCode() != 0) { 334 | log.warn("重试中 code: {} msg: {}", resp1.getCode(), resp1.getMsg()); 335 | TimeUnit.MILLISECONDS.sleep(500); 336 | resp1 = messageService.modifyGptAnswerMessageCard(messageId, title, content); 337 | } 338 | log.info("重试成功"); 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/scheduling/ScheduledTask.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.scheduling; 2 | 3 | import com.rawchen.feishubot.util.chatgpt.AccountUtil; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @Slf4j 10 | public class ScheduledTask { 11 | // @Scheduled(cron = "0 0 0 ? * MON") 12 | public void refreshAccountToken() { 13 | log.info("开始刷新账号token"); 14 | try { 15 | AccountUtil.refreshToken(); 16 | } catch (Exception e) { 17 | log.error("刷新账号token失败", e); 18 | } 19 | } 20 | 21 | /** 22 | * 每隔两个小时执行一次 23 | */ 24 | // @Scheduled(initialDelay = 1000 * 60, fixedRate = 1000 * 60 * 60 * 2) 25 | public void checkAccountLevel() { 26 | try { 27 | AccountUtil.queryAccountLevel(); 28 | } catch (Exception e) { 29 | log.error("检查账号等级失败", e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.service; 2 | 3 | import com.rawchen.feishubot.entity.Account; 4 | import com.rawchen.feishubot.util.chatgpt.AccountUtil; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | 9 | @Service 10 | public class AccountService { 11 | /** 12 | * 从配置文件读取账号信息 13 | * 14 | * @return 15 | */ 16 | public List getAccounts() { 17 | return AccountUtil.readAccounts().getAccounts(); 18 | } 19 | 20 | /** 21 | * 更新账号的token到配置文件 22 | * 23 | * @param accountName 24 | * @param newToken 25 | */ 26 | public void updateTokenForAccount(String accountName, String newToken) { 27 | AccountUtil.updateToken(accountName, newToken); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.service; 2 | 3 | import com.lark.oapi.Client; 4 | import com.lark.oapi.service.im.v1.model.*; 5 | import com.rawchen.feishubot.util.MessageCard; 6 | import com.rawchen.feishubot.util.MessageContent; 7 | import com.rawchen.feishubot.util.StringUtil; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Map; 13 | import java.util.UUID; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | @Slf4j 18 | /** 19 | * 用于飞书机器人发送消息 20 | */ 21 | public class MessageService { 22 | protected final Client client; 23 | 24 | 25 | private CreateMessageReq createMessageReq(String receiveIdType, String receiveId, String msgType, String content) { 26 | 27 | return CreateMessageReq.newBuilder() 28 | .receiveIdType(receiveIdType) 29 | .createMessageReqBody(CreateMessageReqBody.newBuilder() 30 | .receiveId(receiveId) 31 | .msgType(msgType) 32 | .content(content) 33 | .uuid(UUID.randomUUID().toString()) 34 | .build()) 35 | .build(); 36 | } 37 | 38 | private PatchMessageReq createPatchMessageReq(String messageId, String content) { 39 | 40 | return PatchMessageReq.newBuilder() 41 | .messageId(messageId) 42 | .patchMessageReqBody(PatchMessageReqBody.newBuilder() 43 | .content(content) 44 | .build()) 45 | .build(); 46 | } 47 | 48 | /** 49 | * 发送文本消息 50 | * 51 | * @param chatId 用于表示聊天的id 52 | * @param text 发送的文本 53 | * @return 54 | * @throws Exception 55 | */ 56 | public CreateMessageResp sendTextMessageByChatId(String chatId, String text) throws Exception { 57 | CreateMessageReq req = createMessageReq("chat_id", chatId, "text", MessageContent.ofText(text)); 58 | return getCreateMessageResp(req); 59 | } 60 | 61 | private CreateMessageResp getCreateMessageResp(CreateMessageReq req) throws Exception { 62 | CreateMessageResp resp = null; 63 | 64 | resp = client.im().message().create(req); 65 | if (!resp.success()) { 66 | log.error(String.format("code:%s,msg:%s,reqId:%s" 67 | , resp.getCode(), resp.getMsg(), resp.getRequestId())); 68 | throw new Exception("发送text消息失败"); 69 | } 70 | return resp; 71 | } 72 | 73 | /** 74 | * 发送卡片消息 75 | * 76 | * @param chatId 77 | * @param text 78 | * @return 79 | * @throws Exception 80 | */ 81 | public CreateMessageResp sendCardMessage(String chatId, String text) throws Exception { 82 | CreateMessageReq messageReq = createMessageReq("chat_id", chatId, "interactive", text); 83 | return getCreateMessageResp(messageReq); 84 | } 85 | 86 | /** 87 | * 发送用户ChatGpt回复消息卡片的消息 88 | * 89 | * @param chatId 90 | * @param title 91 | * @param answer 92 | * @return 93 | * @throws Exception 94 | */ 95 | public CreateMessageResp sendGptAnswerMessage(String chatId, String title, String answer) throws Exception { 96 | return sendCardMessage(chatId, MessageCard.ofGptAnswerMessageCard(title, answer)); 97 | } 98 | 99 | public CreateMessageResp sendGptAnswerMessageWithSelection(String chatId, String title, String answer, Map selections) throws Exception { 100 | return sendCardMessage(chatId, MessageCard.ofGptAnswerMessageCardWithSelection(title, answer, selections)); 101 | } 102 | 103 | /** 104 | * 修改类型为消息卡片的消息 105 | * 106 | * @param messageId 107 | * @param content 108 | * @return 109 | * @throws Exception 110 | */ 111 | public PatchMessageResp modifyMessageCard(String messageId, String content) throws Exception { 112 | PatchMessageReq req = createPatchMessageReq(messageId, content); 113 | return client.im().message().patch(req); 114 | } 115 | 116 | /** 117 | * 修改用于ChatGpt回复的消息卡片的消息 118 | * 119 | * @param messageId 120 | * @param title 121 | * @param answer 122 | * @return 123 | * @throws Exception 124 | */ 125 | public PatchMessageResp modifyGptAnswerMessageCard(String messageId, String title, String answer) throws Exception { 126 | return modifyMessageCard(messageId, MessageCard.ofGptAnswerMessageCard(title, answer)); 127 | } 128 | 129 | 130 | public PatchMessageResp modifyGptAnswerMessageCardWithSelection(String messageId, String title, String answer, Map selections) throws Exception { 131 | return modifyMessageCard(messageId, MessageCard.ofGptAnswerMessageCardWithSelection(title, answer, selections)); 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.service; 2 | 3 | import com.lark.oapi.Client; 4 | import com.lark.oapi.service.contact.v3.model.GetUserReq; 5 | import com.lark.oapi.service.contact.v3.model.GetUserResp; 6 | import com.lark.oapi.service.contact.v3.model.User; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | @Slf4j 13 | @RequiredArgsConstructor 14 | public class UserService { 15 | protected final Client client; 16 | 17 | public User getUserByOpenId(String openId) throws Exception { 18 | // 创建请求对象 19 | GetUserReq req = GetUserReq.newBuilder() 20 | .userId(openId) 21 | .userIdType("open_id") 22 | .departmentIdType("open_department_id") 23 | .build(); 24 | // 发起请求 25 | GetUserResp resp = client.contact().user().get(req); 26 | 27 | // 处理服务端错误 28 | if (!resp.success()) { 29 | log.error("code:{},msg:{},reqId:{}" 30 | , resp.getCode(), resp.getMsg(), resp.getRequestId()); 31 | throw new Exception("获取用户信息失败"); 32 | } 33 | return resp.getData().getUser(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/MessageCard.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.util.Map; 8 | 9 | /** 10 | * 机器人回复消息卡片消息格式 11 | */ 12 | @Slf4j 13 | public class MessageCard { 14 | private static JSONObject gptAnswerMessageCard; 15 | 16 | private static boolean hasSelection = false; 17 | 18 | private static void initChatGptAnswerMessageCard() { 19 | JSONObject markdownElement = new JSONObject(); 20 | markdownElement.put("tag", "markdown"); 21 | markdownElement.put("content", ""); 22 | 23 | JSONObject title = new JSONObject(); 24 | title.put("content", ""); 25 | title.put("tag", "plain_text"); 26 | 27 | JSONObject header = new JSONObject(); 28 | header.put("template", "blue"); 29 | header.put("title", title); 30 | 31 | JSONObject config = new JSONObject(); 32 | config.put("wide_screen_mode", true); 33 | config.put("update_multi", true); 34 | 35 | // JSONObject line = new JSONObject(); 36 | // line.put("tag", "hr"); 37 | 38 | JSONObject jsonMessage = new JSONObject(); 39 | // jsonMessage.put("elements", new JSONArray().put(markdownElement).put(line)); 40 | JSONArray elements = new JSONArray(); 41 | elements.add(markdownElement); 42 | jsonMessage.put("elements", elements); 43 | 44 | JSONObject jsonBody = new JSONObject(); 45 | jsonBody.put("header", header); 46 | jsonBody.put("config", config); 47 | jsonBody.put("schema", "2.0"); 48 | jsonBody.put("body", jsonMessage); 49 | 50 | gptAnswerMessageCard = jsonBody; 51 | } 52 | 53 | /** 54 | * 普通消息卡片格式 55 | * 56 | * @param title 57 | * @param content 58 | * @return 59 | */ 60 | public static String ofGptAnswerMessageCard(String title, String content) { 61 | if (gptAnswerMessageCard == null) { 62 | initChatGptAnswerMessageCard(); 63 | } 64 | JSONObject markdownElement = gptAnswerMessageCard.getJSONObject("body").getJSONArray("elements").getJSONObject(0); 65 | // 目前消息卡片不支持“```”代码块样式输出,因此可能会将代码块中一起出现“<”、“!”内容误识别xss注入后删掉 66 | // https://open.feishu.cn/document/common-capabilities/message-card/message-cards-content/using-markdown-tags 67 | // 卡片 JSON 2.0已支持基本的Markdown格式,除了![]()此格式飞书对链接做了校验,只能允许上传图片接口获取的key 68 | // https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/content-components/rich-text 69 | content = StringUtil.replaceSpecialSymbol(content); 70 | markdownElement.put("content", content); 71 | JSONObject titleObject = gptAnswerMessageCard.getJSONObject("header").getJSONObject("title"); 72 | titleObject.put("content", title); 73 | return gptAnswerMessageCard.toString(); 74 | } 75 | 76 | /** 77 | * 带模型选择的消息卡片 78 | * 79 | * @param title 80 | * @param content 81 | * @param selections 82 | * @return 83 | */ 84 | public static String ofGptAnswerMessageCardWithSelection(String title, String content, Map selections) { 85 | if (hasSelection) { 86 | gptAnswerMessageCard = null; 87 | } 88 | ofGptAnswerMessageCard(title, content); 89 | JSONObject selection = new JSONObject(); 90 | JSONArray array = new JSONArray(); 91 | array.add(gptAnswerMessageCard.get("elements")); 92 | 93 | JSONObject tip = new JSONObject(); 94 | array.add(tip); 95 | tip.put("tag", "div"); 96 | JSONObject tipText = new JSONObject(); 97 | tipText.put("content", "请选择接下来对话模型(不选则默认当前模型)"); 98 | tipText.put("tag", "plain_text"); 99 | tip.put("text", tipText); 100 | 101 | array.add(selection); 102 | selection.put("tag", "action"); 103 | 104 | JSONArray actions = new JSONArray(); 105 | selection.put("actions", actions); 106 | 107 | JSONObject action = new JSONObject(); 108 | action.put("tag", "select_static"); 109 | actions.add(action); 110 | 111 | JSONObject placeholder = new JSONObject(); 112 | placeholder.put("content", "当前会话模型"); 113 | placeholder.put("tag", "plain_text"); 114 | 115 | action.put("placeholder", placeholder); 116 | 117 | JSONArray options = new JSONArray(); 118 | action.put("options", options); 119 | 120 | for (String s : selections.keySet()) { 121 | JSONObject option = new JSONObject(); 122 | options.add(option); 123 | option.put("value", selections.get(s)); 124 | JSONObject text = new JSONObject(); 125 | text.put("content", s); 126 | text.put("tag", "plain_text"); 127 | option.put("text", text); 128 | } 129 | hasSelection = true; 130 | return gptAnswerMessageCard.toString(); 131 | } 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/MessageContent.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util; 2 | 3 | public class MessageContent { 4 | 5 | public static String ofText(String text) { 6 | return String.format("{\"text\":\"%s\"}", text); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/StringUtil.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * @author RawChen 11 | * @date 2023-08-04 12 | */ 13 | @Slf4j 14 | public class StringUtil { 15 | public static String replaceSpecialSymbol(String str) { 16 | 17 | if (StrUtil.isEmpty(str)) { 18 | return ""; 19 | } 20 | 21 | Pattern pattern = Pattern.compile("!\\[([^\\]]*)]\\(([^\\)]*)\\)"); 22 | Matcher matcher = pattern.matcher(str); 23 | while (matcher.find()) { 24 | // str = str.replaceAll("!\\[([^\\]]*)]\\(([^\\)]*)\\)", "```![Image](http://sample.com/a.jpg)```"); 25 | str = str.replaceAll("!\\[([^\\]]*)]\\(([^\\)]*)\\)", "![Image](img_v3_02h1_6e6d0296-1c9a-4058-9a2c-a167e2cbd56g)"); 26 | // String url = matcher.group(2); 27 | // String description = matcher.group(1); 28 | // System.out.println("URL: " + url); 29 | // System.out.println("Description: " + description); 30 | } 31 | 32 | // str = str.replace(">", ">"); 33 | // str = str.replace("<", "<"); 34 | // str = str.replace("~", "∼"); 35 | // str = str.replace("-", "-"); 36 | // str = str.replace("!", "!"); 37 | // str = str.replace("*", "*"); 38 | // str = str.replace("/", "/"); 39 | // str = str.replace("\\", "\"); 40 | // str = str.replace("[", "["); 41 | // str = str.replace("]", "]"); 42 | // str = str.replace("(", "("); 43 | // str = str.replace(")", ")"); 44 | // str = str.replace("#", "#"); 45 | // str = str.replace(":", ":"); 46 | // str = str.replace("+", "+"); 47 | // str = str.replace("\"", """); 48 | // str = str.replace("'", "'"); 49 | // str = str.replace("`", "`"); 50 | // str = str.replace("$", "$"); 51 | // str = str.replace("_", "_"); 52 | // str = str.replace("-", "-"); 53 | 54 | return str; 55 | } 56 | 57 | public static String escape(String input) { 58 | StringBuilder sb = new StringBuilder(); 59 | 60 | for (int i = 0; i < input.length(); i++) { 61 | char c = input.charAt(i); 62 | 63 | switch (c) { 64 | case '\n': 65 | sb.append("\\n"); 66 | break; 67 | case '\t': 68 | sb.append("\\t"); 69 | break; 70 | case '\r': 71 | sb.append("\\r"); 72 | break; 73 | case '\f': 74 | sb.append("\\f"); 75 | break; 76 | case '\b': 77 | sb.append("\\b"); 78 | break; 79 | case '\\': 80 | sb.append("\\\\"); 81 | break; 82 | case '\'': 83 | sb.append("\\'"); 84 | break; 85 | case '\"': 86 | sb.append("\\\""); 87 | break; 88 | default: 89 | sb.append(c); 90 | } 91 | } 92 | 93 | return sb.toString(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/Task.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util; 2 | 3 | import com.rawchen.feishubot.entity.gpt.Answer; 4 | import com.rawchen.feishubot.util.chatgpt.AnswerProcess; 5 | import lombok.Data; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | @Data 9 | @Slf4j 10 | public class Task { 11 | private AnswerProcess process; 12 | private Answer answer; 13 | private String account; 14 | 15 | public Task(AnswerProcess process, Answer answer, String account) { 16 | this.process = process; 17 | this.answer = answer; 18 | this.account = account; 19 | } 20 | 21 | public void run() { 22 | try { 23 | process.process(answer); 24 | } catch (Exception e) { 25 | log.error("处理ChatGpt响应出错", e); 26 | log.error(answer.toString()); 27 | 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/TaskPool.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.concurrent.BlockingQueue; 8 | import java.util.concurrent.LinkedBlockingQueue; 9 | 10 | 11 | public class TaskPool { 12 | private static final Map> taskPool = new HashMap<>(); 13 | 14 | public static void init(List accounts) { 15 | for (String account : accounts) { 16 | taskPool.put(account, new LinkedBlockingQueue<>()); 17 | } 18 | } 19 | 20 | public static void addTask(Task task) throws InterruptedException { 21 | BlockingQueue queue = taskPool.get(task.getAccount()); 22 | queue.put(task); 23 | } 24 | 25 | public static void runTask() { 26 | Set accounts = taskPool.keySet(); 27 | for (String account : accounts) { 28 | new Thread(() -> { 29 | BlockingQueue queue = taskPool.get(account); 30 | while (true) { 31 | try { 32 | Task task = queue.take(); 33 | task.run(); 34 | } catch (InterruptedException e) { 35 | e.printStackTrace(); 36 | } 37 | } 38 | }).start(); 39 | } 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/chatgpt/AccountPool.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util.chatgpt; 2 | 3 | import com.rawchen.feishubot.entity.Account; 4 | import com.rawchen.feishubot.entity.Status; 5 | import com.rawchen.feishubot.entity.gpt.Models; 6 | import com.rawchen.feishubot.service.AccountService; 7 | import com.rawchen.feishubot.util.TaskPool; 8 | import lombok.Data; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.core.env.Environment; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.StringUtils; 15 | 16 | import javax.annotation.PostConstruct; 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | /** 23 | * 账号池 24 | */ 25 | @Component 26 | @RequiredArgsConstructor 27 | @Slf4j 28 | @Data 29 | 30 | public class AccountPool { 31 | protected final AccountService accountService; 32 | 33 | protected final Environment environment; 34 | 35 | private int size; 36 | 37 | @Value("${proxy.url}") 38 | private String proxyUrl; 39 | 40 | public static Map accountPool = new HashMap<>(); 41 | 42 | public static Map normalPool = new HashMap<>(); 43 | 44 | public static Map plusPool = new HashMap<>(); 45 | 46 | /** 47 | * 初始化账号池 48 | */ 49 | @PostConstruct 50 | public void init() { 51 | List accounts = accountService.getAccounts(); 52 | List usefulAccounts = new ArrayList<>(); 53 | for (Account account : accounts) { 54 | 55 | ChatService chatService = new ChatService(account.getAccount(), account.getPassword(), account.getToken(), proxyUrl); 56 | if (account.getToken() == null) { 57 | log.debug("账号{}未登录", account.getAccount()); 58 | boolean ok = chatService.build(); 59 | if (ok) { 60 | log.debug("账号{}登录成功", account.getAccount()); 61 | account.setToken(chatService.getAccessToken()); 62 | // accountService.updateTokenForAccount(account.getAccount(), chatService.getAccessToken()); 63 | 64 | } else { 65 | //ChatGpt登录失败 66 | log.error("账号{}登录失败", account.getAccount()); 67 | continue; 68 | } 69 | } 70 | 71 | //查询账号是否plus用户 72 | // boolean b = chatService.queryAccountLevel(); 73 | //如果token失效,重新登录,更新token,重新查询一次 74 | // if (!b) { 75 | // log.debug("账号{}登录失效", account.getAccount()); 76 | // log.debug("账号{}重新登录", account.getAccount()); 77 | // boolean ok = chatService.build(); 78 | // if (ok) { 79 | // //重新登录成功 80 | // account.setToken(chatService.getAccessToken()); 81 | // accountService.updateTokenForAccount(account.getAccount(), chatService.getAccessToken()); 82 | // b = chatService.queryAccountLevel(); 83 | // if (!b) { 84 | // continue; 85 | // } 86 | // } else { 87 | // //ChatGpt登录失败 88 | // log.error("账号{}登录失败", account.getAccount()); 89 | // continue; 90 | // } 91 | // } 92 | 93 | 94 | usefulAccounts.add(account.getAccount()); 95 | accountPool.put(account.getAccount(), chatService); 96 | size++; 97 | if (chatService.getLevel() == 3) { 98 | // log.info("账号{}为normal用户", account.getAccount()); 99 | log.info("账号{}为normal用户", account.getToken()); 100 | normalPool.put(account.getAccount(), chatService); 101 | } 102 | if (chatService.getLevel() == 4) { 103 | // log.info("账号{}为plus用户", account.getAccount()); 104 | log.info("账号{}为plus用户", account.getToken()); 105 | plusPool.put(account.getAccount(), chatService); 106 | } 107 | } 108 | 109 | log.info("normal账号池共{}个账号", normalPool.size()); 110 | log.info("plus账号池共{}个账号", plusPool.size()); 111 | TaskPool.init(usefulAccounts); 112 | TaskPool.runTask(); 113 | } 114 | 115 | 116 | /** 117 | * 需要plus模型,则从plus账号池中获取 118 | * 需要normal模型,则从normal账号池中获取 119 | * 120 | * @param model 模型title 121 | * @return 122 | */ 123 | public ChatService getFreeChatService(String model) { 124 | if (!StringUtils.hasText(model)) { 125 | model = Models.NORMAL_DEFAULT_MODEL; 126 | } 127 | 128 | List plusAccountList = new ArrayList<>(); 129 | for (String s : plusPool.keySet()) { 130 | ChatService chatService = plusPool.get(s); 131 | if (chatService.getStatus() == Status.FINISHED) { 132 | plusAccountList.add(chatService); 133 | } 134 | } 135 | 136 | //如果plus账号池中有账号,且需要plus模型,则从plus账号池中获取 137 | //如果需要的模型是空模型(新建会话,使用对应账号默认模型就行),也可以从plus账号池中获取 138 | if (plusAccountList.size() > 0 && (Models.plusModelTitle.contains(model) || model.equals(Models.EMPTY_MODEL))) { 139 | return plusAccountList.get((int) (Math.random() * plusAccountList.size())); 140 | } 141 | 142 | if (Models.plusModelTitle.contains(model)) { 143 | //如果需要plus模型,但是没有plus账号,返回null 144 | return null; 145 | } 146 | List normalAccountList = new ArrayList<>(); 147 | for (String s : normalPool.keySet()) { 148 | ChatService chatService = normalPool.get(s); 149 | if (chatService.getStatus() == Status.FINISHED) { 150 | normalAccountList.add(chatService); 151 | } 152 | } 153 | if (normalAccountList.size() == 0) { 154 | return null; 155 | } else { 156 | return normalAccountList.get((int) (Math.random() * normalAccountList.size())); 157 | } 158 | } 159 | 160 | public void modifyChatService(ChatService chatService) { 161 | log.info("修改账号{}信息", chatService.getAccount()); 162 | Account account = new Account(); 163 | account.setAccount(chatService.getAccount()); 164 | account.setToken(chatService.getAccessToken()); 165 | account.setPassword(chatService.getPassword()); 166 | accountService.updateTokenForAccount(chatService.getAccount(), chatService.getAccessToken()); 167 | } 168 | 169 | public ChatService getChatService(String account) { 170 | if (account == null || account.equals("")) { 171 | if (plusPool.containsKey(account)) { 172 | return getFreeChatService(Models.PLUS_DEFAULT_MODEL); 173 | } else { 174 | return getFreeChatService(Models.NORMAL_DEFAULT_MODEL); 175 | } 176 | } 177 | return accountPool.get(account); 178 | } 179 | 180 | public static void removeAccount(String account) { 181 | log.info("移除账号{}", account); 182 | accountPool.remove(account); 183 | normalPool.remove(account); 184 | plusPool.remove(account); 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/chatgpt/AccountUtil.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util.chatgpt; 2 | 3 | import com.rawchen.feishubot.entity.Account; 4 | import com.rawchen.feishubot.entity.AccountList; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.core.io.FileSystemResource; 7 | import org.yaml.snakeyaml.DumperOptions; 8 | import org.yaml.snakeyaml.Yaml; 9 | import org.yaml.snakeyaml.constructor.Constructor; 10 | import org.yaml.snakeyaml.representer.Representer; 11 | 12 | import java.io.FileWriter; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | /** 19 | * 操作account配置文件工具类 20 | */ 21 | @Slf4j 22 | public class AccountUtil { 23 | private static final String YAML_PATH = "accounts.yaml"; 24 | 25 | public static AccountList readAccounts() { 26 | Yaml yaml = new Yaml(new Constructor(AccountList.class)); 27 | try (InputStream in = new FileSystemResource(YAML_PATH).getInputStream()) { 28 | return yaml.loadAs(in, AccountList.class); 29 | } catch (IOException e) { 30 | throw new RuntimeException("Failed to read YAML", e); 31 | } 32 | } 33 | 34 | public static void writeAccounts(AccountList accountList) { 35 | DumperOptions options = new DumperOptions(); 36 | options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 37 | options.setPrettyFlow(true); 38 | Yaml yaml = new Yaml(new Representer(), options); 39 | try (FileWriter writer = new FileWriter(YAML_PATH)) { 40 | yaml.dump(accountList, writer); 41 | } catch (IOException e) { 42 | throw new RuntimeException("Failed to write YAML", e); 43 | } 44 | } 45 | 46 | public static void queryAccountLevel() { 47 | Set accounts = AccountPool.accountPool.keySet(); 48 | for (String account : accounts) { 49 | ChatService chatService = AccountPool.accountPool.get(account); 50 | if (chatService != null) { 51 | int level = chatService.getLevel(); 52 | chatService.queryAccountLevel(); 53 | if (level != chatService.getLevel()) { 54 | log.info("账户「{}」等级变更: {} -> {}", account, level, chatService.getLevel()); 55 | } 56 | //如果是plus账号降级 57 | //从plus账号池移动到普通账号池 58 | if (level == 4 && chatService.getLevel() == 3) { 59 | AccountPool.plusPool.remove(account); 60 | AccountPool.normalPool.put(account, chatService); 61 | } 62 | 63 | //如果是普通账号升级 64 | //从普通账号池移动到plus账号池 65 | if (level == 3 && chatService.getLevel() == 4) { 66 | AccountPool.normalPool.remove(account); 67 | AccountPool.plusPool.put(account, chatService); 68 | } 69 | } 70 | } 71 | } 72 | 73 | public static void updateToken(String accountName, String newToken) { 74 | AccountList accountList = readAccounts(); 75 | List accounts = accountList.getAccounts(); 76 | for (Account account : accounts) { 77 | if (account.getAccount().equals(accountName)) { 78 | account.setToken(newToken); 79 | break; 80 | } 81 | } 82 | writeAccounts(accountList); 83 | } 84 | 85 | public static void refreshToken() { 86 | AccountList accountList = readAccounts(); 87 | List accounts = accountList.getAccounts(); 88 | for (Account account : accounts) { 89 | if (account.getAccount() != null && account.getPassword() != null) { 90 | ChatService chatService = AccountPool.accountPool.get(account.getAccount()); 91 | if (chatService != null) { 92 | chatService.refreshToken(); 93 | } 94 | } 95 | } 96 | writeAccounts(accountList); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/chatgpt/AnswerProcess.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util.chatgpt; 2 | 3 | import com.rawchen.feishubot.entity.gpt.Answer; 4 | 5 | public interface AnswerProcess { 6 | 7 | void process(Answer answer) throws Exception; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/chatgpt/ChatService.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util.chatgpt; 2 | 3 | import cn.hutool.core.util.EscapeUtil; 4 | import cn.hutool.http.HttpRequest; 5 | import cn.hutool.http.HttpResponse; 6 | import cn.hutool.http.HttpUtil; 7 | import cn.hutool.json.JSONUtil; 8 | import com.alibaba.fastjson.JSONArray; 9 | import com.alibaba.fastjson.JSONObject; 10 | import com.rawchen.feishubot.entity.Status; 11 | import com.rawchen.feishubot.entity.gpt.*; 12 | import com.rawchen.feishubot.entity.gptRequestBody.CreateConversationBody; 13 | import com.rawchen.feishubot.util.StringUtil; 14 | import com.rawchen.feishubot.util.Task; 15 | import com.rawchen.feishubot.util.TaskPool; 16 | import lombok.Data; 17 | import lombok.extern.slf4j.Slf4j; 18 | import java.util.List; 19 | import java.util.UUID; 20 | import java.util.concurrent.Semaphore; 21 | 22 | @Data 23 | @Slf4j 24 | public class ChatService { 25 | 26 | private static final String ACCOUNT_LEVEL_URL = "/chatgpt/backend-api/models?history_and_training_disabled=false"; 27 | private String account; 28 | private String password; 29 | private String accessToken; 30 | private int level; 31 | private volatile Status status; 32 | private Semaphore semaphore = new Semaphore(1); 33 | 34 | private StringBuilder errorString; 35 | 36 | private String proxyUrl; 37 | 38 | private static final String LOGIN_URL = "/chatgpt/login"; 39 | private static final String CHAT_URL = "/v1/chat/completions"; 40 | private static final String LIST_URL = "/chatgpt/backend-api/conversations?offset=0&limit=20"; 41 | private static final String GEN_TITLE_URL = "/chatgpt/backend-api/conversation/gen_title/"; 42 | 43 | 44 | public ChatService() { 45 | this.status = Status.FINISHED; 46 | } 47 | 48 | public ChatService(String account, String password, String accessToken, String proxyUrl) { 49 | this.accessToken = accessToken; 50 | this.account = account; 51 | this.password = password; 52 | this.proxyUrl = proxyUrl; 53 | this.status = Status.FINISHED; 54 | this.level = 3; 55 | } 56 | 57 | /** 58 | * 账户构建:接口登录获取token 59 | * 目前转为三方获取,因此无需登录,直接使用token 60 | * 61 | * @return 62 | */ 63 | public boolean build() { 64 | // if (password == null || password.equals("")) { 65 | // log.error("账号{}密码为空", account); 66 | // return false; 67 | // } 68 | // log.info("账号{}开始登录", account); 69 | // String loginUrl = proxyUrl + LOGIN_URL; 70 | // HashMap paramMap = new HashMap<>(); 71 | // paramMap.put("username", account); 72 | // paramMap.put("password", password); 73 | // 74 | // String params = JSONUtil.toJsonPrettyStr(paramMap); 75 | // String result = HttpUtil.post(loginUrl, params); 76 | // 77 | // JSONObject jsonObject = new JSONObject(result); 78 | // 79 | // if (jsonObject.opt("errorMessage") != null) { 80 | // log.error("账号{}登录失败:{}", account, jsonObject.opt("errorMessage")); 81 | // return false; 82 | // } 83 | // accessTokenaccessToken = jsonObject.optString("accessToken"); 84 | // log.info("账号{}登录成功", account); 85 | return true; 86 | } 87 | 88 | public String getToken() { 89 | return "Bearer " + accessToken; 90 | } 91 | 92 | private void chat(String content, String model, AnswerProcess process, String parentMessageId, String conversationId) throws InterruptedException { 93 | semaphore.acquire(); 94 | try { 95 | String createConversationUrl = proxyUrl + CHAT_URL; 96 | UUID uuid = UUID.randomUUID(); 97 | String messageId = uuid.toString(); 98 | 99 | String param = CreateConversationBody.of(messageId, content, parentMessageId, conversationId, model); 100 | // log.info("param: {}", param); 101 | post(param, createConversationUrl, process); 102 | } finally { 103 | semaphore.release(); 104 | } 105 | } 106 | 107 | /** 108 | * 新建会话 109 | * 110 | * @param content 对话内容 111 | * @param model 模型 112 | * @param process 回调 113 | * @throws InterruptedException 114 | */ 115 | public void newChat(String content, String model, AnswerProcess process) throws InterruptedException { 116 | chat(content, model, process, "", ""); 117 | } 118 | 119 | /** 120 | * 继续会话 121 | * 122 | * @param content 对话内容 123 | * @param model 模型 124 | * @param parentMessageId 父消息id 125 | * @param conversationId 会话id 126 | * @param process 回调 127 | * @throws InterruptedException 128 | */ 129 | public void keepChat(String content, String model, String parentMessageId, String conversationId, AnswerProcess process) throws InterruptedException { 130 | chat(content, model, process, parentMessageId, conversationId); 131 | } 132 | 133 | public void genTitle(String conversationId) { 134 | String listUrl = proxyUrl + GEN_TITLE_URL + conversationId; 135 | HttpResponse response = HttpRequest.get(listUrl).header("Authorization", getToken()).execute(); 136 | log.info(response.body()); 137 | } 138 | 139 | 140 | public void getConversationList() { 141 | String listUrl = proxyUrl + LIST_URL; 142 | HttpResponse response = HttpRequest.get(listUrl).header("Authorization", getToken()).execute(); 143 | } 144 | 145 | /** 146 | * 向gpt发起请求 147 | * 148 | * @param param 请求参数 149 | * @param urlStr 请求的地址 150 | * @param process 响应处理器 151 | */ 152 | private void post(String param, String urlStr, AnswerProcess process) { 153 | Answer answer = new Answer(); 154 | try { 155 | String body = HttpRequest.post(urlStr) 156 | .header("Authorization", getToken()) 157 | .body(param) 158 | .execute() 159 | .body(); 160 | // log.info("Chat API Result: {}", body); 161 | JSONObject jsonObject = JSONObject.parseObject(body); 162 | JSONArray choices = jsonObject.getJSONArray("choices"); 163 | if (choices == null || jsonObject.getJSONObject("error") != null) { 164 | JSONObject error = jsonObject.getJSONObject("error"); 165 | if (error != null) { 166 | String msg = error.getString("message"); 167 | log.error("Chat API Result Error: {}", body); 168 | //异步处理 169 | answer.setSuccess(true); 170 | answer.setFinished(true); 171 | answer.setAnswer(msg); 172 | TaskPool.addTask(new Task(process, answer, this.account)); 173 | } 174 | } else { 175 | JSONObject choiceOne = choices.getJSONObject(0); 176 | JSONObject messageObject = choiceOne.getJSONObject("message"); 177 | String messageContent = messageObject.getString("content"); 178 | log.info("返回结果: {}", StringUtil.escape(messageContent)); 179 | //异步处理 180 | answer.setSuccess(true); 181 | answer.setFinished(true); 182 | answer.setAnswer(messageContent); 183 | TaskPool.addTask(new Task(process, answer, this.account)); 184 | } 185 | 186 | 187 | 188 | // 获取并处理响应 189 | // int status = connection.getResponseCode(); 190 | // Reader streamReader = null; 191 | // boolean error = false; 192 | // errorString = new StringBuilder(); 193 | // if (status > 299) { 194 | // streamReader = new InputStreamReader(connection.getErrorStream()); 195 | // log.error("请求失败,状态码:{}", status); 196 | // error = true; 197 | // } else { 198 | // streamReader = new InputStreamReader(connection.getInputStream()); 199 | // } 200 | 201 | // BufferedReader reader = new BufferedReader(streamReader); 202 | // String line; 203 | 204 | // int count = 0; 205 | // while ((line = reader.readLine()) != null) { 206 | // if (line.length() == 0) { 207 | // continue; 208 | // } 209 | // if (error) { 210 | // errorString.append(line); 211 | // log.error(line); 212 | // 213 | // continue; 214 | // } 215 | // 216 | // try { 217 | // count++; 218 | // answer = parse(line); 219 | // 220 | // if (answer == null) { 221 | // continue; 222 | // } 223 | // 224 | // answer.setSeq(count); 225 | // //每10行 才处理一次 为了防止飞书发消息太快被限制频率 226 | // if (answer.isSuccess() && !answer.isFinished() && count % 10 != 0) { 227 | // continue; 228 | // } 229 | // 230 | // if (answer.isSuccess() && !answer.getMessage().getAuthor().getRole().equals("assistant")) { 231 | // continue; 232 | // } 233 | // 234 | // //异步处理 235 | // TaskPool.addTask(new Task(process, answer, this.account)); 236 | // } catch (Exception e) { 237 | // log.error("解析ChatGpt响应出错", e); 238 | // log.error(line); 239 | // } 240 | // } 241 | 242 | // if (error) { 243 | // answer = new Answer(); 244 | // answer.setError(errorString.toString()); 245 | // answer.setErrorCode(ErrorCode.RESPONSE_ERROR); 246 | // answer.setSuccess(false); 247 | // 248 | // try { 249 | // JSONObject jsonObject = new JSONObject(errorString.toString()); 250 | // String detail = jsonObject.optString("detail"); 251 | // if (detail != null) { 252 | // JSONObject detailObject = new JSONObject(detail); 253 | // String code = detailObject.optString("code"); 254 | // if (code.equals("account_deactivated")) { 255 | // answer.setErrorCode(ErrorCode.ACCOUNT_DEACTIVATED); 256 | // } 257 | // } 258 | // 259 | // 260 | // } catch (JSONException ignored) { 261 | // } 262 | // TaskPool.addTask(new Task(process, answer, this.account)); 263 | // } 264 | 265 | // reader.close(); 266 | // connection.disconnect(); 267 | } catch (Exception e) { 268 | log.error("请求出错", e); 269 | } 270 | } 271 | 272 | 273 | // private Answer parse(String body) { 274 | // System.out.println(body); 275 | // Answer answer; 276 | // 277 | // if (body.startsWith("data: [DONE]") || body.startsWith("data: {\"conversation_id\"")) { 278 | // return null; 279 | // } 280 | // if (body.startsWith("data:")) { 281 | // 282 | // body = body.substring(body.indexOf("{")); 283 | // answer = JSONUtil.toBean(body, Answer.class); 284 | // answer.setSuccess(true); 285 | // if (answer.getMessage().getStatus().equals("finished_successfully")) { 286 | // answer.setFinished(true); 287 | // } 288 | // Message message = answer.getMessage(); 289 | // Content content = message.getContent(); 290 | // List parts = content.getParts(); 291 | // if (parts != null) { 292 | // String part = parts.get(0); 293 | // answer.setAnswer(part); 294 | // } 295 | // if (content.getText() != null) { 296 | // answer.setAnswer(content.getText()); 297 | // } 298 | // 299 | // } else { 300 | // answer = new Answer(); 301 | // answer.setSuccess(false); 302 | // JSONObject jsonObject = new JSONObject(body); 303 | // String detail = jsonObject.optString("detail"); 304 | // if (detail != null && detail.contains("Only one message")) { 305 | // log.warn("账号{}忙碌中", account); 306 | // answer.setErrorCode(ErrorCode.BUSY); 307 | // answer.setError(detail); 308 | // return answer; 309 | // } 310 | // if (detail != null && detail.contains("code")) { 311 | // JSONObject error = jsonObject.optJSONObject("detail"); 312 | // String code = (String) error.opt("code"); 313 | // if (code.equals("invalid_jwt")) { 314 | // answer.setErrorCode(ErrorCode.INVALID_JWT); 315 | // } else if (code.equals("invalid_api_key")) { 316 | // answer.setErrorCode(ErrorCode.INVALID_API_KEY); 317 | // } else if (code.equals("model_cap_exceeded")) { 318 | // answer.setErrorCode(ErrorCode.CHAT_LIMIT); 319 | // } else { 320 | // log.error(body); 321 | // log.warn("账号{} token失效", account); 322 | // } 323 | // answer.setError(error.get("message")); 324 | // return answer; 325 | // } 326 | // log.error("未知错误:{}", body); 327 | // log.error("账号{}未知错误:{}", account, body); 328 | // answer.setError(body); 329 | // } 330 | // return answer; 331 | // } 332 | 333 | public void refreshToken() { 334 | build(); 335 | } 336 | 337 | /** 338 | * 查询账号可用模型从而判断账号是否plus用户 339 | * 340 | * @return 查询是否成功 不成功的原因一般为token失效 341 | */ 342 | public boolean queryAccountLevel() { 343 | String url = proxyUrl + ACCOUNT_LEVEL_URL; 344 | HttpResponse response = HttpRequest.get(url).header("Authorization", getToken()).execute(); 345 | String body = response.body(); 346 | JSONObject jsonObject = JSONObject.parseObject(body); 347 | String models = jsonObject.getString("models"); 348 | if (models == null || models.length() == 0) { 349 | log.warn("账号{}查询模型解析失败 : {}", account, body); 350 | return false; 351 | } 352 | cn.hutool.json.JSONArray objects = JSONUtil.parseArray(models); 353 | List list = JSONUtil.toList(objects, Model.class); 354 | boolean plus = false; 355 | for (Model model : list) { 356 | Models.modelMap.put(model.getTitle(), model); 357 | if (model.getSlug().startsWith("gpt-4")) { 358 | Models.plusModelTitle.add(model.getTitle()); 359 | plus = true; 360 | } else { 361 | Models.normalModelTitle.add(model.getTitle()); 362 | Models.NORMAL_DEFAULT_MODEL = model.getTitle(); 363 | } 364 | } 365 | this.setLevel(plus ? 4 : 3); 366 | return true; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/chatgpt/ConversationPool.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util.chatgpt; 2 | 3 | import com.rawchen.feishubot.entity.Conversation; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 会话池 12 | * 记录用户和对应gpt会话上下文的对应关系 13 | * 通过chatId来区分用户,一个用户对应一个会话 14 | * 从而实现gpt具有上下文能力 15 | */ 16 | @Component 17 | @Slf4j 18 | public class ConversationPool { 19 | public volatile Map conversationMap = new HashMap<>(); 20 | 21 | public void addConversation(String chatId, Conversation conversation) { 22 | // log.info("chatId:{} 会话变更前:{}", chatId, conversationMap.get(chatId)); 23 | // log.info("chatId:{} 会话变更后:{}", chatId, conversation); 24 | conversationMap.put(chatId, conversation); 25 | } 26 | 27 | public Conversation getConversation(String chatId) { 28 | return conversationMap.get(chatId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/rawchen/feishubot/util/chatgpt/RequestIdSet.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot.util.chatgpt; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | /** 7 | * 用来保存飞书推送过来的请求id 8 | * 事件可能会重复推送,所以需要去重 9 | */ 10 | public class RequestIdSet { 11 | public static final Set requestIdSet = new HashSet<>(); 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/rawchen/feishubot/FeishuBotApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.rawchen.feishubot; 2 | 3 | import com.lark.oapi.service.im.v1.model.CreateMessageResp; 4 | import com.lark.oapi.service.im.v1.model.PatchMessageResp; 5 | import com.rawchen.feishubot.service.MessageService; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | @SpringBootTest 11 | class FeishuBotApplicationTests { 12 | 13 | @Autowired 14 | MessageService messageService; 15 | 16 | @Test 17 | void contextLoads() { 18 | } 19 | 20 | @Test 21 | void test01() throws Exception { 22 | messageService.sendTextMessageByChatId("oc_e44b7351982677b1d768c3730cc37c13", "123"); 23 | } 24 | 25 | @Test 26 | void test02() throws Exception { 27 | CreateMessageResp createMessageResp = messageService.sendGptAnswerMessage("oc_e44b7351982677b1d768c3730cc37c13", "title123", "12345"); 28 | System.out.println(createMessageResp.getData().getMessageId()); 29 | } 30 | 31 | @Test 32 | void test03() throws Exception { 33 | messageService.modifyMessageCard("om_ff84ab31f48201d498f24aca3aa5f23e", "34235235"); 34 | } 35 | 36 | @Test 37 | void test04() throws Exception { 38 | String i = "A"; 39 | while (true) { 40 | String temp = i + "A"; 41 | PatchMessageResp patchMessageResp = messageService.modifyGptAnswerMessageCard("om_ff84ab31f48201d498f24aca3aa5f23e", "title123", temp); 42 | System.out.println(patchMessageResp.getMsg()); 43 | i = temp; 44 | try { 45 | Thread.sleep(500); 46 | } catch (InterruptedException e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | nohup java -Xmn48m -Xms128m -Xmx128m -Xss256k -jar FeishuBot.jar >> app.log & 3 | echo $! > /var/run/FeishuBot.pid 4 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PID=$(cat /var/run/FeishuBot.pid) 3 | kill -9 $PID 4 | --------------------------------------------------------------------------------