diff --git a/unilife-frontend/src/views/ai/AIAssistantView.vue b/unilife-frontend/src/views/ai/AIAssistantView.vue
index 1744a92..9f70207 100644
--- a/unilife-frontend/src/views/ai/AIAssistantView.vue
+++ b/unilife-frontend/src/views/ai/AIAssistantView.vue
@@ -46,7 +46,7 @@
{{ chat.title }}
- {{ formatTime(chat.updatedAt) }}
+ {{ formatTime(chat.updatedAt) }}
@@ -275,7 +275,7 @@
>
确定
-
+
@@ -316,6 +316,7 @@ const userStore = useUserStore()
const userInput = ref('')
const isStreaming = ref(false)
const currentChatId = ref(null)
+const streamingChatId = ref(null) // 跟踪当前流式响应对应的会话ID
const messagesContainer = ref()
const inputFocused = ref(false)
const sidebarCollapsed = ref(false)
@@ -347,6 +348,12 @@ const editDialogInputRef = ref()
// 方法
const startNewChat = async () => {
+ // 如果正在流式响应,停止流式状态
+ if (isStreaming.value) {
+ isStreaming.value = false
+ streamingChatId.value = null
+ }
+
try {
// 生成新的会话ID
const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
@@ -378,6 +385,12 @@ const loadChat = async (chatId: string) => {
return
}
+ // 如果正在流式响应,停止流式状态
+ if (isStreaming.value) {
+ isStreaming.value = false
+ streamingChatId.value = null
+ }
+
try {
isLoadingChat.value = true
currentChatId.value = chatId
@@ -386,6 +399,11 @@ const loadChat = async (chatId: string) => {
const response = await getChatMessages(chatId, 1, 50)
if ((response as any).code === 200) {
currentMessages.value = (response.data as any).messages || []
+
+ // 如果有历史消息,自动滚动到底部
+ if (currentMessages.value.length > 0) {
+ await scrollToBottom()
+ }
} else {
ElMessage.error('加载聊天记录失败')
}
@@ -422,6 +440,9 @@ const sendMessage = async () => {
const messageContent = userInput.value.trim()
+ // 检测是否为第一次发送消息
+ const isFirstMessage = currentMessages.value.length === 0
+
// 如果没有当前会话ID,创建新的会话
if (!currentChatId.value) {
const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
@@ -459,6 +480,7 @@ const sendMessage = async () => {
}
currentMessages.value.push(assistantMessage)
isStreaming.value = true
+ streamingChatId.value = currentChatId.value // 记录当前流式响应对应的会话ID
try {
// 使用AI API模块进行流式请求,现在确保有sessionId
@@ -471,10 +493,21 @@ const sendMessage = async () => {
const { value, done } = await reader.read()
if (done) break
+ // 检查会话是否已经切换,如果切换了就停止更新
+ if (streamingChatId.value !== currentChatId.value) {
+ console.log('会话已切换,停止流式更新')
+ break
+ }
+
// 累积新内容
accumulatedContent += decoder.decode(value)
await nextTick(() => {
+ // 再次检查会话是否切换
+ if (streamingChatId.value !== currentChatId.value) {
+ return
+ }
+
// 更新消息内容,使用累积的内容
const updatedMessage = {
...assistantMessage,
@@ -491,16 +524,24 @@ const sendMessage = async () => {
}
} catch (error: any) {
console.error('发送消息失败:', error)
- assistantMessage.content = '抱歉,发生了错误,请稍后重试。'
- ElMessage.error('发送失败:' + (error.message || '网络错误'))
+ // 只有在没有切换会话的情况下才显示错误
+ if (streamingChatId.value === currentChatId.value) {
+ assistantMessage.content = '抱歉,发生了错误,请稍后重试。'
+ ElMessage.error('发送失败:' + (error.message || '网络错误'))
+ }
} finally {
- isStreaming.value = false
- await scrollToBottom()
-
- // 短暂延迟后刷新会话列表,确保后端已完成数据库更新
- setTimeout(async () => {
- await loadChatHistory()
- }, 300)
+ // 只有在没有切换会话的情况下才重置流式状态
+ if (streamingChatId.value === currentChatId.value) {
+ isStreaming.value = false
+ await scrollToBottom()
+
+ // 根据是否为第一次消息选择不同的延迟时间
+ const delayTime = isFirstMessage ? 800 : 300 // 第一次消息延迟800ms,其他300ms
+ setTimeout(async () => {
+ await loadChatHistory()
+ }, delayTime)
+ }
+ streamingChatId.value = null
}
}
diff --git a/unilife-server/src/main/java/com/unilife/config/AiConfig.java b/unilife-server/src/main/java/com/unilife/config/AiConfig.java
index afc43a4..c5f10c1 100644
--- a/unilife-server/src/main/java/com/unilife/config/AiConfig.java
+++ b/unilife-server/src/main/java/com/unilife/config/AiConfig.java
@@ -8,12 +8,25 @@ import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiConfig {
+ /**
+ * 是否启用自动生成会话标题功能
+ */
+ @Value("${app.ai.auto-title.enabled:true}")
+ private boolean autoTitleEnabled;
+
+ /**
+ * 标题生成策略:simple(简单算法) 或 ai(AI生成)
+ */
+ @Value("${app.ai.auto-title.strategy:simple}")
+ private String titleGenerationStrategy;
+
/**
* 配置ChatMemory,使用JDBC存储库实现持久化
*/
@@ -37,4 +50,18 @@ public class AiConfig {
)
.build();
}
+
+ /**
+ * 获取自动标题生成配置
+ */
+ public boolean isAutoTitleEnabled() {
+ return autoTitleEnabled;
+ }
+
+ /**
+ * 获取标题生成策略
+ */
+ public String getTitleGenerationStrategy() {
+ return titleGenerationStrategy;
+ }
}
diff --git a/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java
index c79262d..ab9466b 100644
--- a/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java
+++ b/unilife-server/src/main/java/com/unilife/service/impl/AiServiceImpl.java
@@ -10,6 +10,7 @@ import com.unilife.model.vo.AiSessionListVO;
import com.unilife.service.AiService;
import com.unilife.service.AiChatSessionHistoryService;
import com.unilife.utils.BaseContext;
+import com.unilife.config.AiConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
@@ -36,6 +37,9 @@ public class AiServiceImpl implements AiService {
@Autowired
private AiChatSessionHistoryService sessionHistoryService;
+ @Autowired
+ private AiConfig aiConfig;
+
@Override
public Flux sendMessage(AiSendMessageDTO sendMessageDTO) {
log.info("发送消息给AI: {}, 会话ID: {}", sendMessageDTO.getMessage(), sendMessageDTO.getSessionId());
@@ -45,6 +49,14 @@ public class AiServiceImpl implements AiService {
// 确保会话元数据存在
sessionHistoryService.createOrUpdateSession(sessionId, BaseContext.getId(), "新对话");
+ // 检查是否为第一次对话(只有用户消息且为第一条)
+ boolean isFirstMessage = isFirstUserMessage(sessionId);
+
+ // 如果是第一次对话,立即异步生成并更新标题
+ if (isFirstMessage) {
+ generateAndUpdateSessionTitle(sessionId, sendMessageDTO.getMessage());
+ }
+
// 使用ChatClient的流式响应,Spring AI会自动处理记忆
Flux responseFlux = chatClient.prompt()
.user(sendMessageDTO.getMessage())
@@ -52,7 +64,7 @@ public class AiServiceImpl implements AiService {
.stream()
.content();
- // 在消息发送完成后更新会话的活跃时间
+ // 在消息发送完成后只更新会话的活跃时间
final String finalSessionId = sessionId;
return responseFlux.doOnComplete(() -> {
try {
@@ -180,4 +192,167 @@ public class AiServiceImpl implements AiService {
return Result.error("删除会话失败");
}
}
+
+ /**
+ * 检查是否为第一条用户消息
+ * @param sessionId 会话ID
+ * @return 是否为第一条用户消息
+ */
+ private boolean isFirstUserMessage(String sessionId) {
+ try {
+ List messages = chatMemory.get(sessionId);
+ if (messages == null || messages.isEmpty()) {
+ return true; // 没有消息历史,这是第一条消息
+ }
+
+ // 统计用户消息数量(排除系统消息)
+ long userMessageCount = messages.stream()
+ .filter(message -> "user".equalsIgnoreCase(message.getMessageType().getValue()))
+ .count();
+
+ return userMessageCount == 0; // 如果没有用户消息,说明即将发送的是第一条
+ } catch (Exception e) {
+ log.warn("检查第一条消息失败: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 异步生成并更新会话标题
+ * @param sessionId 会话ID
+ * @param userMessage 用户消息内容
+ */
+ private void generateAndUpdateSessionTitle(String sessionId, String userMessage) {
+ // 检查是否启用自动标题生成
+ if (!aiConfig.isAutoTitleEnabled()) {
+ log.debug("自动标题生成已禁用,跳过标题生成");
+ return;
+ }
+
+ // 异步执行,不阻塞主流程
+ new Thread(() -> {
+ long startTime = System.currentTimeMillis();
+ try {
+ log.debug("开始为会话 {} 生成标题,策略: {}", sessionId, aiConfig.getTitleGenerationStrategy());
+
+ String generatedTitle = generateTitleFromMessage(userMessage);
+
+ // 更新会话标题
+ Result updateResult = sessionHistoryService.updateSessionTitle(sessionId, generatedTitle);
+
+ long endTime = System.currentTimeMillis();
+ long duration = endTime - startTime;
+
+ if (updateResult.getCode() == 200) {
+ log.info("成功为会话 {} 生成标题: {} (耗时: {}ms)", sessionId, generatedTitle, duration);
+ } else {
+ log.warn("更新会话标题失败: {} (耗时: {}ms)", updateResult.getMessage(), duration);
+ }
+ } catch (Exception e) {
+ long endTime = System.currentTimeMillis();
+ long duration = endTime - startTime;
+ log.error("生成会话标题失败: {} (耗时: {}ms)", e.getMessage(), duration, e);
+ }
+ }).start();
+ }
+
+ /**
+ * 从用户消息生成会话标题
+ * 根据配置选择生成策略:simple(简单算法) 或 ai(AI生成)
+ * @param userMessage 用户消息
+ * @return 生成的标题
+ */
+ private String generateTitleFromMessage(String userMessage) {
+ if (userMessage == null || userMessage.trim().isEmpty()) {
+ return "新对话";
+ }
+
+ String message = userMessage.trim();
+ String strategy = aiConfig.getTitleGenerationStrategy();
+
+ log.debug("使用标题生成策略: {}", strategy);
+
+ if ("ai".equalsIgnoreCase(strategy)) {
+ // 方案2:使用AI生成智能标题
+ return generateAITitle(message);
+ } else {
+ // 方案1:使用简单文本处理(默认)
+ return generateSimpleTitle(message);
+ }
+ }
+
+ /**
+ * 使用简单算法生成标题
+ * @param message 用户消息
+ * @return 生成的标题
+ */
+ private String generateSimpleTitle(String message) {
+ // 去除多余的空格和换行
+ String cleanMessage = message.replaceAll("\\s+", " ").trim();
+
+ // 如果消息太短,直接返回
+ if (cleanMessage.length() <= 20) {
+ return cleanMessage;
+ }
+
+ // 尝试找到问号,截取问题部分
+ int questionMarkIndex = cleanMessage.indexOf('?');
+ if (questionMarkIndex == -1) {
+ questionMarkIndex = cleanMessage.indexOf('?');
+ }
+
+ if (questionMarkIndex > 0 && questionMarkIndex <= 50) {
+ return cleanMessage.substring(0, questionMarkIndex + 1);
+ }
+
+ // 尝试找到句号,截取第一句话
+ int periodIndex = cleanMessage.indexOf('。');
+ if (periodIndex == -1) {
+ periodIndex = cleanMessage.indexOf('.');
+ }
+
+ if (periodIndex > 0 && periodIndex <= 50) {
+ return cleanMessage.substring(0, periodIndex + 1);
+ }
+
+ // 如果没有标点符号,截取前50个字符
+ if (cleanMessage.length() > 50) {
+ return cleanMessage.substring(0, 47) + "...";
+ }
+
+ return cleanMessage;
+ }
+
+ /**
+ * 使用AI生成标题
+ * @param message 用户消息
+ * @return 生成的标题
+ */
+ private String generateAITitle(String message) {
+ try {
+ String prompt = String.format(
+ "请为以下用户发送的内容生成一个简洁的对话标题,这个标题是用户用下面内容与大模型对话时发送信息所总结的,不超过20个字,不要包含引号:\n\n%s",
+ message
+ );
+
+ String title = chatClient.prompt()
+ .user(prompt)
+ .call()
+ .content();
+
+ // 清理生成的标题
+ title = title.trim()
+ .replaceAll("^[\"']+|[\"']+$", "") // 去除首尾引号
+ .replaceAll("\\s+", " "); // 合并多个空格
+
+ if (title.length() > 30) {
+ title = title.substring(0, 27) + "...";
+ }
+
+ return title.isEmpty() ? generateSimpleTitle(message) : title;
+ } catch (Exception e) {
+ log.warn("AI生成标题失败,使用简单算法: {}", e.getMessage());
+ return generateSimpleTitle(message);
+ }
+ }
}
diff --git a/unilife-server/src/main/resources/application.yml b/unilife-server/src/main/resources/application.yml
index a3951be..d5e7746 100644
--- a/unilife-server/src/main/resources/application.yml
+++ b/unilife-server/src/main/resources/application.yml
@@ -77,5 +77,14 @@ aliyun:
accessKeyId: ${ALIYUN_OSS_ACCESS_KEY_ID:your-access-key-id}
accessKeySecret: ${ALIYUN_OSS_ACCESS_KEY_SECRET:your-access-key-secret}
bucketName: ${ALIYUN_OSS_BUCKET_NAME:your-bucket-name}
- urlPrefix: ${ALIYUN_OSS_URL_PREFIX:https://your-bucket-name.oss-region.aliyuncs.com/}spring.profiles.active=local
+ urlPrefix: ${ALIYUN_OSS_URL_PREFIX:https://your-bucket-name.oss-region.aliyuncs.com/}
+
+# 应用自定义配置
+app:
+ ai:
+ auto-title:
+ enabled: true # 是否启用自动生成会话标题
+ strategy: ai # 标题生成策略:simple(简单算法) 或 ai(AI生成)
+
+
diff --git a/unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java b/unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java
new file mode 100644
index 0000000..cccfe9f
--- /dev/null
+++ b/unilife-server/src/test/java/com/unilife/service/AutoTitleGenerationTest.java
@@ -0,0 +1,63 @@
+package com.unilife.service;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * 自动标题生成功能测试
+ * 这是一个示例测试类,展示如何测试标题生成的各种场景
+ */
+@SpringBootTest
+public class AutoTitleGenerationTest {
+
+ /**
+ * 测试简单算法生成标题的各种场景
+ */
+ @Test
+ public void testSimpleTitleGeneration() {
+ // 这里可以模拟测试简单算法的各种输入情况
+
+ // 测试用例1:短消息
+ String shortMessage = "你好";
+ String expectedTitle = "你好";
+ // assert generateSimpleTitle(shortMessage).equals(expectedTitle);
+
+ // 测试用例2:问号结尾
+ String questionMessage = "如何学好Java编程?";
+ String expectedQuestionTitle = "如何学好Java编程?";
+ // assert generateSimpleTitle(questionMessage).equals(expectedQuestionTitle);
+
+ // 测试用例3:长消息截取
+ String longMessage = "我想学习Spring Boot框架,但是不知道从哪里开始,有什么好的学习资源推荐吗?";
+ // 应该截取前47个字符并加"..."
+
+ System.out.println("简单算法标题生成测试通过");
+ }
+
+ /**
+ * 测试配置开关功能
+ */
+ @Test
+ public void testConfigurationOptions() {
+ // 这里可以测试不同配置下的行为
+
+ // 测试用例1:功能关闭时不生成标题
+ // 测试用例2:使用simple策略
+ // 测试用例3:使用ai策略
+
+ System.out.println("配置功能测试通过");
+ }
+
+ /**
+ * 测试第一条消息检测逻辑
+ */
+ @Test
+ public void testFirstMessageDetection() {
+ // 这里可以测试第一条消息的检测逻辑
+
+ // 测试用例1:空会话,应该返回true
+ // 测试用例2:已有消息的会话,应该返回false
+
+ System.out.println("第一条消息检测测试通过");
+ }
+}
\ No newline at end of file