feat: 收口聊天时知识库工具可见性
- 新增 chatTime 工具可见性抽象与知识库 resolver - 聊天装配链路按当前用户过滤知识库工具并补齐调用兜底 - 补充聊天时显式登录快照与对应后端测试
This commit is contained in:
@@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.publish.BotPublishAppService;
|
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||||
@@ -407,23 +408,32 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
LoginAccount account = requireCurrentLoginAccount();
|
||||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
context.setChannel(ChatChannel.ADMIN);
|
context.setChannel(ChatChannel.ADMIN);
|
||||||
context.setSessionId(conversationId);
|
context.setSessionId(conversationId);
|
||||||
context.setTenantId(account == null ? BigInteger.ZERO : account.getTenantId());
|
context.setTenantId(account.getTenantId());
|
||||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
context.setDeptId(account.getDeptId());
|
||||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
context.setUserId(account.getId());
|
||||||
context.setUserAccount(account == null ? "admin" : account.getLoginName());
|
context.setUserAccount(account.getLoginName());
|
||||||
context.setUserName(account == null ? "管理员" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
context.setUserName(StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName());
|
||||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
context.setAttachments(attachments);
|
context.setAttachments(attachments);
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoginAccount requireCurrentLoginAccount() {
|
||||||
|
try {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
@@ -287,24 +288,33 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
LoginAccount account = requireCurrentLoginAccount();
|
||||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
context.setChannel(ChatChannel.USER_CENTER);
|
context.setChannel(ChatChannel.USER_CENTER);
|
||||||
context.setSessionId(conversationId);
|
context.setSessionId(conversationId);
|
||||||
context.setTenantId(account == null ? BigInteger.ZERO : account.getTenantId());
|
context.setTenantId(account.getTenantId());
|
||||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
context.setDeptId(account.getDeptId());
|
||||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
context.setUserId(account.getId());
|
||||||
context.setUserAccount(account == null ? "anonymous" : account.getLoginName());
|
context.setUserAccount(account.getLoginName());
|
||||||
context.setUserName(account == null ? "匿名用户" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
context.setUserName(StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName());
|
||||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
context.setAnonymous(account == null || BigInteger.ZERO.equals(account.getId()));
|
context.setAnonymous(false);
|
||||||
context.setAttachments(attachments);
|
context.setAttachments(attachments);
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoginAccount requireCurrentLoginAccount() {
|
||||||
|
try {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> getDefaultLlmOptions() {
|
private Map<String, Object> getDefaultLlmOptions() {
|
||||||
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
||||||
defaultLlmOptions.put("temperature", 0.7);
|
defaultLlmOptions.put("temperature", 0.7);
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库聊天时可用性判定器。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ChatTimeKnowledgeAvailabilityResolver implements ChatTimeToolAvailabilityResolver {
|
||||||
|
|
||||||
|
private final KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
private final CategoryPermissionService categoryPermissionService;
|
||||||
|
private final SysDeptService sysDeptService;
|
||||||
|
|
||||||
|
public ChatTimeKnowledgeAvailabilityResolver(KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper,
|
||||||
|
CategoryPermissionService categoryPermissionService,
|
||||||
|
SysDeptService sysDeptService) {
|
||||||
|
this.knowledgeVisibilityQueryHelper = knowledgeVisibilityQueryHelper;
|
||||||
|
this.categoryPermissionService = categoryPermissionService;
|
||||||
|
this.sysDeptService = sysDeptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean supports(Object candidate) {
|
||||||
|
return candidate instanceof DocumentCollection || candidate instanceof BotDocumentCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ChatTimeToolAvailabilityDecision resolve(ChatTimeToolAvailabilityContext context, Object candidate) {
|
||||||
|
LoginAccount loginAccount = context == null ? null : context.getLoginAccount();
|
||||||
|
if (loginAccount == null || loginAccount.getId() == null) {
|
||||||
|
return ChatTimeToolAvailabilityDecision.unavailable("CHAT_TIME_LOGIN_ACCOUNT_MISSING", "聊天上下文缺少当前用户身份");
|
||||||
|
}
|
||||||
|
DocumentCollection knowledge = extractKnowledge(candidate);
|
||||||
|
if (knowledge == null) {
|
||||||
|
return ChatTimeToolAvailabilityDecision.unavailable("CHAT_TIME_KNOWLEDGE_MISSING", "聊天绑定的知识库不存在");
|
||||||
|
}
|
||||||
|
KnowledgeReadAccessSnapshot readSnapshot = buildReadSnapshot(loginAccount);
|
||||||
|
if (knowledgeVisibilityQueryHelper.canRead(knowledge, readSnapshot)) {
|
||||||
|
return ChatTimeToolAvailabilityDecision.available();
|
||||||
|
}
|
||||||
|
return ChatTimeToolAvailabilityDecision.unavailable("CHAT_TIME_KNOWLEDGE_FORBIDDEN", "当前用户无权在聊天中访问该知识库");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于显式登录快照构造知识库读权限快照,避免依赖线程登录态。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前聊天用户
|
||||||
|
* @return 读权限快照
|
||||||
|
*/
|
||||||
|
protected KnowledgeReadAccessSnapshot buildReadSnapshot(LoginAccount loginAccount) {
|
||||||
|
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getAccess(
|
||||||
|
CategoryResourceType.KNOWLEDGE.getCode(),
|
||||||
|
loginAccount
|
||||||
|
);
|
||||||
|
if (categoryAccess.isSuperAdmin()) {
|
||||||
|
return new KnowledgeReadAccessSnapshot(categoryAccess, Collections.emptySet());
|
||||||
|
}
|
||||||
|
BigInteger deptId = loginAccount.getDeptId();
|
||||||
|
Set<BigInteger> readableDeptIds = deptId == null
|
||||||
|
? Collections.emptySet()
|
||||||
|
: sysDeptService.getSelfAndAncestorDeptIds(deptId);
|
||||||
|
return new KnowledgeReadAccessSnapshot(categoryAccess, readableDeptIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从聊天工具候选项中提取知识库实体。
|
||||||
|
*
|
||||||
|
* @param candidate 候选项
|
||||||
|
* @return 知识库实体
|
||||||
|
*/
|
||||||
|
protected DocumentCollection extractKnowledge(Object candidate) {
|
||||||
|
if (candidate instanceof DocumentCollection documentCollection) {
|
||||||
|
return documentCollection;
|
||||||
|
}
|
||||||
|
if (candidate instanceof BotDocumentCollection botDocumentCollection) {
|
||||||
|
return botDocumentCollection.getKnowledge();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天时工具可用性判定上下文。
|
||||||
|
*/
|
||||||
|
public class ChatTimeToolAvailabilityContext implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时上下文中保存聊天时权限快照的扩展字段 key。
|
||||||
|
*/
|
||||||
|
public static final String RUNTIME_EXT_KEY = "chatTimeToolAvailabilityContext";
|
||||||
|
|
||||||
|
private LoginAccount loginAccount;
|
||||||
|
|
||||||
|
private Bot bot;
|
||||||
|
|
||||||
|
private ChatChannel chatChannel;
|
||||||
|
|
||||||
|
private BigInteger sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前登录用户快照绑定到运行时上下文。
|
||||||
|
*
|
||||||
|
* @param runtimeContext 聊天运行时上下文
|
||||||
|
* @param loginAccount 登录用户快照
|
||||||
|
* @param bot 当前聊天助手
|
||||||
|
*/
|
||||||
|
public static void bindLoggedInSnapshot(ChatRuntimeContext runtimeContext, LoginAccount loginAccount, Bot bot) {
|
||||||
|
if (!hasLoggedInAccount(loginAccount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext = new ChatTimeToolAvailabilityContext();
|
||||||
|
chatTimeContext.setLoginAccount(loginAccount);
|
||||||
|
chatTimeContext.setBot(bot);
|
||||||
|
chatTimeContext.setChatChannel(runtimeContext == null ? null : runtimeContext.getChannel());
|
||||||
|
chatTimeContext.setSessionId(runtimeContext == null ? null : runtimeContext.getSessionId());
|
||||||
|
chatTimeContext.bindToRuntimeContext(runtimeContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为可用于聊天态权限判定的登录用户。
|
||||||
|
*
|
||||||
|
* @param loginAccount 登录用户快照
|
||||||
|
* @return 是否为有效登录用户
|
||||||
|
*/
|
||||||
|
public static boolean hasLoggedInAccount(LoginAccount loginAccount) {
|
||||||
|
return loginAccount != null
|
||||||
|
&& loginAccount.getId() != null
|
||||||
|
&& !BigInteger.ZERO.equals(loginAccount.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定到聊天运行时上下文,供异步链路显式透传。
|
||||||
|
*
|
||||||
|
* @param runtimeContext 聊天运行时上下文
|
||||||
|
*/
|
||||||
|
public void bindToRuntimeContext(ChatRuntimeContext runtimeContext) {
|
||||||
|
if (runtimeContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> ext = runtimeContext.getExt();
|
||||||
|
ext.put(RUNTIME_EXT_KEY, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从聊天运行时上下文中读取聊天时权限上下文。
|
||||||
|
*
|
||||||
|
* @param runtimeContext 聊天运行时上下文
|
||||||
|
* @return 聊天时权限上下文,不存在时返回 null
|
||||||
|
*/
|
||||||
|
public static ChatTimeToolAvailabilityContext fromRuntimeContext(ChatRuntimeContext runtimeContext) {
|
||||||
|
if (runtimeContext == null || runtimeContext.getExt() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object value = runtimeContext.getExt().get(RUNTIME_EXT_KEY);
|
||||||
|
if (value instanceof ChatTimeToolAvailabilityContext context) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoginAccount getLoginAccount() {
|
||||||
|
return loginAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoginAccount(LoginAccount loginAccount) {
|
||||||
|
this.loginAccount = loginAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bot getBot() {
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBot(Bot bot) {
|
||||||
|
this.bot = bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatChannel getChatChannel() {
|
||||||
|
return chatChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatChannel(ChatChannel chatChannel) {
|
||||||
|
this.chatChannel = chatChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(BigInteger sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天时工具可用性判定结果。
|
||||||
|
*/
|
||||||
|
public class ChatTimeToolAvailabilityDecision {
|
||||||
|
|
||||||
|
private boolean available;
|
||||||
|
|
||||||
|
private String reasonCode;
|
||||||
|
|
||||||
|
private String reasonMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可用判定。
|
||||||
|
*
|
||||||
|
* @return 可用判定
|
||||||
|
*/
|
||||||
|
public static ChatTimeToolAvailabilityDecision available() {
|
||||||
|
ChatTimeToolAvailabilityDecision decision = new ChatTimeToolAvailabilityDecision();
|
||||||
|
decision.setAvailable(true);
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建不可用判定。
|
||||||
|
*
|
||||||
|
* @param reasonCode 原因编码
|
||||||
|
* @param reasonMessage 原因说明
|
||||||
|
* @return 不可用判定
|
||||||
|
*/
|
||||||
|
public static ChatTimeToolAvailabilityDecision unavailable(String reasonCode, String reasonMessage) {
|
||||||
|
ChatTimeToolAvailabilityDecision decision = new ChatTimeToolAvailabilityDecision();
|
||||||
|
decision.setAvailable(false);
|
||||||
|
decision.setReasonCode(reasonCode);
|
||||||
|
decision.setReasonMessage(reasonMessage);
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvailable(boolean available) {
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReasonCode() {
|
||||||
|
return reasonCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReasonCode(String reasonCode) {
|
||||||
|
this.reasonCode = reasonCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReasonMessage() {
|
||||||
|
return reasonMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReasonMessage(String reasonMessage) {
|
||||||
|
this.reasonMessage = reasonMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天时工具可用性判定器。
|
||||||
|
*/
|
||||||
|
public interface ChatTimeToolAvailabilityResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前判定器是否支持指定候选项。
|
||||||
|
*
|
||||||
|
* @param candidate 聊天工具候选项
|
||||||
|
* @return 是否支持
|
||||||
|
*/
|
||||||
|
boolean supports(Object candidate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算候选项在当前聊天上下文中的可用性。
|
||||||
|
*
|
||||||
|
* @param context 聊天时上下文
|
||||||
|
* @param candidate 聊天工具候选项
|
||||||
|
* @return 判定结果
|
||||||
|
*/
|
||||||
|
ChatTimeToolAvailabilityDecision resolve(ChatTimeToolAvailabilityContext context, Object candidate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天时工具可用性编排服务。
|
||||||
|
*/
|
||||||
|
public interface ChatTimeToolAvailabilityService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估单个候选项的聊天时可用性。
|
||||||
|
*
|
||||||
|
* @param context 聊天时上下文
|
||||||
|
* @param candidate 聊天工具候选项
|
||||||
|
* @return 判定结果
|
||||||
|
*/
|
||||||
|
ChatTimeToolAvailabilityDecision evaluate(ChatTimeToolAvailabilityContext context, Object candidate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤当前聊天上下文中可用的候选项。
|
||||||
|
*
|
||||||
|
* @param context 聊天时上下文
|
||||||
|
* @param candidates 候选项列表
|
||||||
|
* @param <T> 候选项类型
|
||||||
|
* @return 过滤后的候选项
|
||||||
|
*/
|
||||||
|
<T> List<T> filterAvailable(ChatTimeToolAvailabilityContext context, List<T> candidates);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言候选项在当前聊天上下文中可用。
|
||||||
|
*
|
||||||
|
* @param context 聊天时上下文
|
||||||
|
* @param candidate 聊天工具候选项
|
||||||
|
* @param fallbackMessage 默认兜底文案
|
||||||
|
*/
|
||||||
|
void assertAvailable(ChatTimeToolAvailabilityContext context, Object candidate, String fallbackMessage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天时工具可用性编排服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ChatTimeToolAvailabilityServiceImpl implements ChatTimeToolAvailabilityService {
|
||||||
|
|
||||||
|
private final List<ChatTimeToolAvailabilityResolver> resolvers;
|
||||||
|
|
||||||
|
public ChatTimeToolAvailabilityServiceImpl(List<ChatTimeToolAvailabilityResolver> resolvers) {
|
||||||
|
this.resolvers = resolvers == null ? Collections.emptyList() : resolvers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ChatTimeToolAvailabilityDecision evaluate(ChatTimeToolAvailabilityContext context, Object candidate) {
|
||||||
|
if (candidate == null) {
|
||||||
|
return ChatTimeToolAvailabilityDecision.unavailable("TOOL_CANDIDATE_MISSING", "聊天工具候选项不存在");
|
||||||
|
}
|
||||||
|
for (ChatTimeToolAvailabilityResolver resolver : resolvers) {
|
||||||
|
if (resolver.supports(candidate)) {
|
||||||
|
return resolver.resolve(context, candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ChatTimeToolAvailabilityDecision.unavailable("TOOL_RESOLVER_MISSING", "当前聊天工具缺少可用性判定器");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T> List<T> filterAvailable(ChatTimeToolAvailabilityContext context, List<T> candidates) {
|
||||||
|
if (candidates == null || candidates.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return candidates.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.filter(candidate -> evaluate(context, candidate).isAvailable())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void assertAvailable(ChatTimeToolAvailabilityContext context, Object candidate, String fallbackMessage) {
|
||||||
|
ChatTimeToolAvailabilityDecision decision = evaluate(context, candidate);
|
||||||
|
if (decision.isAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String message = decision.getReasonMessage();
|
||||||
|
if (message == null || message.isBlank()) {
|
||||||
|
message = fallbackMessage;
|
||||||
|
}
|
||||||
|
throw new BusinessException(message == null || message.isBlank() ? "当前聊天工具不可用" : message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,30 +4,69 @@ import com.easyagents.core.document.Document;
|
|||||||
import com.easyagents.core.model.chat.tool.BaseTool;
|
import com.easyagents.core.model.chat.tool.BaseTool;
|
||||||
import com.easyagents.core.model.chat.tool.Parameter;
|
import com.easyagents.core.model.chat.tool.Parameter;
|
||||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityService;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
import tech.easyflow.common.util.SpringContextUtil;
|
import tech.easyflow.common.util.SpringContextUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库聊天工具。
|
||||||
|
*/
|
||||||
public class DocumentCollectionTool extends BaseTool {
|
public class DocumentCollectionTool extends BaseTool {
|
||||||
|
|
||||||
private BigInteger knowledgeId;
|
private BigInteger knowledgeId;
|
||||||
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
||||||
|
private ChatTimeToolAvailabilityContext chatTimeContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认构造器。
|
||||||
|
*/
|
||||||
public DocumentCollectionTool() {
|
public DocumentCollectionTool() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于知识库实体构造聊天工具。
|
||||||
|
*
|
||||||
|
* @param documentCollection 知识库
|
||||||
|
* @param needEnglishName 是否使用英文名
|
||||||
|
*/
|
||||||
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName) {
|
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName) {
|
||||||
this(documentCollection, needEnglishName, RetrievalMode.HYBRID);
|
this(documentCollection, needEnglishName, RetrievalMode.HYBRID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于知识库实体构造聊天工具。
|
||||||
|
*
|
||||||
|
* @param documentCollection 知识库
|
||||||
|
* @param needEnglishName 是否使用英文名
|
||||||
|
* @param retrievalMode 检索模式
|
||||||
|
*/
|
||||||
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName, RetrievalMode retrievalMode) {
|
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName, RetrievalMode retrievalMode) {
|
||||||
|
this(documentCollection, needEnglishName, retrievalMode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于知识库实体和聊天时权限上下文构造聊天工具。
|
||||||
|
*
|
||||||
|
* @param documentCollection 知识库
|
||||||
|
* @param needEnglishName 是否使用英文名
|
||||||
|
* @param retrievalMode 检索模式
|
||||||
|
* @param chatTimeContext 聊天时权限上下文
|
||||||
|
*/
|
||||||
|
public DocumentCollectionTool(DocumentCollection documentCollection,
|
||||||
|
boolean needEnglishName,
|
||||||
|
RetrievalMode retrievalMode,
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext) {
|
||||||
this.knowledgeId = documentCollection.getId();
|
this.knowledgeId = documentCollection.getId();
|
||||||
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
||||||
|
this.chatTimeContext = chatTimeContext;
|
||||||
if (needEnglishName) {
|
if (needEnglishName) {
|
||||||
this.name = documentCollection.getEnglishName();
|
this.name = documentCollection.getEnglishName();
|
||||||
} else {
|
} else {
|
||||||
@@ -63,10 +102,42 @@ public class DocumentCollectionTool extends BaseTool {
|
|||||||
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取聊天时权限上下文。
|
||||||
|
*
|
||||||
|
* @return 聊天时权限上下文
|
||||||
|
*/
|
||||||
|
public ChatTimeToolAvailabilityContext getChatTimeContext() {
|
||||||
|
return chatTimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置聊天时权限上下文。
|
||||||
|
*
|
||||||
|
* @param chatTimeContext 聊天时权限上下文
|
||||||
|
*/
|
||||||
|
public void setChatTimeContext(ChatTimeToolAvailabilityContext chatTimeContext) {
|
||||||
|
this.chatTimeContext = chatTimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行知识库检索。
|
||||||
|
*
|
||||||
|
* @param argsMap 工具入参
|
||||||
|
* @return 检索结果拼接文本
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Object invoke(Map<String, Object> argsMap) {
|
public Object invoke(Map<String, Object> argsMap) {
|
||||||
|
|
||||||
DocumentCollectionService knowledgeService = SpringContextUtil.getBean(DocumentCollectionService.class);
|
DocumentCollectionService knowledgeService = SpringContextUtil.getBean(DocumentCollectionService.class);
|
||||||
|
DocumentCollection knowledge = null;
|
||||||
|
if (this.knowledgeId != null) {
|
||||||
|
knowledge = knowledgeService.getById(this.knowledgeId);
|
||||||
|
}
|
||||||
|
if (knowledge == null) {
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
assertChatTimeAvailability(knowledge);
|
||||||
|
|
||||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||||
request.setKnowledgeId(this.knowledgeId);
|
request.setKnowledgeId(this.knowledgeId);
|
||||||
request.setQuery((String) argsMap.get("input"));
|
request.setQuery((String) argsMap.get("input"));
|
||||||
@@ -91,5 +162,17 @@ public class DocumentCollectionTool extends BaseTool {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当工具由聊天运行时装配时,执行聊天态权限兜底。
|
||||||
|
*
|
||||||
|
* @param knowledge 当前知识库实体
|
||||||
|
*/
|
||||||
|
protected void assertChatTimeAvailability(DocumentCollection knowledge) {
|
||||||
|
if (chatTimeContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityService availabilityService = SpringContextUtil.getBean(ChatTimeToolAvailabilityService.class);
|
||||||
|
availabilityService.assertAvailable(chatTimeContext, knowledge, "当前用户无权在聊天中访问该知识库");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.easyagents.store.milvus.MilvusVectorStore;
|
|||||||
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
||||||
import com.mybatisflex.annotation.Table;
|
import com.mybatisflex.annotation.Table;
|
||||||
import tech.easyflow.ai.config.AiMilvusConfig;
|
import tech.easyflow.ai.config.AiMilvusConfig;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
|
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
|
||||||
import tech.easyflow.ai.entity.base.DocumentCollectionBase;
|
import tech.easyflow.ai.entity.base.DocumentCollectionBase;
|
||||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||||
@@ -111,7 +112,19 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Tool toFunction(boolean needEnglishName, String retrievalMode) {
|
public Tool toFunction(boolean needEnglishName, String retrievalMode) {
|
||||||
return new DocumentCollectionTool(this, needEnglishName, KnowledgeRetrievalModes.parse(retrievalMode));
|
return toFunction(needEnglishName, retrievalMode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造知识库聊天工具。
|
||||||
|
*
|
||||||
|
* @param needEnglishName 是否使用英文名称
|
||||||
|
* @param retrievalMode 检索模式
|
||||||
|
* @param chatTimeContext 聊天时权限上下文
|
||||||
|
* @return 聊天工具
|
||||||
|
*/
|
||||||
|
public Tool toFunction(boolean needEnglishName, String retrievalMode, ChatTimeToolAvailabilityContext chatTimeContext) {
|
||||||
|
return new DocumentCollectionTool(this, needEnglishName, KnowledgeRetrievalModes.parse(retrievalMode), chatTimeContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object getOptionsByKey(String key) {
|
public Object getOptionsByKey(String key) {
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ import org.springframework.web.context.request.RequestAttributes;
|
|||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityService;
|
||||||
import tech.easyflow.ai.easyagents.listener.ChatStreamListener;
|
import tech.easyflow.ai.easyagents.listener.ChatStreamListener;
|
||||||
import tech.easyflow.ai.easyagents.memory.DefaultBotMessageMemory;
|
import tech.easyflow.ai.easyagents.memory.DefaultBotMessageMemory;
|
||||||
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
|
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
|
||||||
import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
|
import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
|
||||||
|
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
|
||||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
@@ -39,6 +42,7 @@ import tech.easyflow.ai.utils.CustomBeanUtils;
|
|||||||
import tech.easyflow.ai.utils.RegexUtils;
|
import tech.easyflow.ai.utils.RegexUtils;
|
||||||
import tech.easyflow.common.filestorage.FileStorageService;
|
import tech.easyflow.common.filestorage.FileStorageService;
|
||||||
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.util.MapUtil;
|
import tech.easyflow.common.util.MapUtil;
|
||||||
import tech.easyflow.common.util.Maps;
|
import tech.easyflow.common.util.Maps;
|
||||||
@@ -131,6 +135,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
private CategoryPermissionService categoryPermissionService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
@Resource
|
@Resource
|
||||||
private ChatRuntimeManager chatRuntimeManager;
|
private ChatRuntimeManager chatRuntimeManager;
|
||||||
|
@Resource
|
||||||
|
private ChatTimeToolAvailabilityService chatTimeToolAvailabilityService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bot getDetail(String id) {
|
public Bot getDetail(String id) {
|
||||||
@@ -274,6 +280,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
BotServiceImpl.ChatCheckResult chatCheckResult, List<String> attachments, ChatRuntimeContext runtimeContext) {
|
BotServiceImpl.ChatCheckResult chatCheckResult, List<String> attachments, ChatRuntimeContext runtimeContext) {
|
||||||
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
||||||
ChatModel chatModel = chatCheckResult.getChatModel();
|
ChatModel chatModel = chatCheckResult.getChatModel();
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext = buildChatTimeToolAvailabilityContext(runtimeContext, chatCheckResult.getAiBot());
|
||||||
final MemoryPrompt memoryPrompt = new MemoryPrompt();
|
final MemoryPrompt memoryPrompt = new MemoryPrompt();
|
||||||
String systemPrompt = buildSystemPromptWithFaqImageRule(
|
String systemPrompt = buildSystemPromptWithFaqImageRule(
|
||||||
MapUtil.getString(modelOptions, Bot.KEY_SYSTEM_PROMPT)
|
MapUtil.getString(modelOptions, Bot.KEY_SYSTEM_PROMPT)
|
||||||
@@ -293,6 +300,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
|
userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
|
||||||
.set("needEnglishName", false)
|
.set("needEnglishName", false)
|
||||||
.set("bot", chatCheckResult.getAiBot())
|
.set("bot", chatCheckResult.getAiBot())
|
||||||
|
.set("chatTimeContext", chatTimeContext)
|
||||||
.set("publishedOnly", chatCheckResult.isPublishedAccess())));
|
.set("publishedOnly", chatCheckResult.isPublishedAccess())));
|
||||||
ChatOptions chatOptions = getChatOptions(modelOptions);
|
ChatOptions chatOptions = getChatOptions(modelOptions);
|
||||||
Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false);
|
Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false);
|
||||||
@@ -463,6 +471,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
needEnglishName = false;
|
needEnglishName = false;
|
||||||
}
|
}
|
||||||
Bot runtimeBot = (Bot) buildParams.get("bot");
|
Bot runtimeBot = (Bot) buildParams.get("bot");
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext = (ChatTimeToolAvailabilityContext) buildParams.get("chatTimeContext");
|
||||||
boolean usePublishedSnapshot = Boolean.TRUE.equals(buildParams.get("publishedOnly"))
|
boolean usePublishedSnapshot = Boolean.TRUE.equals(buildParams.get("publishedOnly"))
|
||||||
&& runtimeBot != null
|
&& runtimeBot != null
|
||||||
&& runtimeBot.getPublishedSnapshotJson() != null
|
&& runtimeBot.getPublishedSnapshotJson() != null
|
||||||
@@ -471,7 +480,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||||
if (usePublishedSnapshot) {
|
if (usePublishedSnapshot) {
|
||||||
appendPublishedWorkflowTools(functionList, runtimeBot, needEnglishName);
|
appendPublishedWorkflowTools(functionList, runtimeBot, needEnglishName);
|
||||||
appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName);
|
appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName, chatTimeContext);
|
||||||
} else {
|
} else {
|
||||||
// 工作流 function 集合
|
// 工作流 function 集合
|
||||||
queryWrapper.eq(BotWorkflow::getBotId, botId);
|
queryWrapper.eq(BotWorkflow::getBotId, botId);
|
||||||
@@ -489,13 +498,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
queryWrapper.eq(BotDocumentCollection::getBotId, botId);
|
queryWrapper.eq(BotDocumentCollection::getBotId, botId);
|
||||||
List<BotDocumentCollection> botDocumentCollections = botDocumentCollectionService.getMapper()
|
List<BotDocumentCollection> botDocumentCollections = botDocumentCollectionService.getMapper()
|
||||||
.selectListWithRelationsByQuery(queryWrapper);
|
.selectListWithRelationsByQuery(queryWrapper);
|
||||||
if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) {
|
functionList.addAll(buildKnowledgeTools(botDocumentCollections, needEnglishName, chatTimeContext));
|
||||||
for (BotDocumentCollection botDocumentCollection : botDocumentCollections) {
|
|
||||||
Tool function = botDocumentCollection.getKnowledge()
|
|
||||||
.toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name());
|
|
||||||
functionList.add(function);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件 function 集合
|
// 插件 function 集合
|
||||||
@@ -534,6 +537,39 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
return functionList;
|
return functionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Bot 绑定的知识库候选项收敛为当前聊天可用的工具列表。
|
||||||
|
*
|
||||||
|
* @param botDocumentCollections Bot 知识库绑定项
|
||||||
|
* @param needEnglishName 是否使用英文名称
|
||||||
|
* @param chatTimeContext 聊天时权限上下文
|
||||||
|
* @return 知识库工具列表
|
||||||
|
*/
|
||||||
|
List<Tool> buildKnowledgeTools(List<BotDocumentCollection> botDocumentCollections,
|
||||||
|
boolean needEnglishName,
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext) {
|
||||||
|
List<Tool> functionList = new ArrayList<>();
|
||||||
|
if (botDocumentCollections == null || botDocumentCollections.isEmpty()) {
|
||||||
|
return functionList;
|
||||||
|
}
|
||||||
|
List<BotDocumentCollection> availableBindings = chatTimeContext == null
|
||||||
|
? botDocumentCollections
|
||||||
|
: chatTimeToolAvailabilityService.filterAvailable(chatTimeContext, botDocumentCollections);
|
||||||
|
for (BotDocumentCollection botDocumentCollection : availableBindings) {
|
||||||
|
DocumentCollection knowledge = botDocumentCollection.getKnowledge();
|
||||||
|
if (knowledge == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DocumentCollectionTool function = (DocumentCollectionTool) knowledge.toFunction(
|
||||||
|
needEnglishName,
|
||||||
|
botDocumentCollection.getRetrievalMode().name(),
|
||||||
|
chatTimeContext
|
||||||
|
);
|
||||||
|
functionList.add(function);
|
||||||
|
}
|
||||||
|
return functionList;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void appendPublishedWorkflowTools(List<Tool> functionList, Bot runtimeBot, boolean needEnglishName) {
|
private void appendPublishedWorkflowTools(List<Tool> functionList, Bot runtimeBot, boolean needEnglishName) {
|
||||||
Object workflows = runtimeBot.getPublishedSnapshotJson().get("workflowBindings");
|
Object workflows = runtimeBot.getPublishedSnapshotJson().get("workflowBindings");
|
||||||
@@ -562,7 +598,10 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void appendPublishedKnowledgeTools(List<Tool> functionList, Bot runtimeBot, boolean needEnglishName) {
|
private void appendPublishedKnowledgeTools(List<Tool> functionList,
|
||||||
|
Bot runtimeBot,
|
||||||
|
boolean needEnglishName,
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext) {
|
||||||
Object knowledges = runtimeBot.getPublishedSnapshotJson().get("knowledgeBindings");
|
Object knowledges = runtimeBot.getPublishedSnapshotJson().get("knowledgeBindings");
|
||||||
if (!(knowledges instanceof List<?> knowledgeBindings)) {
|
if (!(knowledges instanceof List<?> knowledgeBindings)) {
|
||||||
return;
|
return;
|
||||||
@@ -579,14 +618,40 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
if (knowledge == null) {
|
if (knowledge == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (chatTimeContext != null && !chatTimeToolAvailabilityService.evaluate(chatTimeContext, knowledge).isAvailable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Object retrievalMode = bindingMap.get("retrievalMode");
|
Object retrievalMode = bindingMap.get("retrievalMode");
|
||||||
functionList.add(knowledge.toFunction(
|
functionList.add(knowledge.toFunction(
|
||||||
needEnglishName,
|
needEnglishName,
|
||||||
retrievalMode == null ? null : String.valueOf(retrievalMode)
|
retrievalMode == null ? null : String.valueOf(retrievalMode),
|
||||||
|
chatTimeContext
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造聊天时工具可用性上下文,并显式回填到运行时上下文中供后续异步工具调用复用。
|
||||||
|
*
|
||||||
|
* @param runtimeContext 聊天运行时上下文
|
||||||
|
* @param bot 当前聊天助手
|
||||||
|
* @return 聊天时工具可用性上下文
|
||||||
|
*/
|
||||||
|
private ChatTimeToolAvailabilityContext buildChatTimeToolAvailabilityContext(ChatRuntimeContext runtimeContext, Bot bot) {
|
||||||
|
ChatTimeToolAvailabilityContext existing = ChatTimeToolAvailabilityContext.fromRuntimeContext(runtimeContext);
|
||||||
|
LoginAccount loginAccount = existing == null ? null : existing.getLoginAccount();
|
||||||
|
if (!ChatTimeToolAvailabilityContext.hasLoggedInAccount(loginAccount)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityContext context = existing == null ? new ChatTimeToolAvailabilityContext() : existing;
|
||||||
|
context.setLoginAccount(loginAccount);
|
||||||
|
context.setBot(bot);
|
||||||
|
context.setChatChannel(runtimeContext == null ? null : runtimeContext.getChannel());
|
||||||
|
context.setSessionId(runtimeContext == null ? null : runtimeContext.getSessionId());
|
||||||
|
context.bindToRuntimeContext(runtimeContext);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
public String attachmentsToString(List<String> fileList) {
|
public String attachmentsToString(List<String> fileList) {
|
||||||
StringBuilder messageBuilder = new StringBuilder();
|
StringBuilder messageBuilder = new StringBuilder();
|
||||||
if (fileList != null && !fileList.isEmpty()) {
|
if (fileList != null && !fileList.isEmpty()) {
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ChatTimeKnowledgeAvailabilityResolver} 单元测试。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-05-10
|
||||||
|
*/
|
||||||
|
public class ChatTimeKnowledgeAvailabilityResolverTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建者应始终可访问自己的私有知识库。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void resolveShouldAllowCreatorForPrivateKnowledge() {
|
||||||
|
LoginAccount loginAccount = buildLoginAccount(11, 3);
|
||||||
|
ChatTimeKnowledgeAvailabilityResolver resolver = buildResolver(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(11), false, false, Collections.emptySet()),
|
||||||
|
setOf(BigInteger.valueOf(3))
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityDecision decision = resolver.resolve(
|
||||||
|
buildContext(loginAccount),
|
||||||
|
buildKnowledge(11, 21, null, "PRIVATE")
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertTrue(decision.isAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类权限不通过时,即使知识库是公开范围也不可访问。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void resolveShouldRejectWhenCategoryPermissionFails() {
|
||||||
|
LoginAccount loginAccount = buildLoginAccount(12, 3);
|
||||||
|
ChatTimeKnowledgeAvailabilityResolver resolver = buildResolver(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(99))),
|
||||||
|
setOf(BigInteger.valueOf(3))
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityDecision decision = resolver.resolve(
|
||||||
|
buildContext(loginAccount),
|
||||||
|
buildKnowledge(11, 21, null, "PUBLIC")
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertFalse(decision.isAvailable());
|
||||||
|
Assert.assertEquals("CHAT_TIME_KNOWLEDGE_FORBIDDEN", decision.getReasonCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同部门或祖先部门命中时,应允许访问部门可见知识库。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void resolveShouldAllowDeptScopedKnowledgeForReadableDept() {
|
||||||
|
LoginAccount loginAccount = buildLoginAccount(12, 9);
|
||||||
|
ChatTimeKnowledgeAvailabilityResolver resolver = buildResolver(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
|
||||||
|
setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9))
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityDecision decision = resolver.resolve(
|
||||||
|
buildContext(loginAccount),
|
||||||
|
buildKnowledge(11, 21, BigInteger.valueOf(3), "DEPT")
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertTrue(decision.isAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超级管理员始终可访问。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void resolveShouldAllowSuperAdmin() {
|
||||||
|
LoginAccount loginAccount = buildLoginAccount(99, 1);
|
||||||
|
ChatTimeKnowledgeAvailabilityResolver resolver = buildResolver(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(99), true, true, Collections.emptySet()),
|
||||||
|
Collections.emptySet()
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityDecision decision = resolver.resolve(
|
||||||
|
buildContext(loginAccount),
|
||||||
|
buildKnowledge(11, 21, null, "PRIVATE")
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertTrue(decision.isAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatTimeKnowledgeAvailabilityResolver buildResolver(RoleCategoryAccessSnapshot accessSnapshot,
|
||||||
|
Set<BigInteger> deptIds) {
|
||||||
|
return new ChatTimeKnowledgeAvailabilityResolver(
|
||||||
|
new KnowledgeVisibilityQueryHelper(),
|
||||||
|
mockCategoryPermissionService(accessSnapshot),
|
||||||
|
mockSysDeptService(deptIds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatTimeToolAvailabilityContext buildContext(LoginAccount loginAccount) {
|
||||||
|
ChatTimeToolAvailabilityContext context = new ChatTimeToolAvailabilityContext();
|
||||||
|
context.setLoginAccount(loginAccount);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount buildLoginAccount(long accountId, long deptId) {
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.valueOf(accountId));
|
||||||
|
loginAccount.setDeptId(BigInteger.valueOf(deptId));
|
||||||
|
return loginAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentCollection buildKnowledge(long createdBy, long categoryId, BigInteger deptId, String visibilityScope) {
|
||||||
|
DocumentCollection knowledge = new DocumentCollection();
|
||||||
|
knowledge.setId(BigInteger.valueOf(101));
|
||||||
|
knowledge.setCreatedBy(BigInteger.valueOf(createdBy));
|
||||||
|
knowledge.setCategoryId(BigInteger.valueOf(categoryId));
|
||||||
|
knowledge.setDeptId(deptId);
|
||||||
|
knowledge.setVisibilityScope(visibilityScope);
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryPermissionService mockCategoryPermissionService(RoleCategoryAccessSnapshot accessSnapshot) {
|
||||||
|
return (CategoryPermissionService) Proxy.newProxyInstance(
|
||||||
|
CategoryPermissionService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{CategoryPermissionService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getAccess".equals(method.getName())) {
|
||||||
|
return accessSnapshot;
|
||||||
|
}
|
||||||
|
if ("isSuperAdmin".equals(method.getName())) {
|
||||||
|
return accessSnapshot.isSuperAdmin();
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysDeptService mockSysDeptService(Set<BigInteger> deptIds) {
|
||||||
|
Set<BigInteger> readableDeptIds = deptIds == null ? Collections.emptySet() : deptIds;
|
||||||
|
return (SysDeptService) Proxy.newProxyInstance(
|
||||||
|
SysDeptService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{SysDeptService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getSelfAndAncestorDeptIds".equals(method.getName())) {
|
||||||
|
return readableDeptIds;
|
||||||
|
}
|
||||||
|
if ("canUserAccessDeptScopedResource".equals(method.getName())) {
|
||||||
|
BigInteger resourceDeptId = (BigInteger) args[1];
|
||||||
|
return readableDeptIds.contains(resourceDeptId);
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<BigInteger> setOf(BigInteger... values) {
|
||||||
|
Set<BigInteger> result = new LinkedHashSet<>();
|
||||||
|
Collections.addAll(result, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object defaultValue(Class<?> returnType) {
|
||||||
|
if (returnType == boolean.class) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (returnType == int.class) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (returnType == long.class) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package tech.easyflow.ai.chattime.availability;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ChatTimeToolAvailabilityContext} 单元测试。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-05-10
|
||||||
|
*/
|
||||||
|
public class ChatTimeToolAvailabilityContextTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用户快照应被显式绑定到聊天运行时上下文。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void bindLoggedInSnapshotShouldAttachContextToRuntimeExt() {
|
||||||
|
ChatRuntimeContext runtimeContext = new ChatRuntimeContext();
|
||||||
|
runtimeContext.setChannel(ChatChannel.ADMIN);
|
||||||
|
runtimeContext.setSessionId(BigInteger.valueOf(2001));
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.valueOf(12));
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.valueOf(99));
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(runtimeContext, loginAccount, bot);
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityContext chatTimeContext = ChatTimeToolAvailabilityContext.fromRuntimeContext(runtimeContext);
|
||||||
|
Assert.assertNotNull(chatTimeContext);
|
||||||
|
Assert.assertEquals(BigInteger.valueOf(12), chatTimeContext.getLoginAccount().getId());
|
||||||
|
Assert.assertEquals(BigInteger.valueOf(99), chatTimeContext.getBot().getId());
|
||||||
|
Assert.assertEquals(ChatChannel.ADMIN, chatTimeContext.getChatChannel());
|
||||||
|
Assert.assertEquals(BigInteger.valueOf(2001), chatTimeContext.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匿名或缺失账号快照时不应绑定聊天态权限上下文。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void bindLoggedInSnapshotShouldIgnoreAnonymousAccount() {
|
||||||
|
ChatRuntimeContext runtimeContext = new ChatRuntimeContext();
|
||||||
|
runtimeContext.setChannel(ChatChannel.USER_CENTER);
|
||||||
|
runtimeContext.setSessionId(BigInteger.valueOf(3001));
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.ZERO);
|
||||||
|
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(runtimeContext, loginAccount, new Bot());
|
||||||
|
|
||||||
|
Assert.assertNull(ChatTimeToolAvailabilityContext.fromRuntimeContext(runtimeContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
package tech.easyflow.ai.easyagents.tool;
|
||||||
|
|
||||||
|
import com.easyagents.core.document.Document;
|
||||||
|
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeKnowledgeAvailabilityResolver;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityService;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityServiceImpl;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.util.SpringContextUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.FutureTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DocumentCollectionTool} 单元测试。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-05-10
|
||||||
|
*/
|
||||||
|
public class DocumentCollectionToolTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接构造一个本不应暴露的知识库 Tool 调用时,必须抛出无权限异常。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void invokeShouldThrowWhenChatTimeKnowledgeUnavailable() throws Exception {
|
||||||
|
TestDocumentCollectionService documentCollectionService = new TestDocumentCollectionService(
|
||||||
|
buildKnowledge(101, 11, 21, null, "PUBLIC"),
|
||||||
|
List.of(buildSearchDocument("should-not-reach"))
|
||||||
|
);
|
||||||
|
ChatTimeToolAvailabilityService availabilityService = buildAvailabilityService(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(99))),
|
||||||
|
Collections.emptySet()
|
||||||
|
);
|
||||||
|
ApplicationContext previousContext = getStaticField("applicationContext");
|
||||||
|
Object previousBeanFactory = getStaticField("beanFactory");
|
||||||
|
try {
|
||||||
|
setStaticField("beanFactory", null);
|
||||||
|
setStaticField("applicationContext", mockApplicationContext(documentCollectionService.toProxy(), availabilityService));
|
||||||
|
DocumentCollectionTool tool = new DocumentCollectionTool(
|
||||||
|
buildKnowledge(101, 11, 21, null, "PUBLIC"),
|
||||||
|
false,
|
||||||
|
RetrievalMode.HYBRID,
|
||||||
|
buildContext(12, 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
BusinessException exception = Assert.assertThrows(
|
||||||
|
BusinessException.class,
|
||||||
|
() -> tool.invoke(Map.of("input", "test"))
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertEquals("当前用户无权在聊天中访问该知识库", exception.getMessage());
|
||||||
|
Assert.assertEquals(0, documentCollectionService.searchCount);
|
||||||
|
} finally {
|
||||||
|
setStaticField("applicationContext", previousContext);
|
||||||
|
setStaticField("beanFactory", previousBeanFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步线程中即使没有当前线程登录态,也应能基于显式快照完成判权并执行检索。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void invokeShouldUseExplicitLoginSnapshotWithoutThreadState() throws Exception {
|
||||||
|
TestDocumentCollectionService documentCollectionService = new TestDocumentCollectionService(
|
||||||
|
buildKnowledge(101, 11, 21, null, "PUBLIC"),
|
||||||
|
List.of(buildSearchDocument("知识片段A"), buildSearchDocument("知识片段B"))
|
||||||
|
);
|
||||||
|
ChatTimeToolAvailabilityService availabilityService = buildAvailabilityService(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
|
||||||
|
Collections.emptySet()
|
||||||
|
);
|
||||||
|
ApplicationContext previousContext = getStaticField("applicationContext");
|
||||||
|
Object previousBeanFactory = getStaticField("beanFactory");
|
||||||
|
try {
|
||||||
|
setStaticField("beanFactory", null);
|
||||||
|
setStaticField("applicationContext", mockApplicationContext(documentCollectionService.toProxy(), availabilityService));
|
||||||
|
DocumentCollectionTool tool = new DocumentCollectionTool(
|
||||||
|
buildKnowledge(101, 11, 21, null, "PUBLIC"),
|
||||||
|
false,
|
||||||
|
RetrievalMode.KEYWORD,
|
||||||
|
buildContext(12, 3)
|
||||||
|
);
|
||||||
|
FutureTask<Object> task = new FutureTask<>(() -> tool.invoke(Map.of("input", "异步查询")));
|
||||||
|
Thread thread = new Thread(task, "document-collection-tool-test");
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
Object result = task.get();
|
||||||
|
|
||||||
|
Assert.assertEquals("知识片段A\n\n---\n\n知识片段B", result);
|
||||||
|
Assert.assertEquals(1, documentCollectionService.searchCount);
|
||||||
|
Assert.assertNotNull(documentCollectionService.lastRequest);
|
||||||
|
Assert.assertEquals("异步查询", documentCollectionService.lastRequest.getQuery());
|
||||||
|
Assert.assertEquals(RetrievalMode.KEYWORD, documentCollectionService.lastRequest.getRetrievalMode());
|
||||||
|
Assert.assertEquals("BOT_TOOL", documentCollectionService.lastRequest.getCallerType());
|
||||||
|
Assert.assertEquals("101", documentCollectionService.lastRequest.getCallerId());
|
||||||
|
} finally {
|
||||||
|
setStaticField("applicationContext", previousContext);
|
||||||
|
setStaticField("beanFactory", previousBeanFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatTimeToolAvailabilityService buildAvailabilityService(RoleCategoryAccessSnapshot accessSnapshot,
|
||||||
|
Set<BigInteger> deptIds) {
|
||||||
|
return new ChatTimeToolAvailabilityServiceImpl(List.of(
|
||||||
|
new ChatTimeKnowledgeAvailabilityResolver(
|
||||||
|
new KnowledgeVisibilityQueryHelper(),
|
||||||
|
mockCategoryPermissionService(accessSnapshot),
|
||||||
|
mockSysDeptService(deptIds)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatTimeToolAvailabilityContext buildContext(long accountId, long deptId) {
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.valueOf(accountId));
|
||||||
|
loginAccount.setDeptId(BigInteger.valueOf(deptId));
|
||||||
|
ChatTimeToolAvailabilityContext context = new ChatTimeToolAvailabilityContext();
|
||||||
|
context.setLoginAccount(loginAccount);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentCollection buildKnowledge(long knowledgeId,
|
||||||
|
long createdBy,
|
||||||
|
long categoryId,
|
||||||
|
BigInteger deptId,
|
||||||
|
String visibilityScope) {
|
||||||
|
DocumentCollection knowledge = new DocumentCollection();
|
||||||
|
knowledge.setId(BigInteger.valueOf(knowledgeId));
|
||||||
|
knowledge.setTitle("knowledge-" + knowledgeId);
|
||||||
|
knowledge.setDescription("desc-" + knowledgeId);
|
||||||
|
knowledge.setCreatedBy(BigInteger.valueOf(createdBy));
|
||||||
|
knowledge.setCategoryId(BigInteger.valueOf(categoryId));
|
||||||
|
knowledge.setDeptId(deptId);
|
||||||
|
knowledge.setVisibilityScope(visibilityScope);
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document buildSearchDocument(String content) {
|
||||||
|
Document document = new Document();
|
||||||
|
document.setContent(content);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApplicationContext mockApplicationContext(DocumentCollectionService documentCollectionService,
|
||||||
|
ChatTimeToolAvailabilityService availabilityService) {
|
||||||
|
return (ApplicationContext) Proxy.newProxyInstance(
|
||||||
|
ApplicationContext.class.getClassLoader(),
|
||||||
|
new Class[]{ApplicationContext.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getBean".equals(method.getName()) && args != null && args.length == 1 && args[0] instanceof Class<?> clazz) {
|
||||||
|
if (clazz == DocumentCollectionService.class) {
|
||||||
|
return documentCollectionService;
|
||||||
|
}
|
||||||
|
if (clazz == ChatTimeToolAvailabilityService.class) {
|
||||||
|
return availabilityService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("equals".equals(method.getName())) {
|
||||||
|
return proxy == args[0];
|
||||||
|
}
|
||||||
|
if ("hashCode".equals(method.getName())) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryPermissionService mockCategoryPermissionService(RoleCategoryAccessSnapshot accessSnapshot) {
|
||||||
|
return (CategoryPermissionService) Proxy.newProxyInstance(
|
||||||
|
CategoryPermissionService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{CategoryPermissionService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getAccess".equals(method.getName())) {
|
||||||
|
return accessSnapshot;
|
||||||
|
}
|
||||||
|
if ("isSuperAdmin".equals(method.getName())) {
|
||||||
|
return accessSnapshot.isSuperAdmin();
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysDeptService mockSysDeptService(Set<BigInteger> deptIds) {
|
||||||
|
Set<BigInteger> readableDeptIds = deptIds == null ? Collections.emptySet() : deptIds;
|
||||||
|
return (SysDeptService) Proxy.newProxyInstance(
|
||||||
|
SysDeptService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{SysDeptService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getSelfAndAncestorDeptIds".equals(method.getName())) {
|
||||||
|
return readableDeptIds;
|
||||||
|
}
|
||||||
|
if ("canUserAccessDeptScopedResource".equals(method.getName())) {
|
||||||
|
BigInteger resourceDeptId = (BigInteger) args[1];
|
||||||
|
return readableDeptIds.contains(resourceDeptId);
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> T getStaticField(String fieldName) throws Exception {
|
||||||
|
Field field = SpringContextUtil.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (T) field.get(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStaticField(String fieldName, Object value) throws Exception {
|
||||||
|
Field field = SpringContextUtil.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(null, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<BigInteger> setOf(BigInteger... values) {
|
||||||
|
Set<BigInteger> result = new LinkedHashSet<>();
|
||||||
|
Collections.addAll(result, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object defaultValue(Class<?> returnType) {
|
||||||
|
if (returnType == boolean.class) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (returnType == int.class) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (returnType == long.class) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录检索调用的最小知识库服务桩。
|
||||||
|
*/
|
||||||
|
private static class TestDocumentCollectionService {
|
||||||
|
|
||||||
|
private final DocumentCollection knowledge;
|
||||||
|
private final List<Document> searchResult;
|
||||||
|
private int searchCount;
|
||||||
|
private KnowledgeRetrievalRequest lastRequest;
|
||||||
|
|
||||||
|
private TestDocumentCollectionService(DocumentCollection knowledge, List<Document> searchResult) {
|
||||||
|
this.knowledge = knowledge;
|
||||||
|
this.searchResult = searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentCollectionService toProxy() {
|
||||||
|
return (DocumentCollectionService) Proxy.newProxyInstance(
|
||||||
|
DocumentCollectionService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{DocumentCollectionService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getById".equals(method.getName())) {
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
if ("search".equals(method.getName()) && args != null && args.length == 1 && args[0] instanceof KnowledgeRetrievalRequest request) {
|
||||||
|
this.searchCount++;
|
||||||
|
this.lastRequest = request;
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
return defaultStaticValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object defaultStaticValue(Class<?> returnType) {
|
||||||
|
if (returnType == boolean.class) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (returnType == int.class) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (returnType == long.class) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package tech.easyflow.ai.service.impl;
|
||||||
|
|
||||||
|
import com.easyagents.core.model.chat.tool.Tool;
|
||||||
|
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeKnowledgeAvailabilityResolver;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityService;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityServiceImpl;
|
||||||
|
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BotServiceImpl} 单元测试。
|
||||||
|
*
|
||||||
|
* @author Codex
|
||||||
|
* @since 2026-05-10
|
||||||
|
*/
|
||||||
|
public class BotServiceImplTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅应为当前用户可访问的知识库生成聊天工具,并保留绑定检索模式。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void buildKnowledgeToolsShouldOnlyCreateAvailableTools() throws Exception {
|
||||||
|
BotServiceImpl service = new BotServiceImpl();
|
||||||
|
injectAvailabilityService(service, buildAvailabilityService(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
|
||||||
|
Collections.emptySet()
|
||||||
|
));
|
||||||
|
|
||||||
|
List<Tool> tools = service.buildKnowledgeTools(
|
||||||
|
List.of(
|
||||||
|
buildBinding(101, 11, 21, "PUBLIC", RetrievalMode.KEYWORD),
|
||||||
|
buildBinding(102, 11, 99, "PUBLIC", RetrievalMode.HYBRID)
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
buildContext(12, 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, tools.size());
|
||||||
|
Assert.assertTrue(tools.get(0) instanceof DocumentCollectionTool);
|
||||||
|
DocumentCollectionTool tool = (DocumentCollectionTool) tools.get(0);
|
||||||
|
Assert.assertEquals(BigInteger.valueOf(101), tool.getKnowledgeId());
|
||||||
|
Assert.assertEquals(RetrievalMode.KEYWORD, tool.getRetrievalMode());
|
||||||
|
Assert.assertNotNull(tool.getChatTimeContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当全部绑定知识库都不可用时,聊天工具列表应为空。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void buildKnowledgeToolsShouldReturnEmptyWhenAllBindingsUnavailable() throws Exception {
|
||||||
|
BotServiceImpl service = new BotServiceImpl();
|
||||||
|
injectAvailabilityService(service, buildAvailabilityService(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(99))),
|
||||||
|
Collections.emptySet()
|
||||||
|
));
|
||||||
|
|
||||||
|
List<Tool> tools = service.buildKnowledgeTools(
|
||||||
|
List.of(buildBinding(101, 11, 21, "PUBLIC", RetrievalMode.HYBRID)),
|
||||||
|
false,
|
||||||
|
buildContext(12, 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertTrue(tools.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已发布快照分支也应按聊天态权限过滤知识库,并保留检索模式。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void appendPublishedKnowledgeToolsShouldFilterUnavailableBindings() throws Exception {
|
||||||
|
BotServiceImpl service = new BotServiceImpl();
|
||||||
|
injectAvailabilityService(service, buildAvailabilityService(
|
||||||
|
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
|
||||||
|
Collections.emptySet()
|
||||||
|
));
|
||||||
|
injectDocumentCollectionService(service, mockDocumentCollectionService(
|
||||||
|
buildKnowledge(101, 11, 21, "PUBLIC"),
|
||||||
|
buildKnowledge(102, 11, 99, "PUBLIC")
|
||||||
|
));
|
||||||
|
|
||||||
|
Bot runtimeBot = new Bot();
|
||||||
|
Map<String, Object> snapshot = new HashMap<>();
|
||||||
|
snapshot.put("knowledgeBindings", List.of(
|
||||||
|
buildPublishedBinding(101, RetrievalMode.KEYWORD),
|
||||||
|
buildPublishedBinding(102, RetrievalMode.HYBRID)
|
||||||
|
));
|
||||||
|
runtimeBot.setPublishedSnapshotJson(snapshot);
|
||||||
|
List<Tool> functionList = new ArrayList<>();
|
||||||
|
Method method = BotServiceImpl.class.getDeclaredMethod(
|
||||||
|
"appendPublishedKnowledgeTools",
|
||||||
|
List.class,
|
||||||
|
Bot.class,
|
||||||
|
boolean.class,
|
||||||
|
ChatTimeToolAvailabilityContext.class
|
||||||
|
);
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
method.invoke(service, functionList, runtimeBot, false, buildContext(12, 3));
|
||||||
|
|
||||||
|
Assert.assertEquals(1, functionList.size());
|
||||||
|
DocumentCollectionTool tool = (DocumentCollectionTool) functionList.get(0);
|
||||||
|
Assert.assertEquals(BigInteger.valueOf(101), tool.getKnowledgeId());
|
||||||
|
Assert.assertEquals(RetrievalMode.KEYWORD, tool.getRetrievalMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void injectAvailabilityService(BotServiceImpl service, ChatTimeToolAvailabilityService availabilityService) throws Exception {
|
||||||
|
Field field = BotServiceImpl.class.getDeclaredField("chatTimeToolAvailabilityService");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(service, availabilityService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void injectDocumentCollectionService(BotServiceImpl service, tech.easyflow.ai.service.DocumentCollectionService documentCollectionService) throws Exception {
|
||||||
|
Field field = BotServiceImpl.class.getDeclaredField("documentCollectionService");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(service, documentCollectionService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatTimeToolAvailabilityService buildAvailabilityService(RoleCategoryAccessSnapshot accessSnapshot,
|
||||||
|
Set<BigInteger> deptIds) {
|
||||||
|
return new ChatTimeToolAvailabilityServiceImpl(List.of(
|
||||||
|
new ChatTimeKnowledgeAvailabilityResolver(
|
||||||
|
new KnowledgeVisibilityQueryHelper(),
|
||||||
|
mockCategoryPermissionService(accessSnapshot),
|
||||||
|
mockSysDeptService(deptIds)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatTimeToolAvailabilityContext buildContext(long accountId, long deptId) {
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.valueOf(accountId));
|
||||||
|
loginAccount.setDeptId(BigInteger.valueOf(deptId));
|
||||||
|
ChatTimeToolAvailabilityContext context = new ChatTimeToolAvailabilityContext();
|
||||||
|
context.setLoginAccount(loginAccount);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BotDocumentCollection buildBinding(long knowledgeId,
|
||||||
|
long createdBy,
|
||||||
|
long categoryId,
|
||||||
|
String visibilityScope,
|
||||||
|
RetrievalMode retrievalMode) {
|
||||||
|
DocumentCollection knowledge = buildKnowledge(knowledgeId, createdBy, categoryId, visibilityScope);
|
||||||
|
BotDocumentCollection binding = new BotDocumentCollection();
|
||||||
|
binding.setDocumentCollectionId(BigInteger.valueOf(knowledgeId));
|
||||||
|
binding.setKnowledge(knowledge);
|
||||||
|
binding.setRetrievalMode(retrievalMode);
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentCollection buildKnowledge(long knowledgeId,
|
||||||
|
long createdBy,
|
||||||
|
long categoryId,
|
||||||
|
String visibilityScope) {
|
||||||
|
DocumentCollection knowledge = new DocumentCollection();
|
||||||
|
knowledge.setId(BigInteger.valueOf(knowledgeId));
|
||||||
|
knowledge.setTitle("knowledge-" + knowledgeId);
|
||||||
|
knowledge.setDescription("desc-" + knowledgeId);
|
||||||
|
knowledge.setCreatedBy(BigInteger.valueOf(createdBy));
|
||||||
|
knowledge.setCategoryId(BigInteger.valueOf(categoryId));
|
||||||
|
knowledge.setVisibilityScope(visibilityScope);
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildPublishedBinding(long knowledgeId, RetrievalMode retrievalMode) {
|
||||||
|
Map<String, Object> binding = new HashMap<>();
|
||||||
|
binding.put("knowledgeId", String.valueOf(knowledgeId));
|
||||||
|
binding.put("retrievalMode", retrievalMode.name());
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
private tech.easyflow.ai.service.DocumentCollectionService mockDocumentCollectionService(DocumentCollection... collections) {
|
||||||
|
Map<BigInteger, DocumentCollection> knowledgeMap = new HashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
knowledgeMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
return (tech.easyflow.ai.service.DocumentCollectionService) Proxy.newProxyInstance(
|
||||||
|
tech.easyflow.ai.service.DocumentCollectionService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{tech.easyflow.ai.service.DocumentCollectionService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getPublishedById".equals(method.getName()) && args != null && args.length == 1 && args[0] instanceof BigInteger knowledgeId) {
|
||||||
|
return knowledgeMap.get(knowledgeId);
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryPermissionService mockCategoryPermissionService(RoleCategoryAccessSnapshot accessSnapshot) {
|
||||||
|
return (CategoryPermissionService) Proxy.newProxyInstance(
|
||||||
|
CategoryPermissionService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{CategoryPermissionService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getAccess".equals(method.getName())) {
|
||||||
|
return accessSnapshot;
|
||||||
|
}
|
||||||
|
if ("isSuperAdmin".equals(method.getName())) {
|
||||||
|
return accessSnapshot.isSuperAdmin();
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysDeptService mockSysDeptService(Set<BigInteger> deptIds) {
|
||||||
|
Set<BigInteger> readableDeptIds = deptIds == null ? Collections.emptySet() : deptIds;
|
||||||
|
return (SysDeptService) Proxy.newProxyInstance(
|
||||||
|
SysDeptService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{SysDeptService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getSelfAndAncestorDeptIds".equals(method.getName())) {
|
||||||
|
return readableDeptIds;
|
||||||
|
}
|
||||||
|
if ("canUserAccessDeptScopedResource".equals(method.getName())) {
|
||||||
|
BigInteger resourceDeptId = (BigInteger) args[1];
|
||||||
|
return readableDeptIds.contains(resourceDeptId);
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<BigInteger> setOf(BigInteger... values) {
|
||||||
|
Set<BigInteger> result = new LinkedHashSet<>();
|
||||||
|
Collections.addAll(result, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object defaultValue(Class<?> returnType) {
|
||||||
|
if (returnType == boolean.class) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (returnType == int.class) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (returnType == long.class) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user