package tech.easyflow.ai.service.impl;

import com.alibaba.fastjson2.JSON;
import cn.dev33.satoken.stp.StpUtil;
import com.easyagents.core.file2text.File2TextService;
import com.easyagents.core.file2text.source.HttpDocumentSource;
import com.easyagents.core.memory.ChatMemory;
import com.easyagents.core.message.Message;
import com.easyagents.core.message.SystemMessage;
import com.easyagents.core.message.UserMessage;
import com.easyagents.core.model.chat.ChatModel;
import com.easyagents.core.model.chat.ChatOptions;
import com.easyagents.core.model.chat.StreamResponseListener;
import com.easyagents.core.model.chat.tool.Tool;
import com.easyagents.core.prompt.MemoryPrompt;
import com.easyagents.rag.retrieval.RetrievalMode;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
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.*;
import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.BotMapper;
import tech.easyflow.ai.service.*;
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;
import tech.easyflow.common.util.UrlEncoderUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import tech.easyflow.core.runtime.ChatRuntimeManager;
import tech.easyflow.core.runtime.ChatRuntimeMessage;
import tech.easyflow.system.service.CategoryPermissionService;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static tech.easyflow.ai.entity.table.BotPluginTableDef.BOT_PLUGIN;
import static tech.easyflow.ai.entity.table.PluginItemTableDef.PLUGIN_ITEM;

/**
 *  服务层实现。
 *
 * @author michael
 * @since 2024-08-23
 */
