feat: 收口聊天时知识库工具可见性

- 新增 chatTime 工具可见性抽象与知识库 resolver

- 聊天装配链路按当前用户过滤知识库工具并补齐调用兜底

- 补充聊天时显式登录快照与对应后端测试
This commit is contained in:
2026-05-11 20:54:13 +08:00
parent ff863e3c27
commit c1590b0d8a
15 changed files with 1441 additions and 25 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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.Parameter;
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.rag.KnowledgeRetrievalRequest;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.util.SpringContextUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
/**
* 知识库聊天工具。
*/
public class DocumentCollectionTool extends BaseTool {
private BigInteger knowledgeId;
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
private ChatTimeToolAvailabilityContext chatTimeContext;
/**
* 默认构造器。
*/
public DocumentCollectionTool() {
}
/**
* 基于知识库实体构造聊天工具。
*
* @param documentCollection 知识库
* @param needEnglishName 是否使用英文名
*/
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName) {
this(documentCollection, needEnglishName, RetrievalMode.HYBRID);
}
/**
* 基于知识库实体构造聊天工具。
*
* @param documentCollection 知识库
* @param needEnglishName 是否使用英文名
* @param 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.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
this.chatTimeContext = chatTimeContext;
if (needEnglishName) {
this.name = documentCollection.getEnglishName();
} else {
@@ -63,10 +102,42 @@ public class DocumentCollectionTool extends BaseTool {
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
public Object invoke(Map<String, Object> argsMap) {
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();
request.setKnowledgeId(this.knowledgeId);
request.setQuery((String) argsMap.get("input"));
@@ -91,5 +162,17 @@ public class DocumentCollectionTool extends BaseTool {
return sb.toString();
}
/**
* 当工具由聊天运行时装配时,执行聊天态权限兜底。
*
* @param knowledge 当前知识库实体
*/
protected void assertChatTimeAvailability(DocumentCollection knowledge) {
if (chatTimeContext == null) {
return;
}
ChatTimeToolAvailabilityService availabilityService = SpringContextUtil.getBean(ChatTimeToolAvailabilityService.class);
availabilityService.assertAvailable(chatTimeContext, knowledge, "当前用户无权在聊天中访问该知识库");
}
}

View File

@@ -8,6 +8,7 @@ import com.easyagents.store.milvus.MilvusVectorStore;
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
import com.mybatisflex.annotation.Table;
import tech.easyflow.ai.config.AiMilvusConfig;
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
import tech.easyflow.ai.entity.base.DocumentCollectionBase;
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
@@ -111,7 +112,19 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
}
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) {

View File

@@ -24,10 +24,13 @@ import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
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.memory.DefaultBotMessageMemory;
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
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.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.*;
@@ -39,6 +42,7 @@ import tech.easyflow.ai.utils.CustomBeanUtils;
import tech.easyflow.ai.utils.RegexUtils;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.MapUtil;
import tech.easyflow.common.util.Maps;
@@ -131,6 +135,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
private CategoryPermissionService categoryPermissionService;
@Resource
private ChatRuntimeManager chatRuntimeManager;
@Resource
private ChatTimeToolAvailabilityService chatTimeToolAvailabilityService;
@Override
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) {
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
ChatModel chatModel = chatCheckResult.getChatModel();
ChatTimeToolAvailabilityContext chatTimeContext = buildChatTimeToolAvailabilityContext(runtimeContext, chatCheckResult.getAiBot());
final MemoryPrompt memoryPrompt = new MemoryPrompt();
String systemPrompt = buildSystemPromptWithFaqImageRule(
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)
.set("needEnglishName", false)
.set("bot", chatCheckResult.getAiBot())
.set("chatTimeContext", chatTimeContext)
.set("publishedOnly", chatCheckResult.isPublishedAccess())));
ChatOptions chatOptions = getChatOptions(modelOptions);
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;
}
Bot runtimeBot = (Bot) buildParams.get("bot");
ChatTimeToolAvailabilityContext chatTimeContext = (ChatTimeToolAvailabilityContext) buildParams.get("chatTimeContext");
boolean usePublishedSnapshot = Boolean.TRUE.equals(buildParams.get("publishedOnly"))
&& runtimeBot != null
&& runtimeBot.getPublishedSnapshotJson() != null
@@ -471,7 +480,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
QueryWrapper queryWrapper = QueryWrapper.create();
if (usePublishedSnapshot) {
appendPublishedWorkflowTools(functionList, runtimeBot, needEnglishName);
appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName);
appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName, chatTimeContext);
} else {
// 工作流 function 集合
queryWrapper.eq(BotWorkflow::getBotId, botId);
@@ -489,13 +498,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
queryWrapper.eq(BotDocumentCollection::getBotId, botId);
List<BotDocumentCollection> botDocumentCollections = botDocumentCollectionService.getMapper()
.selectListWithRelationsByQuery(queryWrapper);
if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) {
for (BotDocumentCollection botDocumentCollection : botDocumentCollections) {
Tool function = botDocumentCollection.getKnowledge()
.toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name());
functionList.add(function);
}
}
functionList.addAll(buildKnowledgeTools(botDocumentCollections, needEnglishName, chatTimeContext));
}
// 插件 function 集合
@@ -534,6 +537,39 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
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")
private void appendPublishedWorkflowTools(List<Tool> functionList, Bot runtimeBot, boolean needEnglishName) {
Object workflows = runtimeBot.getPublishedSnapshotJson().get("workflowBindings");
@@ -562,7 +598,10 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
}
@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");
if (!(knowledges instanceof List<?> knowledgeBindings)) {
return;
@@ -579,14 +618,40 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
if (knowledge == null) {
continue;
}
if (chatTimeContext != null && !chatTimeToolAvailabilityService.evaluate(chatTimeContext, knowledge).isAvailable()) {
continue;
}
Object retrievalMode = bindingMap.get("retrievalMode");
functionList.add(knowledge.toFunction(
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) {
StringBuilder messageBuilder = new StringBuilder();
if (fileList != null && !fileList.isEmpty()) {