From 5e40dee0e0a4e4cff4a1aafca2444f61c39cbf32 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 11:17:14 +0800
Subject: [PATCH] #AI.会话能力增强
---
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java | 9 +
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java | 8 +
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java | 8 +
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java | 119 +++++++++++++++-
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java | 4
rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java | 3
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java | 16 ++
rsf-admin/src/api/ai/chat.js | 22 ++
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java | 19 ++
version/db/ai_feature.sql | 17 ++
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java | 9 +
rsf-admin/src/layout/AiChatDrawer.jsx | 132 +++++++++++++++++-
12 files changed, 342 insertions(+), 24 deletions(-)
diff --git a/rsf-admin/src/api/ai/chat.js b/rsf-admin/src/api/ai/chat.js
index b11da93..939a2d0 100644
--- a/rsf-admin/src/api/ai/chat.js
+++ b/rsf-admin/src/api/ai/chat.js
@@ -13,9 +13,9 @@
throw new Error(msg || "鑾峰彇 AI 杩愯鏃朵俊鎭け璐�");
};
-export const getAiSessions = async (promptCode = "home.default") => {
+export const getAiSessions = async (promptCode = "home.default", keyword = "") => {
const res = await request.get("ai/chat/sessions", {
- params: { promptCode },
+ params: { promptCode, keyword },
});
const { code, msg, data } = res.data;
if (code === 200) {
@@ -33,6 +33,24 @@
throw new Error(msg || "鍒犻櫎 AI 浼氳瘽澶辫触");
};
+export const renameAiSession = async (sessionId, title) => {
+ const res = await request.post(`ai/chat/session/rename/${sessionId}`, { title });
+ const { code, msg, data } = res.data;
+ if (code === 200) {
+ return data;
+ }
+ throw new Error(msg || "閲嶅懡鍚� AI 浼氳瘽澶辫触");
+};
+
+export const pinAiSession = async (sessionId, pinned) => {
+ const res = await request.post(`ai/chat/session/pin/${sessionId}`, { pinned });
+ const { code, msg, data } = res.data;
+ if (code === 200) {
+ return data;
+ }
+ throw new Error(msg || "鏇存柊 AI 浼氳瘽缃《鐘舵�佸け璐�");
+};
+
export const streamAiChat = async (payload, { signal, onEvent } = {}) => {
const token = getToken();
const response = await fetch(`${PREFIX_BASE_URL}ai/chat/stream`, {
diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index c86e221..e8e2a07 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -6,6 +6,10 @@
Box,
Button,
Chip,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
Divider,
Drawer,
IconButton,
@@ -26,7 +30,11 @@
import CloseIcon from "@mui/icons-material/Close";
import AddCommentOutlinedIcon from "@mui/icons-material/AddCommentOutlined";
import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
-import { getAiRuntime, getAiSessions, removeAiSession, streamAiChat } from "@/api/ai/chat";
+import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
+import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
+import PushPinIcon from "@mui/icons-material/PushPin";
+import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined";
+import { getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, streamAiChat } from "@/api/ai/chat";
const DEFAULT_PROMPT_CODE = "home.default";
@@ -51,6 +59,8 @@
const [streaming, setStreaming] = useState(false);
const [usage, setUsage] = useState(null);
const [drawerError, setDrawerError] = useState("");
+ const [sessionKeyword, setSessionKeyword] = useState("");
+ const [renameDialog, setRenameDialog] = useState({ open: false, sessionId: null, title: "" });
const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
@@ -78,7 +88,7 @@
const initializeDrawer = async (targetSessionId = null) => {
await Promise.all([
loadRuntime(targetSessionId),
- loadSessions(),
+ loadSessions(sessionKeyword),
]);
};
@@ -100,9 +110,9 @@
}
};
- const loadSessions = async () => {
+ const loadSessions = async (keyword = sessionKeyword) => {
try {
- const data = await getAiSessions(DEFAULT_PROMPT_CODE);
+ const data = await getAiSessions(DEFAULT_PROMPT_CODE, keyword);
setSessions(data);
} catch (error) {
const message = error.message || "鑾峰彇 AI 浼氳瘽鍒楄〃澶辫触";
@@ -120,6 +130,16 @@
setUsage(null);
setDrawerError("");
};
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+ const timer = window.setTimeout(() => {
+ loadSessions(sessionKeyword);
+ }, 250);
+ return () => window.clearTimeout(timer);
+ }, [sessionKeyword, open]);
const handleSwitchSession = async (targetSessionId) => {
if (streaming || targetSessionId === sessionId) {
@@ -140,9 +160,52 @@
startNewSession();
await loadRuntime(null);
}
- await loadSessions();
+ await loadSessions(sessionKeyword);
} catch (error) {
const message = error.message || "鍒犻櫎 AI 浼氳瘽澶辫触";
+ setDrawerError(message);
+ notify(message, { type: "error" });
+ }
+ };
+
+ const handlePinSession = async (targetSessionId, pinned) => {
+ if (streaming || !targetSessionId) {
+ return;
+ }
+ try {
+ await pinAiSession(targetSessionId, pinned);
+ notify(pinned ? "浼氳瘽宸茬疆椤�" : "浼氳瘽宸插彇娑堢疆椤�");
+ await loadSessions(sessionKeyword);
+ } catch (error) {
+ const message = error.message || "鏇存柊浼氳瘽缃《鐘舵�佸け璐�";
+ setDrawerError(message);
+ notify(message, { type: "error" });
+ }
+ };
+
+ const openRenameDialog = (item) => {
+ setRenameDialog({
+ open: true,
+ sessionId: item?.sessionId || null,
+ title: item?.title || "",
+ });
+ };
+
+ const closeRenameDialog = () => {
+ setRenameDialog({ open: false, sessionId: null, title: "" });
+ };
+
+ const handleRenameSubmit = async () => {
+ if (streaming || !renameDialog.sessionId) {
+ return;
+ }
+ try {
+ await renameAiSession(renameDialog.sessionId, renameDialog.title);
+ notify("浼氳瘽宸查噸鍛藉悕");
+ closeRenameDialog();
+ await loadSessions(sessionKeyword);
+ } catch (error) {
+ const message = error.message || "閲嶅懡鍚嶄細璇濆け璐�";
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -254,7 +317,7 @@
if (completed) {
await Promise.all([
loadRuntime(completedSessionId),
- loadSessions(),
+ loadSessions(sessionKeyword),
]);
}
}
@@ -313,6 +376,17 @@
鏂板缓浼氳瘽
</Button>
</Stack>
+ <TextField
+ value={sessionKeyword}
+ onChange={(event) => setSessionKeyword(event.target.value)}
+ fullWidth
+ size="small"
+ placeholder="鎼滅储浼氳瘽鏍囬"
+ InputProps={{
+ startAdornment: <SearchOutlinedIcon fontSize="small" sx={{ mr: 1, color: "text.secondary" }} />,
+ }}
+ sx={{ mb: 1.25 }}
+ />
<Paper variant="outlined" sx={{ overflow: "hidden" }}>
{!sessions.length ? (
<Box px={1.5} py={1.25}>
@@ -332,16 +406,41 @@
>
<ListItemText
primary={item.title || `浼氳瘽 ${item.sessionId}`}
- secondary={item.lastMessageTime || `Session ${item.sessionId}`}
+ secondary={item.lastMessagePreview || item.lastMessageTime || `Session ${item.sessionId}`}
primaryTypographyProps={{
noWrap: true,
fontSize: 14,
+ fontWeight: item.pinned ? 700 : 400,
}}
secondaryTypographyProps={{
noWrap: true,
fontSize: 12,
}}
/>
+ <IconButton
+ size="small"
+ edge="end"
+ disabled={streaming}
+ onClick={(event) => {
+ event.stopPropagation();
+ handlePinSession(item.sessionId, !item.pinned);
+ }}
+ title={item.pinned ? "鍙栨秷缃《" : "缃《浼氳瘽"}
+ >
+ {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
+ </IconButton>
+ <IconButton
+ size="small"
+ edge="end"
+ disabled={streaming}
+ onClick={(event) => {
+ event.stopPropagation();
+ openRenameDialog(item);
+ }}
+ title="閲嶅懡鍚嶄細璇�"
+ >
+ <EditOutlinedIcon fontSize="small" />
+ </IconButton>
<IconButton
size="small"
edge="end"
@@ -476,6 +575,25 @@
</Box>
</Box>
</Box>
+ <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
+ <DialogTitle>閲嶅懡鍚嶄細璇�</DialogTitle>
+ <DialogContent>
+ <TextField
+ value={renameDialog.title}
+ onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
+ autoFocus
+ margin="dense"
+ label="浼氳瘽鏍囬"
+ fullWidth
+ />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={closeRenameDialog}>鍙栨秷</Button>
+ <Button onClick={handleRenameSubmit} variant="contained" disabled={streaming || !renameDialog.title.trim()}>
+ 淇濆瓨
+ </Button>
+ </DialogActions>
+ </Dialog>
</Drawer>
);
};
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
index 64e9f06..d31b241 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
@@ -1,6 +1,8 @@
package com.vincent.rsf.server.ai.controller;
import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
import com.vincent.rsf.server.ai.dto.AiChatRequest;
import com.vincent.rsf.server.ai.service.AiChatService;
import com.vincent.rsf.server.system.controller.BaseController;
@@ -30,8 +32,9 @@
@PreAuthorize("isAuthenticated()")
@GetMapping("/ai/chat/sessions")
- public R sessions(@RequestParam(required = false) String promptCode) {
- return R.ok().add(aiChatService.listSessions(promptCode, getLoginUserId(), getTenantId()));
+ public R sessions(@RequestParam(required = false) String promptCode,
+ @RequestParam(required = false) String keyword) {
+ return R.ok().add(aiChatService.listSessions(promptCode, keyword, getLoginUserId(), getTenantId()));
}
@PreAuthorize("isAuthenticated()")
@@ -42,6 +45,18 @@
}
@PreAuthorize("isAuthenticated()")
+ @PostMapping("/ai/chat/session/rename/{sessionId}")
+ public R renameSession(@PathVariable Long sessionId, @RequestBody AiChatSessionRenameRequest request) {
+ return R.ok("Update Success").add(aiChatService.renameSession(sessionId, request, getLoginUserId(), getTenantId()));
+ }
+
+ @PreAuthorize("isAuthenticated()")
+ @PostMapping("/ai/chat/session/pin/{sessionId}")
+ public R pinSession(@PathVariable Long sessionId, @RequestBody AiChatSessionPinRequest request) {
+ return R.ok("Update Success").add(aiChatService.pinSession(sessionId, request, getLoginUserId(), getTenantId()));
+ }
+
+ @PreAuthorize("isAuthenticated()")
@PostMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@RequestBody AiChatRequest request) {
String requestId = StringUtils.hasText(request.getRequestId())
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java
index a54b23c..0846691 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java
@@ -16,6 +16,10 @@
private String promptCode;
+ private Boolean pinned;
+
+ private String lastMessagePreview;
+
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date lastMessageTime;
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java
new file mode 100644
index 0000000..7d38ad6
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java
@@ -0,0 +1,9 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+@Data
+public class AiChatSessionPinRequest {
+
+ private Boolean pinned;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java
new file mode 100644
index 0000000..a90addc
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java
@@ -0,0 +1,9 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+@Data
+public class AiChatSessionRenameRequest {
+
+ private String title;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java
index 18a8bab..67188a8 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java
@@ -40,6 +40,9 @@
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date lastMessageTime;
+ @ApiModelProperty(value = "鏄惁缃《")
+ private Integer pinned;
+
@ApiModelProperty(value = "鐘舵��")
private Integer status;
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java
index 404f30b..db7af54 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java
@@ -3,6 +3,8 @@
import com.vincent.rsf.server.ai.dto.AiChatMemoryDto;
import com.vincent.rsf.server.ai.dto.AiChatMessageDto;
import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
import com.vincent.rsf.server.ai.entity.AiChatSession;
import java.util.List;
@@ -11,11 +13,15 @@
AiChatMemoryDto getMemory(Long userId, Long tenantId, String promptCode, Long sessionId);
- List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode);
+ List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword);
AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed);
void saveRound(AiChatSession session, Long userId, Long tenantId, List<AiChatMessageDto> memoryMessages, String assistantContent);
void removeSession(Long userId, Long tenantId, Long sessionId);
+
+ AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request);
+
+ AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request);
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
index 27ecf4f..ec4d2a9 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
@@ -3,6 +3,8 @@
import com.vincent.rsf.server.ai.dto.AiChatRequest;
import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto;
import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
@@ -11,9 +13,13 @@
AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long userId, Long tenantId);
- List<AiChatSessionDto> listSessions(String promptCode, Long userId, Long tenantId);
+ List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId);
SseEmitter stream(AiChatRequest request, Long userId, Long tenantId);
void removeSession(Long sessionId, Long userId, Long tenantId);
+
+ AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId);
+
+ AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId);
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
index b663a6a..c0a4a61 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
@@ -5,6 +5,8 @@
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.ai.dto.AiChatMemoryDto;
import com.vincent.rsf.server.ai.dto.AiChatMessageDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
import com.vincent.rsf.server.ai.entity.AiChatMessage;
import com.vincent.rsf.server.ai.entity.AiChatSession;
@@ -19,6 +21,7 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.Locale;
@Service
@RequiredArgsConstructor
@@ -47,7 +50,7 @@
}
@Override
- public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode) {
+ public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword) {
ensureIdentity(userId, tenantId);
String resolvedPromptCode = requirePromptCode(promptCode);
List<AiChatSession> sessions = aiChatSessionMapper.selectList(new LambdaQueryWrapper<AiChatSession>()
@@ -56,6 +59,8 @@
.eq(AiChatSession::getPromptCode, resolvedPromptCode)
.eq(AiChatSession::getDeleted, 0)
.eq(AiChatSession::getStatus, StatusType.ENABLE.val)
+ .like(StringUtils.hasText(keyword), AiChatSession::getTitle, keyword == null ? null : keyword.trim())
+ .orderByDesc(AiChatSession::getPinned)
.orderByDesc(AiChatSession::getLastMessageTime)
.orderByDesc(AiChatSession::getId));
if (Cools.isEmpty(sessions)) {
@@ -63,12 +68,7 @@
}
List<AiChatSessionDto> result = new ArrayList<>();
for (AiChatSession session : sessions) {
- result.add(AiChatSessionDto.builder()
- .sessionId(session.getId())
- .title(session.getTitle())
- .promptCode(session.getPromptCode())
- .lastMessageTime(session.getLastMessageTime())
- .build());
+ result.add(buildSessionDto(session));
}
return result;
}
@@ -87,6 +87,7 @@
.setUserId(userId)
.setTenantId(tenantId)
.setLastMessageTime(now)
+ .setPinned(0)
.setStatus(StatusType.ENABLE.val)
.setDeleted(0)
.setCreateBy(userId)
@@ -157,6 +158,40 @@
}
}
+ @Override
+ public AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request) {
+ ensureIdentity(userId, tenantId);
+ if (request == null || !StringUtils.hasText(request.getTitle())) {
+ throw new CoolException("浼氳瘽鏍囬涓嶈兘涓虹┖");
+ }
+ AiChatSession session = requireOwnedSession(sessionId, userId, tenantId);
+ Date now = new Date();
+ AiChatSession update = new AiChatSession()
+ .setId(sessionId)
+ .setTitle(buildSessionTitle(request.getTitle()))
+ .setUpdateBy(userId)
+ .setUpdateTime(now);
+ aiChatSessionMapper.updateById(update);
+ return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+ }
+
+ @Override
+ public AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request) {
+ ensureIdentity(userId, tenantId);
+ if (request == null || request.getPinned() == null) {
+ throw new CoolException("缃《鐘舵�佷笉鑳戒负绌�");
+ }
+ AiChatSession session = requireOwnedSession(sessionId, userId, tenantId);
+ Date now = new Date();
+ AiChatSession update = new AiChatSession()
+ .setId(sessionId)
+ .setPinned(Boolean.TRUE.equals(request.getPinned()) ? 1 : 0)
+ .setUpdateBy(userId)
+ .setUpdateTime(now);
+ aiChatSessionMapper.updateById(update);
+ return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+ }
+
private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) {
return aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>()
.eq(AiChatSession::getUserId, userId)
@@ -175,6 +210,23 @@
.eq(AiChatSession::getUserId, userId)
.eq(AiChatSession::getTenantId, tenantId)
.eq(AiChatSession::getPromptCode, promptCode)
+ .eq(AiChatSession::getDeleted, 0)
+ .eq(AiChatSession::getStatus, StatusType.ENABLE.val)
+ .last("limit 1"));
+ if (session == null) {
+ throw new CoolException("AI 浼氳瘽涓嶅瓨鍦ㄦ垨鏃犳潈璁块棶");
+ }
+ return session;
+ }
+
+ private AiChatSession requireOwnedSession(Long sessionId, Long userId, Long tenantId) {
+ if (sessionId == null) {
+ throw new CoolException("AI 浼氳瘽 ID 涓嶈兘涓虹┖");
+ }
+ AiChatSession session = aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>()
+ .eq(AiChatSession::getId, sessionId)
+ .eq(AiChatSession::getUserId, userId)
+ .eq(AiChatSession::getTenantId, tenantId)
.eq(AiChatSession::getDeleted, 0)
.eq(AiChatSession::getStatus, StatusType.ENABLE.val)
.last("limit 1"));
@@ -266,8 +318,57 @@
if (!StringUtils.hasText(titleSeed)) {
throw new CoolException("AI 浼氳瘽鏍囬涓嶈兘涓虹┖");
}
- String title = titleSeed.trim().replace("\r", " ").replace("\n", " ");
- return title.length() > 60 ? title.substring(0, 60) : title;
+ String title = titleSeed.trim()
+ .replace("\r", " ")
+ .replace("\n", " ")
+ .replaceAll("\\s+", " ");
+ int punctuationIndex = findSummaryBreakIndex(title);
+ if (punctuationIndex > 0) {
+ title = title.substring(0, punctuationIndex).trim();
+ }
+ return title.length() > 48 ? title.substring(0, 48) : title;
+ }
+
+ private int findSummaryBreakIndex(String title) {
+ String[] separators = {"銆�", "锛�", "锛�", ".", "!", "?"};
+ int result = -1;
+ for (String separator : separators) {
+ int index = title.indexOf(separator);
+ if (index > 0 && (result < 0 || index < result)) {
+ result = index;
+ }
+ }
+ return result;
+ }
+
+ private AiChatSessionDto buildSessionDto(AiChatSession session) {
+ AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>()
+ .eq(AiChatMessage::getSessionId, session.getId())
+ .eq(AiChatMessage::getDeleted, 0)
+ .orderByDesc(AiChatMessage::getSeqNo)
+ .orderByDesc(AiChatMessage::getId)
+ .last("limit 1"));
+ return AiChatSessionDto.builder()
+ .sessionId(session.getId())
+ .title(session.getTitle())
+ .promptCode(session.getPromptCode())
+ .pinned(session.getPinned() != null && session.getPinned() == 1)
+ .lastMessagePreview(buildLastMessagePreview(lastMessage))
+ .lastMessageTime(session.getLastMessageTime())
+ .build();
+ }
+
+ private String buildLastMessagePreview(AiChatMessage message) {
+ if (message == null || !StringUtils.hasText(message.getContent())) {
+ return null;
+ }
+ String preview = message.getContent().trim()
+ .replace("\r", " ")
+ .replace("\n", " ")
+ .replaceAll("\\s+", " ");
+ String prefix = "assistant".equalsIgnoreCase(message.getRole()) ? "AI: " : "浣�: ";
+ String normalized = prefix + preview;
+ return normalized.length() > 80 ? normalized.substring(0, 80) : normalized;
}
private void ensureIdentity(Long userId, Long tenantId) {
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
index 05dc09b..280914f 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
@@ -12,6 +12,8 @@
import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto;
import com.vincent.rsf.server.ai.dto.AiChatStatusDto;
import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
import com.vincent.rsf.server.ai.dto.AiResolvedConfig;
import com.vincent.rsf.server.ai.entity.AiParam;
import com.vincent.rsf.server.ai.entity.AiPrompt;
@@ -98,9 +100,9 @@
}
@Override
- public List<AiChatSessionDto> listSessions(String promptCode, Long userId, Long tenantId) {
+ public List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId) {
AiResolvedConfig config = aiConfigResolverService.resolve(promptCode, tenantId);
- return aiChatMemoryService.listSessions(userId, tenantId, config.getPromptCode());
+ return aiChatMemoryService.listSessions(userId, tenantId, config.getPromptCode(), keyword);
}
@Override
@@ -109,6 +111,16 @@
}
@Override
+ public AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId) {
+ return aiChatMemoryService.renameSession(userId, tenantId, sessionId, request);
+ }
+
+ @Override
+ public AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId) {
+ return aiChatMemoryService.pinSession(userId, tenantId, sessionId, request);
+ }
+
+ @Override
public SseEmitter stream(AiChatRequest request, Long userId, Long tenantId) {
SseEmitter emitter = new SseEmitter(AiDefaults.SSE_TIMEOUT_MS);
CompletableFuture.runAsync(() -> doStream(request, userId, tenantId, emitter), aiChatTaskExecutor);
diff --git a/version/db/ai_feature.sql b/version/db/ai_feature.sql
index d5cd06f..529ea30 100644
--- a/version/db/ai_feature.sql
+++ b/version/db/ai_feature.sql
@@ -76,6 +76,7 @@
`user_id` bigint(20) NOT NULL COMMENT '鐢ㄦ埛 ID',
`tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛',
`last_message_time` datetime DEFAULT NULL COMMENT '鏈�鍚庢秷鎭椂闂�',
+ `pinned` tinyint(1) DEFAULT '0' COMMENT '鏄惁缃《',
`status` int(11) DEFAULT '1' COMMENT '鐘舵��',
`deleted` int(11) DEFAULT '0' COMMENT '鍒犻櫎鏍囪',
`create_time` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
@@ -117,6 +118,22 @@
EXECUTE builtin_code_stmt;
DEALLOCATE PREPARE builtin_code_stmt;
+SET @chat_session_pinned_exists := (
+ SELECT COUNT(1)
+ FROM `information_schema`.`COLUMNS`
+ WHERE `TABLE_SCHEMA` = DATABASE()
+ AND `TABLE_NAME` = 'sys_ai_chat_session'
+ AND `COLUMN_NAME` = 'pinned'
+);
+SET @chat_session_pinned_sql := IF(
+ @chat_session_pinned_exists = 0,
+ 'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `pinned` tinyint(1) DEFAULT ''0'' COMMENT ''鏄惁缃《'' AFTER `last_message_time`',
+ 'SELECT 1'
+);
+PREPARE chat_session_pinned_stmt FROM @chat_session_pinned_sql;
+EXECUTE chat_session_pinned_stmt;
+DEALLOCATE PREPARE chat_session_pinned_stmt;
+
BEGIN;
INSERT INTO `sys_ai_prompt`
(`id`, `name`, `code`, `scene`, `system_prompt`, `user_prompt_template`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
--
Gitblit v1.9.1