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
+