package com.ruoyi.llm.service.impl;

import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONArray;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.SnowFlake;
import com.ruoyi.llm.domain.CmcAgent;
import com.ruoyi.llm.domain.CmcChat;
import com.ruoyi.llm.domain.CmcTopic;
import com.ruoyi.llm.domain.CmcDocument;
import com.ruoyi.llm.mapper.CmcAgentMapper;
import com.ruoyi.llm.mapper.CmcChatMapper;
import com.ruoyi.llm.mapper.CmcDocumentMapper;
import com.ruoyi.llm.service.ICmcAgentService;
import com.ruoyi.llm.service.ICmcTopicService;
import com.ruoyi.common.utils.milvus.MilvusConnectionPool;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.bgesmallzhv15.BgeSmallZhV15EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.IndexParam;
import io.milvus.v2.service.vector.request.SearchReq;
import io.milvus.v2.service.vector.request.SearchReq.SearchReqBuilder;
import io.milvus.v2.service.vector.request.data.BaseVector;
import io.milvus.v2.service.vector.request.data.FloatVec;
import io.milvus.v2.service.vector.response.SearchResp;

import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.usermodel.Paragraph;
import org.apache.poi.hwpf.usermodel.Range;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.noear.solon.ai.AiUsage;
import org.noear.solon.ai.agent.simple.SimpleAgent;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.ChatSession;
import org.noear.solon.ai.chat.message.ChatMessage;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.ai.chat.session.InMemoryChatSession;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
import org.springframework.beans.factory.annotation.Autowired;

import reactor.core.publisher.Flux;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.llm.domain.Block;
import com.ruoyi.llm.domain.ChapterStreamResponse;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 智能体Service业务层处理
 *
 * @author ruoyi
 * @date 2025-07-17
 */
@Service
public class CmcAgentServiceImpl implements ICmcAgentService {
    @Autowired
    private CmcAgentMapper cmcAgentMapper;

    @Autowired
    private CmcDocumentMapper cmcDocumentMapper;

    @Autowired
    private CmcChatMapper cmcChatMapper;

    @Autowired
    private ICmcTopicService cmcTopicService;

    @Autowired
    private MilvusConnectionPool milvusConnectionPool;

    private String processValue = "";

    private static final EmbeddingModel embeddingModel = new BgeSmallZhV15EmbeddingModel();

    @Value("${cmc.llmService.url}")
    private String llmServiceUrl;

    @Value("${cmc.milvusService.url}")
    private String milvusServiceUrl;

    /**
     * 查询智能体
     *
     * @param agentId 智能体主键
     * @return 智能体
     */
    @Override
    public CmcAgent selectCmcAgentByAgentId(Integer agentId) {
        return cmcAgentMapper.selectCmcAgentByAgentId(agentId);
    }

    /**
     * 查询智能体列表
     *
     * @param cmcAgent 智能体
     * @return 智能体
     */
    @Override
    public List<CmcAgent> selectCmcAgentList(CmcAgent cmcAgent) {
        return cmcAgentMapper.selectCmcAgentList(cmcAgent);
    }

    /**
     * 获取开场白
     *
     * @param agentName 智能体名称
     * @return 结果
     */
    @Override
    public String getOpening(String agentName) {
        String content = "";
        if (agentName.contains("技术标书"))
            content = "我是投标文件写作助手，我将助您完成技术文件部分撰写。请上传招标询价文件，分析后将依招标服务要求，运用技术方案知识库，为您提供参考。";
        else if (agentName.contains("检查"))
            content = "我是文档检查助手，我将助您完成错别字检查。请上传文件。";
        return content;
    }

    /**
     * 上传单文件
     *
     * @param file 文件
     * @return 结果
     */
    @Override
    public JSONObject uploadDocument(MultipartFile file, String agentName) throws Throwable {
        processValue = "";
        String prefixPath = "/upload/agent/" + agentName;
        File profilePath = new File(RuoYiConfig.getProfile() + prefixPath);
        if (!profilePath.exists())
            profilePath.mkdirs();
        String chatId = new SnowFlake().generateId();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("chatId", chatId);
        File transferFile = new File(profilePath + "/" + file.getOriginalFilename());
        if (!transferFile.exists())
            file.transferTo(transferFile);
        processValue = "上传完成:100%";
        CmcDocument cmcDocument = new CmcDocument();
        cmcDocument.setDocumentId(new SnowFlake().generateId());
        cmcDocument.setChatId(chatId);
        cmcDocument.setPath(prefixPath + "/" + file.getOriginalFilename());
        if (file.getOriginalFilename().endsWith(".pdf"))
            cmcDocument.setPath(prefixPath + "/" + file.getOriginalFilename().replace(".pdf", ".docx"));
        cmcDocumentMapper.insertCmcDocument(cmcDocument);
        String message = "";
        CmcChat cmcChat = new CmcChat();
        cmcChat.setChatId(jsonObject.getString("chatId"));
        cmcChat.setInputTime(new Date());
        if (agentName.contains("技术设计书"))
            cmcChat.setInput("项目任务书地址：" + prefixPath + "/" + file.getOriginalFilename());
        else
            cmcChat.setInput("招标文件地址：" + prefixPath + "/" + file.getOriginalFilename());
        cmcChat.setUserId(SecurityUtils.getUserId());
        cmcChatMapper.insertCmcChat(cmcChat);
        if (agentName.contains("技术标书")) {
            String[] filenameSplit = file.getOriginalFilename().split("\\.");
            String outputFilename = prefixPath + "/" + file.getOriginalFilename()
                    .replace(filenameSplit[filenameSplit.length - 2],
                            filenameSplit[filenameSplit.length - 2] + "_" + agentName);
            if (file.getOriginalFilename().endsWith(".doc"))
                outputFilename = outputFilename.replace(".doc", ".docx");
            if (file.getOriginalFilename().endsWith(".pdf"))
                outputFilename = outputFilename.replace(".pdf", ".docx");

            // 分割文档（只调用一次）
            List<TextSegment> segments = splitDocument(transferFile, 300, 50);

            // 分析项目概况
            String projectOverview = analyzeProjectOverview(profilePath + "/" + file.getOriginalFilename(), segments);
            jsonObject.put("projectOverview", projectOverview);

            // 分析评分标准
            String scoringRequirements = "";
            if (agentName.contains("技术标书"))
                scoringRequirements = analyzeScoringRequirements(segments, agentName);
            jsonObject.put("scoringRequirements", scoringRequirements);

            // 提取输出文件名（不含路径）
            String outputFileNameOnly = new File(outputFilename).getName();

            // 生成详细目录结构（包含二三级标题）并写入文件
            JSONArray detailedDirectoryTree = generateDetailedDirectory(outputFileNameOnly, agentName, segments);
            String detailedDirectory = buildDirectoryText(detailedDirectoryTree);
            jsonObject.put("detailedDirectory", detailedDirectoryTree);
            jsonObject.put("directoryText", detailedDirectory);

            if (agentName.contains("技术标书"))
                message = "好的，我已经收到您上传的招标文件。以下为根据招标文件生成的章节大纲：\n\n" + detailedDirectory + "\n\n" +
                        "若您对章节标题有异议，请打开" + "【<a href='/profile" + outputFilename + "'> 技术文件 " + "</a>】"
                        + "进行修改，后续将根据修改后的章节标题，帮您生成对应章节内容。\n\n" +
                        "思考时间可能较长，请耐心等待！\n";
            else
                message = "好的，我已经收到您上传的项目任务书。以下为根据项目任务书生成的章节大纲：\n\n" + detailedDirectory + "\n\n" +
                        "若您对章节标题有异议，请打开" + "【<a href='/profile" + outputFilename + "'> 项目任务书 " + "</a>】"
                        + "进行修改，后续将根据修改后的章节标题，帮您生成对应章节内容。\n\n" +
                        "思考时间可能较长，请耐心等待！\n";

            // 返回输出文件名，以便前端在保存目录时使用
            jsonObject.put("filename", file.getOriginalFilename()
                    .replace(filenameSplit[filenameSplit.length - 2],
                            filenameSplit[filenameSplit.length - 2] + "_" + agentName));

            // 根据 agentName 查询 agentId
            CmcAgent queryAgent = new CmcAgent();
            queryAgent.setAgentName(agentName);
            List<CmcAgent> agentList = cmcAgentMapper.selectCmcAgentList(queryAgent);
            Integer agentId = null;
            if (!agentList.isEmpty()) {
                agentId = agentList.get(0).getAgentId();
            }

            // 去除文件名后缀作为 topic 名称
            String originalFilename = file.getOriginalFilename();
            String topic = originalFilename.substring(0, originalFilename.lastIndexOf('.'));

            // 创建 topic
            CmcTopic cmcTopic = new CmcTopic();
            cmcTopic.setTopicId(new SnowFlake().generateId());
            cmcTopic.setAgentId(agentId);
            cmcTopic.setTopic(topic);
            cmcTopicService.insertCmcTopic(cmcTopic);
            String topicId = cmcTopic.getTopicId();

            // 合并 projectOverview、scoringRequirements、detailedDirectory 作为 output
            StringBuilder output = new StringBuilder();
            output.append("【项目概况】\n").append(projectOverview).append("\n\n");
            // 当agentname包含"技术"且包含"询价"时，使用"应价人须知"，否则使用"评分标准"
            String scoringTitle = agentName.contains("技术标书") && agentName.contains("询价") ? "【应价人须知】" : "【评分标准】";
            output.append(scoringTitle).append("\n").append(scoringRequirements).append("\n\n");
            output.append("【详细目录】\n").append(detailedDirectory);

            // 更新 cmc_chat 表
            cmcChat.setTopicId(topicId);
            cmcChat.setOutput(output.toString());
            cmcChat.setOutputTime(new Date());
            cmcChatMapper.updateCmcChat(cmcChat);
            jsonObject.put("topicId", topicId);
        } else if (agentName.contains("检查")) {
            message = generateAnswerWithDocumentContent(profilePath + "/" + file.getOriginalFilename());
        }
        jsonObject.put("assistantMessage", message);
        return jsonObject;
    }

