本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

260 lines
10 KiB

package com.wok.supportbot.controller;
import com.wok.supportbot.app.SuggestionGenerator;
import com.wok.supportbot.cache.SuggestionCache;
import com.wok.supportbot.service.ConversationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 会话管理控制器
* 提供会话列表、详情、消息、删除、导出等 API
*/
@RestController
@Slf4j
public class ConversationController {
@Autowired
private ConversationService conversationService;
@Autowired
private SuggestionCache suggestionCache;
@Autowired
private SuggestionGenerator suggestionGenerator;
// ==================== 会话列表 ====================
/**
* 获取会话列表(分页)
* 仅限管理员和客服人员使用,SDK 端应调用 /ai/sdk/conversation/list
*
* @param page 页码(默认1)
* @param size 每页大小(默认10)
* @param keyword 搜索关键词(可选,按会话ID或消息内容模糊匹配)
* @return 分页会话列表
*/
@GetMapping("/conversation/list")
@PreAuthorize("hasAnyRole('admin','cs_agent','viewer')")
public ResponseEntity<Map<String, Object>> listConversations(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String accountId,
@RequestParam(required = false) Long roleId,
@RequestParam(required = false) String sortField,
@RequestParam(required = false) String sortOrder) {
try {
Map<String, Object> result = conversationService.listConversations(page, size, keyword, accountId, roleId, sortField, sortOrder);
Map<String, Object> data = new java.util.HashMap<>();
data.put("success", true);
data.put("data", result.get("records"));
data.put("total", result.get("total"));
data.put("page", result.get("page"));
data.put("size", result.get("size"));
data.put("pages", result.get("pages"));
return ResponseEntity.ok(data);
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
// ==================== 会话详情 ====================
/**
* 截断会话:删除指定用户消息及其之后的全部消息(用于"编辑历史消息并重发")。
* 仅限管理员和客服操作,SDK 端应调用 /ai/sdk/conversation/{id}/truncate
*
* @param conversationId 会话ID
* @param body { userTurn: 第几条用户消息(1-based) }
* @return 逻辑删除的消息条数
*/
@PostMapping("/conversation/{id}/truncate")
@PreAuthorize("hasAnyRole('admin','cs_agent')")
public ResponseEntity<Map<String, Object>> truncateConversation(
@PathVariable("id") String conversationId,
@RequestBody Map<String, Object> body) {
try {
Object raw = body.get("userTurn");
int userTurn = raw instanceof Number number ? number.intValue() : Integer.parseInt(String.valueOf(raw));
int deleted = conversationService.truncateFromUserTurn(conversationId, userTurn);
return ResponseEntity.ok(Map.of(
"success", true,
"deletedMessages", deleted
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "截断失败:" + e.getMessage()
));
}
}
/**
* 获取会话详情
*
* @param conversationId 会话ID
* @return 会话详情
*/
@GetMapping("/conversation/{id}")
@PreAuthorize("hasAnyRole('admin','cs_agent','viewer','kb_operator')")
public ResponseEntity<Map<String, Object>> getConversationDetail(@PathVariable("id") String conversationId) {
try {
Map<String, Object> detail = conversationService.getConversationDetail(conversationId);
if ((int) detail.get("messageCount") == 0) {
return ResponseEntity.status(404).body(Map.of(
"success", false,
"message", "会话不存在或无任何消息"
));
}
return ResponseEntity.ok(Map.of(
"success", true,
"data", detail
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 获取会话消息列表
* 仅限管理员和客服查看,SDK 端应调用 /ai/sdk/conversation/{id}/messages
*
* @param conversationId 会话ID
* @return 消息列表
*/
@GetMapping("/conversation/{id}/messages")
@PreAuthorize("hasAnyRole('admin','cs_agent','viewer')")
public ResponseEntity<Map<String, Object>> getConversationMessages(@PathVariable("id") String conversationId) {
try {
List<Map<String, Object>> messages = conversationService.getConversationMessages(conversationId);
return ResponseEntity.ok(Map.of(
"success", true,
"data", messages,
"total", messages.size()
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 获取会话的 AI 推荐问题列表(管理后台 ChatPanel 用,JwtAuthFilter 守卫)。
* <p>
* 与 /ai/suggestions 和 /open-api/suggestions 共享同一 SuggestionCache 实例,
* AI 主回复不再同步生成 suggestions,本接口按需异步生成。
*
* @param conversationId 会话 ID(即 chatId)
* @return { success: true, data: { suggestions: [...] } }
*/
@GetMapping("/conversation/{id}/suggestions")
@PreAuthorize("hasAnyRole('admin','cs_agent','viewer')")
public ResponseEntity<Map<String, Object>> getConversationSuggestions(@PathVariable("id") String conversationId) {
try {
List<String> suggestions = suggestionGenerator.generate(conversationId);
return ResponseEntity.ok(Map.of(
"success", true,
"data", Map.of("suggestions", suggestions)
));
} catch (Exception e) {
log.error("获取会话建议问题失败: conversationId={}", conversationId, e);
return ResponseEntity.ok(Map.of(
"success", true,
"data", Map.of("suggestions", List.of())
));
}
}
// ==================== 删除会话 ====================
/**
* 删除会话(逻辑删除该会话下的所有消息)
* 仅限管理员操作
*
* @param conversationId 会话ID
* @return 删除结果
*/
@DeleteMapping("/conversation/{id}")
@PreAuthorize("hasAnyRole('admin','cs_agent')")
public ResponseEntity<Map<String, Object>> deleteConversation(@PathVariable("id") String conversationId) {
try {
int count = conversationService.deleteConversation(conversationId);
// 删除会话时同步清理推荐问题缓存
suggestionCache.evict(conversationId);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "会话删除成功",
"deletedMessages", count
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "删除失败:" + e.getMessage()
));
}
}
// ==================== 导出会话 ====================
/**
* 导出会话记录为文本
* 仅限管理员和客服使用
*
* @param conversationId 会话ID
* @return 格式化后的会话文本
*/
@GetMapping(value = "/conversation/{id}/export", produces = MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8")
@PreAuthorize("hasAnyRole('admin','cs_agent','viewer')")
public ResponseEntity<String> exportConversation(@PathVariable("id") String conversationId) {
try {
String content = conversationService.exportConversation(conversationId);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=conversation_" + conversationId + ".txt")
.body(content);
} catch (Exception e) {
return ResponseEntity.status(500).body("导出失败:" + e.getMessage());
}
}
// ==================== 统计 ====================
/**
* 获取会话统计信息
* 仅限管理员和客服使用
*
* @return 统计信息
*/
@GetMapping("/conversation/stats")
@PreAuthorize("hasAnyRole('admin','cs_agent','viewer')")
public ResponseEntity<Map<String, Object>> getConversationStats() {
try {
Map<String, Object> stats = conversationService.getConversationStats();
return ResponseEntity.ok(Map.of(
"success", true,
"data", stats
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询统计失败:" + e.getMessage()
));
}
}
}