# ChatGLM-Java-SDK **Repository Path**: BlueVale/ChatGLM-Java-SDK ## Basic Information - **Project Name**: ChatGLM-Java-SDK - **Description**: 更易用的 Java 的 ChatGLM SDK,更便捷的智谱开放 API - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 5 - **Forks**: 1 - **Created**: 2023-10-25 - **Last Updated**: 2024-11-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 智谱 AI 大模型自定义 ChatGLM4-Java-SDK - [English Doc](https://github.com/AstralQuanta/ChatGLM-Java-SDK/blob/main/README.md) > > 此项目是由 **Java** 的 **JDK11** 的长期版本开发 ---- ## :triangular_flag_on_post: 当前 **ChatGLM Java SDK** 最新版本为 0.1.1-Beta。 支持版本: JDK 11+ **Java Maven Dependency (BlueChatGLM)调用** ``` top.pulselink bluechatglm 0.1.1-Beta ``` **Java Gradle (BlueChatGLM)调用** ``` implementation group: 'top.pulselink', name: 'bluechatglm', version: '0.1.1-Beta' ``` **Java sbt (BlueChatGLM)调用** ``` libraryDependencies += "top.pulselink" % "bluechatglm" % "0.1.1-Beta" ``` ## 1. Utils 工具 ### 1.1 NTP 网络时间服务器 它通过互联网或局域网上的时间服务器来提供高精度,高安全的时间信息,确保所有设备都使用相同的时间是关键的。这里的应用是对于 `JWT` 验证使用 ``` //获取网络时间协议服务器(NTP Server) private long getNTPTime() throws IOException { int port = 123; InetAddress address = InetAddress.getByName("ntp.aliyun.com"); try (DatagramSocket socket = new DatagramSocket()) { byte[] buf = new byte[48]; buf[0] = 0x1B; DatagramPacket requestPacket = new DatagramPacket(buf, buf.length, address, port); socket.send(requestPacket); DatagramPacket responsePacket = new DatagramPacket(new byte[48], 48); socket.receive(responsePacket); byte[] rawData = responsePacket.getData(); long secondsSince1900 = 0; for (int i = 40; i <= 43; i++) { secondsSince1900 = (secondsSince1900 << 8) | (rawData[i] & 0xff); } return (secondsSince1900 - 2208988800L) * 1000; } } ``` ### 1.2 保存 API 密钥 保存 **API** 密钥并将其存储在调用 `chatglm_api_key` txt 文件的本地文件中: ``` private static String loadApiKey() { //加载 API 密钥 try (BufferedReader reader = new BufferedReader(new FileReader(API_KEY_FILE))) { return reader.readLine(); } catch (IOException e) { return null; // If the file doesn't exist or an error occurs, return null } } private static void saveApiKey(String apiKey) { //保存 API 密钥 try (BufferedWriter writer = new BufferedWriter(new FileWriter(API_KEY_FILE))) { writer.write(apiKey); } catch (IOException e) { e.printStackTrace(); // Handle the exception according to your needs } } ``` ### 1.3 保存聊天内容文件 用户聊天和 **ChatGLM** 回复将保存在`chatglm_history.txt`中。 ``` private void createHistoryFileIfNotExists() { //创建聊天内容文件 Path filePath = Paths.get(historyFilePath); if (!Files.exists(filePath)) { //如果文件不存在 try { Files.createFile(filePath); } catch (IOException e) { e.printStackTrace(); } } } ``` 聊天内容**txt**文件将在每个会话结束时删除,强行退出并不会删除文件。 ``` private void registerShutdownHook() { //关闭程序的时候删除历史聊天记录 Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { Files.deleteIfExists(Paths.get(historyFilePath)); //删除历史文件 } catch (IOException e) { e.printStackTrace(); } })); } ``` ---- ## 2. 易于使用的 SDK ### 2.1 调用并使用 Maven 库 > > 相对于很多人来说,使用这个 **SDK** 的难度较低🤩。以下的三个示例是使用 **Scanner** 输入你的问题,控制台将输出 **ChatGLM** 回答: 调用**SSE请求**,示例代码如下 (目前已解决无法输入中文等问题,可以正常使用): ``` public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String apiKeyss = loadApiKey(); //加载 API 密钥 if (apiKeyss == null) { //如果不存在文件或者密钥为空,则需要输入密钥 System.out.println("Enter your API key:"); apiKeyss = scanner.nextLine(); saveApiKey(apiKeyss); } while (scanner.hasNext()) { String userInput = scanner.nextLine(); ChatClient chats = new ChatClient(apiKeyss); //初始 ChatClient (实例化) chats.registerShutdownHook(); //删除聊天的历史文件 chats.SSEInvoke(userInput); //将你输入的问题赋值给流式请求的 System.out.print(chats.getResponseMessage()); //打印出 ChatGLM 的回答内容 System.out.println(); } } ``` 调用**异步请求**,示例代码如下: ``` public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String apiKeyss = loadApiKey(); //加载 API 密钥 if (apiKeyss == null) { //如果不存在文件或者密钥为空,则需要输入密钥 System.out.println("Enter your API key:"); apiKeyss = scanner.nextLine(); saveApiKey(apiKeyss); } while (scanner.hasNext()) { String userInput = scanner.nextLine(); ChatClient chats = new ChatClient(apiKeyss); //初始 ChatClient (实例化) chats.registerShutdownHook(); //删除聊天的历史文件 chats.AsyncInvoke(userInput); //将你输入的问题赋值给异步请求的 System.out.print(chats.getResponseMessage()); //打印出 ChatGLM 的回答内容 System.out.println(); } } ``` 调用**同步请求**,示例代码如下: ``` public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String apiKeyss = loadApiKey(); //加载 API 密钥 if (apiKeyss == null) { //如果不存在文件或者密钥为空,则需要输入密钥 System.out.println("Enter your API key:"); apiKeyss = scanner.nextLine(); saveApiKey(apiKeyss); } while (scanner.hasNext()) { String userInput = scanner.nextLine(); ChatClient chats = new ChatClient(apiKeyss); //初始 ChatClient (实例化) chats.registerShutdownHook(); //删除聊天的历史文件 chats.SyncInvoke(userInput); //将你输入的问题赋值给同步请求的 System.out.print(chats.getResponseMessage()); //打印出 ChatGLM 的回答内容 System.out.println(); } } ``` ### 2.2 资深开发者👨🏼‍💻 **对于资深开发者,我们会后续跟进开发工作,目前的版本是 **ChatGLM-4** 的语言模型版本,并且已经解决了SSE中文输入看不懂的问题,当然我们也希望其他的 开发商为本项目提供技术支持! 先感谢您!** ---- ## 3.项目介绍 ### **CustomJWT** 是对于这个项目的自定制而写的,后期会继续开发更新,拓展这个项目 根据 **JWT.io** 这个网站进行了解以及原理的学习,对于这个项目的**JWT** 验证,**Java**实现起来还是较容易实现的,其中使用的部分是 `Base64Url` 而不是常规的 `Base64` **编码 Base64Url** 使用的编辑如下: ``` private String encodeBase64Url(byte[] data) { String base64url = Base64.getUrlEncoder().withoutPadding().encodeToString(data); //将输入的内容转换成 Base64Url return base64url; //返回 base64url } ``` ---- 创建 **JWT**,实现 **Header** 验证: ``` protected String createJWT() { String encodedHeader = encodeBase64Url(header.getBytes()); String encodedPayload = encodeBase64Url(payload.getBytes()); String toSign = encodedHeader + "." + encodedPayload; byte[] signatureBytes = generateSignature(toSign, secret, algorithm); String calculatedSignature = encodeBase64Url(signatureBytes); return toSign + "." + calculatedSignature; } ``` ---- 验证 **JWT** 签名部分是否与输出的结果一致: ``` protected boolean verifyJWT(String jwt) { jwt = jwt.trim(); String[] parts = jwt.split("\\."); if (parts.length != 3) { return false; } String encodedHeader = parts[0]; String encodedPayload = parts[1]; String signature = parts[2]; String toVerify = encodedHeader + "." + encodedPayload; byte[] calculatedSignatureBytes = generateSignature(toVerify, secret, algorithm); String calculatedSignature = encodeBase64Url(calculatedSignatureBytes); return calculatedSignature.equals(signature); } ``` ### 请求调用🌐 在**同步请求**和**SSE请求**中使用的请求方式如下: ``` HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("Accept", "application/json") .header("Content-Type", "application/json;charset=UTF-8") .header("Authorization", "Bearer " + token) .POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody)) .build(); ``` 使用 **POST** 方法制作 **jsonRequestBody** ,如下文所示(同步方法的 `Stream 为 false`): ``` String jsonRequestBody = String.format("{\"model\":\"%s\", \"messages\":[{\"role\":\"%s\",\"content\":\"%s\"},{\"role\":\"%s\",\"content\":\"%s\"}], \"stream\":false,\"temperture\":%f,\"top_p\":%f}", Language_Model, system_role, system_content, user_role, message, temp_float, top_p_float); ``` #### SSE 流式传输模型(可以正常使用!完美支持) 这里我们将使用 **concurrent.Flow** 方法来解决**SSE**流处理的问题: ``` public class SSESubscriber implements Flow.Subscriber { private Flow.Subscription subscription; @Override public void onSubscribe(Flow.Subscription subscription) { this.subscription = subscription; subscription.request(1); } @Override public void onNext(String item) { if (item.startsWith("data: ")) { String jsonData = item.substring("data: ".length()); //System.out.println("Received SSE item: " + jsonData); //Debug if (!jsonData.equals("[DONE]")) { responseDataBody(jsonData.replaceAll("Invalid JSON format: \\[\"DONE\"\\]", "")); } } subscription.request(1); } @Override public void onError(Throwable throwable) { System.out.println("Error in SSESubscriber: " + throwable.getMessage()); } @Override public void onComplete() { //System.out.println("SSESubscriber completed"); } } ``` **并在此处调用并接收 chatglm-4 消息:** ``` try (JsonReader jsonReader = new JsonReader(new StringReader(responseData))) { jsonReader.setLenient(true); JsonElement jsonElement = JsonParser.parseReader(jsonReader); if (jsonElement.isJsonObject()) { JsonObject jsonResponse = jsonElement.getAsJsonObject(); if (jsonResponse.has("choices")) { JsonArray choices = jsonResponse.getAsJsonArray("choices"); if (!choices.isEmpty()) { JsonObject choice = choices.get(0).getAsJsonObject(); if (choice.has("delta")) { JsonObject delta = choice.getAsJsonObject("delta"); if (delta.has("content")) { String content = delta.get("content").getAsString(); getMessage = convertUnicodeEmojis(content); getMessage = getMessage.replaceAll("\"", "") .replaceAll("\\\\n\\\\n", "\n") .replaceAll("\\\\nn", "\n") .replaceAll("\\n", "\n") .replaceAll("\\\\", "") .replaceAll("\\\\", ""); for (char c : getMessage.toCharArray()) { charQueue.offer(c); } while (!charQueue.isEmpty()) { queueResult.append(charQueue.poll()); } } } } } } else { System.out.println("Invalid JSON format: " + jsonElement); } } catch (IOException e) { System.out.println("Error reading JSON: " + e.getMessage()); } ``` #### 异步请求传输模型(AsyncInvokeModel:推荐使用,速度快) 这里采用的是`HTTPRequest`方法,来接收消息: ``` String jsonRequestBody = String.format("{\"model\":\"%s\", \"messages\":[{\"role\":\"%s\",\"content\":\"%s\"},{\"role\":\"%s\",\"content\":\"%s\"}],\"temperture\":%f,\"top_p\":%f}", Language_Model, system_role, system_content, user_role, message, temp_float, top_p_float); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("Accept", "application/json") .header("Content-Type", "application/json;charset=UTF-8") .header("Authorization", "Bearer " + token) .POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody)) .build(); ``` 整体使用的是异步发送信息,这样的好处是可以减少线程阻塞,这里的`code`和`status`是获取错误消息。当你得到一个`request_id` 的时候,再进行查询 ``` if (response.statusCode() == 200) { //When the response value is 200, output the corresponding parameters of the interface for an asynchronous request. processResponseData(response.body()); return CompletableFuture.completedFuture(response.body()); } else { JsonObject errorResponse = JsonParser.parseString(response.body()).getAsJsonObject(); if (errorResponse.has("id") && errorResponse.has("task_status")) { int code = errorResponse.get("id").getAsInt(); String status = errorResponse.get("task_status").getAsString(); throw new RuntimeException("HTTP request failure, Your request id is: " + code + ", Status: " + status); } else { return CompletableFuture.failedFuture(new RuntimeException("HTTP request failure, Code: " + response.statusCode())); } } }); ``` 当你得到需要的**Task_id**的时候,进行**GET**请求查询(部分代码): ``` .....略 .sendAsync(HttpRequest.newBuilder() .uri(URI.create(checkUrl + ID)) .header("Accept", "application/json") .header("Content-Type", "application/json;charset=UTF-8") .header("Authorization", "Bearer " + token) .GET() .build(), HttpResponse.BodyHandlers.ofString()) .thenCompose(response -> { if (response.statusCode() == 200) { return CompletableFuture.completedFuture(response.body()); } else { return CompletableFuture.failedFuture(new RuntimeException("HTTP request failure, Code: " + response.statusCode())); } }); ``` 最后通过**JSON**的提取,提取代码示例为: ``` try { JsonObject jsonResponse = JsonParser.parseString(responseData).getAsJsonObject(); if (jsonResponse.has("choices")) { JsonArray choices = jsonResponse.getAsJsonArray("choices"); if (!choices.isEmpty()) { JsonObject choice = choices.get(0).getAsJsonObject(); if (choice.has("message")) { JsonObject message = choice.getAsJsonObject("message"); if (message.has("content")) { String content = message.get("content").getAsString(); getMessage = convertUnicodeEmojis(content); getMessage = getMessage.replaceAll("\"", "") .replaceAll("\\\\n\\\\n", "\n") .replaceAll("\\\\nn", "\n") .replaceAll("\\n", "\n") .replaceAll("\\\\", "") .replaceAll("\\\\", ""); } } } } } catch (JsonSyntaxException e) { System.out.println("Error processing task status: " + e.getMessage()); } ``` #### 同步请求传输模型(InvokeModel:推荐使用,速度较快) **同步请求**还算不错,运行的时候一般情况下都还算快,当然同步的缺点就是请求量过大可能会阻塞线程(`单线程`) 这里直接说明关于处理信息这一块,这一块就是解析 **JSON** 也没有其他的东西了,示例代码: ``` try { JsonObject jsonResponse = JsonParser.parseString(responseData).getAsJsonObject(); if (jsonResponse.has("choices")) { JsonArray choices = jsonResponse.getAsJsonArray("choices"); if (!choices.isEmpty()) { JsonObject choice = choices.get(0).getAsJsonObject(); if (choice.has("message")) { JsonObject message = choice.getAsJsonObject("message"); if (message.has("content")) { String content = message.get("content").getAsString(); getMessage = convertUnicodeEmojis(content); getMessage = getMessage.replaceAll("\"", "") .replaceAll("\\\\n\\\\n", "\n") .replaceAll("\\\\nn", "\n") .replaceAll("\\n", "\n") .replaceAll("\\\\", "") .replaceAll("\\\\", ""); } } } } } catch (JsonSyntaxException e) { System.out.println("Error processing task status: " + e.getMessage()); } ``` > 总体下来,介绍本项目三种请求方式应该还是相对简单,如果有任何问题,可以在 **Issue** 处发起讨论🥳,也希望各路大神的对这个项目的支援!再次感谢🎉! --- ## 4.结语 > > 感谢您打开我的项目,这是一个自主开发 ChatGLM Java SDK 的开发项目,为了解决官方的 SDK 问题我也在努力开发和更新这个项目,当然我个人也会继续开发这个项目,我也比较坚持开源的原则,毕竟白嫖不香嘛(doge)。最后也希望越来越多的人一起参与🚀 谢谢大家看到最后!😆👏 ---- **最后的最后感恩 gson 的 jar 包开发人员**👩‍💻👨‍💻