本地 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.
 
 
 
 
 

307 lines
12 KiB

package com.wok.supportbot.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@Slf4j
public class CustomerServiceRoleService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ObjectMapper objectMapper;
public List<Map<String, Object>> listRoles() {
return listRoles(false);
}
/**
* @param includeDisabled true=包含已停用角色(管理页用);false=仅启用角色(对话页用)
*/
public List<Map<String, Object>> listRoles(boolean includeDisabled) {
StringBuilder roleSql = new StringBuilder(
"SELECT id::text AS id, role_key, name, description, prompt, enabled, allowed_mcp_tools "
+ "FROM customer_service_role WHERE is_delete = false ");
if (!includeDisabled) {
roleSql.append("AND enabled = true ");
}
roleSql.append("ORDER BY id ASC");
List<Map<String, Object>> roles = jdbcTemplate.queryForList(roleSql.toString());
// 查询角色-分类关联
String categorySql = """
SELECT rc.role_id::text AS role_id, rc.category_id::text AS category_id, c.name AS category_name
FROM customer_service_role_category rc
LEFT JOIN knowledge_category c ON c.id = rc.category_id AND c.is_delete = false
WHERE rc.is_delete = false
ORDER BY rc.role_id ASC, rc.id ASC
""";
List<Map<String, Object>> relations = jdbcTemplate.queryForList(categorySql);
Map<String, List<String>> categoryIdsByRole = new LinkedHashMap<>();
Map<String, List<Map<String, Object>>> categoriesByRole = new LinkedHashMap<>();
for (Map<String, Object> relation : relations) {
String roleId = String.valueOf(relation.get("role_id"));
String categoryId = String.valueOf(relation.get("category_id"));
categoryIdsByRole.computeIfAbsent(roleId, key -> new ArrayList<>()).add(categoryId);
categoriesByRole.computeIfAbsent(roleId, key -> new ArrayList<>()).add(Map.of(
"id", categoryId,
"name", Objects.toString(relation.get("category_name"), "")
));
}
for (Map<String, Object> role : roles) {
String roleId = String.valueOf(role.get("id"));
role.put("categoryIds", categoryIdsByRole.getOrDefault(roleId, List.of()));
role.put("categories", categoriesByRole.getOrDefault(roleId, List.of()));
}
return roles;
}
@Transactional(rollbackFor = Exception.class)
public void updateRoleCategories(Long roleId, List<Long> categoryIds) {
jdbcTemplate.update("UPDATE customer_service_role_category SET is_delete = true WHERE role_id = ?", roleId);
if (categoryIds == null || categoryIds.isEmpty()) {
return;
}
List<Long> normalizedIds = categoryIds.stream()
.filter(Objects::nonNull)
.filter(id -> id > 0)
.distinct()
.toList();
for (Long categoryId : normalizedIds) {
jdbcTemplate.update("""
INSERT INTO customer_service_role_category (role_id, category_id, is_delete)
VALUES (?, ?, false)
ON CONFLICT (role_id, category_id)
DO UPDATE SET is_delete = false
""", roleId, categoryId);
}
}
/**
* 新增角色。role_key 缺省时自动生成。
*/
@Transactional(rollbackFor = Exception.class)
public void createRole(String roleKey, String name, String description, String prompt, Boolean enabled) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("角色名称不能为空");
}
String key = (roleKey == null || roleKey.trim().isEmpty())
? "role_" + System.currentTimeMillis()
: roleKey.trim();
jdbcTemplate.update("""
INSERT INTO customer_service_role (role_key, name, description, prompt, enabled)
VALUES (?, ?, ?, ?, ?)
""",
key, name.trim(), description, prompt,
enabled != null ? enabled : true);
}
/**
* 编辑角色基本信息(不含知识库分类,分类走 {@link #updateRoleCategories})。
*/
@Transactional(rollbackFor = Exception.class)
public void updateRole(Long roleId, String name, String description, String prompt, Boolean enabled) {
if (roleId == null || roleId <= 0) {
throw new IllegalArgumentException("角色ID无效");
}
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("角色名称不能为空");
}
jdbcTemplate.update("""
UPDATE customer_service_role
SET name = ?, description = ?, prompt = ?, enabled = ?, update_time = CURRENT_TIMESTAMP
WHERE id = ? AND is_delete = false
""",
name.trim(), description, prompt,
enabled != null ? enabled : true,
roleId);
}
/**
* 逻辑删除角色,并连带逻辑删除其知识库分类关联。
*/
@Transactional(rollbackFor = Exception.class)
public void deleteRole(Long roleId) {
if (roleId == null || roleId <= 0) {
return;
}
jdbcTemplate.update("UPDATE customer_service_role SET is_delete = true, update_time = CURRENT_TIMESTAMP WHERE id = ?", roleId);
jdbcTemplate.update("UPDATE customer_service_role_category SET is_delete = true WHERE role_id = ?", roleId);
}
/**
* 更新角色的 MCP 工具权限。
*
* @param roleId 角色ID
* @param allowedMcpTools 允许的 MCP 工具名列表;["*"]=全部允许,空/null=不允许任何工具
*/
@Transactional(rollbackFor = Exception.class)
public void updateRoleMcpTools(Long roleId, List<String> allowedMcpTools) {
if (roleId == null || roleId <= 0) {
throw new IllegalArgumentException("角色ID无效");
}
String json;
try {
json = objectMapper.writeValueAsString(allowedMcpTools != null ? allowedMcpTools : List.of());
} catch (Exception e) {
json = "[]";
}
jdbcTemplate.update(
"UPDATE customer_service_role SET allowed_mcp_tools = ?::jsonb, update_time = CURRENT_TIMESTAMP WHERE id = ? AND is_delete = false",
json, roleId);
}
/**
* 服务端解析角色的知识库范围与人设。
* 用于对话时强制约束:角色只能检索其绑定分类下的内容,由后端决定,客户端无法越权跨域。
*
* @param roleId 角色ID
* @return 角色范围;角色不存在或被禁用时返回 {@link RoleScope#empty()}
*/
public RoleScope getRoleScope(Long roleId) {
if (roleId == null || roleId <= 0) {
return RoleScope.empty();
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT name, prompt, allowed_mcp_tools FROM customer_service_role WHERE id = ? AND is_delete = false AND enabled = true",
roleId);
if (rows.isEmpty()) {
return RoleScope.empty();
}
String name = Objects.toString(rows.get(0).get("name"), "");
String prompt = Objects.toString(rows.get(0).get("prompt"), "");
List<Long> categoryIds = jdbcTemplate.queryForList(
"SELECT category_id FROM customer_service_role_category WHERE role_id = ? AND is_delete = false",
Long.class, roleId);
// 解析 allowed_mcp_tools JSONB 字段
List<String> allowedMcpTools = parseAllowedMcpTools(rows.get(0).get("allowed_mcp_tools"));
return new RoleScope(true, name, prompt, categoryIds, allowedMcpTools);
}
/**
* 解析 allowed_mcp_tools JSONB 字段为 List<String>
* 兼容 PostgreSQL JSONB、Java List、String 等多种输入类型
*/
private List<String> parseAllowedMcpTools(Object raw) {
if (raw == null) {
return List.of();
}
try {
if (raw instanceof List<?> list) {
return list.stream().map(Objects::toString).toList();
}
String json = raw.toString().trim();
if (json.isEmpty() || "[]".equals(json) || "null".equals(json)) {
return List.of();
}
return objectMapper.readValue(json, new TypeReference<>() {});
} catch (Exception e) {
log.warn("解析 allowed_mcp_tools 失败: {}", raw, e);
return List.of();
}
}
/**
* 精简角色信息(Token 换取接口用)
*/
public record RoleBrief(Long id, String roleKey, String name) {}
/**
* 查询所有启用的客服角色(精简字段)
*
* @return 精简角色信息列表
*/
public List<RoleBrief> listEnabledRolesBrief() {
String sql = "SELECT id, role_key, name FROM customer_service_role " +
"WHERE enabled = true AND is_delete = false ORDER BY id";
return jdbcTemplate.query(sql, (rs, i) -> new RoleBrief(
rs.getLong("id"),
rs.getString("role_key"),
rs.getString("name")
));
}
/**
* 根据 ID 列表查询启用的客服角色(精简字段,用于 API Key 角色绑定的 Token 换取)
*
* @param roleIds 角色 ID 列表
* @return 精简角色信息列表
*/
public List<RoleBrief> listRolesByIds(List<Long> roleIds) {
if (roleIds == null || roleIds.isEmpty()) {
return List.of();
}
String placeholders = roleIds.stream().map(id -> "?").collect(Collectors.joining(","));
String sql = "SELECT id, role_key, name FROM customer_service_role " +
"WHERE enabled = true AND is_delete = false AND id IN (" + placeholders + ") ORDER BY id";
return jdbcTemplate.query(sql, (rs, i) -> new RoleBrief(
rs.getLong("id"),
rs.getString("role_key"),
rs.getString("name")
), roleIds.toArray());
}
/**
* 角色范围:是否命中角色、角色人设、可检索的知识库分类、MCP 工具权限。
*/
public record RoleScope(boolean present, String name, String prompt, List<Long> categoryIds, List<String> allowedMcpTools) {
public static RoleScope empty() {
return new RoleScope(false, "", "", List.of(), List.of());
}
public boolean hasRole() {
return present;
}
/** 是否允许使用 MCP 工具(非空即允许) */
public boolean isMcpToolsAllowed() {
return allowedMcpTools != null && !allowedMcpTools.isEmpty();
}
/** 是否允许使用所有 MCP 工具(["*"] 表示全部允许) */
public boolean isAllMcpToolsAllowed() {
return allowedMcpTools != null && allowedMcpTools.contains("*");
}
/**
* 组合角色系统提示词:当前客服角色 + 人设;都为空时返回 null(退回基础提示词)。
*/
public String systemPrompt() {
if (!present) {
return null;
}
String trimmedName = name == null ? "" : name.trim();
String trimmedPrompt = prompt == null ? "" : prompt.trim();
if (trimmedName.isEmpty() && trimmedPrompt.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
// if (!trimmedName.isEmpty()) {
// sb.append("当前客服角色:").append(trimmedName).append("。");
// }
if (!trimmedPrompt.isEmpty()) {
sb.append(trimmedPrompt);
}
return sb.toString();
}
}
}