@Service
public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotService {

    private static final Logger log = LoggerFactory.getLogger(BotServiceImpl.class);
    private static final String FAQ_IMAGE_SYSTEM_RULE = "当知识工具返回 Markdown 图片（格式：![描述](URL)）时，你必须在最终回答中保留并输出对应的图片 Markdown，禁止改写、替换或省略图片 URL。";
    private static final String EXTRA_KNOWLEDGE_PRIORITY_RULE = "若当前会话显式选择了额外知识库，请优先参考这些额外知识库；仅在额外知识库无法回答时，再回退到助手默认绑定知识库。";
    private static final int MAX_EXTRA_KNOWLEDGE_COUNT = 3;

    public static class ChatCheckResult {
        private Bot aiBot;
        private Map<String, Object> modelOptions;
        private ChatModel chatModel;
        private String conversationIdStr;
        private boolean publishedAccess;

        public Bot getAiBot() {return aiBot;}

        public void setAiBot(Bot aiBot) {this.aiBot = aiBot;}

        public Map<String, Object> getModelOptions() {return modelOptions;}

        public void setModelOptions(Map<String, Object> modelOptions) {this.modelOptions = modelOptions;}

        public ChatModel getChatModel() {return chatModel;}

        public void setChatModel(ChatModel chatModel) {this.chatModel = chatModel;}

        public String getConversationIdStr() {return conversationIdStr;}

        public void setConversationIdStr(String conversationIdStr) {this.conversationIdStr = conversationIdStr;}

        public boolean isPublishedAccess() {return publishedAccess;}

        public void setPublishedAccess(boolean publishedAccess) {this.publishedAccess = publishedAccess;}
    }

    @Resource(name = "sseThreadPool")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Resource
    private ModelService modelService;
    @Resource
    private BotWorkflowService botWorkflowService;
    @Resource
    private WorkflowService workflowService;
    @Resource
    private BotDocumentCollectionService botDocumentCollectionService;
    @Resource
    private DocumentCollectionService documentCollectionService;
    @Resource
    private BotPluginService botPluginService;
    @Resource
    private PluginService pluginService;
    @Resource
    private PluginItemService pluginItemService;
    @Resource
    private BotMcpService botMcpService;
    @Resource
    private McpService mcpService;
    @Resource(name = "default")
    FileStorageService storageService;
    @Resource
    private CategoryPermissionService categoryPermissionService;
    @Resource
    private ChatRuntimeManager chatRuntimeManager;
    @Resource
    private ChatTimeToolAvailabilityService chatTimeToolAvailabilityService;

    @Override
    public Bot getDetail(String id) {
        Bot aiBot = null;

        if (id.matches(RegexUtils.ALL_NUMBER)) {
            aiBot = getById(id);

            if (aiBot == null) {
                aiBot = getByAlias(id);
            }

        } else {
            aiBot = getByAlias(id);
        }

        return aiBot;
    }

    @Override
    public Bot getByAlias(String alias) {
        QueryWrapper queryWrapper = QueryWrapper.create();
        queryWrapper.eq(Bot::getAlias, alias);
        return getOne(queryWrapper);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Bot getPublishedDetail(String id) {
        return toPublishedView(getDetail(id));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Bot getPublishedById(BigInteger id) {
        return toPublishedView(getById(id));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Bot toPublishedView(Bot bot) {
        if (bot == null) {
            return null;
        }
        Map<String, Object> snapshot = bot.getPublishedSnapshotJson();
        if (snapshot == null || snapshot.isEmpty()) {
            return bot;
        }
        Bot published = JSON.parseObject(JSON.toJSONString(snapshot), Bot.class);
        if (published == null) {
            return bot;
        }
        published.setId(bot.getId());
        published.setTenantId(bot.getTenantId());
        published.setCategoryId(bot.getCategoryId());
        published.setDeptId(bot.getDeptId());
        published.setCreated(bot.getCreated());
        published.setCreatedBy(bot.getCreatedBy());
        published.setModified(bot.getModified());
        published.setModifiedBy(bot.getModifiedBy());
        published.setPublishStatus(bot.getPublishStatus());
        published.setCurrentApprovalInstanceId(bot.getCurrentApprovalInstanceId());
        published.setPublishedSnapshotJson(bot.getPublishedSnapshotJson());
        published.setPublishedAt(bot.getPublishedAt());
        published.setPublishedBy(bot.getPublishedBy());
        if (published.getPublishStatus() == null) {
            published.setPublishStatus(PublishStatus.DRAFT.getCode());
        }
        return published;
    }

    public SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId, ChatCheckResult chatCheckResult) {
        return checkChatBeforeStart(botId, prompt, conversationId, chatCheckResult, false, null);
    }

    @Override
    public SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId,
                                           ChatCheckResult chatCheckResult, BigInteger regenerateRoundId) {
        return checkChatBeforeStart(botId, prompt, conversationId, chatCheckResult, false, regenerateRoundId);
    }

    @Override
    public SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId,
                                           ChatCheckResult chatCheckResult, boolean publishedOnly) {
        return checkChatBeforeStart(botId, prompt, conversationId, chatCheckResult, publishedOnly, null);
    }

    @Override
    public SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId,
                                           ChatCheckResult chatCheckResult, boolean publishedOnly, BigInteger regenerateRoundId) {
        if (!StringUtils.hasLength(prompt)) {
            return ChatSseUtil.sendSystemError(conversationId, "提示词不能为空");
        }
        if (!StringUtils.hasLength(conversationId)) {
            return ChatSseUtil.sendSystemError(conversationId, "conversationId不能为空");
        }
        Bot aiBot = this.getById(botId);
        if (aiBot == null) {
            return ChatSseUtil.sendSystemError(conversationId, "聊天助手不存在");
        }
        if (StpUtil.isLogin()) {
            try {
                categoryPermissionService.assertCategoryResourceVisible("BOT", aiBot.getCreatedBy(), aiBot.getCategoryId(), "无权限访问聊天助手");
            } catch (BusinessException e) {
                return ChatSseUtil.sendSystemError(conversationId, e.getMessage());
            }
        }
        if (aiBot.getModelId() == null) {
            return ChatSseUtil.sendSystemError(conversationId, "请配置大模型!");
        }
        boolean login = StpUtil.isLogin();
        boolean anonymousAccount = false;
        if (login) {
            try {
                anonymousAccount = SaTokenUtil.getLoginAccount() != null
                        && BigInteger.ZERO.equals(SaTokenUtil.getLoginAccount().getId());
            } catch (Exception ignored) {
                anonymousAccount = false;
            }
        }
        if ((!login || anonymousAccount) && !aiBot.isAnonymousEnabled()) {
            return ChatSseUtil.sendSystemError(conversationId, "此聊天助手不支持匿名访问");
        }
        if (publishedOnly && login && !anonymousAccount) {
            if (PublishStatus.from(aiBot.getPublishStatus()) != PublishStatus.PUBLISHED) {
                return ChatSseUtil.sendSystemError(conversationId, "聊天助手尚未发布");
            }
            aiBot = toPublishedView(aiBot);
            chatCheckResult.setPublishedAccess(true);
        } else if (!login || anonymousAccount) {
            Bot publishedBot = toPublishedView(aiBot);
            if (!PublishStatus.from(aiBot.getPublishStatus()).isExternallyVisible()) {
                return ChatSseUtil.sendSystemError(conversationId, "聊天助手尚未发布");
            }
            aiBot = publishedBot;
            chatCheckResult.setPublishedAccess(true);
        } else {
            chatCheckResult.setPublishedAccess(false);
        }
        Map<String, Object> modelOptions = aiBot.getModelOptions();
        Model model = modelService.getModelInstance(aiBot.getModelId());
        if (model == null) {
            return ChatSseUtil.sendSystemError(conversationId, "模型不存在，请检查配置");
        }
        ChatModel chatModel = model.toChatModel();
        if (chatModel == null) {
            return ChatSseUtil.sendSystemError(conversationId, "对话模型获取失败，请检查配置");
        }
        chatCheckResult.setAiBot(aiBot);
        chatCheckResult.setModelOptions(modelOptions);
        chatCheckResult.setChatModel(chatModel);
        chatCheckResult.setConversationIdStr(conversationId);
        return null;
    }

    @Override
    public SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List<Map<String, String>> messages,
                                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 = buildSystemPrompt(
                MapUtil.getString(modelOptions, Bot.KEY_SYSTEM_PROMPT),
                runtimeContext
        );
        Integer maxMessageCount = MapUtil.getInteger(modelOptions, Bot.KEY_MAX_MESSAGE_COUNT);
        if (maxMessageCount != null) {
            memoryPrompt.setMaxAttachedMessageCount(maxMessageCount);
        }
        if (StringUtils.hasLength(systemPrompt)) {
            memoryPrompt.setSystemMessage(SystemMessage.of(systemPrompt));
        }
        String attachmentsToString = attachmentsToString(attachments);
        if (StringUtils.hasLength(attachmentsToString)) {
            prompt = "【用户问题】：\n" + prompt + "\n\n请基于用户上传的附件内容回答用户问题： \n" +  "【用户上传的附件内容】：\n" + attachmentsToString ;
        }
        UserMessage userMessage = new UserMessage(prompt);
        userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
                .set("needEnglishName", true)
                .set("bot", chatCheckResult.getAiBot())
                .set("chatTimeContext", chatTimeContext)
                .set("publishedOnly", chatCheckResult.isPublishedAccess())
                .set("extraKnowledgeIds", resolveExtraKnowledgeIds(runtimeContext))));
        ChatOptions chatOptions = getChatOptions(modelOptions);
        Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false);
        chatOptions.setThinkingEnabled(enableDeepThinking);
        ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
        SseEmitter emitter = chatSseEmitter.getEmitter();
        int historyLimit = resolveHistoryLimit(maxMessageCount);
        if (messages != null && !messages.isEmpty()) {
            ChatMemory defaultChatMemory = new DefaultBotMessageMemory(conversationId, chatSseEmitter, messages);
            memoryPrompt.setMemory(defaultChatMemory);
        } else {
            memoryPrompt.setMemory(new RuntimeChatMemory(
                    conversationId,
                    chatRuntimeManager.loadMessages(runtimeContext, historyLimit)
            ));
        }
        chatRuntimeManager.prepareSession(runtimeContext);
        chatRuntimeManager.recordUserMessage(runtimeContext, buildUserRuntimeMessage(runtimeContext, prompt, attachments));
        memoryPrompt.addMessage(userMessage);
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        threadPoolTaskExecutor.execute(() -> {
            ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
            RequestContextHolder.setRequestAttributes(sra, true);
            StreamResponseListener streamResponseListener = new ChatStreamListener(
                    conversationId.toString(),
                    chatModel,
                    memoryPrompt,
                    chatSseEmitter,
                    chatOptions,
                    chatRuntimeManager,
                    runtimeContext,
                    new ChatAssistantAccumulator()
            );
            chatModel.chatStream(memoryPrompt, streamResponseListener, chatOptions);
        });

        return emitter;
    }

    /**
     * 第三方使用Apikey访问聊天
     * @param botId
     * @return
     */
    @Override
    public SseEmitter startPublicChat(BigInteger botId, String prompt, List<Message> messages,
                                      BotServiceImpl.ChatCheckResult chatCheckResult, ChatRuntimeContext runtimeContext) {
        Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
        ChatOptions chatOptions = getChatOptions(modelOptions);
        Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false);
        chatOptions.setThinkingEnabled(enableDeepThinking);
        ChatModel chatModel = chatCheckResult.getChatModel();
        String systemPrompt = buildSystemPrompt(
                MapUtil.getString(modelOptions, Bot.KEY_SYSTEM_PROMPT),
                runtimeContext
        );
        UserMessage userMessage = new UserMessage(prompt);
        userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
                .set("needEnglishName", true)
                .set("needAccountId", false)
                .set("bot", chatCheckResult.getAiBot())
                .set("publishedOnly", chatCheckResult.isPublishedAccess())
                .set("extraKnowledgeIds", resolveExtraKnowledgeIds(runtimeContext))
        ));
        ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
        SseEmitter emitter = chatSseEmitter.getEmitter();
        ChatMemory defaultChatMemory = new PublicBotMessageMemory(chatSseEmitter, messages);
        final MemoryPrompt memoryPrompt = new MemoryPrompt();
        memoryPrompt.setMemory(defaultChatMemory);
        if (StringUtils.hasLength(systemPrompt)) {
            memoryPrompt.setSystemMessage(SystemMessage.of(systemPrompt));
        }
        memoryPrompt.addMessage(userMessage);
        chatRuntimeManager.prepareSession(runtimeContext);
        chatRuntimeManager.recordUserMessage(runtimeContext, buildUserRuntimeMessage(runtimeContext, prompt, null));
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
        threadPoolTaskExecutor.execute(() -> {
            RequestContextHolder.setRequestAttributes(sra, true);
            StreamResponseListener streamResponseListener = new ChatStreamListener(
                    chatCheckResult.getConversationIdStr(),
                    chatModel,
                    memoryPrompt,
                    chatSseEmitter,
                    chatOptions,
                    chatRuntimeManager,
                    runtimeContext,
                    new ChatAssistantAccumulator()
            );
            chatModel.chatStream(memoryPrompt, streamResponseListener, chatOptions);
        });

        return emitter;
    }

    @Override
    public void updateBotLlmId(Bot aiBot) {
        Bot bot = getById(aiBot.getId());

        if (bot == null) {
            log.error("修改bot的llmId失败，bot不存在！");
            throw new BusinessException("bot不存在！");
        }

        bot.setModelId(aiBot.getModelId());

        updateById(bot, false);

    }


    @Override
    public boolean updateById(Bot entity) {
        Bot aiBot = getById(entity.getId());
        if (aiBot == null) {
            throw new BusinessException("bot 不存在");
        }

        CustomBeanUtils.copyPropertiesIgnoreNull(entity, aiBot);

        if ("".equals(aiBot.getAlias())) {
            aiBot.setAlias(null);
        }


        return super.updateById(aiBot, false);
    }

    public static ChatOptions getChatOptions(Map<String, Object> llmOptions) {
        ChatOptions defaultOptions = new ChatOptions();
        if (llmOptions != null) {
            Object topK = llmOptions.get("topK");
            Object maxReplyLength = llmOptions.get("maxReplyLength");
            Object temperature = llmOptions.get("temperature");
            Object topP = llmOptions.get("topP");
            Object thinkingEnabled = llmOptions.get("thinkingEnabled");

            if (topK != null) {
                defaultOptions.setTopK(Integer.parseInt(String.valueOf(topK)));
            }
            if (maxReplyLength != null) {
                defaultOptions.setMaxTokens(Integer.parseInt(String.valueOf(maxReplyLength)));
            }
            if (temperature != null) {
                defaultOptions.setTemperature(Float.parseFloat(String.valueOf(temperature)));
            }
            if (topP != null) {
                defaultOptions.setTopP(Float.parseFloat(String.valueOf(topP)));
            }
            if (thinkingEnabled != null) {
                defaultOptions.setThinkingEnabled(Boolean.parseBoolean(String.valueOf(thinkingEnabled)));
            }

        }
        return defaultOptions;
    }

    private List<Tool> buildFunctionList(Map<String, Object> buildParams) {

        if (buildParams == null || buildParams.isEmpty()) {
            throw new IllegalArgumentException("buildParams is empty");
        }

        List<Tool> functionList = new ArrayList<>();

        BigInteger botId = (BigInteger) buildParams.get("botId");
        if (botId == null) {
            throw new IllegalArgumentException("botId is empty");
        }
        Boolean needEnglishName = (Boolean) buildParams.get("needEnglishName");
        if (needEnglishName == null) {
            needEnglishName = false;
        }
        Bot runtimeBot = (Bot) buildParams.get("bot");
        ChatTimeToolAvailabilityContext chatTimeContext = (ChatTimeToolAvailabilityContext) buildParams.get("chatTimeContext");
        List<BigInteger> extraKnowledgeIds = sanitizeExtraKnowledgeIds((List<BigInteger>) buildParams.get("extraKnowledgeIds"));
        boolean usePublishedSnapshot = Boolean.TRUE.equals(buildParams.get("publishedOnly"))
                && runtimeBot != null
                && runtimeBot.getPublishedSnapshotJson() != null
                && PublishStatus.from(runtimeBot.getPublishStatus()).isExternallyVisible();

        QueryWrapper queryWrapper = QueryWrapper.create();
        Set<BigInteger> existingKnowledgeIds = new LinkedHashSet<>();
        appendExtraKnowledgeTools(functionList, extraKnowledgeIds, needEnglishName, chatTimeContext, existingKnowledgeIds);
        if (usePublishedSnapshot) {
            appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName, chatTimeContext, existingKnowledgeIds);
            appendPublishedWorkflowTools(functionList, runtimeBot, needEnglishName);
        } else {
            // 工作流 function 集合
            queryWrapper.eq(BotWorkflow::getBotId, botId);
            List<BotWorkflow> botWorkflows = botWorkflowService.getMapper()
                    .selectListWithRelationsByQuery(queryWrapper);
            if (botWorkflows != null && !botWorkflows.isEmpty()) {
                for (BotWorkflow botWorkflow : botWorkflows) {
                    Tool function = botWorkflow.getWorkflow().toFunction(needEnglishName);
                    functionList.add(function);
                }
            }

            // 知识库 function 集合
            queryWrapper = QueryWrapper.create();
            queryWrapper.eq(BotDocumentCollection::getBotId, botId);
            List<BotDocumentCollection> botDocumentCollections = botDocumentCollectionService.getMapper()
                    .selectListWithRelationsByQuery(queryWrapper);
            functionList.addAll(buildKnowledgeTools(botDocumentCollections, needEnglishName, chatTimeContext, existingKnowledgeIds));
        }

        // 插件 function 集合
        queryWrapper = QueryWrapper.create();
        queryWrapper.select(BOT_PLUGIN.PLUGIN_ITEM_ID).eq(BotPlugin::getBotId, botId);
        List<BigInteger> pluginToolIds = botPluginService.getMapper()
                .selectListWithRelationsByQueryAs(queryWrapper, BigInteger.class);
        if (pluginToolIds != null && !pluginToolIds.isEmpty()) {
            QueryWrapper queryTool = QueryWrapper.create()
                    .select(PLUGIN_ITEM.ALL_COLUMNS)
                    .from(PLUGIN_ITEM)
                    .where(PLUGIN_ITEM.ID.in(pluginToolIds));
            List<PluginItem> pluginItems = pluginItemService.getMapper().selectListWithRelationsByQuery(queryTool);
            if (pluginItems != null && !pluginItems.isEmpty()) {
                for (PluginItem pluginItem : pluginItems) {
                    if (pluginItem.getPluginId() != null) {
                        Plugin plugin = pluginService.getById(pluginItem.getPluginId());
                        if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
                            continue;
                        }
                    }
                    functionList.add(pluginItem.toFunction());
                }
            }
        }

        // MCP function 集合
        queryWrapper = QueryWrapper.create();
        queryWrapper.eq(BotMcp::getBotId, botId);
        List<BotMcp> botMcpList = botMcpService.getMapper().selectListWithRelationsByQuery(queryWrapper);
        botMcpList.forEach(botMcp -> {
            Tool tool = mcpService.toFunction(botMcp);
            functionList.add(tool);
        });

        return functionList;
    }

    /**
     * 将 Bot 绑定的知识库候选项收敛为当前聊天可用的工具列表。
     *
     * @param botDocumentCollections Bot 知识库绑定项
     * @param needEnglishName 是否使用英文名称
     * @param chatTimeContext 聊天时权限上下文
     * @return 知识库工具列表
     */
    List<Tool> buildKnowledgeTools(List<BotDocumentCollection> botDocumentCollections,
                                   boolean needEnglishName,
                                   ChatTimeToolAvailabilityContext chatTimeContext) {
        return buildKnowledgeTools(botDocumentCollections, needEnglishName, chatTimeContext, new LinkedHashSet<>());
    }

    /**
     * 将 Bot 绑定的知识库候选项收敛为当前聊天可用的工具列表，并与已选临时知识库去重。
     *
     * @param botDocumentCollections Bot 知识库绑定项
     * @param needEnglishName 是否使用英文名称
     * @param chatTimeContext 聊天时权限上下文
     * @param existingKnowledgeIds 已装配知识库 ID 集
     * @return 知识库工具列表
     */
    List<Tool> buildKnowledgeTools(List<BotDocumentCollection> botDocumentCollections,
                                   boolean needEnglishName,
                                   ChatTimeToolAvailabilityContext chatTimeContext,
                                   Set<BigInteger> existingKnowledgeIds) {
        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 || isDuplicateKnowledge(existingKnowledgeIds, knowledge.getId())) {
                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");
        if (!(workflows instanceof List<?> workflowBindings)) {
            return;
        }
        for (Object item : workflowBindings) {
            if (!(item instanceof Map<?, ?> workflowMap)) {
                continue;
            }
            Object workflowId = workflowMap.get("workflowId");
            if (workflowId == null) {
                continue;
            }
            Workflow workflow = workflowService.getPublishedById(new BigInteger(String.valueOf(workflowId)));
            if (workflow == null) {
                continue;
            }
            WorkflowTool tool = new WorkflowTool(
                    workflow,
                    needEnglishName,
                    PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId()))
            );
            functionList.add(tool);
        }
    }

    @SuppressWarnings("unchecked")
    private void appendPublishedKnowledgeTools(List<Tool> functionList,
                                               Bot runtimeBot,
                                               boolean needEnglishName,
                                               ChatTimeToolAvailabilityContext chatTimeContext,
                                               Set<BigInteger> existingKnowledgeIds) {
        Object knowledges = runtimeBot.getPublishedSnapshotJson().get("knowledgeBindings");
        if (!(knowledges instanceof List<?> knowledgeBindings)) {
            return;
        }
        for (Object item : knowledgeBindings) {
            if (!(item instanceof Map<?, ?> bindingMap)) {
                continue;
            }
            Object knowledgeId = bindingMap.get("knowledgeId");
            if (knowledgeId == null) {
                continue;
            }
            DocumentCollection knowledge = documentCollectionService.getPublishedById(new BigInteger(String.valueOf(knowledgeId)));
            if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
                continue;
            }
            if (chatTimeContext != null && !chatTimeToolAvailabilityService.evaluate(chatTimeContext, knowledge).isAvailable()) {
                continue;
            }
            if (isDuplicateKnowledge(existingKnowledgeIds, knowledge.getId())) {
                continue;
            }
            Object retrievalMode = bindingMap.get("retrievalMode");
            functionList.add(knowledge.toFunction(
                    needEnglishName,
                    retrievalMode == null ? null : String.valueOf(retrievalMode),
                    chatTimeContext
            ));
        }
    }

    /**
     * 组装会话级临时知识库工具，并按用户选择顺序优先插入。
     *
     * @param functionList 工具集合
     * @param extraKnowledgeIds 额外知识库 ID
     * @param needEnglishName 是否使用英文名称
     * @param chatTimeContext 聊天时权限上下文
     * @param existingKnowledgeIds 已装配知识库 ID 集
     */
    protected void appendExtraKnowledgeTools(List<Tool> functionList,
                                             List<BigInteger> extraKnowledgeIds,
                                             boolean needEnglishName,
                                             ChatTimeToolAvailabilityContext chatTimeContext,
                                             Set<BigInteger> existingKnowledgeIds) {
        if (extraKnowledgeIds == null || extraKnowledgeIds.isEmpty()) {
            return;
        }
        for (BigInteger knowledgeId : extraKnowledgeIds) {
            if (knowledgeId == null || isDuplicateKnowledge(existingKnowledgeIds, knowledgeId)) {
                continue;
            }
            DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
            if (knowledge == null) {
                throw new BusinessException("额外知识库不存在");
            }
            if (PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
                throw new BusinessException("额外知识库未发布，无法用于聊天");
            }
            if (chatTimeContext != null && !chatTimeToolAvailabilityService.evaluate(chatTimeContext, knowledge).isAvailable()) {
                throw new BusinessException("当前用户无权使用所选知识库");
            }
            functionList.add(documentCollectionService.toPublishedView(knowledge)
                    .toFunction(needEnglishName, RetrievalMode.HYBRID.name(), 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()) {
            File2TextService fileTextService = new File2TextService();
            for (int i = 0; i < fileList.size(); i++) {
                String fileUrl = fileList.get(i);
                String encodedUrl = UrlEncoderUtil.getEncodedUrl(fileUrl);
                String result = fileTextService.extractTextFromSource(new HttpDocumentSource(encodedUrl));
                if (result != null) {
                    if (i > 0) {
                        messageBuilder.append("\n\n");
                    }
                    messageBuilder.append("附件").append(i + 1).append("，文件名为：").append(PathGeneratorUtil.getPureFileName(fileUrl)).append("，内容为：  \n").append(result);
                }
                storageService.delete(fileUrl);
            }
        }
        return messageBuilder.toString();
    }

    private int resolveHistoryLimit(Integer maxMessageCount) {
        if (maxMessageCount == null || maxMessageCount <= 0) {
            return 20;
        }
        return Math.min(maxMessageCount, 200);
    }

    private ChatRuntimeMessage buildUserRuntimeMessage(ChatRuntimeContext context, String prompt, List<String> attachments) {
        ChatRuntimeMessage runtimeMessage = new ChatRuntimeMessage();
        runtimeMessage.setRole("user");
        runtimeMessage.setContentType("TEXT");
        runtimeMessage.setContentText(prompt);
        runtimeMessage.setCreatedAt(new Date());
        runtimeMessage.setSenderId(context.getUserId());
        runtimeMessage.setSenderName(resolveUserDisplayName(context));
        if (attachments != null && !attachments.isEmpty()) {
            Map<String, Object> payload = new LinkedHashMap<>();
            payload.put("attachments", attachments);
            runtimeMessage.setContentPayload(payload);
        }
        return runtimeMessage;
    }

    private String resolveUserDisplayName(ChatRuntimeContext context) {
        if (context.getUserName() != null && !context.getUserName().isBlank()) {
            return context.getUserName();
        }
        return context.getUserAccount();
    }

    private String buildSystemPrompt(String systemPrompt, ChatRuntimeContext runtimeContext) {
        String mergedPrompt = appendPromptRule(systemPrompt, FAQ_IMAGE_SYSTEM_RULE);
        if (hasExtraKnowledgeSelection(runtimeContext)) {
            mergedPrompt = appendPromptRule(mergedPrompt, EXTRA_KNOWLEDGE_PRIORITY_RULE);
        }
        return mergedPrompt;
    }

    private String appendPromptRule(String systemPrompt, String rule) {
        if (!StringUtils.hasLength(systemPrompt)) {
            return rule;
        }
        if (systemPrompt.contains(rule)) {
            return systemPrompt;
        }
        return systemPrompt + "\n\n" + rule;
    }

    private List<BigInteger> resolveExtraKnowledgeIds(ChatRuntimeContext runtimeContext) {
        if (runtimeContext == null || runtimeContext.getExt() == null) {
            return List.of();
        }
        Object rawValue = runtimeContext.getExt().get(ChatRuntimeExtKeys.EXTRA_KNOWLEDGE_IDS);
        if (!(rawValue instanceof List<?> rawList) || rawList.isEmpty()) {
            return List.of();
        }
        List<BigInteger> values = new ArrayList<>(rawList.size());
        for (Object item : rawList) {
            if (item == null) {
                continue;
            }
            values.add(new BigInteger(String.valueOf(item)));
        }
        return values;
    }

    private List<BigInteger> sanitizeExtraKnowledgeIds(List<BigInteger> extraKnowledgeIds) {
        if (extraKnowledgeIds == null || extraKnowledgeIds.isEmpty()) {
            return List.of();
        }
        LinkedHashSet<BigInteger> dedup = new LinkedHashSet<>();
        for (BigInteger knowledgeId : extraKnowledgeIds) {
            if (knowledgeId != null) {
                dedup.add(knowledgeId);
            }
        }
        if (dedup.size() > MAX_EXTRA_KNOWLEDGE_COUNT) {
            throw new BusinessException("额外知识库最多选择 3 个");
        }
        return new ArrayList<>(dedup);
    }

    private boolean hasExtraKnowledgeSelection(ChatRuntimeContext runtimeContext) {
        return !resolveExtraKnowledgeIds(runtimeContext).isEmpty();
    }

    private boolean isDuplicateKnowledge(Set<BigInteger> existingKnowledgeIds, BigInteger knowledgeId) {
        return knowledgeId != null && existingKnowledgeIds != null && !existingKnowledgeIds.add(knowledgeId);
    }

}
