From 467290b9089b4ce6daa970946d363c72a6215540 Mon Sep 17 00:00:00 2001 From: xiongxiaoyang <1179705413@qq.com> Date: Sun, 16 Mar 2025 13:03:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=20Spring=20AI=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=BA=E7=A1=80=E7=9A=84=20AI=20=E5=86=99?= =?UTF-8?q?=E4=BD=9C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- novel-front/pom.xml | 6 + .../novel/controller/AuthorController.java | 52 +++++ .../java2nb/novel/core/config/AiConfig.java | 47 ++++ .../src/main/resources/application.yml | 10 +- .../templates/author/content_add.html | 220 ++++++++++++++++-- pom.xml | 7 + 6 files changed, 325 insertions(+), 17 deletions(-) create mode 100644 novel-front/src/main/java/com/java2nb/novel/core/config/AiConfig.java diff --git a/novel-front/pom.xml b/novel-front/pom.xml index 21cc448..76d7ef1 100644 --- a/novel-front/pom.xml +++ b/novel-front/pom.xml @@ -55,6 +55,12 @@ com.fasterxml.jackson.core jackson-databind + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + diff --git a/novel-front/src/main/java/com/java2nb/novel/controller/AuthorController.java b/novel-front/src/main/java/com/java2nb/novel/controller/AuthorController.java index 2b4912e..41ba628 100644 --- a/novel-front/src/main/java/com/java2nb/novel/controller/AuthorController.java +++ b/novel-front/src/main/java/com/java2nb/novel/controller/AuthorController.java @@ -12,9 +12,11 @@ import com.java2nb.novel.entity.AuthorIncomeDetail; import com.java2nb.novel.entity.Book; import com.java2nb.novel.service.AuthorService; import com.java2nb.novel.service.BookService; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.*; import java.util.Date; @@ -32,6 +34,8 @@ public class AuthorController extends BaseController{ private final BookService bookService; + private final ChatClient chatClient; + /** * 校验笔名是否存在 * */ @@ -220,6 +224,54 @@ public class AuthorController extends BaseController{ } + /** + * AI扩写 + */ + @PostMapping("ai/expand") + public RestResult expandText(@RequestParam("text") String text, @RequestParam("ratio") Double ratio) { + String prompt = "请将以下文本扩写为原长度的" + ratio/100 + "倍:" + text; + return RestResult.ok(chatClient.prompt() + .user(prompt) + .call() + .content()); + } + + /** + * AI缩写 + */ + @PostMapping("ai/condense") + public RestResult condenseText(@RequestParam("text") String text, @RequestParam("ratio") Integer ratio) { + String prompt = "请将以下文本缩写为原长度的" + 100/ratio + "分之一:" + text; + return RestResult.ok(chatClient.prompt() + .user(prompt) + .call() + .content()); + } + + /** + * AI续写 + */ + @PostMapping("ai/continue") + public RestResult continueText(@RequestParam("text") String text, @RequestParam("length") Integer length) { + String prompt = "请续写以下文本,续写长度约为" + length + "字:" + text; + return RestResult.ok(chatClient.prompt() + .user(prompt) + .call() + .content()); + } + + /** + * AI润色 + */ + @PostMapping("ai/polish") + public RestResult polishText(@RequestParam("text") String text) { + String prompt = "请润色优化以下文本,保持原意:" + text; + return RestResult.ok(chatClient.prompt() + .user(prompt) + .call() + .content()); + } + diff --git a/novel-front/src/main/java/com/java2nb/novel/core/config/AiConfig.java b/novel-front/src/main/java/com/java2nb/novel/core/config/AiConfig.java new file mode 100644 index 0000000..6cd0740 --- /dev/null +++ b/novel-front/src/main/java/com/java2nb/novel/core/config/AiConfig.java @@ -0,0 +1,47 @@ +package com.java2nb.novel.core.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +/** + * Ai 相关配置 + * + * @author xiongxiaoyang + * @date 2025/2/19 + */ +@Configuration +@Slf4j +public class AiConfig { + + /** + * 目的:配置自定义的 RestClientBuilder 对象 + *

+ * 原因:Spring AI 框架的 ChatClient 内部通过 RestClient(Spring Framework 6 和 Spring Boot 3 中引入) 发起 HTTP REST 请求与远程的大模型服务进行通信, + * 如果项目中没有配置自定义的 RestClientBuilder 对象, 那么在 RestClient 的自动配置类 org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration + * 中配置的 RestClientBuilder 对象会使用 Spring 容器中提供的 HttpMessageConverters, 由于本项目中配置了 spring.jackson.generator.write-numbers-as-strings + * = true, 所以 Spring 容器中的 HttpMessageConverters 在 RestClient 发起 HTTP REST 请求转换 Java 对象为 JSON 字符串时会自动将 Number 类型的 + * Java 对象属性转换为字符串而导致请求参数错误 + *

+ * 示例:"temperature": 0.7 =》"temperature": "0.7" + * {"code":20015,"message":"The parameter is invalid. Please check again.","data":null} + */ + @Bean + public RestClient.Builder restClientBuilder() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + // 连接超时时间 + factory.setConnectTimeout(5000); + // 读取超时时间 + factory.setReadTimeout(60000); + return RestClient.builder().requestFactory(factory); + } + + @Bean + public ChatClient chatClient(ChatClient.Builder chatClientBuilder) { + return chatClientBuilder.build(); + } + +} diff --git a/novel-front/src/main/resources/application.yml b/novel-front/src/main/resources/application.yml index 26cb4c3..13c725d 100644 --- a/novel-front/src/main/resources/application.yml +++ b/novel-front/src/main/resources/application.yml @@ -46,7 +46,15 @@ book: value: 5 - +--- #--------------------- Spring AI 配置---------------------- +spring: + ai: + openai: + api-key: sk-nnhjmxuljagcuubbovjztbhkiawqaabzziazeurppinxtgva + base-url: https://api.siliconflow.cn + chat: + options: + model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B diff --git a/novel-front/src/main/resources/templates/author/content_add.html b/novel-front/src/main/resources/templates/author/content_add.html index 183bee0..771f2fd 100644 --- a/novel-front/src/main/resources/templates/author/content_add.html +++ b/novel-front/src/main/resources/templates/author/content_add.html @@ -8,6 +8,79 @@ 作家管理系统-小说精品屋 + @@ -46,18 +119,29 @@

  • 章节名: -
  • - 章节内容:
  • -

  • +
  • + 章节内容: +
  • + - 是否收费: -
  • 免费 - 收费
  • + 是否收费: +
  • 免费 + 收费 +
  • -
  • +
  • @@ -113,9 +197,10 @@ var lock = false; + function addBookContent() { - if(lock){ + if (lock) { return; } lock = true; @@ -125,14 +210,14 @@ var indexName = $("#bookIndex").val(); - if(!indexName){ + if (!indexName) { $("#LabErr").html("章节名不能为空!"); lock = false; return; } var content = $("#bookContent").val(); - if(!content){ + if (!content) { $("#LabErr").html("章节内容不能为空!"); lock = false; return; @@ -142,17 +227,15 @@ var isVip = $("input:checked[name=isVip]").val(); - - $.ajax({ type: "POST", url: "/author/addBookContent", - data: {'bookId':bookId,'indexName':indexName,'content':content,'isVip':isVip}, + data: {'bookId': bookId, 'indexName': indexName, 'content': content, 'isVip': isVip}, dataType: "json", success: function (data) { if (data.code == 200) { - window.location.href = '/author/index_list.html?bookId='+bookId; + window.location.href = '/author/index_list.html?bookId=' + bookId; } else { @@ -169,5 +252,110 @@ } + + // 打字机效果函数 + function typeWriter(textarea, text, speed = 50) { + let i = 0; + const timer = setInterval(() => { + if (i < text.length) { + textarea.val(textarea.val() + text.charAt(i)); + i++; + // 滚动到底部 + textarea.scrollTop(textarea[0].scrollHeight); + } else { + clearInterval(timer); + } + }, speed); + } + + $('.ai-toolbar .ai-link').click(function(e){ + e.preventDefault(); // 阻止默认链接行为 + const type = $(this).data('type'); + const textarea = $('#bookContent'); + const selectedText = textarea.val().substring(textarea[0].selectionStart, textarea[0].selectionEnd); + + // 检查是否选中文本 + if (!selectedText) { + layer.msg('请先选中要处理的文本'); + return; + } + + const loading = layer.load(1, {shade: 0.3}); + + // 参数配置 + let params = {text: selectedText}; + if(type === 'expand' || type === 'condense'){ + layer.prompt({ + title: '请输入比例', + value: 2, + btn: ['确定', '取消'], + btn2: function(){ + layer.close(loading); + }, + cancel: function(){ + layer.close(loading); + } + }, function(value, index){ + if(isNaN(Number(value)) || isNaN(parseFloat(value))){ + layer.msg('请输入正确的比例'); + return; + } + if(type === 'expand' && value <= 1){ + layer.msg('请输入正确的比例'); + return; + } + if(type === 'condense' && (value <=0 || value >= 1)){ + layer.msg('请输入正确的比例'); + return; + } + params.ratio = parseFloat(value) * 100; + layer.close(index); + sendRequest(type, params, loading, textarea); + }); + return; + }else if(type === 'continue'){ + layer.prompt({ + title: '请输入续写长度(字数)', + value: 200, + btn: ['确定', '取消'], + btn2: function(){ + layer.close(loading); + }, + cancel: function(){ + layer.close(loading); + } + }, function(value, index){ + if(!Number.isInteger(Number(value)) || value <= 0){ + layer.msg('请输入正确的长度'); + return; + } + params.length = parseInt(value); + layer.close(index); + sendRequest(type, params, loading, textarea); + }); + return; + } + + sendRequest(type, params, loading, textarea); + }); + + function sendRequest(type, params, loading, textarea){ + $.ajax({ + url: '/author/ai/' + type, + type: 'POST', + data: params, + success: function(res){ + layer.close(loading); + // 将生成的内容追加到文本末尾 + const newText = "\n\n" + res.data; // 添加换行符分隔 + typeWriter(textarea, newText); // 使用打字机效果 + }, + error: function(){ + layer.msg('请求失败,请稍后重试'); + layer.close(loading); + } + }); + } + diff --git a/pom.xml b/pom.xml index 9152b2b..b22d4af 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,13 @@ novel-common ${project.version} + + org.springframework.ai + spring-ai-bom + 1.0.0-M6 + pom + import +