Commit 42330ffc940cf4651afb5dd07ca50885a2159e1b
1 parent
e32a1205
feat(B): multi-named conversations + Redis-persisted memory + sidebar
- RedisChatMemoryStore (ChatMemoryStore): persist per-conversation messages to Redis (chat:mem:{convId}, 30d TTL) -> survives restart; agent memory now Redis-backed via MessageWindowChatMemory.builder().chatMemoryStore(...)
- ConversationService: per-user conversation metadata (chat:convs:{userId} hash), title from first message, list/create/delete/history
- ConversationController: GET/POST/DELETE /api/agent/conversations + GET /{id}/messages
- AgentChatController: touch conversation on each turn
- chat.html: conversation sidebar (list/switch/new/delete), load history on switch, refresh on send
Verified: multi-turn memory recalls facts; after full restart, conversation list + history + memory all persist (agent still recalls prior-turn facts).
Showing
6 changed files
with
330 additions
and
4 deletions
src/main/java/com/xly/config/AgentConfig.java
| @@ -44,12 +44,17 @@ public class AgentConfig { | @@ -44,12 +44,17 @@ public class AgentConfig { | ||
| 44 | @Bean | 44 | @Bean |
| 45 | public ReActAgent reActAgent(SystemPromptService systemPromptService, | 45 | public ReActAgent reActAgent(SystemPromptService systemPromptService, |
| 46 | KgQueryTool kgQueryTool, | 46 | KgQueryTool kgQueryTool, |
| 47 | - ErpReadTool erpReadTool) { | 47 | + ErpReadTool erpReadTool, |
| 48 | + RedisChatMemoryStore memoryStore) { | ||
| 48 | String systemPrompt = systemPromptService.buildSystemPrompt(); | 49 | String systemPrompt = systemPromptService.buildSystemPrompt(); |
| 49 | return AiServices.builder(ReActAgent.class) | 50 | return AiServices.builder(ReActAgent.class) |
| 50 | .streamingChatModel(agentStreamingModel()) | 51 | .streamingChatModel(agentStreamingModel()) |
| 51 | .tools(kgQueryTool, erpReadTool) | 52 | .tools(kgQueryTool, erpReadTool) |
| 52 | - .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20)) | 53 | + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder() |
| 54 | + .id(memoryId) | ||
| 55 | + .maxMessages(30) | ||
| 56 | + .chatMemoryStore(memoryStore) | ||
| 57 | + .build()) | ||
| 53 | .systemMessageProvider(memoryId -> systemPrompt) | 58 | .systemMessageProvider(memoryId -> systemPrompt) |
| 54 | .build(); | 59 | .build(); |
| 55 | } | 60 | } |
src/main/java/com/xly/config/RedisChatMemoryStore.java
0 → 100644
| 1 | +package com.xly.config; | ||
| 2 | + | ||
| 3 | +import dev.langchain4j.data.message.ChatMessage; | ||
| 4 | +import dev.langchain4j.data.message.ChatMessageDeserializer; | ||
| 5 | +import dev.langchain4j.data.message.ChatMessageSerializer; | ||
| 6 | +import dev.langchain4j.store.memory.chat.ChatMemoryStore; | ||
| 7 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 8 | +import org.springframework.stereotype.Component; | ||
| 9 | + | ||
| 10 | +import java.time.Duration; | ||
| 11 | +import java.util.ArrayList; | ||
| 12 | +import java.util.List; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * Redis 持久化的 ChatMemoryStore —— 会话记忆按 conversationId 存到 Redis,重启/换实例不丢, | ||
| 16 | + * 也让「重登录后接回上一次会话」成立(会话状态按稳定身份+conversationId 存,而非按 token)。 | ||
| 17 | + * | ||
| 18 | + * <p>键:{@code chat:mem:{conversationId}};值:LangChain4j 序列化后的消息 JSON。30 天 TTL。 | ||
| 19 | + */ | ||
| 20 | +@Component | ||
| 21 | +public class RedisChatMemoryStore implements ChatMemoryStore { | ||
| 22 | + | ||
| 23 | + private static final String PREFIX = "chat:mem:"; | ||
| 24 | + private static final Duration TTL = Duration.ofDays(30); | ||
| 25 | + | ||
| 26 | + private final StringRedisTemplate redis; | ||
| 27 | + | ||
| 28 | + public RedisChatMemoryStore(StringRedisTemplate redis) { | ||
| 29 | + this.redis = redis; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @Override | ||
| 33 | + public List<ChatMessage> getMessages(Object memoryId) { | ||
| 34 | + String json = redis.opsForValue().get(PREFIX + memoryId); | ||
| 35 | + if (json == null || json.isBlank()) { | ||
| 36 | + return new ArrayList<>(); | ||
| 37 | + } | ||
| 38 | + return ChatMessageDeserializer.messagesFromJson(json); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public void updateMessages(Object memoryId, List<ChatMessage> messages) { | ||
| 43 | + redis.opsForValue().set(PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages), TTL); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + @Override | ||
| 47 | + public void deleteMessages(Object memoryId) { | ||
| 48 | + redis.delete(PREFIX + memoryId); | ||
| 49 | + } | ||
| 50 | +} |
src/main/java/com/xly/service/ConversationService.java
0 → 100644
| 1 | +package com.xly.service; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | +import com.xly.config.RedisChatMemoryStore; | ||
| 5 | +import dev.langchain4j.data.message.AiMessage; | ||
| 6 | +import dev.langchain4j.data.message.ChatMessage; | ||
| 7 | +import dev.langchain4j.data.message.UserMessage; | ||
| 8 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 9 | +import org.springframework.stereotype.Service; | ||
| 10 | + | ||
| 11 | +import java.util.ArrayList; | ||
| 12 | +import java.util.LinkedHashMap; | ||
| 13 | +import java.util.List; | ||
| 14 | +import java.util.Map; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 会话(conversation)元数据管理 —— 支持「多条命名会话/用户」。 | ||
| 18 | + * | ||
| 19 | + * <p>每用户一个会话列表存 Redis Hash {@code chat:convs:{userId}}(field=convId,value=JSON{id,title,updatedAt}); | ||
| 20 | + * 消息本体存在 {@link RedisChatMemoryStore}({@code chat:mem:{convId}})。标题取自该会话的首条用户消息。 | ||
| 21 | + */ | ||
| 22 | +@Service | ||
| 23 | +public class ConversationService { | ||
| 24 | + | ||
| 25 | + private static final String CONVS_KEY = "chat:convs:"; | ||
| 26 | + | ||
| 27 | + private final StringRedisTemplate redis; | ||
| 28 | + private final RedisChatMemoryStore memoryStore; | ||
| 29 | + private final ObjectMapper mapper; | ||
| 30 | + | ||
| 31 | + public ConversationService(StringRedisTemplate redis, RedisChatMemoryStore memoryStore, ObjectMapper mapper) { | ||
| 32 | + this.redis = redis; | ||
| 33 | + this.memoryStore = memoryStore; | ||
| 34 | + this.mapper = mapper; | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + /** 新建一个空会话,返回 convId。 */ | ||
| 38 | + public String create(String userId) { | ||
| 39 | + String convId = "c-" + System.currentTimeMillis() + "-" + Integer.toHexString((int) (Math.random() * 0xFFFFF)); | ||
| 40 | + writeMeta(userId, convId, "新会话"); | ||
| 41 | + return convId; | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + /** 每次对话时调用:会话不存在则建、标题空则用首条消息命名,并刷新 updatedAt。 */ | ||
| 45 | + public void touch(String userId, String convId, String firstMsg) { | ||
| 46 | + Object existing = redis.opsForHash().get(CONVS_KEY + userId, convId); | ||
| 47 | + String title = null; | ||
| 48 | + if (existing != null) { | ||
| 49 | + try { | ||
| 50 | + title = mapper.readTree(existing.toString()).path("title").asText(null); | ||
| 51 | + } catch (Exception ignore) { | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + if (title == null || title.isBlank() || "新会话".equals(title)) { | ||
| 55 | + title = deriveTitle(firstMsg); | ||
| 56 | + } | ||
| 57 | + writeMeta(userId, convId, title); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + private void writeMeta(String userId, String convId, String title) { | ||
| 61 | + try { | ||
| 62 | + Map<String, Object> m = new LinkedHashMap<>(); | ||
| 63 | + m.put("id", convId); | ||
| 64 | + m.put("title", title); | ||
| 65 | + m.put("updatedAt", System.currentTimeMillis()); | ||
| 66 | + redis.opsForHash().put(CONVS_KEY + userId, convId, mapper.writeValueAsString(m)); | ||
| 67 | + } catch (Exception ignore) { | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + /** 该用户的会话列表,按最近更新倒序。 */ | ||
| 72 | + public List<Map<String, Object>> list(String userId) { | ||
| 73 | + Map<Object, Object> all = redis.opsForHash().entries(CONVS_KEY + userId); | ||
| 74 | + List<Map<String, Object>> out = new ArrayList<>(); | ||
| 75 | + for (Object v : all.values()) { | ||
| 76 | + try { | ||
| 77 | + @SuppressWarnings("unchecked") | ||
| 78 | + Map<String, Object> m = mapper.readValue(v.toString(), Map.class); | ||
| 79 | + out.add(m); | ||
| 80 | + } catch (Exception ignore) { | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | + out.sort((a, b) -> Long.compare(num(b.get("updatedAt")), num(a.get("updatedAt")))); | ||
| 84 | + return out; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + public void delete(String userId, String convId) { | ||
| 88 | + redis.opsForHash().delete(CONVS_KEY + userId, convId); | ||
| 89 | + memoryStore.deleteMessages(convId); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + /** 会话历史,映射为 {role:user|ai, content}。跳过系统消息、工具调用中间消息、工具结果。 */ | ||
| 93 | + public List<Map<String, String>> history(String convId) { | ||
| 94 | + List<Map<String, String>> out = new ArrayList<>(); | ||
| 95 | + for (ChatMessage m : memoryStore.getMessages(convId)) { | ||
| 96 | + if (m instanceof UserMessage um && um.hasSingleText()) { | ||
| 97 | + out.add(Map.of("role", "user", "content", um.singleText())); | ||
| 98 | + } else if (m instanceof AiMessage am && !am.hasToolExecutionRequests() | ||
| 99 | + && am.text() != null && !am.text().isBlank()) { | ||
| 100 | + out.add(Map.of("role", "ai", "content", am.text())); | ||
| 101 | + } | ||
| 102 | + } | ||
| 103 | + return out; | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + private String deriveTitle(String s) { | ||
| 107 | + if (s == null) { | ||
| 108 | + return "新会话"; | ||
| 109 | + } | ||
| 110 | + String t = s.strip().replace("\n", " "); | ||
| 111 | + if (t.isEmpty()) { | ||
| 112 | + return "新会话"; | ||
| 113 | + } | ||
| 114 | + return t.length() > 18 ? t.substring(0, 18) + "…" : t; | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + private long num(Object o) { | ||
| 118 | + try { | ||
| 119 | + return Long.parseLong(String.valueOf(o)); | ||
| 120 | + } catch (Exception e) { | ||
| 121 | + return 0L; | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | +} |
src/main/java/com/xly/web/AgentChatController.java
| @@ -2,6 +2,7 @@ package com.xly.web; | @@ -2,6 +2,7 @@ package com.xly.web; | ||
| 2 | 2 | ||
| 3 | import com.fasterxml.jackson.databind.ObjectMapper; | 3 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 4 | import com.xly.agent.ReActAgent; | 4 | import com.xly.agent.ReActAgent; |
| 5 | +import com.xly.service.ConversationService; | ||
| 5 | import dev.langchain4j.service.TokenStream; | 6 | import dev.langchain4j.service.TokenStream; |
| 6 | import org.slf4j.Logger; | 7 | import org.slf4j.Logger; |
| 7 | import org.slf4j.LoggerFactory; | 8 | import org.slf4j.LoggerFactory; |
| @@ -36,11 +37,13 @@ public class AgentChatController { | @@ -36,11 +37,13 @@ public class AgentChatController { | ||
| 36 | 37 | ||
| 37 | private final ReActAgent agent; | 38 | private final ReActAgent agent; |
| 38 | private final ObjectMapper mapper; | 39 | private final ObjectMapper mapper; |
| 40 | + private final ConversationService conversations; | ||
| 39 | private final ExecutorService exec = Executors.newCachedThreadPool(); | 41 | private final ExecutorService exec = Executors.newCachedThreadPool(); |
| 40 | 42 | ||
| 41 | - public AgentChatController(ReActAgent agent, ObjectMapper mapper) { | 43 | + public AgentChatController(ReActAgent agent, ObjectMapper mapper, ConversationService conversations) { |
| 42 | this.agent = agent; | 44 | this.agent = agent; |
| 43 | this.mapper = mapper; | 45 | this.mapper = mapper; |
| 46 | + this.conversations = conversations; | ||
| 44 | } | 47 | } |
| 45 | 48 | ||
| 46 | /** 前端请求体:身份字段透传(M1 只用 userid + conversationId + text)。 */ | 49 | /** 前端请求体:身份字段透传(M1 只用 userid + conversationId + text)。 */ |
| @@ -58,6 +61,9 @@ public class AgentChatController { | @@ -58,6 +61,9 @@ public class AgentChatController { | ||
| 58 | ? req.conversationId | 61 | ? req.conversationId |
| 59 | : ((req.userid == null ? "anon" : req.userid) + ":default"); | 62 | : ((req.userid == null ? "anon" : req.userid) + ":default"); |
| 60 | 63 | ||
| 64 | + // 登记/更新会话(用于多会话侧栏;标题取首条消息) | ||
| 65 | + conversations.touch(req.userid == null ? "anon" : req.userid, convId, userInput); | ||
| 66 | + | ||
| 61 | exec.submit(() -> { | 67 | exec.submit(() -> { |
| 62 | try { | 68 | try { |
| 63 | TokenStream ts = agent.chat(convId, userInput); | 69 | TokenStream ts = agent.chat(convId, userInput); |
src/main/java/com/xly/web/ConversationController.java
0 → 100644
| 1 | +package com.xly.web; | ||
| 2 | + | ||
| 3 | +import com.xly.service.ConversationService; | ||
| 4 | +import org.springframework.web.bind.annotation.DeleteMapping; | ||
| 5 | +import org.springframework.web.bind.annotation.GetMapping; | ||
| 6 | +import org.springframework.web.bind.annotation.PathVariable; | ||
| 7 | +import org.springframework.web.bind.annotation.PostMapping; | ||
| 8 | +import org.springframework.web.bind.annotation.RequestBody; | ||
| 9 | +import org.springframework.web.bind.annotation.RequestMapping; | ||
| 10 | +import org.springframework.web.bind.annotation.RequestParam; | ||
| 11 | +import org.springframework.web.bind.annotation.RestController; | ||
| 12 | + | ||
| 13 | +import java.util.List; | ||
| 14 | +import java.util.Map; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 会话管理 REST —— 支撑前端的「多命名会话」侧栏(列表 / 新建 / 删除 / 历史)。 | ||
| 18 | + */ | ||
| 19 | +@RestController | ||
| 20 | +@RequestMapping("/api/agent/conversations") | ||
| 21 | +public class ConversationController { | ||
| 22 | + | ||
| 23 | + private final ConversationService conversations; | ||
| 24 | + | ||
| 25 | + public ConversationController(ConversationService conversations) { | ||
| 26 | + this.conversations = conversations; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + /** 列出某用户的会话(最近更新在前)。 */ | ||
| 30 | + @GetMapping | ||
| 31 | + public List<Map<String, Object>> list(@RequestParam("userid") String userid) { | ||
| 32 | + return conversations.list(userid); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + /** 新建空会话,返回 conversationId。 */ | ||
| 36 | + @PostMapping | ||
| 37 | + public Map<String, String> create(@RequestBody Map<String, String> body) { | ||
| 38 | + String userid = body.getOrDefault("userid", "anon"); | ||
| 39 | + return Map.of("conversationId", conversations.create(userid)); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + /** 删除会话(含其消息记忆)。 */ | ||
| 43 | + @DeleteMapping("/{convId}") | ||
| 44 | + public Map<String, Object> delete(@PathVariable("convId") String convId, | ||
| 45 | + @RequestParam("userid") String userid) { | ||
| 46 | + conversations.delete(userid, convId); | ||
| 47 | + return Map.of("ok", true); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + /** 会话历史消息(用于切换会话时回填)。 */ | ||
| 51 | + @GetMapping("/{convId}/messages") | ||
| 52 | + public List<Map<String, String>> messages(@PathVariable("convId") String convId) { | ||
| 53 | + return conversations.history(convId); | ||
| 54 | + } | ||
| 55 | +} |
src/main/resources/templates/chat.html
| @@ -423,11 +423,16 @@ | @@ -423,11 +423,16 @@ | ||
| 423 | <option value="process">小羚羊印刷行业大模型</option> | 423 | <option value="process">小羚羊印刷行业大模型</option> |
| 424 | <option value="general">qwen2.5:14b</option> | 424 | <option value="general">qwen2.5:14b</option> |
| 425 | </select> | 425 | </select> |
| 426 | - <button class="model-selector" onclick="clearChat()">清空对话</button> | 426 | + <button class="model-selector" onclick="newConversation()">新建会话</button> |
| 427 | </div> | 427 | </div> |
| 428 | </div> | 428 | </div> |
| 429 | 429 | ||
| 430 | <div class="chat-body"> | 430 | <div class="chat-body"> |
| 431 | + <div class="sidebar"> | ||
| 432 | + <div class="sidebar-title">会话</div> | ||
| 433 | + <button class="preset-question" style="text-align:center;font-weight:600;background:#eef;" onclick="newConversation()">+ 新建会话</button> | ||
| 434 | + <div id="convList"></div> | ||
| 435 | + </div> | ||
| 431 | <div class="chat-main"> | 436 | <div class="chat-main"> |
| 432 | <div class="messages-container"> | 437 | <div class="messages-container"> |
| 433 | <div class="chat-messages" id="chatMessages"> | 438 | <div class="chat-messages" id="chatMessages"> |
| @@ -495,8 +500,88 @@ | @@ -495,8 +500,88 @@ | ||
| 495 | $('#messageInput').focus(); | 500 | $('#messageInput').focus(); |
| 496 | bindKeyboardEvents(); | 501 | bindKeyboardEvents(); |
| 497 | ensureInputAtBottom(); | 502 | ensureInputAtBottom(); |
| 503 | + initConversations(); | ||
| 498 | }); | 504 | }); |
| 499 | 505 | ||
| 506 | + // ====================== 多命名会话(侧栏) ====================== | ||
| 507 | + function convApi(path, opts) { | ||
| 508 | + return fetch(CONFIG.backendUrl + '/api/agent/conversations' + path, opts || {}); | ||
| 509 | + } | ||
| 510 | + | ||
| 511 | + async function initConversations() { | ||
| 512 | + try { | ||
| 513 | + const res = await convApi('?userid=' + encodeURIComponent(userid)); | ||
| 514 | + const list = await res.json(); | ||
| 515 | + renderConvList(list); | ||
| 516 | + if (list && list.length > 0) { | ||
| 517 | + await switchConversation(list[0].id); | ||
| 518 | + } | ||
| 519 | + } catch (e) { console.log('会话初始化失败', e); } | ||
| 520 | + } | ||
| 521 | + | ||
| 522 | + async function loadConversations() { | ||
| 523 | + try { | ||
| 524 | + const res = await convApi('?userid=' + encodeURIComponent(userid)); | ||
| 525 | + renderConvList(await res.json()); | ||
| 526 | + } catch (e) { /* ignore */ } | ||
| 527 | + } | ||
| 528 | + | ||
| 529 | + function renderConvList(list) { | ||
| 530 | + const box = $('#convList'); | ||
| 531 | + box.empty(); | ||
| 532 | + (list || []).forEach(c => { | ||
| 533 | + const isActive = c.id === conversationId; | ||
| 534 | + const item = $('<div class="preset-question conv-item"></div>') | ||
| 535 | + .css({display:'flex', justifyContent:'space-between', alignItems:'center', gap:'6px'}); | ||
| 536 | + if (isActive) item.css({background:'#667eea', color:'#fff', borderColor:'#667eea'}); | ||
| 537 | + const title = $('<span></span>').text(c.title || '会话') | ||
| 538 | + .css({flex:'1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}); | ||
| 539 | + const del = $('<span title="删除">×</span>').css({cursor:'pointer', fontWeight:'bold', opacity:'0.6'}); | ||
| 540 | + title.on('click', () => switchConversation(c.id)); | ||
| 541 | + del.on('click', (ev) => { ev.stopPropagation(); deleteConversation(c.id); }); | ||
| 542 | + item.append(title).append(del); | ||
| 543 | + box.append(item); | ||
| 544 | + }); | ||
| 545 | + } | ||
| 546 | + | ||
| 547 | + async function switchConversation(convId) { | ||
| 548 | + if (convId === conversationId) return; | ||
| 549 | + conversationId = convId; | ||
| 550 | + $('#chatMessages').empty(); | ||
| 551 | + try { | ||
| 552 | + const res = await convApi('/' + convId + '/messages'); | ||
| 553 | + const msgs = await res.json(); | ||
| 554 | + (msgs || []).forEach(m => addMessage(m.content, m.role === 'user' ? 'user' : 'ai')); | ||
| 555 | + if (!msgs || msgs.length === 0) addMessage('(空会话)请提问。', 'ai'); | ||
| 556 | + } catch (e) { console.log('加载历史失败', e); } | ||
| 557 | + renderConvList(await (await convApi('?userid=' + encodeURIComponent(userid))).json()); | ||
| 558 | + ensureInputAtBottom(); | ||
| 559 | + } | ||
| 560 | + | ||
| 561 | + async function newConversation() { | ||
| 562 | + try { | ||
| 563 | + const res = await convApi('', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({userid: userid}) }); | ||
| 564 | + conversationId = (await res.json()).conversationId; | ||
| 565 | + } catch (e) { | ||
| 566 | + conversationId = 'c-' + Date.now() + '-' + Math.random().toString(36).slice(2,8); | ||
| 567 | + } | ||
| 568 | + $('#chatMessages').empty(); | ||
| 569 | + addMessage('新会话已开始,请提问。', 'ai'); | ||
| 570 | + loadConversations(); | ||
| 571 | + $('#messageInput').focus(); | ||
| 572 | + } | ||
| 573 | + | ||
| 574 | + async function deleteConversation(convId) { | ||
| 575 | + if (!confirm('删除这个会话?')) return; | ||
| 576 | + try { await convApi('/' + convId + '?userid=' + encodeURIComponent(userid), { method:'DELETE' }); } catch (e) {} | ||
| 577 | + if (convId === conversationId) { | ||
| 578 | + conversationId = 'c-' + Date.now() + '-' + Math.random().toString(36).slice(2,8); | ||
| 579 | + $('#chatMessages').empty(); | ||
| 580 | + addMessage('会话已删除,可新建或选择其它会话。', 'ai'); | ||
| 581 | + } | ||
| 582 | + loadConversations(); | ||
| 583 | + } | ||
| 584 | + | ||
| 500 | function generateRandomString(length) { | 585 | function generateRandomString(length) { |
| 501 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | 586 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
| 502 | let result = ''; | 587 | let result = ''; |
| @@ -645,6 +730,7 @@ | @@ -645,6 +730,7 @@ | ||
| 645 | button.prop('disabled', false); | 730 | button.prop('disabled', false); |
| 646 | input.focus(); | 731 | input.focus(); |
| 647 | scrollToBottom(); | 732 | scrollToBottom(); |
| 733 | + loadConversations(); | ||
| 648 | } | 734 | } |
| 649 | } | 735 | } |
| 650 | 736 |