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
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()
|
|
));
|
|
}
|
|
}
|
|
|
|
}
|