    /**
     * 上传修改文件
     *
     * @param file 文件
     * @return 结果
     */
    @Override
    public JSONObject uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode)
            throws IOException, Throwable {
        processValue = "";
        String prefixPath = "/upload/agent/" + agentName;
        File profilePath = new File(RuoYiConfig.getProfile() + prefixPath);
        if (!profilePath.exists())
            profilePath.mkdirs();
        String chatId = new SnowFlake().generateId();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("chatId", chatId);
        File transferFile = new File(profilePath + "/" + file.getOriginalFilename());
        if (!transferFile.exists())
            file.transferTo(transferFile);
        else {
            Path inputFilePath = Paths.get(profilePath + "/" + file.getOriginalFilename());
            Files.deleteIfExists(inputFilePath);
            file.transferTo(transferFile);
        }
        processValue = "文件上传完成:100%";

        // 根据 topicId 获取 cmc_chat 中的 output 内容（包含项目概况和评分标准）
        CmcChat chatQuery = new CmcChat();
        chatQuery.setTopicId(topicId);
        List<CmcChat> chatList = cmcChatMapper.selectCmcChatList(chatQuery);
        if (chatList.isEmpty() || chatList.get(0).getOutput() == null) {
            jsonObject.put("assistantMessage", "未找到对应的对话记录或分析信息");
            return jsonObject;
        }

        // 获取原始招标文件路径
        String inputPath = RuoYiConfig.getProfile() + chatList.get(0).getInput().split("：")[1];

        // 从 output 中解析项目概况和评分标准
        String output = chatList.get(0).getOutput();
        String projectOverview = parseProjectOverview(output);
        String scoringRequirements = parseScoringRequirements(output);

        // 保存上传的技术文件
        CmcDocument cmcDocument = new CmcDocument();
        cmcDocument.setDocumentId(new SnowFlake().generateId());
        cmcDocument.setChatId(jsonObject.getString("chatId"));
        cmcDocument.setPath(prefixPath + "/" + file.getOriginalFilename());
        if (file.getOriginalFilename().endsWith(".pdf"))
            cmcDocument.setPath(prefixPath + "/" + file.getOriginalFilename().replace(".pdf", ".docx"));
        cmcDocumentMapper.insertCmcDocument(cmcDocument);

        // 保存聊天记录
        CmcChat cmcChat = new CmcChat();
        cmcChat.setChatId(jsonObject.getString("chatId"));
        cmcChat.setTopicId(topicId);
        cmcChat.setInputTime(new Date());
        cmcChat.setInput("投标文件地址：" + prefixPath + "/" + file.getOriginalFilename());
        cmcChat.setUserId(SecurityUtils.getUserId());
        cmcChatMapper.insertCmcChat(cmcChat);

        // 保存文档到本地文件系统
        try (InputStream fileInputStream = new FileInputStream(profilePath + "/" + file.getOriginalFilename());
                XWPFDocument doc = new XWPFDocument(fileInputStream)) {
            try (FileOutputStream out = new FileOutputStream(profilePath + "/" + file.getOriginalFilename())) {
                doc.write(out);
            }
        }

        String message = "";
        if (agentName.contains("技术标书")) {
            String docPath = profilePath + "/" + file.getOriginalFilename();
            // 优先从 Word 文档提取段落 text 格式的叶子标题
            List<String> allTitles = extractSubTitles(docPath, "技术文件");
            if (allTitles.isEmpty()) {
                allTitles = extractTitlesFromOutput(output);
            }

            // 根据是否有选中节点来决定生成哪些章节内容
            List<String> targetTitles = new ArrayList<>();

            if (selectedNode != null && !selectedNode.isEmpty()) {
                String selectedParagraphTitle = toParagraphText(selectedNode);
                // 选中节点：获取该节点及其子节点的标题
                List<String> selectedTitles = extractSubTitles(docPath, selectedNode);
                if (selectedTitles.isEmpty()) {
                    // 叶子节点：使用与段落 text 一致的标题
                    targetTitles.add(selectedParagraphTitle);
                } else {
                    targetTitles.addAll(selectedTitles);
                }
            } else {
                // 未选中节点：生成整个目录的章节内容
                targetTitles.addAll(allTitles);
            }

            if (targetTitles.isEmpty()) {
                message = "未找到可生成的章节标题";
            } else {
                // 生成章节内容（传入项目概况和评分标准）
                Map<String, String> titleContentMap = generateChaptersContent(inputPath,
                        profilePath + "/" + file.getOriginalFilename(), targetTitles, projectOverview,
                        scoringRequirements, agentName);

                // 构建返回消息
                StringBuilder messageBuilder = new StringBuilder();
                messageBuilder.append("章节内容生成完成！\n\n");
                messageBuilder.append("已生成内容的章节：\n");
                for (String title : titleContentMap.keySet()) {
                    String content = titleContentMap.get(title);
                    if (!content.equals("该章节内容生成失败，请手动填写。")) {
                        messageBuilder.append("• ").append(title).append("\n");
                    }
                }
                messageBuilder.append("\n技术文件已更新，请查看：\n");
                messageBuilder.append("【<a href='/profile")
                        .append(profilePath.toString().replace("\\", "/").replace(RuoYiConfig.getProfile(), ""))
                        .append("/").append(file.getOriginalFilename()).append("'>点击查看技术文件</a>】\n\n");
                messageBuilder.append("如需修改特定章节，请选择章节标题名称。\n");

                message = messageBuilder.toString();

                // 添加章节内容到返回结果
                JSONObject chapterContent = new JSONObject();
                for (Map.Entry<String, String> entry : titleContentMap.entrySet()) {
                    chapterContent.put(entry.getKey(), entry.getValue());
                }
                jsonObject.put("chapterContent", chapterContent);
            }
        }

        jsonObject.put("assistantMessage", message);
        return jsonObject;
    }

    /**
     * 从 cmc_chat 的 output 中解析项目概况
     */
    private String parseProjectOverview(String output) {
        String projectOverview = "";
        if (output.contains("【项目概况】")) {
            int startIndex = output.indexOf("【项目概况】") + "【项目概况】".length();
            // 同时匹配"评分标准"和"应价人须知"
            int scoringIndex = output.indexOf("【评分标准】");
            int biddingIndex = output.indexOf("【应价人须知】");
            int endIndex = -1;
            if (scoringIndex > 0 && biddingIndex > 0) {
                endIndex = Math.min(scoringIndex, biddingIndex);
            } else if (scoringIndex > 0) {
                endIndex = scoringIndex;
            } else if (biddingIndex > 0) {
                endIndex = biddingIndex;
            }
            if (endIndex > startIndex) {
                projectOverview = output.substring(startIndex, endIndex).trim();
            } else {
                projectOverview = output.substring(startIndex).trim();
            }
        }
        return projectOverview;
    }

    /**
     * 从 cmc_chat 的 output 中解析评分标准（支持"评分标准"和"应价人须知"两种标题）
     */
    private String parseScoringRequirements(String output) {
        String scoringRequirements = "";
        // 同时匹配"评分标准"和"应价人须知"
        int scoringStart = output.indexOf("【评分标准】");
        int biddingStart = output.indexOf("【应价人须知】");

        if (scoringStart >= 0 || biddingStart >= 0) {
            int startIndex;
            if (scoringStart >= 0 && biddingStart >= 0) {
                // 取出现位置较早的那个
                if (scoringStart < biddingStart) {
                    startIndex = scoringStart + "【评分标准】".length();
                } else {
                    startIndex = biddingStart + "【应价人须知】".length();
                }
            } else if (scoringStart >= 0) {
                startIndex = scoringStart + "【评分标准】".length();
            } else {
                startIndex = biddingStart + "【应价人须知】".length();
            }

            int endIndex = output.indexOf("【详细目录】");
            if (endIndex > startIndex) {
                scoringRequirements = output.substring(startIndex, endIndex).trim();
            } else {
                scoringRequirements = output.substring(startIndex).trim();
            }
        }
        return scoringRequirements;
    }

    /**
     * 生成指定章节列表的内容（并行优化版）
     */
    private Map<String, String> generateChaptersContent(String inputPath, String templatePath,
            List<String> targetTitles, String projectOverview, String scoringRequirements, String agentName)
            throws Throwable {
        // 构建招标文件内容的嵌入向量存储（inputPath 是招标文件路径，通过 topicId 查询得到）
        File tenderFile = new File(inputPath);
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
        List<TextSegment> segments = splitDocument(tenderFile, 300, 50);
        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
        embeddingStore.addAll(embeddings, segments);

        // 为目标章节生成内容 - 使用线程安全的集合
        Map<String, String> titleContentMap = new ConcurrentHashMap<>();
        List<String> successTitles = Collections.synchronizedList(new ArrayList<>());
        List<String> failureTitles = Collections.synchronizedList(new ArrayList<>());

        // 创建线程池 - 根据CPU核心数设置线程数，最多5个并行任务
        int parallelism = Math.min(targetTitles.size(), Math.max(2, Runtime.getRuntime().availableProcessors()));
        ExecutorService executor = Executors.newFixedThreadPool(parallelism, new ThreadFactory() {
            private final AtomicInteger counter = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r, "ChapterGenerator-" + counter.getAndIncrement());
                t.setDaemon(true);
                return t;
            }
        });

        try {
            // 使用 CompletableFuture 并行处理所有章节
            List<CompletableFuture<Void>> futures = new ArrayList<>();
            AtomicInteger completedCount = new AtomicInteger(0);
            int totalCount = targetTitles.size();

            for (String title : targetTitles) {
                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                    try {
                        // 1. 从招标文件中检索相关内容
                        List<String> tenderContent = retrieveFromTenderDocument(embeddingStore, title);

                        // 2. 从两个知识库中检索相似章节内容（technical 和 solution）
                        List<JSONObject> technicalContexts = retrieveFromMilvus("technical", title, 10,
                                "title != \"目录\"");
                        List<JSONObject> solutionContexts = retrieveFromMilvus("solution", title, 10,
                                "title != \"目录\"");

                        // 合并两个知识库的检索结果
                        List<JSONObject> knowledgeContexts = new ArrayList<>();
                        if (technicalContexts != null) {
                            knowledgeContexts.addAll(technicalContexts);
                        }
                        if (solutionContexts != null) {
                            knowledgeContexts.addAll(solutionContexts);
                        }

                        // 3. 构建针对该章节的提示语（包含项目概况和评分标准）
                        String prompt = buildChapterPrompt(title, tenderContent, knowledgeContexts, projectOverview,
                                scoringRequirements, agentName);

                        // 4. 生成章节内容
                        String chapterContent = generateAnswer(prompt);

                        // 5. 存储章节内容
                        titleContentMap.put(title, chapterContent);
                        successTitles.add(title);

                    } catch (Throwable e) {
                        failureTitles.add(title);
                        System.err.println("生成章节 " + title + " 内容时出错: " + e.getMessage());
                        titleContentMap.put(title, "该章节内容生成失败，请手动填写。");
                    } finally {
                        // 更新进度
                        int completed = completedCount.incrementAndGet();
                        processValue = String.format("正在生成章节内容: %d/%d (%.1f%%)",
                                completed, totalCount, (completed * 100.0) / totalCount);
                    }
                }, executor);

                futures.add(future);
            }

            // 等待所有任务完成
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();

        } finally {
            // 关闭线程池
            executor.shutdown();
            executor.awaitTermination(5, TimeUnit.MINUTES);
        }

        // 将生成的内容写入技术文件
        writeContentToDocument(titleContentMap, templatePath);

        return titleContentMap;
    }

    /**
     * 上传多文件
     *
     * @param fileList 文件
     * @return 结果
     */
    @Override
    public String uploadDocumentList(MultipartFile[] fileList, String agentName) throws IOException {
        File profilePath = new File(RuoYiConfig.getProfile() + "/upload/agent/" + agentName);
        if (!profilePath.exists())
            profilePath.mkdirs();
        for (MultipartFile file : fileList) {
            File transferFile = new File(profilePath + File.separator + file.getOriginalFilename());
            if (!transferFile.exists())
                file.transferTo(transferFile);
        }
        return "上传成功";
    }

    /**
     * 上传多文件
     *
     * @return 结果
     */
    public String getProcess() {
        return processValue;
    }

    /**
     * 新增智能体
     *
     * @param cmcAgent 智能体
     * @return 结果
     */
    @Override
    public int insertCmcAgent(CmcAgent cmcAgent) {
        cmcAgent.setCreateTime(DateUtils.getNowDate());
        return cmcAgentMapper.insertCmcAgent(cmcAgent);
    }

    /**
     * 修改智能体
     *
     * @param cmcAgent 智能体
     * @return 结果
     */
    @Override
    public int updateCmcAgent(CmcAgent cmcAgent) {
        return cmcAgentMapper.updateCmcAgent(cmcAgent);
    }

    /**
     * 批量删除智能体
     *
     * @param agentIds 需要删除的智能体主键
     * @return 结果
     */
    @Override
    public int deleteCmcAgentByAgentIds(Integer[] agentIds) {
        return cmcAgentMapper.deleteCmcAgentByAgentIds(agentIds);
    }

    /**
     * 删除智能体信息
     *
     * @param agentId 智能体主键
     * @return 结果
     */
    @Override
    public int deleteCmcAgentByAgentId(Integer agentId) {
        return cmcAgentMapper.deleteCmcAgentByAgentId(agentId);
    }

    /**
     * 调用LLM生成回答 - 检查文档内容
     */
    public String generateAnswerWithDocumentContent(String documentPath) throws Throwable {
        File profilePath = new File(documentPath);
        List<TextSegment> segments = splitDocument(profilePath, 10000, 0);
        StringBuilder sb = new StringBuilder();
        for (TextSegment segment : segments) {
            String prompt = "文档内容如下，帮我检查文档中用字、语言表达是否准确：\n" + segment.text();
            sb.append("片段").append(segments.indexOf(segment) + 1).append("：\n").append(generateAnswer(prompt));
            processValue = "检查中: "
                    + Double.parseDouble(
                            String.format("%.2f%n", (double) (segments.indexOf(segment) + 1) / segments.size() * 100))
                    + "%";
        }
        sb.append("综合以上各片段检查内容修改建议，给出完整修改意见");
        return generateAnswer(sb.toString());
    }

    /**
     * 从招标文件中检索与指定标题相关的内容
     */
    private List<String> retrieveFromTenderDocument(InMemoryEmbeddingStore<TextSegment> embeddingStore, String title) {
        List<String> relevantContent = new ArrayList<>();

        try {
            Embedding queryEmbedding = embeddingModel.embed(title).content();
            EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
                    .queryEmbedding(queryEmbedding)
                    .minScore(0.7) // 降低阈值以获取更多相关内容
                    .build();

            List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
            results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());

            for (EmbeddingMatch<TextSegment> match : results) {
                relevantContent.add(match.embedded().text());
            }
        } catch (Exception e) {
            System.err.println("从招标文件检索内容时出错: " + e.getMessage());
        }
        return relevantContent;
    }

    /**
     * 构建针对特定章节的提示语
     */
    private String buildChapterPrompt(String title, List<String> tenderContent, List<JSONObject> knowledgeContexts,
            String projectOverview, String scoringRequirements, String agentName) {
        StringBuilder prompt = new StringBuilder();

        // 基础指令
        prompt.append("你是一位专业的技术文件撰写专家。请根据以下信息为技术文件章节生成详细内容。\n\n");

        // 章节信息
        prompt.append("【目标章节】: ").append(title).append("\n\n");

        // 项目概况
        if (projectOverview != null && !projectOverview.isEmpty()) {
            prompt.append("【项目概况】:\n");
            prompt.append(projectOverview).append("\n\n");
        }

        // 评分标准/应价人须知
        if (scoringRequirements != null && !scoringRequirements.isEmpty()) {
            // 当agentname包含"技术标书"且包含"询价"时，使用"应价人须知"，否则使用"评分标准"
            String scoringTitle = agentName != null && agentName.contains("技术标书") && agentName.contains("询价")
                    ? "【应价人须知】"
                    : "【评分标准】";
            prompt.append(scoringTitle).append(":\n");
            prompt.append(scoringRequirements).append("\n\n");
        }

        // 招标文件相关内容
        if (!tenderContent.isEmpty()) {
            prompt.append("【招标文件要求】:\n");
            for (int i = 0; i < tenderContent.size(); i++) {
                prompt.append(i + 1).append(". ").append(tenderContent.get(i)).append("\n");
            }
            prompt.append("\n");
        }

        // 知识库参考内容
        if (!knowledgeContexts.isEmpty()) {
            prompt.append("【参考模板内容】:\n");
            for (int i = 0; i < knowledgeContexts.size(); i++) {
                JSONObject context = knowledgeContexts.get(i);
                prompt.append("参考文件 ").append(i + 1).append(" (").append(context.getString("file_name")).append("):\n");
                prompt.append(context.getString("content")).append("\n\n");
            }
        }

        // 具体要求
        prompt.append("【撰写要求】:\n");
        prompt.append("1. 内容必须与招标文件要求高度匹配\n");
        prompt.append("2. 参考模板内容的结构和表达方式\n");
        prompt.append("3. 内容要专业、具体、可操作\n");
        prompt.append("4. 语言要规范、准确，符合技术文件标准\n");
        prompt.append("5. 内容要完整，包含必要的技术细节\n");
        prompt.append("6. 避免空洞的套话，要有实质性的技术内容\n");
        prompt.append("7. 合理分段，每个段落要独立、完整，避免段落过长\n\n");

        // 输出格式要求
        prompt.append("【输出格式】:\n");
        prompt.append("请直接输出章节的正文内容（不要包含三级标题），正文内容为总-分结构，别写成总-分-总结构。");
        prompt.append("【标题编号规范】:\n");
        if (agentName.contains("技术设计书"))
            prompt.append(
                    "如果需要在章节内添加子标题，必须严格遵循层级编号格式，最多到第六级标题（如1.1,1.1.1,1.1.1.1,1.1.1.1.1,1.1.1.1.1.1,1.1.1.1.1.1），且子标题后应添加换行符。\\n");
        else
            prompt.append(
                    "如果需要在章节内添加子标题，必须严格遵循层级编号格式，最多到第六级标题（如6.1,6.1.1,6.1.1.1,6.1.1.1.1,6.1.1.1.1.1,6.1.1.1.1.1.1），且子标题后应添加换行符。\n");
        prompt.append("各级标题下，正文文字叙述部份的分点说明序号分为六级，\n");
        prompt.append("第一级按阿拉伯数字序号编排，如1,2,3\n");
        prompt.append("第二级按带右圆括号的阿拉伯数字序号编排，如1）,2）,3）\n");
        prompt.append("第三级按带圆括号的英文小写字母顺序编排（1）,（2）,（3）\n");
        prompt.append("第四级按英文小写字母顺序编排，如a,b,c\n");
        prompt.append("第五级按带右圆括号的英文小写字母顺序编排，如a）,b）,c）\n");
        prompt.append("第六级按带圆括号的英文小写字母顺序编排，如（a）,（b）,（c）\n");
        prompt.append("分点序号前均空两个汉字，无括号后空一个汉字，有括号的紧挨编写内容，所有的括号均为全角符号。\n");
        prompt.append("所有子标题必须基于父标题编号逐级递增，保持编号体系的一致性。\n\n");

        prompt.append("请开始生成当前章节 ").append(title).append(" 的详细内容：");

        return prompt.toString();
    }

    /**
     * 将生成的内容写入文档（非流式版本，向后兼容）
     */
    private void writeContentToDocument(Map<String, String> titleContentMap, String absolutePath)
            throws IOException {
        writeContentToDocument(titleContentMap, absolutePath, null, null);
    }

    /**
     * 将生成的内容写入文档（流式版本，支持进度推送）
     */
    private void writeContentToDocument(Map<String, String> titleContentMap, String absolutePath,
            SseEmitter emitter, com.fasterxml.jackson.databind.ObjectMapper objectMapper)
            throws IOException {
        File file = new File(absolutePath);
        XWPFDocument document = null;

        // 先读取文件到内存，确保输入流关闭后再进行写入
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            document = new XWPFDocument(fileInputStream);
        }

        try {
            List<XWPFParagraph> paragraphs = document.getParagraphs();
            List<String> titles = new ArrayList<>(titleContentMap.keySet());
            List<String> contents = new ArrayList<>();

            // 预扫描文档中所有标题段落的位置（基于大纲级别，避免将正文误判为标题）
            List<Integer> headingIndices = new ArrayList<>();
            List<String> headingTexts = new ArrayList<>();
            for (int i = 0; i < paragraphs.size(); i++) {
                XWPFParagraph paragraph = paragraphs.get(i);
                int level = getDocxOutlineLevel(paragraph, document);
                if (level > 0) {
                    String paragraphText = paragraph.getText().trim();
                    if (!paragraphText.isEmpty()) {
                        headingIndices.add(i);
                        headingTexts.add(paragraphText);
                    }
                }
            }

            // targetTiles 的 title 应与段落 text 精确匹配（自动编号模板下段落不含编号前缀）
            List<Integer> titlePositions = new ArrayList<>();
            for (String title : titles) {
                String paragraphTitle = toParagraphText(title);
                for (int i = 0; i < headingTexts.size(); i++) {
                    if (headingTexts.get(i).equals(paragraphTitle)) {
                        titlePositions.add(headingIndices.get(i));
                        contents.add(titleContentMap.getOrDefault(title, ""));
                        break;
                    }
                }
            }

            // 按标题在文档中的位置降序排序（与 contents 同步排序），确保从后往前处理避免索引偏移
            if (titlePositions.size() > 1) {
                for (int i = 0; i < titlePositions.size() - 1; i++) {
                    for (int j = i + 1; j < titlePositions.size(); j++) {
                        if (titlePositions.get(i) < titlePositions.get(j)) {
                            int tmpPos = titlePositions.get(i);
                            titlePositions.set(i, titlePositions.get(j));
                            titlePositions.set(j, tmpPos);
                            String tmpContent = contents.get(i);
                            contents.set(i, contents.get(j));
                            contents.set(j, tmpContent);
                        }
                    }
                }
            }

            // 统一处理所有匹配到的标题：先删除旧内容，再插入新内容
            for (int i = 0; i < titlePositions.size(); i++) {
                int titlePos = titlePositions.get(i);
                String content = contents.get(i);

                // 查找下一个标题段落的实际位置
                int endPos = paragraphs.size();
                for (int headingIndex : headingIndices) {
                    if (headingIndex > titlePos) {
                        endPos = headingIndex;
                        break;
                    }
                }

                // 删除标题后面到下一个标题之前的所有段落（原有章节内容）
                // 从后往前删除，避免索引偏移
                for (int j = endPos - 1; j > titlePos; j--) {
                    document.removeBodyElement(j);
                }

                // 在标题后面插入新内容
                int insertPos = titlePos + 1;
                generateWordDocument(content, document, insertPos);
            }
            // 保存文档（此时输入流已关闭）
            try (FileOutputStream out = new FileOutputStream(absolutePath)) {
                document.write(out);
            }

            // 标记写入完成
            processValue = "文档写入完成:100%";
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * 调用LLM生成目录结构
     */
    public JSONArray generateDetailedDirectory(String fileName, String agentName, List<TextSegment> segments)
            throws Throwable {
        StringBuilder sb = new StringBuilder("");
        if (agentName.contains("技术设计书")) {
            sb.append("项目设计书内容：\n\n");
            for (TextSegment segment : segments) {
                sb.append(segment.toString()).append("\n\n");
            }
        } else if (agentName.contains("技术标书")) {
            sb.append("招标文件内容：\n\n");

            InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
            List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
            embeddingStore.addAll(embeddings, segments);
            Embedding queryEmbedding = embeddingModel.embed("技术文件工作大纲要求").content();
            EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
                    .queryEmbedding(queryEmbedding)
                    .maxResults(5)
                    .minScore(0.7)
                    .build();
            List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
            results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
            for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
                String requests = embeddingMatch.embedded().toString();
                sb.append(requests).append("\n\n");
            }
        }
        // 根据招标文件工作大纲要求生成二级标题
        if (agentName.contains("技术标书"))
            sb.append("请基于上述招标文件中提到的")
                    .append("技术文件工作大纲要求")
                    .append("，先列出二级章节标题，严格按以下格式，仅输出标题列表：\n")
                    .append("6.1 XX\n" +
                            "6.2 XX\n" +
                            "6.3 XX\n" +
                            "......\n" +
                            "6.n-1 XX\n" +
                            "6.n XX\n");
        else if (agentName.contains("技术设计书"))
            sb.append("请基于上述项目设计书内容")
                    .append("，先列出二级章节标题，严格按以下格式，仅输出标题列表：\n")
                    .append("1.1 XX\n" +
                            "1.2 XX\n" +
                            "1.3 XX\n" +
                            "......\n" +
                            "1.n-1 XX\n" +
                            "1.n XX\n");
        String directoryContent = writeChapters(sb.toString(), fileName, agentName);
        return parseDirectoryToTree(directoryContent);
    }

    /**
     * 将目录文本解析为树结构
     */
    private JSONArray parseDirectoryToTree(String directoryContent) {
        JSONArray tree = new JSONArray();
        Map<String, JSONObject> level1Map = new LinkedHashMap<>();
        Map<String, JSONObject> level2Map = new LinkedHashMap<>();
        Map<String, JSONObject> level3Map = new LinkedHashMap<>();

        String[] lines = directoryContent.split("\n");
        for (String line : lines) {
            line = line.trim().replace("*", "").replace("#", "").replace("-", "");
            if ((!line.startsWith("6.") && !line.startsWith("1.")) || line.isEmpty()) {
                continue;
            }

            String[] parts = line.split(" ", 2);
            if (parts.length < 2) {
                continue;
            }

            String number = parts[0].trim();
            String title = parts[1].trim();
            int dotCount = number.split("\\.").length - 1;

            if (dotCount == 1) {
                JSONObject level1 = new JSONObject();
                level1.put("title", number + " " + title);
                level1.put("children", new JSONArray());
                level1Map.put(number, level1);
            } else if (dotCount == 2) {
                JSONObject level2 = new JSONObject();
                level2.put("title", number + " " + title);
                level2.put("children", new JSONArray());
                level2Map.put(number, level2);

                String parentNumber = number.substring(0, number.lastIndexOf("."));
                if (level1Map.containsKey(parentNumber)) {
                    level1Map.get(parentNumber).getJSONArray("children").add(level2);
                }
            } else if (dotCount == 3) {
                JSONObject level3 = new JSONObject();
                level3.put("title", number + " " + title);
                level3Map.put(number, level3);

                String parentNumber = number.substring(0, number.lastIndexOf("."));
                if (level2Map.containsKey(parentNumber)) {
                    level2Map.get(parentNumber).getJSONArray("children").add(level3);
                }
            }
        }

        for (Map.Entry<String, JSONObject> entry : level1Map.entrySet()) {
            tree.add(entry.getValue());
        }

        return tree;
    }

    /**
     * 将目录树结构转换为文本格式
     */
    private String buildDirectoryText(JSONArray tree) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < tree.size(); i++) {
            JSONObject level1 = tree.getJSONObject(i);
            sb.append(level1.getString("title")).append("\n");
            if (level1.containsKey("children")) {
                JSONArray children = level1.getJSONArray("children");
                for (int j = 0; j < children.size(); j++) {
                    JSONObject level2 = children.getJSONObject(j);
                    sb.append(level2.getString("title")).append("\n");
                    if (level2.containsKey("children")) {
                        JSONArray grandchildren = level2.getJSONArray("children");
                        for (int k = 0; k < grandchildren.size(); k++) {
                            JSONObject level3 = grandchildren.getJSONObject(k);
                            sb.append(level3.getString("title")).append("\n");
                        }
                    }
                }
            }
        }
        return sb.toString();
    }

    /**
     * 分析招标文件内容（公共方法）
     * 
     * @param uploadFilePath   文件路径
     * @param query            查询关键词
     * @param analysisPrompt   分析提示语
     * @param progressStartMsg 进度开始消息
     * @param progressFoundMsg 进度找到消息
     * @param progressEndMsg   进度结束消息
     * @return 分析结果
     */
    private String analyzeDocumentContent(List<TextSegment> segments, String query, String analysisPrompt,
            String progressStartMsg, String progressFoundMsg, String progressEndMsg) throws Throwable {
        processValue = progressStartMsg;
        StringBuilder sb = new StringBuilder("招标文件内容：\n\n");
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
        embeddingStore.addAll(embeddings, segments);

        // 搜索相关内容
        Embedding queryEmbedding = embeddingModel.embed(query).content();
        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
                .queryEmbedding(queryEmbedding)
                .maxResults(5)
                .minScore(0.7)
                .build();
        List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
        results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());

        for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
            String content = embeddingMatch.embedded().toString();
            sb.append(content).append("\n\n");
        }

        // 添加分析提示
        sb.append(analysisPrompt);

        processValue = progressFoundMsg;
        String result = generateAnswer(sb.toString());
        processValue = progressEndMsg;
        return result;
    }

    /**
     * 分析项目概况
     */
    public String analyzeProjectOverview(String uploadFilePath, List<TextSegment> segments) throws Throwable {
        String query = uploadFilePath.split("/")[uploadFilePath.split("/").length - 1] + "项目概况";
        String analysisPrompt = "请基于上述招标文件内容，分析项目概况。\n" +
                "请以清晰的结构输出分析结果。\n";
        return analyzeDocumentContent(segments, query, analysisPrompt,
                "分析项目概况中: 0%",
                "已查到项目概况: 50%",
                "分析项目概况中: 100%");
    }

    /**
     * 分析评分标准/应价人须知
     */
    public String analyzeScoringRequirements(List<TextSegment> segments, String agentName)
            throws Throwable {
        String query = "评分标准、评标办法、打分规则";
        String analysisPrompt = "请基于上述招标文件内容，分析评分标准。\n" +
                "请以清晰的结构输出分析结果，包括各项的具体分值和评分细则。\n";

        // 当agentname包含"技术标书"且包含"询价"时，使用"应价人须知"相关查询
        if (agentName.contains("技术标书") && agentName.contains("询价")) {
            query = "应价人须知、报价要求、响应要求、投标报价";
            analysisPrompt = "请基于上述招标文件内容，分析应价人须知。\n" +
                    "请以清晰的结构输出分析结果，包括报价要求、响应要求等内容。\n";
            return analyzeDocumentContent(segments, query, analysisPrompt,
                    "分析应价人须知中: 0%",
                    "已查到应价人须知: 50%",
                    "分析应价人须知中: 100%");
        } else if (agentName.contains("技术标书")) {
            query = "技术部分评分标准、评标办法、打分规则";
        } else if (agentName.contains("商务")) {
            query = "商务部分评分标准、评标办法、打分规则";
        }

        return analyzeDocumentContent(segments, query, analysisPrompt,
                "分析评分标准中: 0%",
                "已查到评分标准: 50%",
                "分析评分标准中: 100%");
    }

    /**
     * 编排章节
     * 
     * @return
     */
    public String writeChapters(String prompt, String fileName, String agentName) throws Throwable {
        String chapters2 = generateAnswer(prompt);

        // 根据技术文档知识库生成二级标题下三级标题
        StringBuilder chapter3 = new StringBuilder();
        List<String> chapter2List = new ArrayList<>();
        String[] contentLines = chapters2.split("\n");
        for (String line : contentLines) {
            if (line.startsWith("6.") || line.startsWith("1."))
                chapter2List.add(line.replace("*", "").replace("#", "").replace("-", ""));
        }
        processValue = "已生成二级标题:100%";
        for (String chapter2 : chapter2List) {
            String orderNum = chapter2.split(" ")[0];
            StringBuilder sb = new StringBuilder("二级标题如下：\n").append(chapter2)
                    .append("\n 请以上述二级标题作为检索词，到技术知识库中检索最相似的标题，结合检索结果编排该二级标题下的三级标题，严格按以下格式，仅输出三级标题列表：\n")
                    .append(orderNum).append(".1 XX\n")
                    .append(orderNum).append(".2 XX\n")
                    .append(orderNum).append(".3 XX\n");

            List<JSONObject> contexts = retrieveFromMilvus("technical", chapter2, 30, "title == \"目录\"");
            for (JSONObject context : contexts) {
                sb.append("参考内容").append(": ")
                        .append(context.getString("content")).append("\n\n");
            }
            chapter3.append(generateAnswer(sb.toString()));
            processValue = "正在根据技术文档知识库生成二级标题下三级标题:" + Double.parseDouble(
                    String.format("%.2f%n", (double) (chapter2List.indexOf(chapter2) + 1) / chapter2List.size() * 100))
                    + "%";
        }
        String sb = "二级标题如下：\n" + chapters2 +
                "\n 三级标题如下：" + chapter3 +
                "请将上述二级与对应的三级标题进行合并，严格按以下格式输出完整大纲，仅输出标题：\n";
        if (agentName.contains("技术标书"))
            sb += "6 技术文件\n" +
                    "6.1 XX\n" +
                    "6.1.1 XX\n" +
                    "6.1.2 XX\n" +
                    "6.2 XX\n" +
                    "6.2.1 XX\n" +
                    "6.2.2 XX\n" +
                    "6.3 XX\n" +
                    "6.3.1 XX\n" +
                    "6.3.2 XX\n" +
                    "...... \n" +
                    "6.n XX\n" +
                    "6.n.1 XX\n" +
                    "6.n 2 XX\n";
        if (agentName.contains("技术设计书"))
            sb += "1 技术设计书\n" +
                    "1.1 XX\n" +
                    "1.1.1 XX\n" +
                    "1.1.2 XX\n" +
                    "1.2 XX\n" +
                    "1.2.1 XX\n" +
                    "1.2.2 XX\n" +
                    "1.3 XX\n" +
                    "1.3.1 XX\n" +
                    "1.3.2 XX\n" +
                    "...... \n" +
                    "1.n XX\n" +
                    "1.n.1 XX\n" +
                    "1.n 2 XX\n";
        String content = generateAnswer(sb);
        writeTitles(content, fileName, agentName);
        return content;
    }

    /**
     * 调用LLM生成回答
     *
     * @return
     */
    public String generateAnswer(String prompt) throws Throwable, IOException {
        ChatModel chatModel = ChatModel.of(llmServiceUrl)
                .model("Qwen")
                .timeout(Duration.ofSeconds(240))
                .build();
        SimpleAgent robot = SimpleAgent.of(chatModel)
                .build();
        List<ChatMessage> messages = new ArrayList<>();
        messages.add(ChatMessage.ofUser(prompt));
        ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();

        Prompt prompt1 = Prompt.of(prompt).attrPut("session", chatSession);
        String content = robot.prompt(prompt1).call().getContent();
        return content;
    }

    /**
     * 流式调用LLM生成回答 - 使用Flux（包含usage信息）
     * 
     * @param prompt     提示词
     * @param tokenUsage 用于收集token用量的数组 [promptTokens, completionTokens,
     *                   totalTokens]
     * @return Flux<String> 流式输出（包含内容）
     */
    public Flux<String> generateAnswerFluxWithUsage(String prompt, AtomicLong[] tokenUsage) {
        ChatModel chatModel = ChatModel.of(llmServiceUrl)
                .model("Qwen")
                .timeout(Duration.ofSeconds(240))
                .build();
        SimpleAgent robot = SimpleAgent.of(chatModel)
                .build();
        List<ChatMessage> messages = new ArrayList<>();
        messages.add(ChatMessage.ofUser(prompt));
        ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();

        Prompt prompt1 = Prompt.of(prompt).attrPut("session", chatSession);

        // 估算prompt的token数量（中文大约1个字符=1个token）
        if (tokenUsage != null && tokenUsage.length >= 3 && tokenUsage[0].get() == 0) {
            tokenUsage[0].set((long) prompt.length());
        }

        return robot.prompt(prompt1).stream()
                .doOnNext(resp -> {
                    if (tokenUsage != null && tokenUsage.length >= 3) {
                        try {
                            // 优先使用vLLM返回的usage信息（在最后一个chunk中）
                            // AiUsage usage = resp.getUsage();
                            // if (usage != null) {
                            // Long pt = usage.promptTokens();
                            // Long ct = usage.completionTokens();
                            // Long tt = usage.totalTokens();

                            // if (pt > 0)
                            // tokenUsage[0].set(pt);
                            // if (ct > 0)
                            // tokenUsage[1].set(ct);
                            // if (tt > 0)
                            // tokenUsage[2].set(tt);
                            // } else {
                            // 如果没有usage信息（中间chunk），估算completion tokens
                            String content = resp.getContent();
                            if (content != null && !content.isEmpty()) {
                                // 中文大约1个字符=1个token，实时累计
                                long currentCompletion = tokenUsage[1].get();
                                long increment = content.length();
                                tokenUsage[1].set(currentCompletion + increment);
                                tokenUsage[2].set(tokenUsage[0].get() + tokenUsage[1].get());
                            }
                            // }
                        } catch (Exception e) {
                            // 如果获取usage失败，也尝试估算
                            try {
                                String content = resp.getContent();
                                if (content != null && !content.isEmpty()) {
                                    long currentCompletion = tokenUsage[1].get();
                                    long increment = content.length();
                                    tokenUsage[1].set(currentCompletion + increment);
                                    tokenUsage[2].set(tokenUsage[0].get() + tokenUsage[1].get());
                                }
                            } catch (Exception ex) {
                            }
                        }
                    }
                })
                .filter(resp -> resp.getContent() != null && !resp.getContent().isEmpty())
                .map(resp -> resp.getContent());
    }

    /**
     * 从Milvus检索相关文档
     * 
     * @return
     */
    public List<JSONObject> retrieveFromMilvus(String collectionName, String query, int topK, String filter) {
        List<JSONObject> resultList = new ArrayList<>();
        List<List<SearchResp.SearchResult>> searchResultList = retrieve(collectionName, query, topK, filter);
        if (searchResultList == null) {
            return resultList;
        }
        for (List<SearchResp.SearchResult> searchResult : searchResultList) {
            if (searchResult == null || searchResult.isEmpty()) {
                continue;
            }
            JSONObject result = new JSONObject();
            result.put("content", searchResult.get(0).getEntity().get("content"));
            resultList.add(result);
        }
        return resultList;
    }

    /**
     * 检索知识库（使用连接池优化）
     */
    private List<List<SearchResp.SearchResult>> retrieve(String collectionName, String query, int topK, String filter) {
        MilvusClientV2 client = null;
        try {
            client = milvusConnectionPool.getClient();
            milvusConnectionPool.ensureCollectionLoaded(client, collectionName);

            List<BaseVector> queryVector = Collections
                    .singletonList(new FloatVec(embeddingModel.embed(query).content().vector()));

            Map<String, Object> searchParams = new HashMap<>();
            searchParams.put("nprobe", 8);

            SearchReqBuilder<?, ?> builder = SearchReq.builder()
                    .collectionName(collectionName)
                    .data(queryVector)
                    .topK(topK)
                    .outputFields(Arrays.asList("file_name", "file_type", "content"))
                    .annsField("embedding")
                    .metricType(IndexParam.MetricType.COSINE)
                    .searchParams(searchParams);

            if (filter != null && !filter.isEmpty()) {
                builder.filter(filter);
            }

            SearchReq searchReq = builder.build();
            SearchResp searchResp = client.search(searchReq);
            return searchResp.getSearchResults();

        } catch (Exception e) {
            System.err.println("从Milvus检索时出错: " + e.getMessage());
            throw new RuntimeException("检索知识库失败", e);
        } finally {
            if (client != null) {
                milvusConnectionPool.returnClient(client);
            }
        }
    }

    /**
     * 写入章节大纲
     * 
     * @return
     */
    public void writeTitles(String content, String fileName, String agentName) throws IOException {
        List<String> chapters = new ArrayList<>();
        String[] contentLines = content.split("\n");
        for (String line : contentLines) {
            if (line.startsWith("6.") || line.startsWith("1."))
                chapters.add(line.replace("*", "").replace("#", "").replace("-", ""));
        }
        String prefixPath = "/upload/agent/" + agentName;
        String templatePath = RuoYiConfig.getProfile() + prefixPath + "/" + fileName;

        // 先从模板读取文档到内存
        XWPFDocument document = null;
        try (InputStream inputStream = new FileInputStream(
                RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx")) {
            document = new XWPFDocument(inputStream);
        }

        try {
            // 在内存中修改文档
            for (int i = 0; i < chapters.size(); i++) {
                XWPFParagraph contentParagraph = document.createParagraph();
                String chapterLine = chapters.get(i).trim();
                // 去掉编号前缀（如 "6.1 "、"6.1.1 "），只保留标题文本，由Word自动编号
                String titleText = chapterLine.replaceFirst("^\\d+(\\.\\d+)*\\s*", "");
                int dotNum = chapterLine.split("\\.").length - 1;
                contentParagraph.setStyle(String.valueOf(dotNum + 2));

                XWPFRun run = contentParagraph.createRun();
                run.setText(titleText);
                // 每个标题已是独立段落（段落符），不再插入分页符/分节符
            }

            // 直接写入文件（FileOutputStream会自动创建或覆盖文件，无需提前删除）
            try (FileOutputStream out = new FileOutputStream(templatePath)) {
                document.write(out);
            }
        } finally {
            if (document != null) {
                document.close();
            }
        }
    }

    /**
     * 将带编号的标题转为 Word 段落文本（自动编号模板下段落 text 不含编号前缀）
     */
    private String toParagraphText(String title) {
        if (title == null) {
            return "";
        }
        return title.trim().replaceFirst("^\\d+(\\.\\d+)*\\s*", "");
    }

    /**
     * 获取DOCX段落的大纲级别
     */
    private int getDocxOutlineLevel(XWPFParagraph paragraph, XWPFDocument document) {
        int level = 0;
        String styleId = paragraph.getStyle();
        if (styleId != null) {
            XWPFStyle style = document.getStyles().getStyle(styleId);
            if (style != null) {
                CTPPrGeneral stylePPr = style.getCTStyle().getPPr();
                if (stylePPr != null && stylePPr.getOutlineLvl() != null) {
                    level = stylePPr.getOutlineLvl().getVal().intValue() + 1;
                }
            }
        }
        return level;
    }

    /**
     * 获取最低级别子标题列表
     */
    /**
     * 保存目录到Word文件
     */
    @Override
    public JSONObject writeTitles(JSONObject data) throws IOException {
        JSONObject result = new JSONObject();
        try {
            String filename = data.getString("filename");
            String agentName = data.getString("agentName");
            JSONArray titlesArray = data.getJSONArray("titles");

            // 获取 topicId，用于更新数据库
            String topicId = data.getString("topicId");

            // 构建目录字符串
            StringBuilder titlesContent = new StringBuilder();
            buildTitlesContent(titlesArray, titlesContent);

            // 调用现有的writeTitles方法写入Word文件
            writeTitles(titlesContent.toString(), filename, agentName);

            // 更新数据库中的 output 字段
            if (topicId != null && !topicId.isEmpty()) {
                updateOutputWithTitles(topicId, titlesContent.toString());
            }

            result.put("code", 200);
            result.put("message", "目录保存成功");
        } catch (Exception e) {
            e.printStackTrace();
            result.put("code", 500);
            result.put("message", "目录保存失败: " + e.getMessage());
        }
        return result;
    }

    /**
     * 构建目录内容字符串
     */
    private void buildTitlesContent(JSONArray titlesArray, StringBuilder content) {
        for (int i = 0; i < titlesArray.size(); i++) {
            JSONObject titleObj = titlesArray.getJSONObject(i);
            String title = titleObj.getString("title");
            content.append(title).append("\n");

            if (titleObj.containsKey("children")) {
                JSONArray children = titleObj.getJSONArray("children");
                buildTitlesContent(children, content);
            }
        }
    }

    /**
     * 更新数据库中的 output 字段，添加目录内容
     */
    private void updateOutputWithTitles(String topicId, String titlesContent) {
        try {
            // 根据 topicId 查询现有的 cmc_chat 记录
            CmcChat chatQuery = new CmcChat();
            chatQuery.setTopicId(topicId);
            List<CmcChat> chatList = cmcChatMapper.selectCmcChatList(chatQuery);

            if (!chatList.isEmpty()) {
                CmcChat cmcChat = chatList.get(0);
                String existingOutput = cmcChat.getOutput();

                // 如果已有 output，替换【详细目录】部分；否则创建新的 output
                String newOutput;
                if (existingOutput != null && !existingOutput.isEmpty()) {
                    // 替换【详细目录】部分
                    if (existingOutput.contains("【详细目录】")) {
                        int startIndex = existingOutput.indexOf("【详细目录】");
                        int endIndex = existingOutput.length();
                        // 查找下一个【】标签作为结束
                        int nextTagIndex = existingOutput.indexOf("【", startIndex + "【详细目录】".length());
                        if (nextTagIndex > startIndex) {
                            endIndex = nextTagIndex;
                        }
                        String before = existingOutput.substring(0, startIndex);
                        String after = existingOutput.substring(endIndex);
                        newOutput = before + "【详细目录】\n" + titlesContent + after;
                    } else {
                        // 如果没有【详细目录】标签，添加到末尾
                        newOutput = existingOutput + "\n\n【详细目录】\n" + titlesContent;
                    }
                } else {
                    // 创建新的 output
                    newOutput = "【详细目录】\n" + titlesContent;
                }

                // 更新数据库
                cmcChat.setOutput(newOutput);
                cmcChat.setOutputTime(new Date());
                cmcChatMapper.updateCmcChat(cmcChat);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("更新数据库 output 字段失败: " + e.getMessage());
        }
    }

    public List<String> extractSubTitles(String filename, String question) throws IOException {
        List<String> subTitles = new ArrayList<>();

        boolean foundParent = false;
        int parentLevel = -1;
        // 用于跟踪当前路径
        List<XWPFParagraph> currentPath = new ArrayList<>();

        try (InputStream fileInputStream = new FileInputStream(filename);
                XWPFDocument document = new XWPFDocument(fileInputStream)) {

            for (XWPFParagraph paragraph : document.getParagraphs()) {
                String text = paragraph.getText();
                int level = getDocxOutlineLevel(paragraph, document);
                if (level > 0) {
                    // 维护当前路径
                    while (!currentPath.isEmpty()) {
                        XWPFParagraph lastPara = currentPath.get(currentPath.size() - 1);
                        int lastLevel = getDocxOutlineLevel(lastPara, document);
                        if (lastLevel <= 0) {
                            break;
                        }

                        if (lastLevel >= level) {
                            currentPath.remove(currentPath.size() - 1);
                        } else {
                            break;
                        }
                    }
                    currentPath.add(paragraph);
                    if (foundParent) {
                        if (level > parentLevel) { // 是子标题
                            if (isLeafNode(document, paragraph, level)) {
                                // 返回段落 text，与 targetTiles 的 title 保持一致
                                subTitles.add(text.replace("\n", ""));
                            }
                        } else {
                            // 遇到同级或更高级别的标题，停止搜索
                            break;
                        }
                    } else if (text.equals(question)) {
                        foundParent = true;
                        parentLevel = level;
                    }
                }
            }

            processValue = "已获取最低级别子标题列表:100%";
            return subTitles;

        }
    }

    // 检查一个标题是否是叶子节点（没有更低级别的子标题）
    private boolean isLeafNode(XWPFDocument doc, XWPFParagraph p, int level) {
        int index = doc.getPosOfParagraph(p);
        if (index == -1 || index >= doc.getParagraphs().size() - 1) {
            return true;
        }

        // 检查后续段落
        XWPFParagraph nextP = doc.getParagraphs().get(index + 1);
        int nextLevel = getDocxOutlineLevel(nextP, doc);
        if (nextLevel > 0) {
            // 遇到同级或更高级别标题，是叶子节点
            return nextLevel <= level; // 存在更低级别的标题，不是叶子节点
        }
        return true;
    }

    /**
     * 获取二、三级标题列表
     */
    public String extractTitles(String filename) throws IOException {
        StringBuilder subTitles = new StringBuilder();
        try (InputStream fileInputStream = new FileInputStream(filename);
                XWPFDocument document = new XWPFDocument(fileInputStream)) {
            for (XWPFParagraph paragraph : document.getParagraphs()) {
                String text = paragraph.getText().trim();
                int level = getDocxOutlineLevel(paragraph, document);
                // 判断主标题（大纲级别3或4，即原来的heading 3或heading 4）
                if (level == 3 || level == 4) {
                    subTitles.append(text).append("\n");
                }
            }
        }
        return subTitles.toString();
    }

    /**
     * 获取DOC段落的大纲级别
     */
    private int getDocOutlineLevel(Paragraph paragraph) {
        int level = 0;
        short styleIndex = paragraph.getStyleIndex();
        if (styleIndex >= 1 && styleIndex <= 9) {
            level = styleIndex;
        }
        return level;
    }

    /**
     * 从数据库output字段中提取章节标题列表（叶子节点）
     */
    private List<String> extractTitlesFromOutput(String output) {
        List<String> titles = new ArrayList<>();

        // 提取【详细目录】部分
        if (output.contains("【详细目录】")) {
            int startIndex = output.indexOf("【详细目录】") + "【详细目录】".length();
            int endIndex = output.length();
            int nextTagIndex = output.indexOf("【", startIndex + 1);
            if (nextTagIndex > startIndex) {
                endIndex = nextTagIndex;
            }

            String directoryContent = output.substring(startIndex, endIndex).trim();
            String[] lines = directoryContent.split("\n");

            // 使用栈来跟踪当前路径，只提取叶子节点标题
            Stack<String> levelStack = new Stack<>();

            for (String line : lines) {
                line = line.trim().replace("*", "").replace("#", "").replace("-", "");
                if (!line.startsWith("6.") && !line.startsWith("1.") || line.isEmpty()) {
                    continue;
                }

                String[] parts = line.split(" ", 2);
                if (parts.length < 2) {
                    continue;
                }

                String number = parts[0].trim();
                int dotCount = number.split("\\.").length - 1;

                // 维护栈：弹出同级或更高级别的标题
                while (!levelStack.isEmpty()) {
                    String topNumber = levelStack.peek().split(" ", 2)[0].trim();
                    int topDotCount = topNumber.split("\\.").length - 1;
                    if (topDotCount >= dotCount) {
                        levelStack.pop();
                    } else {
                        break;
                    }
                }

                // 如果栈顶是当前标题的直接父节点，且下一行不是子标题，则当前标题是叶子节点
                levelStack.push(line);

                // 检查是否是叶子节点（没有子标题）
                // 通过检查该行后面是否有更高级别的标题来判断
                // 只有三级标题（6.x.x）才视为叶子节点，转为段落 text 格式
                if (dotCount == 2) {
                    titles.add(line);
                }
            }
        }

        return titles;
    }

    /**
     * 检查DOCX段落是否是三级标题
     */
    private boolean isDocxLevel3Title(String paraText, XWPFDocument document) {
        for (XWPFParagraph p : document.getParagraphs()) {
            if (p.getText().trim().equals(paraText.trim())) {
                return getDocxOutlineLevel(p, document) == 3;
            }
        }
        return false;
    }

    /**
     * 按三级标题分割DOCX内容
     */
    private void splitDocxByLevel3(String content, XWPFDocument document, List<TextSegment> segments) {
        StringBuilder currentLevel3Content = new StringBuilder();
        String[] paragraphs = content.split("\n");
        for (String para : paragraphs) {
            if (para.trim().isEmpty())
                continue;

            if (isDocxLevel3Title(para.trim(), document)) {
                if (currentLevel3Content.length() != 0) {
                    TextSegment segment = TextSegment.from(currentLevel3Content.toString());
                    segments.add(segment);
                    currentLevel3Content = new StringBuilder();
                }
                currentLevel3Content.append(para).append("\n");
            } else {
                if (currentLevel3Content.length() != 0) {
                    currentLevel3Content.append(para).append("\n");
                }
            }
        }
        if (currentLevel3Content.length() != 0) {
            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
            segments.add(segment);
        }
    }

    /**
     * 从 DOCX 文档中提取表格内容
     */
    private List<TextSegment> extractTablesFromDocx(XWPFDocument document) {
        List<TextSegment> tableSegments = new ArrayList<>();
        List<XWPFTable> tables = document.getTables();

        for (int i = 0; i < tables.size(); i++) {
            XWPFTable table = tables.get(i);
            StringBuilder tableContent = new StringBuilder();

            // 添加表格标识
            tableContent.append("[表格 ").append(i + 1).append("]\n");

            // 提取表格内容
            for (XWPFTableRow row : table.getRows()) {
                StringBuilder rowContent = new StringBuilder();
                for (XWPFTableCell cell : row.getTableCells()) {
                    String cellText = cell.getText().trim();
                    if (!cellText.isEmpty()) {
                        rowContent.append(cellText).append(" | ");
                    }
                }
                if (rowContent.length() > 0) {
                    // 移除末尾的分隔符
                    if (rowContent.length() >= 3) {
                        rowContent.setLength(rowContent.length() - 3);
                    }
                    tableContent.append(rowContent).append("\n");
                }
            }

            // 只有当表格内容有实际内容时才添加
            if (tableContent.length() > 10) {
                TextSegment segment = TextSegment.from(tableContent.toString());
                tableSegments.add(segment);
            }
        }

        return tableSegments;
    }

    /**
     * 从 DOC 文档中提取表格内容
     */
    private List<TextSegment> extractTablesFromDoc(HWPFDocument document) {
        List<TextSegment> tableSegments = new ArrayList<>();
        Range range = document.getRange();

        int tableCount = 0;
        StringBuilder tableContent = new StringBuilder();
        boolean inTable = false;

        for (int i = 0; i < range.numParagraphs(); i++) {
            Paragraph paragraph = range.getParagraph(i);
            String text = paragraph.text().trim();

            // 检测表格段落
            if (paragraph.isInTable()) {
                if (!inTable) {
                    // 开始新表格
                    tableCount++;
                    tableContent = new StringBuilder();
                    tableContent.append("[表格 ").append(tableCount).append("]\n");
                    inTable = true;
                }

                // 添加表格行内容
                if (!text.isEmpty()) {
                    tableContent.append(text).append("\n");
                }
            } else if (inTable) {
                // 表格结束
                inTable = false;
                if (tableContent.length() > 10) {
                    TextSegment segment = TextSegment.from(tableContent.toString());
                    tableSegments.add(segment);
                }
            }
        }

        // 处理最后一个表格
        if (inTable && tableContent.length() > 10) {
            TextSegment segment = TextSegment.from(tableContent.toString());
            tableSegments.add(segment);
        }

        return tableSegments;
    }

    /**
     * 按三级标题分割DOC内容
     */
    private void splitDocByLevel3(String content, HWPFDocument document, List<TextSegment> segments) {
        StringBuilder currentLevel3Content = new StringBuilder();
        Range level2Range = document.getRange();
        boolean foundCurrentLevel2 = false;

        for (int j = 0; j < level2Range.numParagraphs(); j++) {
            Paragraph p = level2Range.getParagraph(j);
            String paraText = p.text().trim();
            if (paraText.isEmpty())
                continue;

            int pLevel = getDocOutlineLevel(p);
            if (pLevel == 2 && paraText.equals(content.split("\n")[0].trim())) {
                foundCurrentLevel2 = true;
            }

            if (foundCurrentLevel2) {
                pLevel = getDocOutlineLevel(p);

                if (pLevel == 3) {
                    if (currentLevel3Content.length() != 0) {
                        TextSegment segment = TextSegment.from(currentLevel3Content.toString());
                        segments.add(segment);
                        currentLevel3Content = new StringBuilder();
                    }
                    currentLevel3Content.append(paraText).append("\n");
                } else if (pLevel == 2 && !paraText.equals(content.split("\n")[0].trim())) {
                    break;
                } else {
                    if (currentLevel3Content.length() != 0 || pLevel == 2) {
                        currentLevel3Content.append(paraText).append("\n");
                    }
                }
            }
        }

        if (currentLevel3Content.length() != 0) {
            TextSegment segment = TextSegment.from(currentLevel3Content.toString());
            segments.add(segment);
        }
    }

    /**
     * 分割文档
     */
    private List<TextSegment> splitDocument(File transferFile, int maxSegmentSizeInChars, int maxOverlapSizeInChars)
            throws Throwable {
        String filename = transferFile.getName().toLowerCase();
        try (InputStream fileInputStream = new FileInputStream(transferFile)) {
            if (filename.endsWith(".docx")) {
                List<TextSegment> segments = new ArrayList<>();
                try (XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream)) {
                    StringBuilder currentLevel2Content = new StringBuilder();
                    boolean inLevel2Section = false;

                    for (XWPFParagraph paragraph : xwpfDocument.getParagraphs()) {
                        String text = paragraph.getText().trim();
                        if (text.isEmpty())
                            continue;

                        int paraLevel = getDocxOutlineLevel(paragraph, xwpfDocument);

                        if (paraLevel == 2) {
                            if (currentLevel2Content.length() != 0) {
                                int level2Length = currentLevel2Content.toString().getBytes().length;
                                if (level2Length <= 65535) {
                                    TextSegment segment = TextSegment.from(currentLevel2Content.toString());
                                    segments.add(segment);
                                } else {
                                    splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
                                }
                                currentLevel2Content = new StringBuilder();
                            }
                            currentLevel2Content.append(text).append("\n");
                            inLevel2Section = true;
                        } else {
                            if (inLevel2Section) {
                                currentLevel2Content.append(text).append("\n");
                            }
                        }
                    }

                    if (currentLevel2Content.length() != 0) {
                        int level2Length = currentLevel2Content.toString().getBytes().length;
                        if (level2Length <= 65535) {
                            TextSegment segment = TextSegment.from(currentLevel2Content.toString());
                            segments.add(segment);
                        } else {
                            splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
                        }
                    }

                    // 提取文档中的所有表格
                    List<TextSegment> tableSegments = extractTablesFromDocx(xwpfDocument);
                    segments.addAll(tableSegments);
                }
                return segments;
            } else if (filename.endsWith(".doc")) {
                List<TextSegment> segments = new ArrayList<>();
                try (HWPFDocument hwpfDocument = new HWPFDocument(fileInputStream)) {
                    StringBuilder currentLevel2Content = new StringBuilder();
                    boolean inLevel2Section = false;

                    Range range = hwpfDocument.getRange();
                    for (int i = 0; i < range.numParagraphs(); i++) {
                        Paragraph paragraph = range.getParagraph(i);
                        String text = paragraph.text().trim();
                        if (text.isEmpty())
                            continue;

                        int paraLevel = getDocOutlineLevel(paragraph);

                        if (paraLevel == 2) {
                            if (currentLevel2Content.length() != 0) {
                                int level2Length = currentLevel2Content.toString().getBytes().length;
                                if (level2Length <= 65535) {
                                    TextSegment segment = TextSegment.from(currentLevel2Content.toString());
                                    segments.add(segment);
                                } else {
                                    splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
                                }
                                currentLevel2Content = new StringBuilder();
                            }
                            currentLevel2Content.append(text).append("\n");
                            inLevel2Section = true;
                        } else {
                            if (inLevel2Section) {
                                currentLevel2Content.append(text).append("\n");
                            }
                        }
                    }

                    if (currentLevel2Content.length() != 0) {
                        int level2Length = currentLevel2Content.toString().getBytes().length;
                        if (level2Length <= 65535) {
                            TextSegment segment = TextSegment.from(currentLevel2Content.toString());
                            segments.add(segment);
                        } else {
                            splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
                        }
                    }

                    // 提取文档中的所有表格
                    List<TextSegment> tableSegments = extractTablesFromDoc(hwpfDocument);
                    segments.addAll(tableSegments);
                }
                return segments;
            } else if (filename.endsWith(".pdf")) {
                Document document = new ApachePdfBoxDocumentParser().parse(fileInputStream);
                DocumentByParagraphSplitter splitter = new DocumentByParagraphSplitter(maxSegmentSizeInChars,
                        maxOverlapSizeInChars);
                return splitter.split(document);
            } else if (filename.endsWith(".txt")) {
                Document document = new TextDocumentParser().parse(fileInputStream);
                DocumentByParagraphSplitter splitter = new DocumentByParagraphSplitter(maxSegmentSizeInChars,
                        maxOverlapSizeInChars);
                return splitter.split(document);
            } else {
                throw new UnsupportedOperationException("不支持文件类型: " + filename);
            }
        }
    }

    public void generateWordDocument(String content, XWPFDocument document, int insertPos) {
        try {
            // 规范化锚点：在 insertPos 对应段落之前插入；若越界则附加到末尾
            List<XWPFParagraph> paragraphs = document.getParagraphs();
            if (paragraphs.isEmpty()) {
                document.createParagraph();
                paragraphs = document.getParagraphs();
            }
            if (insertPos < 0) {
                insertPos = 0;
            }
            if (insertPos >= paragraphs.size()) {
                // 在文末追加一个空段落作为锚点
                XWPFParagraph tail = document.createParagraph();
                paragraphs = document.getParagraphs();
                insertPos = paragraphs.indexOf(tail);
            }

            // 将连续的多个换行符替换为单个换行符
            String normalizedContent = content.replaceAll("\n{2,}", "\n");
            String[] lines = normalizedContent.split("\n");
            boolean inTable = false;
            List<String> tableLines = new ArrayList<>();

            // 将内容解析为块（普通段落/表格/标题/列表项等），然后倒序插入，确保顺序正确
            List<Block> blocks = new ArrayList<>();

            for (String raw : lines) {
                String line = raw.trim();
                if (line.isEmpty()) {
                    blocks.add(new Block("para", "")); // 空段落
                    continue;
                }

                if (line.matches("^#{4,6}\\s+.*")) {
                    // 四、五、六级标题（####/#####/######），去掉编号前缀，由Word自动编号
                    int hashCount = 0;
                    while (hashCount < line.length() && line.charAt(hashCount) == '#') {
                        hashCount++;
                    }
                    String title = line.substring(hashCount).trim();
                    title = title.replaceFirst("^\\d+(\\.\\d+)*\\.?\\s*", "");
                    blocks.add(new Block("heading", title, hashCount));
                } else if (line.startsWith("#")) {
                    // 跳过前三级标题（#、##、###）
                    continue;
                } else if (line.startsWith("|")) {
                    // 表格模式开始
                    if (!inTable) {
                        inTable = true;
                        tableLines.clear();
                    }
                    tableLines.add(line);
                } else if (inTable && !line.startsWith("|")) {
                    // 表格结束，收集为一个块
                    inTable = false;
                    if (!tableLines.isEmpty()) {
                        blocks.add(new Block(new ArrayList<>(tableLines)));
                        tableLines.clear();
                    }
                    // 当前行按普通段落处理
                    blocks.add(new Block("para", line));
                } else if (inTable) {
                    tableLines.add(line);
                } else {
                    // 匹配格式：1. **文本**（数字前缀+加粗）
                    Pattern numberedBoldPattern = Pattern.compile("^(\\d+\\.\\s+)\\*\\*([^*]+)\\*\\*(.*)$");
                    Matcher nbm = numberedBoldPattern.matcher(line);
                    if (nbm.matches()) {
                        String numPrefix = nbm.group(1);
                        String bold = nbm.group(2);
                        String suf = nbm.group(3);
                        if (suf == null)
                            suf = "";
                        blocks.add(new Block("numberedBold", numPrefix, bold, suf));
                        continue;
                    }
                    // 匹配格式：**文本**
                    Pattern boldOnlyPattern = Pattern.compile("\\*\\*([^*]+)\\*\\*(.*)");
                    Matcher boldMatcher = boldOnlyPattern.matcher(line);
                    if (boldMatcher.matches()) {
                        String boldText = boldMatcher.group(1); // "数据校验"
                        String suffix = boldMatcher.group(2); // "："或后续文本，可能为空
                        if (suffix == null)
                            suffix = "";
                        blocks.add(new Block("boldOnly", boldText, suffix));
                        continue;
                    }
                    // 如果不是 boldOnly 格式，继续后续处理
                    if (line.startsWith("![") && line.contains("](") && line.contains(")")) {
                        // 图片 markdown，转成说明文本
                        String imageText = extractImageDescription(line);
                        blocks.add(new Block("imageText", imageText));
                    } else if (line.contains("http") && (line.contains(".png") || line.contains(".jpg")
                            || line.contains(".jpeg") || line.contains(".gif"))) {
                        blocks.add(new Block("imageText", "图片链接: " + line));
                    } else {
                        blocks.add(new Block("para", line));
                    }
                }
            }
            // 收尾：如果文件最后是表格仍未输出
            if (inTable && !tableLines.isEmpty()) {
                blocks.add(new Block(new ArrayList<>(tableLines)));
                tableLines.clear();
            }

            // 倒序插入到 insertPos 前（即插在 insertPos 之前，从后往前插保证最终顺序）
            for (int i = blocks.size() - 1; i >= 0; i--) {
                Block b = blocks.get(i);
                // 每次获取最新段落列表和锚点，避免索引失效
                paragraphs = document.getParagraphs();
                if (insertPos >= paragraphs.size()) {
                    XWPFParagraph tail = document.createParagraph();
                    paragraphs = document.getParagraphs();
                    insertPos = paragraphs.indexOf(tail);
                }
                XmlCursor cursor = paragraphs.get(insertPos).getCTP().newCursor();

                switch (b.getType()) {
                    case "heading": {
                        XWPFParagraph p = document.insertNewParagraph(cursor);
                        // 使用模板文件的对应等级标题样式，由Word自动编号
                        if (b.getHeadingLevel() != null) {
                            p.setStyle(String.valueOf(b.getHeadingLevel() + 1));
                        }
                        XWPFRun run = p.createRun();
                        run.setText(b.getText());
                        break;
                    }
                    case "numberedBold": {
                        XWPFParagraph p = document.insertNewParagraph(cursor);
                        // 数字前缀（不加粗）
                        if (b.getNumberPrefix() != null) {
                            XWPFRun r1 = p.createRun();
                            r1.setText(b.getNumberPrefix());
                            r1.setFontSize(12);
                        }
                        // 加粗文本
                        XWPFRun r2 = p.createRun();
                        r2.setText(b.getBoldText() != null ? b.getBoldText() : "");
                        r2.setFontSize(12);
                        r2.setBold(true);
                        // 后缀（不加粗）
                        if (b.getSuffixText() != null && !b.getSuffixText().isEmpty()) {
                            XWPFRun r3 = p.createRun();
                            r3.setText(b.getSuffixText());
                            r3.setFontSize(12);
                        }
                        break;
                    }
                    case "boldOnly": {
                        // 处理格式：**文本** 或 **文本**： 转换为 Word 中的部分加粗
                        XWPFParagraph p = document.insertNewParagraph(cursor);

                        // 加粗文本部分
                        XWPFRun run1 = p.createRun();
                        run1.setText(b.getBoldText());
                        run1.setFontSize(12);
                        run1.setBold(true);

                        // 后缀部分（不加粗，如冒号及后续内容，如果有的话）
                        if (b.getSuffixText() != null && !b.getSuffixText().isEmpty()) {
                            XWPFRun run2 = p.createRun();
                            run2.setText(b.getSuffixText());
                            run2.setFontSize(12);
                        }
                        break;
                    }
                    case "imageText": {
                        XWPFParagraph p = document.insertNewParagraph(cursor);
                        XWPFRun run = p.createRun();
                        run.setText(b.getText());
                        run.setFontSize(12);
                        run.setItalic(true);
                        run.setColor("808080");
                        break;
                    }
                    case "table": {
                        // 解析 markdown 表格并插入在 cursor 位置
                        List<String[]> tableData = new ArrayList<>();
                        Pattern sep = Pattern.compile("^\\|\\s*(-+\\s*\\|\\s*)*-+\\s*\\|?$");
                        for (String tl : b.getTable()) {
                            if (sep.matcher(tl).matches())
                                continue;
                            String t = tl.trim();
                            if (t.startsWith("|"))
                                t = t.substring(1);
                            if (t.endsWith("|"))
                                t = t.substring(0, t.length() - 1);
                            String[] cells = t.split("\\|", -1);
                            for (int c = 0; c < cells.length; c++)
                                cells[c] = cells[c].trim();
                            tableData.add(cells);
                        }
                        if (!tableData.isEmpty()) {
                            XWPFTable table = document.insertNewTbl(cursor);
                            table.setStyleID("10");
                            // 初始化行列
                            int rows = tableData.size();
                            int cols = tableData.get(0).length;
                            while (table.getNumberOfRows() < rows)
                                table.createRow();
                            for (int r = 0; r < rows; r++) {
                                XWPFTableRow tr = table.getRow(r);
                                // 首行已存在的列数可能不足，补齐
                                while (tr.getTableCells().size() < cols)
                                    tr.addNewTableCell();
                                String[] rowData = tableData.get(r);
                                for (int c = 0; c < cols; c++) {
                                    XWPFTableCell cell = tr.getCell(c);
                                    cell.removeParagraph(0);
                                    XWPFParagraph cp = cell.addParagraph();
                                    cp.setIndentationFirstLine(0);
                                    String cellText = rowData[c];
                                    if (r == 0) {
                                        // 表头行整体加粗
                                        XWPFRun cr = cp.createRun();
                                        cr.setText(cellText);
                                        cr.setBold(true);
                                        cp.setAlignment(ParagraphAlignment.CENTER);
                                    } else if (cellText.contains("**")) {
                                        // 非表头行，解析行内 **文本** 加粗
                                        Matcher m = Pattern.compile("\\*\\*([^*]+)\\*\\*").matcher(cellText);
                                        int lastEnd = 0;
                                        while (m.find()) {
                                            if (m.start() > lastEnd) {
                                                XWPFRun cr = cp.createRun();
                                                cr.setText(cellText.substring(lastEnd, m.start()));
                                            }
                                            XWPFRun boldRun = cp.createRun();
                                            boldRun.setText(m.group(1));
                                            boldRun.setBold(true);
                                            lastEnd = m.end();
                                        }
                                        if (lastEnd < cellText.length()) {
                                            XWPFRun cr = cp.createRun();
                                            cr.setText(cellText.substring(lastEnd));
                                        }
                                        cp.setAlignment(ParagraphAlignment.LEFT);
                                    } else {
                                        XWPFRun cr = cp.createRun();
                                        cr.setText(cellText);
                                        cp.setAlignment(ParagraphAlignment.LEFT);
                                    }
                                }
                            }
                            table.setWidth("100%");
                        }
                        break;
                    }
                    case "para":
                    default: {
                        XWPFParagraph p = document.insertNewParagraph(cursor);
                        if (b.getText() != null && b.getText().contains("**")) {
                            // 解析行内 **文本** 加粗
                            Matcher m = Pattern.compile("\\*\\*([^*]+)\\*\\*").matcher(b.getText());
                            int lastEnd = 0;
                            while (m.find()) {
                                // 加粗前的普通文本
                                if (m.start() > lastEnd) {
                                    XWPFRun run = p.createRun();
                                    run.setText(b.getText().substring(lastEnd, m.start()));
                                    run.setFontSize(12);
                                }
                                // 加粗文本
                                XWPFRun boldRun = p.createRun();
                                boldRun.setText(m.group(1));
                                boldRun.setFontSize(12);
                                boldRun.setBold(true);
                                lastEnd = m.end();
                            }
                            // 剩余文本
                            if (lastEnd < b.getText().length()) {
                                XWPFRun run = p.createRun();
                                run.setText(b.getText().substring(lastEnd));
                                run.setFontSize(12);
                            }
                        } else {
                            XWPFRun run = p.createRun();
                            if (b.getText() != null) {
                                run.setText(b.getText());
                            }
                            run.setFontSize(12);
                        }
                        break;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 从图片Markdown格式中提取描述文本
     */
    private String extractImageDescription(String imageLine) {
        // 格式: ![描述](链接)
        if (imageLine.startsWith("![") && imageLine.contains("](") && imageLine.contains(")")) {
            int descStart = imageLine.indexOf('[') + 1;
            int descEnd = imageLine.indexOf(']');
            int urlStart = imageLine.indexOf('(') + 1;
            int urlEnd = imageLine.indexOf(')');

            if (descStart > 0 && descEnd > descStart && urlStart > descEnd && urlEnd > urlStart) {
                String description = imageLine.substring(descStart, descEnd);
                String url = imageLine.substring(urlStart, urlEnd);
                return "图: " + description + " (" + url + ")";
            }
        }
        return imageLine; // 如果格式不正确，返回原文本
    }

    /**
     * 流式生成章节内容（SSE方式）- 真正的逐字输出
     */
    @Override
    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode)
            throws Throwable {
        // 设置SSE超时时间为10分钟
        SseEmitter emitter = new SseEmitter(600000L);

        // 设置回调
        emitter.onCompletion(() -> {
        });
        emitter.onTimeout(() -> {
            emitter.complete();
        });
        emitter.onError(e -> System.err.println("SSE连接错误: " + e.getMessage()));

        // 在主线程中预先获取用户ID，避免异步线程中丢失SecurityContext

        Long userId = SecurityUtils.getUserId();

        // 使用独立线程池处理，避免阻塞主线程
        ExecutorService executor = Executors.newSingleThreadExecutor();

        executor.execute(() -> {
            ObjectMapper objectMapper = new ObjectMapper();
            Map<String, String> titleContentMap = new HashMap<>();

            try {
                String profilePath = RuoYiConfig.getProfile() + "/upload/agent/" + agentName;
                File filePath = new File(profilePath);
                if (!filePath.exists()) {
                    filePath.mkdirs();
                }
                String filename = file.getOriginalFilename();
                File transferFile = new File(profilePath + "/" + filename);
                file.transferTo(transferFile);

                // 获取项目概况和评分标准（需要先获取，因为要从中提取目录标题）
                CmcChat chatQuery = new CmcChat();
                chatQuery.setTopicId(topicId);
                List<CmcChat> chatList = cmcChatMapper.selectCmcChatList(chatQuery);

                if (chatList == null || chatList.isEmpty()) {
                    throw new RuntimeException("未找到话题信息，topicId: " + topicId);
                }

                String output = chatList.get(0).getOutput();
                String projectOverview = parseProjectOverview(output);
                String scoringRequirements = parseScoringRequirements(output);

                // 从 output 的【详细目录】中提取带编号的叶子标题（6.x.x）
                List<String> allTitles = extractTitlesFromOutput(output);

                List<String> targetTitles = new ArrayList<>();

                if (selectedNode != null && !selectedNode.isEmpty()) {
                    // 从 allTitles 中筛选出 selectedNode 对应编号前缀的子章节
                    // selectedNode 格式如 "6.1 技术方案"，子章节编号前缀为 "6.1."
                    String selectedNumber = selectedNode.trim().split("\\s+")[0];
                    String prefix = selectedNumber + ".";
                    for (String title : allTitles) {
                        if (title.startsWith(prefix) || title.trim().startsWith(selectedNumber + ".")) {
                            targetTitles.add(title);
                        }
                    }
                    // 如果没有匹配到子章节（可能是叶子节点），直接使用 selectedNode
                    if (targetTitles.isEmpty()) {
                        targetTitles.add(selectedNode);
                    }
                } else {
                    targetTitles.addAll(allTitles);
                }

                int totalChapters = targetTitles.size();
                int chapterIndex = 0;

                // 构建招标文件嵌入向量存储
                File tenderFile = transferFile;
                InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
                List<TextSegment> segments = splitDocument(tenderFile, 300, 50);
                List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
                embeddingStore.addAll(embeddings, segments);

                for (String title : targetTitles) {
                    chapterIndex++;
                    final int finalChapterIndex = chapterIndex;

                    try {
                        // 发送章节开始标记
                        ChapterStreamResponse startResponse = ChapterStreamResponse.content(title, "", false,
                                finalChapterIndex, totalChapters);
                        emitter.send(SseEmitter.event()
                                .name("chapter")
                                .data(objectMapper.writeValueAsString(startResponse)));

                        // 检索相关内容
                        List<String> tenderContent = retrieveFromTenderDocument(embeddingStore, title);
                        List<JSONObject> technicalContexts = retrieveFromMilvus("technical", title, 10,
                                "title != \"目录\"");
                        List<JSONObject> solutionContexts = retrieveFromMilvus("solution", title, 10,
                                "title != \"目录\"");

                        List<JSONObject> knowledgeContexts = new ArrayList<>();
                        if (technicalContexts != null) {
                            knowledgeContexts.addAll(technicalContexts);
                        }
                        if (solutionContexts != null) {
                            knowledgeContexts.addAll(solutionContexts);
                        }

                        String prompt = buildChapterPrompt(title, tenderContent, knowledgeContexts, projectOverview,
                                scoringRequirements, agentName);

                        // 使用 CountDownLatch 等待当前章节生成完成
                        CountDownLatch latch = new CountDownLatch(1);
                        StringBuilder fullContent = new StringBuilder();
                        // 用于累计token用量（使用AtomicLong以便在lambda中修改）
                        AtomicLong[] tokenUsage = new AtomicLong[3];
                        tokenUsage[0] = new AtomicLong(0); // promptTokens
                        tokenUsage[1] = new AtomicLong(0); // completionTokens
                        tokenUsage[2] = new AtomicLong(0); // totalTokens
                        AtomicBoolean cancelled = new AtomicBoolean(false);

                        generateAnswerFluxWithUsage(prompt, tokenUsage).subscribe(
                                content -> {
                                    if (cancelled.get()) {
                                        return;
                                    }
                                    try {
                                        if (content != null && !content.isEmpty()) {
                                            fullContent.append(content);

                                            // 逐字发送到前端，并带上当前已累计的 token 用量
                                            ChapterStreamResponse response = ChapterStreamResponse.content(
                                                    title, content, false, finalChapterIndex, totalChapters);
                                            response.setPromptTokens(tokenUsage[0].get());
                                            response.setCompletionTokens(tokenUsage[1].get());
                                            response.setTotalTokens(tokenUsage[2].get());
                                            emitter.send(SseEmitter.event()
                                                    .name("chapter")
                                                    .data(objectMapper.writeValueAsString(response)));
                                        }
                                    } catch (Exception e) {
                                        cancelled.set(true);
                                    }
                                },
                                error -> {
                                    if (cancelled.getAndSet(true)) {
                                        return;
                                    }
                                    try {
                                        ChapterStreamResponse errorResponse = ChapterStreamResponse.error(title,
                                                error.getMessage());
                                        emitter.send(SseEmitter.event()
                                                .name("chapter")
                                                .data(objectMapper.writeValueAsString(errorResponse)));
                                    } catch (Exception sendEx) {
                                    }
                                    titleContentMap.put(title, "该章节内容生成失败，请手动填写。");
                                    latch.countDown();
                                },
                                () -> {
                                    if (cancelled.getAndSet(true)) {
                                        return;
                                    }
                                    try {
                                        // 发送章节完成标记（包含token用量）
                                        ChapterStreamResponse completeResponse = ChapterStreamResponse.content(
                                                title, "", true, finalChapterIndex, totalChapters);
                                        completeResponse.setPromptTokens(tokenUsage[0].get());
                                        completeResponse.setCompletionTokens(tokenUsage[1].get());
                                        completeResponse.setTotalTokens(tokenUsage[2].get());

                                        emitter.send(SseEmitter.event()
                                                .name("chapter")
                                                .data(objectMapper.writeValueAsString(completeResponse)));

                                        // 保存完整内容
                                        String chapterContent = fullContent.toString();
                                        titleContentMap.put(title, chapterContent);
                                        saveChapterToChat(topicId, title, chapterContent, userId);
                                    } catch (Exception e) {
                                    } finally {
                                        latch.countDown();
                                    }
                                });

                        // 等待当前章节生成完成
                        latch.await();

                    } catch (Exception e) {
                        System.err.println("生成章节 " + title + " 内容时出错: " + e.getMessage());
                        try {
                            ChapterStreamResponse errorResponse = ChapterStreamResponse.error(title, e.getMessage());
                            emitter.send(SseEmitter.event()
                                    .name("chapter")
                                    .data(objectMapper.writeValueAsString(errorResponse)));
                        } catch (Exception sendEx) {
                            System.err.println("发送错误消息失败: " + sendEx.getMessage());
                        }
                        titleContentMap.put(title, "该章节内容生成失败，请手动填写。");
                    }
                }

                // 写入Word文档
                String templatePath = profilePath + "/" + filename;
                writeContentToDocument(titleContentMap, templatePath, emitter, objectMapper);

                // 发送完成信号
                ChapterStreamResponse completedResponse = ChapterStreamResponse.completed();
                emitter.send(SseEmitter.event()
                        .name("completed")
                        .data(objectMapper.writeValueAsString(completedResponse)));

                emitter.complete();

            } catch (Throwable e) {
                try {
                    emitter.send(SseEmitter.event()
                            .name("error")
                            .data("生成过程中发生错误: " + e.getMessage()));
                } catch (IOException sendEx) {
                    System.err.println("发送错误消息失败: " + sendEx.getMessage());
                }
                emitter.completeWithError(e);
            } finally {
                executor.shutdown();
            }
        });

        return emitter;
    }

    private void saveChapterToChat(String topicId, String title, String content, Long userId) {
        try {
            // 查询同一topic下是否已存在相同的input
            CmcChat queryChat = new CmcChat();
            queryChat.setTopicId(topicId);
            queryChat.setInput(title);
            List<CmcChat> existingChats = cmcChatMapper.selectCmcChatList(queryChat);

            if (!existingChats.isEmpty()) {
                // 已存在，更新
                CmcChat existingChat = existingChats.get(0);
                existingChat.setOutput(content);
                existingChat.setOutputTime(new Date());
                cmcChatMapper.updateCmcChat(existingChat);
            } else {
                // 不存在，新增
                CmcChat cmcChat = new CmcChat();
                cmcChat.setChatId(new SnowFlake().generateId().toString());
                cmcChat.setTopicId(topicId);
                cmcChat.setUserId(userId);
                cmcChat.setInput(title);
                cmcChat.setInputTime(new Date());
                cmcChat.setOutput(content);
                cmcChat.setOutputTime(new Date());
                cmcChatMapper.insertCmcChat(cmcChat);
            }
        } catch (Exception e) {
            System.err.println("保存章节到cmc_chat表失败: " + e.getMessage());
            e.printStackTrace();
        }
    }

}