diff --git a/CozeProxy/build.gradle b/CozeProxy/build.gradle new file mode 100644 index 0000000..8218ebb --- /dev/null +++ b/CozeProxy/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '7.1.2' +} + +build{ + dependsOn { + shadowJar + } +} + +jar { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(JavaExec).configureEach { + systemProperty 'file.encoding', 'UTF-8' +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +group = 'catx.feitu' +version = '1.0.0213' + +repositories { + mavenCentral() +} + +dependencies { + // Discord API + implementation 'org.javacord:javacord:3.8.0' + + // Slack API + implementation "com.slack.api:bolt-socket-mode:1.38.1" + implementation "com.slack.api:bolt-servlet:1.38.1" + implementation "com.slack.api:bolt-jetty:1.38.1" + implementation "org.slf4j:slf4j-simple:1.7.36" +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/ConversationManage/ConversationData.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/ConversationManage/ConversationData.java new file mode 100644 index 0000000..8d92230 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/ConversationManage/ConversationData.java @@ -0,0 +1,23 @@ +package catx.feitu.CozeProxy.ConversationManage; + +import java.util.concurrent.ConcurrentHashMap; + +public class ConversationData { + public ConcurrentHashMap conversations = new ConcurrentHashMap<>(); + public ConversationData() { } + public ConversationData(ConcurrentHashMap conversations) { + this.conversations = conversations; + } + public void put(String name,String conversationID) { + conversations.put(name, conversationID); + } + + public void remove(String name) { + conversations.remove(name); + } + public String get(String name) { return conversations.getOrDefault(name, name); // 索引为空那么传入的可能是频道ID 直接返回 + } + public ConcurrentHashMap getMap() { + return conversations; + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/ConversationManage/ConversationHelper.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/ConversationManage/ConversationHelper.java new file mode 100644 index 0000000..0ff30e1 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/ConversationManage/ConversationHelper.java @@ -0,0 +1,29 @@ +package catx.feitu.CozeProxy.ConversationManage; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public class ConversationHelper { + public static String conversation2JsonString(ConversationData conversation) { + return JSON.toJSONString(conversation.getMap()); + } + public static ConversationData jsonString2Conversation(String jsonString) { + TypeReference> typeRef = new TypeReference>() {}; + ConcurrentHashMap conversation = JSON.parseObject(jsonString, typeRef); + return new ConversationData(conversation); + } + public static List conversationGetIdAsList (ConversationData conversation) { + List idList = Collections.synchronizedList(new ArrayList<>()); + idList.addAll(conversation.getMap().values()); + return idList; + } + public static List conversationGetNameAsList (ConversationData conversation) { + ConcurrentHashMap map = conversation.getMap(); + return new ArrayList<>(map.keySet()); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/CozeGPT.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/CozeGPT.java new file mode 100644 index 0000000..8dabf8e --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/CozeGPT.java @@ -0,0 +1,361 @@ +package catx.feitu.CozeProxy; + +import catx.feitu.CozeProxy.ConversationManage.ConversationData; +import catx.feitu.coze_discord_bridge.api.Exceptions.*; +import catx.feitu.CozeProxy.FunctionalInterface.ChatStreamEvent; +import catx.feitu.coze_discord_bridge.api.Listen.MessageListener; +import catx.feitu.coze_discord_bridge.api.LockManage.LockManage; +import catx.feitu.CozeProxy.MessageManage.BotGenerateStatusManage; +import catx.feitu.CozeProxy.MessageManage.BotResponseManage; +import catx.feitu.CozeProxy.MessageManage.BotResponseType; +import catx.feitu.coze_discord_bridge.api.Types.ConversationInfo; +import catx.feitu.coze_discord_bridge.api.Types.GPTFile; +import catx.feitu.coze_discord_bridge.api.Types.GenerateMessage; + +import org.javacord.api.DiscordApi; +import org.javacord.api.DiscordApiBuilder; +import org.javacord.api.entity.channel.*; +import org.javacord.api.entity.intent.Intent; +import org.javacord.api.entity.message.Message; +import org.javacord.api.entity.server.Server; +import org.javacord.api.entity.user.User; +import org.javacord.api.entity.user.UserStatus; + +import java.io.ByteArrayInputStream; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class CozeGPT { + public Server server = null; + private final CozeGPTConfig config; + + public BotResponseManage BotResponseManage = new BotResponseManage(); + private final BotGenerateStatusManage BotGenerateStatusManage = new BotGenerateStatusManage(); + private final LockManage LockManage = new LockManage(); + private MessageListener MessageListener; + public ConversationData conversations = new ConversationData(); + public DiscordApi discord_api; + + + public CozeGPT(CozeGPTConfig config) { + this.config = config; + } + public CozeGPT(CozeGPTConfig config,boolean autoLogin) throws Exception { + this.config = config; + if (autoLogin) { + this.Login(); + } + } + /** + * Discord登录 + */ + public void Login() { + if (discord_api != null) { + discord_api.disconnect(); + } + discord_api = new DiscordApiBuilder() + .setToken(config.Discord_Bot_Token) + .addIntents(Intent.MESSAGE_CONTENT) + .setProxy(config.Proxy) + .login() + .join(); + this.MessageListener = new MessageListener(this.BotResponseManage ,this.BotGenerateStatusManage ,this.config); + discord_api.addListener(this.MessageListener); + } + /** + * Discord登出 + * + * @throws Exception 如果Bot未登录,可能会抛出 BotNotLoginException 异常 + */ + public void Logout() throws Exception { + if (discord_api == null) { + throw new BotNotLoginException(); + } + discord_api.disconnect(); + discord_api = null; + } + /** + * 获取标记 + */ + public String getMark() { + return config.Mark; + } + /** + * 设置标记 + */ + public void setMark(String mark) { + config.Mark = mark; + } + /** + * 发送一条消息到对话列表并等待Bot回复 + * + * @param Prompts 提示词,即用户发送的消息,可以填入url上传附件. + * @param ConversationID 对话ID,可通过 CreateConversation(String name); 获取,必须提供,用于储存上下文信息. + * @param Files 上传本地文件,可为多个. + * @param event 填入以支持消息流返回,返回 true 继续生成,返回 false 停止生成. + * @return 生成的消息信息. + * @throws Exception 如果消息生成过程遇到任何问题,则抛出异常. + */ + public GenerateMessage Chat(String Prompts, String ConversationID, List Files, ChatStreamEvent event) throws Exception { + if (Objects.equals(Prompts, "")) { + throw new InvalidPromptException(); + } + // 取服务器对象 + private_getServer(); + // 取对应子频道 + Optional channel = server.getChannelById(conversations.get(ConversationID)); + if (channel.isEmpty()) { + throw new InvalidConversationException(ConversationID); + } + TextChannel textChannel = (TextChannel) channel.get(); + // 锁定 避免同频道多次对话 + LockManage.getLock(textChannel.getIdAsString()).lock(); + // 初始化回复记录 + this.BotGenerateStatusManage.clearGenerateStatus(textChannel.getIdAsString()); + this.BotResponseManage.clearMsg(textChannel.getIdAsString()); + // 开始 + try { + CompletableFuture send = null; + if (Prompts.length() > 2000) { // 长文本发送消息 + if (!config.Disable_2000Limit_Unlock) { + throw new PromptTooLongException(Prompts,2000); + } + // 仅提及(@)唤醒机器人 + send = textChannel.sendMessage( + "<@" + config.CozeBot_id + ">" + ); + send = send.thenCompose(message -> textChannel.sendMessage( new ByteArrayInputStream(Prompts.getBytes()),"Prompt.txt")); + } else { + send = textChannel.sendMessage( // 默认发送消息 + "<@" + config.CozeBot_id + ">" + Prompts + ); + } + // 发送附件(图片)处理 + if (Files != null) { + for (GPTFile file : Files) { + send = send.thenCompose(message -> textChannel.sendMessage(file.GetByteArrayInputStream(),file.GetFileName())); + } + } + // 发送消息 + send.join(); + // 在此之下为bot回复消息处理阶段 -> 5s 超时 + boolean BotStartGenerate = false; + int attempt = 0; // 重试次数 + int maxRetries = 25; // 最大尝试次数 + while (!BotStartGenerate) { + attempt++; + if (attempt > maxRetries) { + throw new RecvMsgException("超时无回应:未开始生成"); + } + BotStartGenerate = this.BotGenerateStatusManage.getGenerateStatus(textChannel.getIdAsString()); + // 等待200ms + try { Thread.sleep(200); } catch (InterruptedException ignored) {} + } + BotResponseType Response = new BotResponseType(); + String LatestMessage = ""; + attempt = 0; // 重置重试次数 + maxRetries = 120; // 最大尝试次数 + // 超时 2 分钟 + while (!Response.IsCompleted(config.generate_timeout)) { + attempt++; + if (attempt > maxRetries) { + throw new RecvMsgException("超时无回应:超过设定时间"); + } + try { + Response = this.BotResponseManage.getMsg(textChannel.getIdAsString()); + } catch (NullPointerException ignored) {} + if (!event.handle(Response.prompt, Response.prompt.replace(LatestMessage,""))) { + throw new StopGenerateException(); + } + LatestMessage = Response.prompt; + try { Thread.sleep(500); } catch (InterruptedException ignored) {} + } + GenerateMessage return_info = new GenerateMessage(); + return_info.Message = Response.prompt; + return_info.Files = Response.files; + LockManage.getLock(textChannel.getIdAsString()).unlock(); + return return_info; + } catch (Exception e) { + LockManage.getLock(textChannel.getIdAsString()).unlock(); + throw e; + } + } + /** + * 发送一条消息到对话列表并等待Bot回复 + * + * @param Prompts 提示词,即用户发送的消息,可以填入url上传附件. + * @param ConversationID 对话ID,可通过 CreateConversation(String name); 获取,必须提供,用于储存上下文信息. + * @param Files 上传本地文件,可为多个. + * @return 生成的消息信息. + * @throws Exception 如果消息生成过程遇到任何问题,则抛出异常. + */ + public GenerateMessage Chat(String Prompts, String ConversationID, List Files) throws Exception { + return Chat(Prompts ,ConversationID ,Files ,(ALLGenerateMessages, NewGenerateMessage) -> { return true; }); + } + /** + * 发送一条消息到对话列表并等待Bot回复 + * + * @param Prompts 提示词,即用户发送的消息,可以填入url上传附件. + * @param ConversationID 对话ID,可通过 CreateConversation(String name); 获取,必须提供,用于储存上下文信息. + * @param event 填入以支持消息流返回,返回 true 继续生成,返回 false 停止生成. + * @return 生成的消息信息. + * @throws Exception 如果消息生成过程遇到任何问题,则抛出异常. + */ + public GenerateMessage Chat(String Prompts, String ConversationID, ChatStreamEvent event) throws Exception { + return Chat(Prompts ,ConversationID ,null ,event); + } + /** + * 发送一条消息到对话列表并等待Bot回复 + * + * @param Prompts 提示词,即用户发送的消息,可以填入url上传附件. + * @param ConversationID 对话ID,可通过 CreateConversation(String name); 获取,必须提供,用于储存上下文信息. + * @return 生成的消息信息. + * @throws Exception 如果消息生成过程遇到任何问题,则抛出异常. + */ + public GenerateMessage Chat(String Prompts, String ConversationID) throws Exception { + return Chat(Prompts ,ConversationID ,(ALLGenerateMessages, NewGenerateMessage) -> { return true; }); + } + /** + * 创建新的对话列表 + * + * @param ConversationName 对话名词,如果没有关闭对话名词索引功能则后续可以通过对话名词调用对话 + * @return 对话ID,一段数字,后续可通过此ID调用 + * @throws Exception 如果遇到任何问题,则抛出异常. + */ + public String CreateConversation (String ConversationName) throws Exception { + private_getServer(); + // 分类处理 + Optional Category = discord_api.getChannelById(config.Discord_CreateChannel_Category); + ChannelCategory category = (ChannelCategory) Category.orElse(null); + // 已有对话名称检查 + String ChannelID = conversations.get(ConversationName); + if (server.getChannelById(ChannelID).isPresent()) { + throw new ConversationAlreadyExistsException(ConversationName); + } + ServerTextChannel channel = server.createTextChannelBuilder() + .setName(ConversationName) + .setCategory(category) + .create() + .join(); + // 写入存储 + conversations.put(channel.getName(),channel.getIdAsString()); + // 返回数据 + return channel.getIdAsString(); + } + /** + * 创建新的对话列表 + * + * @return 对话ID,一段数字,后续可通过此ID调用 + * @throws Exception 如果遇到任何问题,则抛出异常. + */ + public String CreateConversation () throws Exception { + private_getServer(); + // 分类处理 + Optional Category = discord_api.getChannelById(config.Discord_CreateChannel_Category); + ChannelCategory category = (ChannelCategory) Category.orElse(null); + // 创建 + ServerTextChannel channel = server.createTextChannelBuilder() + .setName("default") + .setCategory(category) + .create() + .join(); + // 返回数据 + return channel.getIdAsString(); + } + /** + * 删除对话列表 + * + * @param ConversationName 对话名词/对话ID + * @throws Exception 如果遇到任何问题,则抛出异常. + */ + public void DeleteConversation (String ConversationName) throws Exception { + private_getServer(); + Optional channel = server.getChannelById(conversations.get(ConversationName)); + if (channel.isEmpty()) { + throw new InvalidConversationException(ConversationName); + } + channel.get().delete().join(); + conversations.remove(ConversationName); + } + /** + * 修改某个对话的名词 + * 注:对话ID无法修改 + * + * @param OldConversationName 旧的对话名词/对话ID + * @param NewConversationName 新的对话名词 + * @throws Exception 如果遇到任何问题,则抛出异常. + */ + public String RenameConversation (String OldConversationName, String NewConversationName) throws Exception { + private_getServer(); + Optional channel = server.getChannelById(conversations.get(OldConversationName)); + if (channel.isEmpty()) { + throw new InvalidConversationException(OldConversationName); + } + String ChannelID = conversations.get(NewConversationName); + if (server.getChannelById(ChannelID).isPresent()) { + throw new ConversationAlreadyExistsException(NewConversationName); + } + channel.get().updateName(NewConversationName).join(); + conversations.remove(OldConversationName); + conversations.put(NewConversationName,ChannelID); + return channel.get().getIdAsString(); + } + /** + * 修改某个对话的名词 + * 注:对话ID无法修改 + * + * @param ConversationName 对话名词/对话ID + * @return 对话信息 + * @throws Exception 如果遇到任何问题,则抛出异常. + */ + public ConversationInfo GetConversationInfo (String ConversationName) throws Exception { + private_getServer(); + String ChannelID = conversations.get(ConversationName); + Optional Channel = server.getChannelById(ChannelID); + if (Channel.isEmpty()) { + throw new InvalidConversationException(ConversationName); + } + ConversationInfo return_info = new ConversationInfo(); + return_info.Name = Channel.get().getName(); + return_info.ID = Channel.get().getIdAsString(); + return return_info; + } + public boolean IsCozeBotOnline() throws Exception { + private_getServer(); + Optional cozeBot = server.getMemberById(config.CozeBot_id); + if (cozeBot.isEmpty()) { + throw new InvalidCozeBotUserIDException(); + } + return cozeBot.get().getStatus() != UserStatus.OFFLINE; + } + /** + * 取出最后一次发送消息时间 + * @return 返回 Instant 类型 如果没有记录 返回类创建时间 + */ + public Instant getLatestSendMsgInstant () { + return this.MessageListener.getLatestSendMsgInstant(); + } + /** + * 取出最后一次接收Coze Bot消息时间 + * @return 返回 Instant 类型 如果没有记录 返回类创建时间 + */ + public Instant getLatestReceiveCozeMsgInstant () { + return this.MessageListener.getLatestReceiveCozeMsgInstant(); + } + + public void private_getServer() throws Exception { + if (discord_api == null) { + throw new BotNotLoginException(); + } + if (server == null) { + Optional optionalServer = discord_api.getServerById(config.Server_id); + if (optionalServer.isEmpty()) { + throw new InvalidDiscordServerException(); + } + server = optionalServer.get(); + } + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/CozeGPTConfig.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/CozeGPTConfig.java new file mode 100644 index 0000000..c0be1a6 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/CozeGPTConfig.java @@ -0,0 +1,41 @@ +package catx.feitu.CozeProxy; + +public class CozeGPTConfig { + /** + * #Discord bot token 获取方法 + * 浏览器打开 Discord开发者平台 + * 创建Application > 点击Bot > 点击 Reset Token 然后复制过来即可 + * 注意 还需要打开Privileged Gateway Intents下面的选项 (MESSAGE CONTENT INTENT一定要开) + */ + public String Discord_Bot_Token = ""; + /** + * 连接到Discord服务器时所使用的代理 + * 对于某些无法直连Discord的地区很有帮助 + * 设置为 null 则不适用 + */ + public java.net.Proxy Proxy = null; + /** + * Discord服务器ID + * 打开Discord开发者模式后直接右键右侧服务器列表中Bot所在且有管理权限的服务器即可复制ID + */ + public String Server_id = ""; + /** + * Discord中被Coze托管的机器人用户ID + * 打开Discord开发者模式后在服务器内直接右键机器人即可复制ID + */ + public String CozeBot_id = ""; + + public String Discord_CreateChannel_Category = ""; + + public long generate_timeout = 10000; + + public boolean Disable_CozeBot_ReplyMsgCheck = false; + public boolean Disable_Name_Cache = false; + public boolean Disable_2000Limit_Unlock = false; + + /** + * 额外标记 可填写任意内容 + */ + public String Mark = ""; + +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/BotNotLoginException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/BotNotLoginException.java new file mode 100644 index 0000000..4a2e79c --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/BotNotLoginException.java @@ -0,0 +1,7 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class BotNotLoginException extends Exception { + public BotNotLoginException() { + super(); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/ConversationAlreadyExistsException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/ConversationAlreadyExistsException.java new file mode 100644 index 0000000..1b6e314 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/ConversationAlreadyExistsException.java @@ -0,0 +1,12 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class ConversationAlreadyExistsException extends Exception { + private String Prv_Conversation = ""; + public ConversationAlreadyExistsException(String Conversation) { + super(Conversation); + Prv_Conversation = Conversation; + } + public String Get_Conversation() { + return Prv_Conversation; + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidConfigException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidConfigException.java new file mode 100644 index 0000000..3b91782 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidConfigException.java @@ -0,0 +1,21 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class InvalidConfigException extends Exception { + private String Prv_Config_name = ""; + private String Prv_message = ""; + public InvalidConfigException(String Config_name,String message) { + super(Config_name + ":" + message); + Prv_Config_name = Config_name; + Prv_message = message; + } + public InvalidConfigException(String Config_name) { + super(Config_name); + Prv_Config_name = Config_name; + } + public String Get_Invalid_ConfigName() { + return Prv_Config_name; + } + public String Get_message() { + return Prv_message; + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidConversationException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidConversationException.java new file mode 100644 index 0000000..3018080 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidConversationException.java @@ -0,0 +1,12 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class InvalidConversationException extends Exception { + private String Prv_Conversation = ""; + public InvalidConversationException(String Conversation) { + super(Conversation); + Prv_Conversation = Conversation; + } + public String Get_Conversation() { + return Prv_Conversation; + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidCozeBotUserIDException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidCozeBotUserIDException.java new file mode 100644 index 0000000..02130e7 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidCozeBotUserIDException.java @@ -0,0 +1,7 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class InvalidCozeBotUserIDException extends Exception { + public InvalidCozeBotUserIDException() { + super(); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidDiscordServerException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidDiscordServerException.java new file mode 100644 index 0000000..dce3b1e --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidDiscordServerException.java @@ -0,0 +1,7 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class InvalidDiscordServerException extends Exception { + public InvalidDiscordServerException() { + super(); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidPromptException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidPromptException.java new file mode 100644 index 0000000..3ea0d6a --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/InvalidPromptException.java @@ -0,0 +1,7 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class InvalidPromptException extends Exception { + public InvalidPromptException() { + super(); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/PromptTooLongException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/PromptTooLongException.java new file mode 100644 index 0000000..7a23c8b --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/PromptTooLongException.java @@ -0,0 +1,20 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class PromptTooLongException extends Exception { + private String Prv_Prompt = ""; + private long Prv_Limit = 0; + public PromptTooLongException(String Prompt,long Limit) { + super(); + Prv_Prompt = Prompt; + Prv_Limit = Limit; + } + public long GetPromptLength () { + return Prv_Prompt.length(); + } + public long GetLimitLength () { + return Prv_Limit; + } + public String GetPrompt () { + return Prv_Prompt; + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/RecvMsgException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/RecvMsgException.java new file mode 100644 index 0000000..825e0c3 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/RecvMsgException.java @@ -0,0 +1,13 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class RecvMsgException extends Exception { + public RecvMsgException() { + super(); + } + public RecvMsgException(String Message) { + super(Message); + } + public RecvMsgException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/SendMessageException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/SendMessageException.java new file mode 100644 index 0000000..efdb4a5 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/SendMessageException.java @@ -0,0 +1,13 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class SendMessageException extends Exception { + public SendMessageException() { + super(); + } + public SendMessageException(String Message) { + super(Message); + } + public SendMessageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/StopGenerateException.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/StopGenerateException.java new file mode 100644 index 0000000..c02cb95 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Exceptions/StopGenerateException.java @@ -0,0 +1,7 @@ +package catx.feitu.CozeProxy.Exceptions; + +public class StopGenerateException extends Exception { + public StopGenerateException() { + super(); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/FunctionalInterface/ChatStreamEvent.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/FunctionalInterface/ChatStreamEvent.java new file mode 100644 index 0000000..6a10fd5 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/FunctionalInterface/ChatStreamEvent.java @@ -0,0 +1,6 @@ +package catx.feitu.CozeProxy.FunctionalInterface; + +@FunctionalInterface +public interface ChatStreamEvent { + boolean handle(String ALLGenerateMessages, String NewGenerateMessage); +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Listen/MessageListener.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Listen/MessageListener.java new file mode 100644 index 0000000..4142c3d --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Listen/MessageListener.java @@ -0,0 +1,97 @@ +package catx.feitu.CozeProxy.Listen; + +import catx.feitu.CozeProxy.CozeGPTConfig; +import catx.feitu.CozeProxy.MessageManage.BotGenerateStatusManage; +import catx.feitu.CozeProxy.MessageManage.BotResponseManage; +import catx.feitu.CozeProxy.MessageManage.BotResponseType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.javacord.api.entity.message.embed.Embed; +import org.javacord.api.event.message.MessageCreateEvent; +import org.javacord.api.event.message.MessageEditEvent; +import org.javacord.api.event.user.UserStartTypingEvent; +import org.javacord.api.listener.message.MessageCreateListener; +import org.javacord.api.listener.message.MessageEditListener; +import org.javacord.api.listener.user.UserStartTypingListener; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class MessageListener implements MessageCreateListener, MessageEditListener, UserStartTypingListener { + private static final Logger logger = LogManager.getLogger(MessageListener.class); + private final BotResponseManage botResponseManage; + private final BotGenerateStatusManage botGenerateStatusManage; + private final CozeGPTConfig config; + /** + * 最后一次发送消息时间 + */ + private Instant latestSendMessage = Instant.now(); + /** + * 最后一次接收消息时间 + */ + private Instant latestReceiveCozeMessage = Instant.now(); + public MessageListener(BotResponseManage botResponseManage, BotGenerateStatusManage botGenerateStatusManage, CozeGPTConfig config) { + this.botResponseManage = botResponseManage; + this.botGenerateStatusManage = botGenerateStatusManage; + this.config = config; + } + + @Override + public void onUserStartTyping(UserStartTypingEvent event) { + if(Objects.equals(event.getUserIdAsString(), config.CozeBot_id)) { + this.botGenerateStatusManage.saveGenerateStatus(event.getChannel().getIdAsString()); + logger.info("[CozeBot Start Generate] " + event.getChannel().getIdAsString()); + } + } + @Override + public void onMessageCreate(MessageCreateEvent event) { + latestSendMessage = Instant.now(); + if (Objects.equals(event.getMessageAuthor().getIdAsString(), event.getApi().getYourself().getIdAsString())) { + logger.info("[Send] " + event.getChannel().getIdAsString() + ": " + event.getMessageContent()); + } + } + + @Override + public void onMessageEdit(MessageEditEvent event) { + if (Objects.equals(event.getMessageAuthor().getIdAsString(), config.CozeBot_id)) { + latestReceiveCozeMessage = Instant.now(); + if (config.Disable_CozeBot_ReplyMsgCheck || event.getMessage().getMentionedUsers().contains(event.getApi().getYourself())) { + boolean Done100 = !event.getMessage().getComponents().isEmpty(); //存在按钮 = 100%响应完毕 + if (Done100) { + logger.info("[CozeBot] " + event.getChannel().getIdAsString() + ":" + event.getMessageContent()); + } + List embeds = event.getMessage().getEmbeds(); // 获取消息中的所有嵌入内容 + List files = new ArrayList<>(); // 存储嵌入图片的URL + for (Embed embed : embeds) { + if (embed.getImage().isPresent()) { + if (Done100) { + logger.info("[CozeBot] 图片URL -> " + embed.getImage().get().getUrl().toString()); + } + files.add(embed.getImage().get().getUrl().toString()); + } + } + BotResponseType Response = new BotResponseType(); + Response.prompt = event.getMessageContent(); + Response.files = files; + Response.SetCompleted(Done100); + this.botResponseManage.saveMsg(event.getChannel().getIdAsString(),Response); + } + } + } + /** + * 取出最后一次发送消息时间 + * @return 返回 Instant 类型 + */ + public Instant getLatestSendMsgInstant () { + return latestSendMessage; + } + /** + * 取出最后一次接收Coze Bot消息时间 + * @return 返回 Instant 类型 + */ + public Instant getLatestReceiveCozeMsgInstant () { + return latestReceiveCozeMessage; + } +} \ No newline at end of file diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/LockManage/LockManage.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/LockManage/LockManage.java new file mode 100644 index 0000000..9949727 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/LockManage/LockManage.java @@ -0,0 +1,12 @@ +package catx.feitu.CozeProxy.LockManage; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class LockManage { + private final ConcurrentHashMap lockMap = new ConcurrentHashMap<>(); + public Lock getLock(String key) { + return lockMap.computeIfAbsent(key, k -> new ReentrantLock()); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotGenerateStatusManage.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotGenerateStatusManage.java new file mode 100644 index 0000000..67c69a4 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotGenerateStatusManage.java @@ -0,0 +1,19 @@ +package catx.feitu.CozeProxy.MessageManage; + +import java.util.concurrent.ConcurrentHashMap; + +public class BotGenerateStatusManage { + private final ConcurrentHashMap startGenerates = new ConcurrentHashMap<>(); + + public void saveGenerateStatus(String channelID) { + startGenerates.put(channelID, true); + } + + public void clearGenerateStatus(String channelID) { + startGenerates.remove(channelID); + } + + public boolean getGenerateStatus(String channelID) { + return startGenerates.getOrDefault(channelID, false); + } +} \ No newline at end of file diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotResponseManage.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotResponseManage.java new file mode 100644 index 0000000..6239e52 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotResponseManage.java @@ -0,0 +1,20 @@ +package catx.feitu.CozeProxy.MessageManage; + +import java.util.concurrent.ConcurrentHashMap; + +public class BotResponseManage { + private final ConcurrentHashMap ResponseMap = new ConcurrentHashMap<>(); + public void saveMsg(String channelID, BotResponseType response) { + ResponseMap.put(channelID, response); + } + public BotResponseType getMsg(String channelID) throws NullPointerException { + if (!ResponseMap.containsKey(channelID)) { + throw new NullPointerException(); + } + return ResponseMap.get(channelID); + } + public void clearMsg(String channelID) { + ResponseMap.remove(channelID); + } + +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotResponseType.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotResponseType.java new file mode 100644 index 0000000..517d445 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/MessageManage/BotResponseType.java @@ -0,0 +1,18 @@ +package catx.feitu.CozeProxy.MessageManage; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public class BotResponseType { + public String prompt = ""; + public List files =new ArrayList<>(); + public long timestamp = -1; + public boolean IsCompleted (long timeout) { + return (this.timestamp == 0 || + Instant.now().toEpochMilli() - this.timestamp > timeout) && this.timestamp != -1; + } + public void SetCompleted (boolean completed) { + this.timestamp = completed ? 0 : Instant.now().toEpochMilli(); + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Listener/DiscordListener.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Listener/DiscordListener.java new file mode 100644 index 0000000..94ee036 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Listener/DiscordListener.java @@ -0,0 +1,61 @@ +package catx.feitu.CozeProxy.Protocol.Listener; + +import catx.feitu.CozeProxy.Protocol.UniversalEventListen; +import catx.feitu.CozeProxy.Protocol.UniversalMessage; +import org.javacord.api.entity.message.embed.Embed; +import org.javacord.api.event.message.MessageCreateEvent; +import org.javacord.api.event.message.MessageEditEvent; +import org.javacord.api.event.user.UserStartTypingEvent; +import org.javacord.api.listener.message.MessageCreateListener; +import org.javacord.api.listener.message.MessageEditListener; +import org.javacord.api.listener.user.UserStartTypingListener; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class DiscordListener implements MessageCreateListener, MessageEditListener, UserStartTypingListener{ + public UniversalEventListen handle; + public DiscordListener(UniversalEventListen handle) { + this.handle = handle; + } + + @Override + public void onUserStartTyping(UserStartTypingEvent event) { + handle.onUserStartTyping(null ,event.getChannel().getIdAsString(), event.getUserIdAsString()); + } + @Override + public void onMessageCreate(MessageCreateEvent event) { + List embeds = event.getMessage().getEmbeds(); // 获取消息中的所有嵌入内容 + List files = new CopyOnWriteArrayList<>(); // 存储嵌入附件URL + for (Embed embed : embeds) { + if (embed.getImage().isPresent()) { + files.add(embed.getImage().get().getUrl().toString()); + } + } + handle.onMessageCreate(null ,event.getChannel().getIdAsString(), + event.getMessage().getAuthor().getIdAsString(), + new UniversalMessage() + .setContent(event.getMessageContent()) + .setFiles(files) + .setHasButton(!event.getMessage().getComponents().isEmpty()) + ); + } + + @Override + public void onMessageEdit(MessageEditEvent event) { + List embeds = event.getMessage().getEmbeds(); // 获取消息中的所有嵌入内容 + List files = new CopyOnWriteArrayList<>(); // 存储嵌入附件URL + for (Embed embed : embeds) { + if (embed.getImage().isPresent()) { + files.add(embed.getImage().get().getUrl().toString()); + } + } + handle.onMessageEdit(null ,event.getChannel().getIdAsString(), + event.getMessage().getAuthor().getIdAsString(), + new UniversalMessage() + .setContent(event.getMessageContent()) + .setFiles(files) + .setHasButton(!event.getMessage().getComponents().isEmpty()) + ); + } +} \ No newline at end of file diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Listener/SlackListener.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Listener/SlackListener.java new file mode 100644 index 0000000..362e0e4 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Listener/SlackListener.java @@ -0,0 +1,40 @@ +package catx.feitu.CozeProxy.Protocol.Listener; + +import catx.feitu.CozeProxy.Protocol.UniversalEventListen; +import catx.feitu.CozeProxy.Protocol.UniversalMessage; +import com.slack.api.app_backend.events.payload.EventsApiPayload; +import com.slack.api.bolt.handler.BoltEventHandler; +import com.slack.api.bolt.response.Response; +import com.slack.api.bolt.context.builtin.EventContext; +import com.slack.api.methods.SlackApiException; +import com.slack.api.model.event.MessageEvent; + + +import java.io.IOException; + +public class SlackListener implements BoltEventHandler { + public UniversalEventListen handle; + public SlackListener(UniversalEventListen handle) { + this.handle = handle; + } + + @Override + public Response apply(EventsApiPayload event, EventContext context) throws IOException, SlackApiException { + /* + List attachments = event.getEvent().getAttachments(); + if (attachments != null && !attachments.isEmpty()) { + for (Attachment attachment : attachments) { + // 打印每个附件的信息, 示例中仅打印附件的回退文本 + System.out.println(attachment.get()); + } + } + */ + handle.onMessageCreate(null ,event.getEvent().getChannel(), + event.getEvent().getUser(), + new UniversalMessage() + .setContent(event.getEvent().getText()) + .setHasButton(true) + ); + return context.ack(); + } +} \ No newline at end of file diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Protocol.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Protocol.java new file mode 100644 index 0000000..2efca1a --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Protocol.java @@ -0,0 +1,43 @@ +package catx.feitu.CozeProxy.Protocol; + +import catx.feitu.CozeProxy.Protocol.Listener.DiscordListener; +import catx.feitu.CozeProxy.Protocol.Listener.SlackListener; +import com.slack.api.bolt.App; +import com.slack.api.bolt.AppConfig; +import com.slack.api.bolt.socket_mode.SocketModeApp; +import com.slack.api.model.event.MessageEvent; +import org.javacord.api.DiscordApi; +import org.javacord.api.DiscordApiBuilder; +import org.javacord.api.entity.intent.Intent; + +import java.io.IOException; +import java.net.Proxy; + +public class Protocol { + public String apiSelected; + public DiscordApi api_discord; + public App api_slack; + public UniversalEventListen eventListen; + public Protocol (String protocol , String token) { + + } + public void login (String protocol , String token , Proxy proxy) throws Exception { + switch (protocol){ + case "discord": + api_discord.addListener(new DiscordListener(eventListen)); + api_discord = new DiscordApiBuilder() + .setToken(token) + .addIntents(Intent.MESSAGE_CONTENT) + .setProxy(proxy) + .login() + .join(); + case "slack": + AppConfig config = new AppConfig(); + config.setSingleTeamBotToken(token); + api_slack = new App(config); + api_slack.event(MessageEvent.class, new SlackListener(eventListen)); + new SocketModeApp(api_slack).start(); + + } + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Protocols.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Protocols.java new file mode 100644 index 0000000..6acf425 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/Protocols.java @@ -0,0 +1,6 @@ +package catx.feitu.CozeProxy.Protocol; + +public class Protocols { + public static String DISCORD = "discord"; + public static String SLACK = "slack"; +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/UniversalEventListen.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/UniversalEventListen.java new file mode 100644 index 0000000..07f19d7 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/UniversalEventListen.java @@ -0,0 +1,9 @@ +package catx.feitu.CozeProxy.Protocol; + +public interface UniversalEventListen { + + void onUserStartTyping(String serverID ,String channelID ,String userID); + void onMessageCreate(String serverID ,String channelID ,String userID ,UniversalMessage message); + void onMessageEdit(String serverID ,String channelID ,String userID ,UniversalMessage message); + +} \ No newline at end of file diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/UniversalMessage.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/UniversalMessage.java new file mode 100644 index 0000000..7a8f9e9 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Protocol/UniversalMessage.java @@ -0,0 +1,26 @@ +package catx.feitu.CozeProxy.Protocol; + +import java.util.List; + +public class UniversalMessage { + public String content; + public List files; + public boolean hasButton; + public UniversalMessage () { } + public UniversalMessage setContent (String content) { + UniversalMessage build = this; + build.content = content; + return build; + } + public UniversalMessage setFiles (List content) { + UniversalMessage build = this; + build.files = content; + return build; + } + public UniversalMessage setHasButton (boolean hasButton) { + UniversalMessage build = this; + build.hasButton = hasButton; + return build; + } + +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/ConversationInfo.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/ConversationInfo.java new file mode 100644 index 0000000..080b051 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/ConversationInfo.java @@ -0,0 +1,8 @@ +package catx.feitu.CozeProxy.Types; + +public class ConversationInfo { + + public String Name = ""; + + public String ID = ""; +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/GPTFile.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/GPTFile.java new file mode 100644 index 0000000..b4e14f8 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/GPTFile.java @@ -0,0 +1,28 @@ +package catx.feitu.CozeProxy.Types; + +import java.io.*; +import java.util.Base64; + +public class GPTFile { + private final ByteArrayInputStream file; + private final String name; + public GPTFile(String base64, String fileName) { + file = new ByteArrayInputStream(Base64.getDecoder().decode(base64)); + name = fileName; + } + public GPTFile(byte[] bytes, String fileName) { + file = new ByteArrayInputStream(bytes); + name = fileName; + } + public GPTFile(ByteArrayInputStream fileInputStream, String fileName) { + + file = fileInputStream; + name = fileName; + } + public ByteArrayInputStream GetByteArrayInputStream() { + return file; + } + public String GetFileName() { + return name; + } +} diff --git a/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/GenerateMessage.java b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/GenerateMessage.java new file mode 100644 index 0000000..7462304 --- /dev/null +++ b/CozeProxy/src/main/java/catx.feitu.CozeProxy/Types/GenerateMessage.java @@ -0,0 +1,12 @@ +package catx.feitu.CozeProxy.Types; + +import java.util.ArrayList; +import java.util.List; + +public class GenerateMessage { + + public String Message = ""; + + public List Files = new ArrayList<>(); + +}