RuoYi + RAGFlow 搭建私有化知识库完整集成实践(二)

文章来源声明: 原文作者:Su573; 来源站点:掘金; 原文链接:https://juejin.cn/post/7687914732575932456; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

架构与表设计思路清晰,无 GPU 部署路径和五种开放范围权限方案对私有化知识库落地很有参考价值,尤其适合若依系团队做企业内知识库。

> 上一篇介绍了整体架构与选型思路。本篇记录一下实现层面:RAGFlow 私有化部署(无 GPU 环境)、8 个功能模块的数据表设计与核心逻辑,以及 Java 后端对接 RAGFlow API 的完整代码。重点讲解知识库的**五种开放范围权限设计**。

一、RAGFlow 私有化部署

1.1 硬件要求

以实际开发环境为例:CPU 10 核 / 内存 32G / 无 GPU(笔记本),完全可运行。

资源本项目实际说明
CPU10 核文档解析阶段为主要开销
内存32G(Docker 分配 8~12G 即可)RAGFlow 服务 + 解析任务
GPU向量化走 API,本地无推理负担
磁盘SSD 500G文档原件 + 向量库

关键认知:RAG 方案不做模型训练与微调,算力消耗集中在"文档解析 + 向量化"的一次性过程,日常问答阶段只做检索 + 生成,因此对硬件要求远低于训练/微调方案,只不过经过实践,CPU模式下的文档切片解析速度确实一般,正式环境需要GPU加速。

1.2 Docker Compose 部署ragFlow

<span># 克隆项目</span>
git <span>clone</span> https://github.com/infiniflow/ragflow.git
<span>cd</span> ragflow/docker

<span># 配置环境变量</span>
<span>cp</span> .env.example .<span>env</span>
vim .<span>env</span>

<span># 启动(首次拉取镜像,v0.27.0 建议锁定镜像 tag)</span>
docker compose up -d

ragFlow启用以后登录页如下:

1.3 模型配置(开发环境我用的是硅基流动 API)

RAGFlow 支持配置 OpenAI 兼容的模型供应商。开发阶段选择硅基流动(SiliconFlow)线上 API,主要原因是简化本地环境、提升开发效率——不需要在笔记本上部署嵌入模型和对话模型,即可快速验证业务逻辑。

1.4 模型选择

三个模型各司其职:

模型类型作用
BAAI/bge-m3向量化把文档切块转成向量,支持多语言与长文本(免费)
BAAI/bge-reranker-v2-m3重排序检索结果精排,显著提升准确率(免费)
deepseek-ai/DeepSeek-V4-Flash对话生成基于检索结果生成答案

二、若依侧:8 个功能模块的设计

2.1 模块菜单结构

AI知识库问答
├── 模型管理          <span># 三类模型统一维护</span>
├── 知识库管理        <span># 知识库 CRUD + 开放范围</span>
├── 知识库文档        <span># 文档上传/解析/状态</span>
├── RAG提示词         <span># 助手系统提示词模板</span>
├── 对话助手管理      <span># 助手 = 知识库 + 模型 + 提示词</span>
├── RAG会话明细       <span># 每次问答的详细记录</span>
├── 用户会话管理      <span># 按用户维度管理会话</span>
└── AI智能问答        <span># 最终用户工作台</span>

2.2 模型管理

数据表 kb_model

字段类型说明
model\_namevarchar(100)模型名称,如 BAAI/bge-m3
model\_typevarchar(20)模型类型:**向量化模型 / 重排序模型 / 大语言模型**
platformvarchar(50)模型平台:硅基流动、本地、OpenAI 等
deploy\_typevarchar(20)部署方式:公有云 / 本地
api\_base\_urlvarchar(200)API 地址
api\_keyvarchar(200)密钥(密文存储)
statuschar(1)是否启用

界面提供**"验证"按钮**:点击后实际调用一次模型 API(向量化模型跑一次 embedding、大模型跑一次补全),把连通性和响应时间直接反馈到界面上,避免配错模型到问答环节才发现。

——三类模型(重排序/向量化/大语言)各维护一条记录,全部启用,平台为"硅基流动",部署方式"公有云",每条记录支持验证/修改/删除。

2.3 知识库管理 + 开放范围(核心权限设计)

整个 AI 模块共 10 张业务表,统一 rag_ 前缀,与若依 sys_ 体系隔离,方便单独备份与维护:

表名中文名归属模块职责
rag\_llm\_modelRAG大模型配置模型管理LLM / Embedding 模型与密钥维护
rag\_knowledge\_baseRAG知识库管理知识库管理知识库主表,含开放范围 pub\_area
rag\_knowledge\_base\_auth知识库权限表知识库管理指定用户 / 部门 / 角色授权明细
rag\_documentRAG知识库文档知识库文档文档元数据 + 解析 / 向量化状态
rag\_parse\_taskRAG文档解析任务队列知识库文档解析任务异步队列
rag\_prompt\_templateRAG提示词模板表RAG提示词系统 / 用户提示词模板
rag\_chat\_botRAG对话助手配置对话助手管理助手 = 知识库集合 + 模型 + 提示词 + 检索参数
rag\_chat\_bot\_user\_sortRAG对话助手用户排序表对话助手管理每用户独立助手卡片顺序
rag\_conversationRAG用户会话表用户会话管理会话主表(对应 RAGFlow conversation)
rag\_conversation\_msgRAG会话消息明细RAG会话明细问答明细 + 引用来源 + token 消耗

通用设计规范(所有主表统一遵守):

  • del_flag 逻辑删除、is_enable 启用开关、create_by / create_time / update_by / update_time 审计字段,与若依 BaseEntity 对齐;
  • 对接 RAGFlow 的表(知识库 / 文档 / 助手 / 会话)主键一律用 varchar(64) 直接存 RAGFlow 侧 ID,避免本地自增主键与远端 ID 的映射表;
  • 状态类字段用数字或短字符串枚举,注释里写明含义(如 parse_status:0 待解析 / 1 解析中 / 2 成功 / 3 失败)。

2.4 模型管理

数据表 rag_llm_model(RAG大模型配置):

字段类型说明
idbigint主键
model\_namevarchar(128)模型标识名称,传给 RAGFlow API,如 BAAI/bge-m3
model\_aliasvarchar(128)前端显示别名
model\_typevarchar(32)模型类型:llm 大模型 / embedding 向量化模型
model\_platformvarchar(64)模型平台:ollama / openai / qwen / 硅基流动等
api\_keyvarchar(300)API KEY
base\_urlvarchar(300)API 地址(OpenAI 兼容接口)
deploy\_modechar(1)部署方式:1 云 / 2 本地
descriptionvarchar(512)描述
is\_enablechar(1)是否启用
del\_flagchar(1)逻辑删除
create\_by / create\_time / update\_by / update\_timevarchar(64) / datetime若依审计字段

界面提供**"验证"按钮**:点击后实际调用一次模型 API(向量化模型跑一次 embedding、大模型跑一次补全),把连通性和响应时间直接反馈到界面上,本质是参考ragFlow的模型验证功能。

——向量化 / 大语言模型各维护一条记录,全部启用,平台为"硅基流动",部署方式"公有云",每条记录支持验证/修改/删除。

2.5 知识库管理 + 开放范围(核心权限设计)

数据表 rag_knowledge_base(RAG知识库管理):

字段类型说明
idvarchar(64)主键(对应 RAGFlow dataset\_id)
kb\_namevarchar(128)知识库名称
kb\_descvarchar(512)知识库描述
avatarvarchar(255)知识库封面图标
chunk\_methodvarchar(32)RAGFlow 分片解析模板,默认 General
business\_typetinyint业务分类:1 通用文档库 / 2 网页抓取库 / 3 FAQ问答库(本地业务,不传给 RAGFlow)
embedding\_modelvarchar(128)绑定向量化模型
chunk\_sizeint分片大小 token(对应 chunk\_token\_num)
chunk\_overlapint分片重叠值
rag\_statustinyint知识库状态:0 未初始化 / 1 正常 / 2 同步中 / 3 异常失败
rag\_msgvarchar(1024)状态备注 / 异常信息
total\_doc\_numint文档总数量(冗余)
total\_chunk\_numbigint向量分片总数(冗余)
**pub\_area**varchar(30)**开放范围**:private 私有 / public 公开 / assign\_user 指定用户 / assign\_dept 指定部门 / assign\_role 指定角色
is\_enablechar(1)是否启用
del\_flagchar(1)逻辑删除
create\_by / create\_time / update\_by / update\_timevarchar(64) / datetime若依审计字段

开放范围是知识库权限的核心,创建知识库时直接配置:

开放范围(pub\_area)含义可见人群
private私有仅创建人可见可用
public公开所有登录用户
assign\_user指定用户仅勾选的用户可见
assign\_dept指定部门部门内成员可见(含子部门)
assign\_role指定角色拥有指定角色的人可见

权限明细用一张授权表 rag_knowledge_base_auth(知识库权限表)实现,通过 auth_subject_type 区分主体类型——相比"每个类型一张关联表",新增 / 取消授权只插删一条记录,后端只写一套代码:

字段类型说明
idbigint主键
resource\_idvarchar(64)关联知识库 ID(rag\_knowledge\_base.id)
pub\_areavarchar(20)开放范围冗余(private / public / assign\_user / assign\_dept / assign\_role)
auth\_subject\_typevarchar(20)授权主体类型:user 用户 / dept 部门 / role 角色
auth\_subject\_idvarchar(64)授权主体 ID:用户 ID / 部门 ID / 角色 ID
create\_by / create\_time / update\_timevarchar(64) / datetime若依审计字段

唯一索引 uk_resource_subject(resource_id, auth_subject_type, auth_subject_id):同一知识库对同一主体只允许一条授权,天然防重复。

——新增知识库表单:知识库名称、向量化模型、分片解析模板、开放范围(私有/公开/指定用户/指定部门/指定角色单选)、封面、描述。

2.6 知识库文档管理

数据表 rag_document(RAG知识库文档):

字段类型说明
idvarchar(64)主键(对应 RAGFlow 文档 ID)
kb\_idvarchar(64)关联知识库 rag\_knowledge\_base.id
doc\_namevarchar(256)文档名称
doc\_typevarchar(32)文档类型:pdf / docx / txt / md / url
file\_pathvarchar(512)文件存储路径 / 网页 url
file\_sizebigint文件大小(字节)
page\_countintPDF 页数
parse\_statustinyint解析状态:0 待解析 / 1 解析中 / 2 解析成功 / 3 解析失败
vector\_statustinyint向量化状态:0 待向量化 / 1 向量化中 / 2 成功 / 3 失败
status\_msgvarchar(1024)解析 / 向量化失败日志
chunk\_countint文档分片向量数量(冗余)
sourcevarchar(64)来源:upload 上传 / crawl 网页抓取
is\_enablechar(1)是否启用
del\_flagchar(1)逻辑删除
create\_by / create\_time / update\_by / update\_timevarchar(64) / datetime若依审计字段

这里有两个关键点:

  • 主键直接用 RAGFlow 返回的文档 ID,省掉本地 ID 与远端 ID 的映射表;
  • 增加 status_msg(失败原因留痕)、page_count(PDF 页数)、chunk_count(分片冗余)等运维字段。

文档上传后进入解析任务队列 rag_parse_task(RAG文档解析任务队列):

字段类型说明
idbigint任务 ID
kb\_idvarchar(64)知识库 ID
doc\_idvarchar(64)文档 ID
doc\_namevarchar(255)文档名称
statuschar(1)任务状态:0 待执行 / 1 执行中 / 2 成功 / 3 失败
error\_msgvarchar(1000)失败原因
remarkvarchar(500)备注
create\_by / create\_time / update\_by / update\_timevarchar(64) / datetime若依审计字段

上传接口只做两件事:落库 rag_document + 插入一条 rag_parse_task 任务,立即返回;后端定时任务(@Scheduled)轮询 status = '0' 的任务,逐个调用 RAGFlow 解析接口,完成后回写 rag_document.parse_status / vector_status。异步削峰,还有一个同时大批量上传、解析文档的问题,我也做了处理,后面单独详细讲一下。

页面提供:上传文档 / 批量删除 / 全部解析三个核心操作,左侧为知识库列表(切换知识库查看各自文档),右侧为文档表格,实时展示解析状态。

——知识库 001 下 6 份文档,pdf/docx 混合,全部"解析成功",支持按文档名称/类型/解析状态/是否启用筛选,禁用的文档不参与rag检索。

2.7 RAG 提示词

数据表 rag_prompt_template(RAG提示词模板表):

字段类型说明
idbigint主键
template\_namevarchar(128)模板名称
template\_typevarchar(32)模板类型:rag\_qa RAG问答 / summary 摘要 / translate 翻译
system\_promptlongtext系统提示词(角色约束,核心 prompt)
user\_promptlongtext用户前置模板,可留空
descriptionvarchar(512)模板描述说明
is\_defaulttinyint是否默认模板:0 否 / 1 是
is\_enablechar(1)是否启用
del\_flagchar(1)逻辑删除
create\_by / create\_time / update\_by / update\_timevarchar(64) / datetime若依审计字段

维护助手的系统提示词模板,比如限定回答范围、要求引用来源、禁止编造等。提示词作为独立模块管理,方便运营人员随时调整而不用改代码;rag_chat_bot.prompt_template_id 引用本表 id,一个模板可被多个助手复用。

2.8 对话助手管理

数据表 rag_chat_bot(RAG对话助手配置):

字段类型说明
idvarchar(64)主键(即 RAGFlow chat\_assistant id)
bot\_namevarchar(128)助手名称
bot\_descvarchar(512)助手描述
prompt\_template\_idbigint关联提示词模板 rag\_prompt\_template.id
kb\_code\_listtext绑定知识库 kb\_code 集合,JSON 数组 \["kb01","kb02"\]
llm\_modelvarchar(128)选用大模型名称
temperaturedecimal(3,2)温度参数,默认 0.10
max\_tokensint最大输出 token,默认 1024
top\_nint检索返回 topN 分片数量,默认 3
similarity\_thresholddecimal(3,2)相似度阈值,默认 0.20
is\_streamtinyint是否流式输出:0 否 / 1 是
refine\_multiturnchar(1)是否开启多轮查询改写
rerank\_enablechar(1)是否开启 rerank 重排序
is\_enablechar(1)是否启用助手
del\_flagchar(1)逻辑删除
create\_by / create\_time / update\_by / update\_timevarchar(64) / datetime若依审计字段

助手 = 知识库集合 + 大模型 + 提示词 + 检索参数的绑定关系。这一块有三个关键点:

  • 一个助手可绑定多个知识库kb_code_list 存 JSON 数组),问答时多库联合检索;
  • 检索参数入库top_n / similarity_threshold / rerank_enable / refine_multiturn / temperature / max_tokens),前端可调,后端组装 RAGFlow 请求体时透传,为了降低使用门槛,最初的设计是做一个默认最优配置,将对话助手的大量配置隐藏,让用户能以极低的学习成本来使用这套知识库检索系统;
  • 助手 ID 即 RAGFlow chat_assistant ID,本地零映射。

业务上可以建多个助手:制度问答助手、技术文档助手、档案查询助手……每个助手面向不同知识库和场景。助手卡片在首页的排序由 rag_chat_bot_user_sort(RAG对话助手用户排序表)维护:

字段类型说明
idbigint主键
user\_idbigint用户 ID(sys\_user.user\_id)
chat\_bot\_idvarchar(64)对话助手 ID(rag\_chat\_bot.id)
sort\_noint排序号(越小越靠前,从 1 开始)
create\_time / update\_timedatetime创建 / 更新时间

唯一索引 uk_user_bot(user_id, chat_bot_id):每用户对同一助手只有一条排序记录,前端拖拽调整时 upsert,实现每用户独立卡片顺序

——3 个助手(通用知识问答/对话助手002/我的私人对话助手),均绑定知识库001、使用 deepseek-ai/DeepSeek-V4-Flash。

2.9 RAG 会话明细 + 用户会话管理

数据表 rag_conversation(RAG用户会话表):

字段类型说明
idvarchar(64)主键(即 RAGFlow conversation\_id)
bot\_idvarchar(64)关联助手 rag\_chat\_bot.id
user\_idvarchar(64)若依系统用户账号
conv\_titlevarchar(256)会话标题,AI 自动生成或用户修改,默认"新对话"
is\_enablechar(1)会话是否有效:N 关闭 / Y 正常
del\_flagchar(1)逻辑删除
create\_timedatetime会话创建时间
update\_timedatetime会话最后更新时间

数据表 rag_conversation_msg(RAG会话消息明细):

字段类型说明
idbigint主键
conv\_idvarchar(64)关联会话 rag\_conversation.id
rolevarchar(20)角色:user / assistant
questiontext用户问题(role=user 有效)
answerlongtextAI 回答内容(role=assistant 有效)
referencetext引用知识库文档来源 JSON 数组
token\_costint消耗 token 数量
cost\_msbigint请求耗时毫秒
create\_timedatetime消息时间

设计要点:

  • 会话主键直接用 RAGFlow 的 conversation_id,若依侧与 RAGFlow 侧一一对应,多轮续聊时直接透传,无映射成本;
  • 查询按 conv_id 走索引(idx_conv_id),会话列表按 user_id + del_flag 过滤。

这两个模块的价值:可审计、可复盘。每次问答的问题、答案、引用来源等都留痕,方便排查问题或扩展统计功能。

三、Java 后端对接 RAGFlow API

其实后端的设计思路也很容易理解,首先在配置文件配置ragFlow的相关信息,然后写一个对接ragFlow API接口的工具类,然后按需调用就行了。

3.1 配置

ragflow:
  base-url: http://127.0.0.1:90
  api-key: ragflow-2rl-xxxxGoqxxxp0Bqa-xxxxx-xxxx
  <span>#sse超时,毫秒</span>
  sse-timeout: 600000

3.2 核心API工具类(知识库增删改、文档增删解析、创建对话助手等)

这个类其实本质就是把ragFlow的API调用都落实到java类中,完整的API登录ragFlow都可以看到,如下代码可供参考。

package com.ruoyi.rag.controller;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.rag.domain.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Component
public class RagFlowApiClient {

    @Value(<span>"<span>${ragflow.base-url}</span>"</span>)
    private String baseUrl;
    @Value(<span>"<span>${ragflow.api-key}</span>"</span>)
    private String apiKey;

    private final RestTemplate restTemplate = new RestTemplate();

    private HttpHeaders <span><span>getHeader</span></span>() {
        HttpHeaders headers = new HttpHeaders();
        headers.set(<span>"Authorization"</span>, <span>"Bearer "</span> + apiKey);
        headers.setContentType(MediaType.APPLICATION_JSON);
        <span>return</span> headers;
    }

    /**
     * 获取multipart表单请求头,用于文件上传/web抓取
     */
    private HttpHeaders <span><span>getMultipartHeader</span></span>() {
        HttpHeaders headers = new HttpHeaders();
        headers.set(<span>"Authorization"</span>, <span>"Bearer "</span> + apiKey);
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        <span>return</span> headers;
    }

    /**
     * 创建知识库
     * @param ragKnowledgeBase 知识库名称
     * @<span>return</span> ragflow kb_id
     */
    public String createKnowledgeBase(RagKnowledgeBase ragKnowledgeBase) {
        String url = baseUrl + <span>"/api/v1/datasets"</span>;
        Map<String,Object> body = new HashMap<>();
        body.put(<span>"name"</span>, ragKnowledgeBase.getKbName());
        body.put(<span>"description"</span>, ragKnowledgeBase.getKbDesc());
        body.put(<span>"embedding_model"</span>, ragKnowledgeBase.getEmbeddingModel());
        body.put(<span>"chunk_method"</span>, ragKnowledgeBase.getChunkMethod());

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body),getHeader());
        ResponseEntity<String> resp = restTemplate.postForEntity(url,entity,String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"创建知识库失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
        JSONObject data = json.getJSONObject(<span>"data"</span>);
        <span>return</span> data.getString(<span>"id"</span>);
    }


    /**
     * 删除单个dataset
     * @param datasetId ragflow dataset_id
     */
    public void deleteSingleDataset(String datasetId) {
        <span>if</span>(datasetId == null || datasetId.isBlank()){
            <span>return</span>;
        }
        String url = baseUrl + <span>"/api/v1/datasets"</span>;
        Map<String,Object> body = new HashMap<>();
        // 就算删一个,也要包进数组
        body.put(<span>"ids"</span>, List.of(datasetId));

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(
                url,
                HttpMethod.DELETE,
                entity,
                String.class
        );
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"删除知识库失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
    }

    //==================== 文档上传相关 ====================

    /**
     * <span>type</span>=<span>local</span> 上传本地文件(File对象,支持多文件)
     * @param datasetId 知识库<span>id</span>
     * @param files 文件列表
     * @<span>return</span> 返回新建文档<span>id</span>集合
     */
    public List<String> uploadLocalDocuments(String datasetId, List<File> files) {
        <span>if</span>(datasetId == null || datasetId.isBlank()){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        <span>if</span>(files == null || files.isEmpty()){
            throw new IllegalArgumentException(<span>"待上传文件不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents"</span>;

        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
        <span>for</span> (File file : files) {
            FileSystemResource resource = new FileSystemResource(file);
            form.add(<span>"file"</span>, resource);
        }

        HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, getMultipartHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow上传文档失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
        JSONArray dataArray = json.getJSONArray(<span>"data"</span>);
        List<String> docIds = new ArrayList<>();
        <span>for</span> (int i = 0; i < dataArray.size(); i++) {
            JSONObject item = dataArray.getJSONObject(i);
            docIds.add(item.getString(<span>"id"</span>));
        }
        <span>return</span> docIds;
    }

    /**
     * <span>type</span>=<span>local</span> 适配前端 MultipartFile 上传
     * @param datasetId 知识库<span>id</span>
     * @param multipartFiles spring接收的文件
     * @<span>return</span> 文档<span>id</span>列表
     */
    public List<RagDocument> uploadLocalMultipartFiles(String datasetId, List<MultipartFile> multipartFiles) {
        <span>if</span>(StringUtils.isBlank(datasetId)){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        <span>if</span>(multipartFiles == null || multipartFiles.isEmpty()){
            throw new IllegalArgumentException(<span>"待上传文件不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents"</span>;

        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
        <span>for</span> (MultipartFile mf : multipartFiles) {
            ByteArrayResource resource;
            try {
                resource = new ByteArrayResource(mf.getBytes()){
                    @Override
                    public String <span><span>getFilename</span></span>() {
                        <span>return</span> mf.getOriginalFilename();
                    }
                };
            } catch (Exception e) {
                throw new RuntimeException(<span>"读取上传文件字节失败"</span>, e);
            }
            form.add(<span>"file"</span>, resource);
        }

        HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, getMultipartHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow上传文档失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
        JSONArray dataArray = json.getJSONArray(<span>"data"</span>);
        List<RagDocument> resultList = new ArrayList<>();

        <span>for</span> (int i = 0; i < dataArray.size(); i++) {
            JSONObject item = dataArray.getJSONObject(i);
            RagDocument ragDoc = new RagDocument();

            // ✅ RagFlow文档ID直接存入主键<span>id</span>
            ragDoc.setId(item.getString(<span>"id"</span>));
            // 知识库<span>id</span>
            ragDoc.setKbId(datasetId);
            //文档名称
            ragDoc.setDocName(item.getString(<span>"name"</span>));
            //文件大小,字节
            Long size = item.getLong(<span>"size"</span>);
            ragDoc.setFileSize(size);
            //pdf页数
            Long pageCount = item.getLong(<span>"page_count"</span>);
            ragDoc.setPageCount(pageCount);
            //来源 <span>local</span>本地上传
            ragDoc.setSource(<span>"local"</span>);
            //上传完成未启动解析,待解析
            ragDoc.setParseStatus(0);  //0待解析 1解析中 2解析成功 3解析失败
            ragDoc.setIsEnable(<span>"Y"</span>);
            ragDoc.setDelFlag(<span>"0"</span>);

            //截取文件后缀作为docType
            String fileName = item.getString(<span>"name"</span>);
            <span>if</span>(fileName != null && fileName.contains(<span>"."</span>)){
                String suffix = fileName.substring(fileName.lastIndexOf(<span>"."</span>)+1).toLowerCase();
                ragDoc.setDocType(suffix);
            }
            resultList.add(ragDoc);
        }
        <span>return</span> resultList;
    }

    /**
     * <span>type</span>=web 抓取网页生成文档
     * @param datasetId 知识库<span>id</span>
     * @param docName 文档名称
     * @param crawlUrl 网页地址
     * @<span>return</span> 文档<span>id</span>
     */
    public String uploadWebDocument(String datasetId, String docName, String crawlUrl) {
        <span>if</span>(datasetId == null || datasetId.isBlank()){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents?type=web"</span>;

        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
        form.add(<span>"name"</span>, docName);
        form.add(<span>"url"</span>, crawlUrl);

        HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(form, getMultipartHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow抓取网页文档失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
        JSONArray dataArray = json.getJSONArray(<span>"data"</span>);
        JSONObject item = dataArray.getJSONObject(0);
        <span>return</span> item.getString(<span>"id"</span>);
    }

    /**
     * 查询ragflow单文档信息,使用list接口带<span>id</span>过滤
     * @param datasetId 知识库dataset_id
     * @param docId ragflow文档<span>id</span>
     * @<span>return</span> null=ragflow侧还未生成该文档;返回JSONObject文档对象
     */
    public JSONObject getDocumentInfo(String datasetId, String docId) {
        <span>if</span>(StringUtils.isAnyBlank(datasetId,docId)){
            <span>return</span> null;
        }
        String encodeDocId = URLEncoder.encode(docId, StandardCharsets.UTF_8);
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents?id="</span> + encodeDocId + <span>"&page=1&page_size=1"</span>;

        HttpEntity<Void> httpEntity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp;
        try {
            resp = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
        }catch (Exception e){
            <span>return</span> null;
        }
        JSONObject json = JSON.parseObject(resp.getBody());
        <span>if</span>(!Objects.equals(json.getInteger(<span>"code"</span>),0)){
            <span>return</span> null;
        }
        JSONObject dataObj = json.getJSONObject(<span>"data"</span>);
        JSONArray docsArr = dataObj.getJSONArray(<span>"docs"</span>);
        <span>if</span>(docsArr == null || docsArr.isEmpty()){
            <span>return</span> null;
        }
        <span>return</span> docsArr.getJSONObject(0);
    }

    /**
     * <span>type</span>=empty 创建空文档
     * @param datasetId 知识库<span>id</span>
     * @param docName 文档名称
     * @<span>return</span> 文档<span>id</span>
     */
    public String createEmptyDocument(String datasetId, String docName) {
        <span>if</span>(datasetId == null || datasetId.isBlank()){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents?type=empty"</span>;

        Map<String,Object> body = new HashMap<>();
        body.put(<span>"name"</span>, docName);

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow创建空文档失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
        JSONArray dataArray = json.getJSONArray(<span>"data"</span>);
        JSONObject item = dataArray.getJSONObject(0);
        <span>return</span> item.getString(<span>"id"</span>);
    }



    /**
     * 【内置分块流水线】启动文档解析(普通知识库绝大多数用这个)
     * POST /api/v1/datasets/{dataset_id}/chunks
     * @param datasetId 知识库<span>id</span>
     * @param docIdList document_ids列表
     */
    public void parseDocuments(String datasetId, List<String> docIdList) {
        <span>if</span>(StringUtils.isBlank(datasetId)){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        <span>if</span>(docIdList == null || docIdList.isEmpty()){
            throw new IllegalArgumentException(<span>"文档id列表不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/chunks"</span>;
        Map<String,Object> body = new HashMap<>();
        body.put(<span>"document_ids"</span>, docIdList);

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body),getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url,HttpMethod.POST,entity,String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow启动解析任务失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
    }

    /**
     * 停止解析(内置流水线)
     * DELETE /api/v1/datasets/{dataset_id}/chunks
     * @param datasetId
     * @param docIdList
     */
    public void stopParseDocuments(String datasetId, List<String> docIdList){
        <span>if</span>(StringUtils.isBlank(datasetId)){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        <span>if</span>(docIdList == null || docIdList.isEmpty()){
            throw new IllegalArgumentException(<span>"文档id列表不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/chunks"</span>;
        Map<String,Object> body = new HashMap<>();
        body.put(<span>"document_ids"</span>, docIdList);

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body),getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url,HttpMethod.DELETE,entity,String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow停止解析失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
    }

    /**
     * 删除知识库下的单个文档
     * DELETE /api/v1/datasets/{dataset_id}/documents
     * 注意:RAGFlow删除文档走的是集合路径(body传ids数组),单个文档路径不支持DELETE(会返回405)
     * @param datasetId 知识库<span>id</span>
     * @param docId 文档<span>id</span>
     */
    public void deleteDocument(String datasetId, String docId){
        <span>if</span>(StringUtils.isBlank(datasetId)){
            throw new IllegalArgumentException(<span>"datasetId不能为空"</span>);
        }
        <span>if</span>(StringUtils.isBlank(docId)){
            throw new IllegalArgumentException(<span>"文档id不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents"</span>;
        Map<String,Object> body = new HashMap<>();
        // 就算删一个,也要包进数组
        body.put(<span>"ids"</span>, List.of(docId));
        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());

        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow删除文档失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
    }




    /**
     * Create chat assistant 创建聊天助手 POST /api/v1/chats
     * @param name 助手名称【必填】
     * @param icon <span>base64</span>头像,可以null
     * @param dataset_ids 绑定知识库uuid列表 List<String>,可以null
     * @param llm_id 模型<span>id</span>,可以null
     * @param llmSetting llm_setting对象,助手大模型推理参数配置,可以null
     *        llm_setting包含属性说明:
     *          <span>"model_type"</span>: string 模型类型标识,仅支持<span>"chat"</span>和<span>"image2text"</span>;不传或其他值默认当作<span>"chat"</span>。
     *          <span>"temperature"</span>: <span>float</span> 控制模型输出随机性,数值越低回答越保守,越高越有创造性,默认0.1。
     *          <span>"top_p"</span>: <span>float</span> 核采样阈值,从概率最高的词汇中做采样,截断低概率词汇,默认0.3。
     *          <span>"presence_penalty"</span>: <span>float</span> 存在惩罚,对对话中已出现过的词施加惩罚,减少内容重复,默认0.4。
     *          <span>"frequency_penalty"</span>: <span>float</span> 频率惩罚,降低高频重复用词倾向,默认0.7。
     * @param promptConfig prompt_config对象,用于定义大模型需要遵守的指令,可以null
     *        prompt_config包含属性说明:
     *          <span>"system"</span>: string 系统提示词内容。
     *          <span>"prologue"</span>: string 用户打开对话时展示的开场白。
     *          <span>"parameters"</span>: object[] 系统提示词中使用的自定义变量数组。注意:
     *              knowledge 为保留变量,代表检索出来的知识库片段;
     *              system内所有变量需要使用大括号包裹。
     *          <span>"empty_response"</span>: string 用户提问未检索到知识库内容时返回的兜底回答;留空则允许模型自由生成回答。
     *          <span>"quote"</span>: boolean 是否展示引用来源切片,默认<span>true</span>。
     *          <span>"tts"</span>: boolean 是否开启语音朗读TTS。
     *          <span>"refine_multiturn"</span>: boolean 是否开启多轮问题改写优化。
     *          <span>"use_kg"</span>: boolean 是否启用知识图谱检索。
     *          <span>"reasoning"</span>: boolean 是否开启模型推理思考模式。
     *          <span>"cross_languages"</span>: list[string] 跨语言检索支持的语言列表。
     *          <span>"web_search_provider"</span>: string 联网搜索服务商,可选值 <span>"tavily"</span>、<span>"querit"</span>,不传默认tavily。
     *          <span>"tavily_api_key"</span>: string Tavily联网搜索API密钥。
     *          <span>"querit_api_key"</span>: string Querit联网搜索API密钥,使用该参数时web_search_provider必须指定为querit。
     *          <span>"toc_enhance"</span>: boolean 是否开启目录增强检索(针对带目录长文档优化检索)。
     * @param similarityThreshold <span>float</span> 检索相似度阈值,可以null
     * @param vectorSimilarityWeight <span>float</span> 向量相似度权重,可以null
     * @param topN int 检索结果topN,可以null
     * @param topK int 检索结果topK,可以null
     * @param rerank_id 重排模型<span>id</span>,可以null
     * @<span>return</span> 返回RAGFlow返回完整data JSONObject,取出<span>id</span>即为chatAssistantId
     */

    public JSONObject createChatAssistant(String name,
                                          String icon,
                                          List<String> dataset_ids,
                                          String llm_id,
                                          RagChatBotLlmSetting llmSetting,
                                          RagChatBotPromptConfig promptConfig,
                                          Float similarityThreshold,
                                          Float vectorSimilarityWeight,
                                          Integer topN,
                                          Integer topK,
                                          String rerank_id) {
        <span>if</span>(StringUtils.isBlank(name)){
            throw new IllegalArgumentException(<span>"chat assistant name不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats"</span>;
        Map<String,Object> body = new HashMap<>();
        body.put(<span>"name"</span>, name);
        <span>if</span>(StringUtils.isNotBlank(icon)){
            body.put(<span>"icon"</span>, icon);
        }
        <span>if</span>(dataset_ids != null){
            body.put(<span>"dataset_ids"</span>, dataset_ids);
        }
        <span>if</span>(StringUtils.isNotBlank(llm_id)){
            body.put(<span>"llm_id"</span>, llm_id);
        }
        <span>if</span>(llmSetting != null ){
            body.put(<span>"llm_setting"</span>, llmSetting);
        }
        <span>if</span>(promptConfig != null){
            body.put(<span>"prompt_config"</span>, promptConfig);
        }
        <span>if</span>(similarityThreshold != null){
            body.put(<span>"similarity_threshold"</span>, similarityThreshold);
        }
        <span>if</span>(vectorSimilarityWeight != null){
            body.put(<span>"vector_similarity_weight"</span>, vectorSimilarityWeight);
        }
        <span>if</span>(topN != null){
            body.put(<span>"top_n"</span>, topN);
        }
        <span>if</span>(topK != null){
            body.put(<span>"top_k"</span>, topK);
        }
        <span>if</span>(StringUtils.isNotBlank(rerank_id)){
            body.put(<span>"rerank_id"</span>, rerank_id);
        }

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            String detail = Objects.toString(msg,<span>"未知错误"</span>);
            throw new RuntimeException(<span>"创建对话助手失败:"</span> + detail + <span>",返回完整响应:"</span> + json);
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }


    /**
     * Get chat assistant GET /api/v1/chats/{chatId} 查询单个助手
     * @param chatId chat助手<span>id</span>
     * @<span>return</span> data原始JSONObject
     */
    public JSONObject getChatAssistant(String chatId) {
        <span>if</span>(StringUtils.isBlank(chatId)){
            throw new IllegalArgumentException(<span>"chatId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats/"</span> + chatId;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow查询chat assistant失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }

    /**
     * Delete chat assistant DELETE /api/v1/chats/{chatId} 删除单个助手
     */
    public void deleteChatAssistant(String chatId) {
        <span>if</span>(StringUtils.isBlank(chatId)){
            throw new IllegalArgumentException(<span>"chatId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats/"</span> + chatId;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow删除chat assistant失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
    }

    /**
     * Delete chat assistants 批量删除助手 DELETE /api/v1/chats
     * @param ids 要删除chatId列表;如果deleteAll=<span>true</span>,ids传null
     * @param deleteAll 是否删除全部当前用户助手
     */
    public void batchDeleteChatAssistant(List<String> ids, Boolean deleteAll) {
        String url = baseUrl + <span>"/api/v1/chats"</span>;
        Map<String,Object> body = new HashMap<>();
        <span>if</span>(ids != null){
            body.put(<span>"ids"</span>, ids);
        }
        <span>if</span>(deleteAll != null){
            body.put(<span>"delete_all"</span>, deleteAll);
        }
        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow批量删除chat assistant失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
    }

    /**
     * List chat assistants GET /api/v1/chats 查询助手列表
     * @param page 页码,null默认1
     * @param pageSize 每页大小,null默认30
     * @param orderby 排序字段 create_time / update_time
     * @param desc 是否降序 null默认<span>true</span>
     * @param keywords 名称模糊搜索
     * @param ownerIds 租户<span>id</span>过滤
     * @param <span>id</span> 精确chatId匹配
     * @param name 精确名称匹配
     * @<span>return</span> 返回完整返回体data对象,包含chats数组、total
     */
    public JSONObject listChatAssistant(Integer page,
                                        Integer pageSize,
                                        String orderby,
                                        Boolean desc,
                                        String keywords,
                                        List<String> ownerIds,
                                        String <span>id</span>,
                                        String name) {
        StringBuilder sb = new StringBuilder(baseUrl + <span>"/api/v1/chats?"</span>);
        <span>if</span>(page != null){
            sb.append(<span>"page="</span>).append(page).append(<span>"&"</span>);
        }
        <span>if</span>(pageSize != null){
            sb.append(<span>"page_size="</span>).append(pageSize).append(<span>"&"</span>);
        }
        <span>if</span>(StringUtils.isNotBlank(orderby)){
            sb.append(<span>"orderby="</span>).append(URLEncoder.encode(orderby, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        }
        <span>if</span>(desc != null){
            sb.append(<span>"desc="</span>).append(desc).append(<span>"&"</span>);
        }
        <span>if</span>(StringUtils.isNotBlank(keywords)){
            sb.append(<span>"keywords="</span>).append(URLEncoder.encode(keywords, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        }
        <span>if</span>(ownerIds != null && !ownerIds.isEmpty()){
            <span>for</span>(String oid : ownerIds){
                sb.append(<span>"owner_ids="</span>).append(URLEncoder.encode(oid, StandardCharsets.UTF_8)).append(<span>"&"</span>);
            }
        }
        <span>if</span>(StringUtils.isNotBlank(<span>id</span>)){
            sb.append(<span>"id="</span>).append(URLEncoder.encode(<span>id</span>, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        }
        <span>if</span>(StringUtils.isNotBlank(name)){
            sb.append(<span>"name="</span>).append(URLEncoder.encode(name, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        }
        //去掉末尾&
        String url = sb.toString();
        <span>if</span>(url.endsWith(<span>"&"</span>)){
            url = url.substring(0, url.length()-1);
        }

        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span>(!Objects.equals(code,0)){
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow查询chat assistant列表失败:"</span>+ Objects.toString(msg,<span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }




    /**
     * 流式版本,stream=<span>true</span>,返回BufferedReader读取SSE原始行(移除Flux、WebClient)
     */
    public BufferedReader chatCompletionsV2Stream(String chatId,
                                                  List<Map<String, Object>> messages,
                                                  String llmId,
                                                  String sessionId) {
        <span>if</span> (StringUtils.isBlank(chatId)) {
            throw new IllegalArgumentException(<span>"chatId不能为空"</span>);
        }
        <span>if</span> (messages == null || messages.isEmpty()) {
            throw new IllegalArgumentException(<span>"messages对话消息列表不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chat/completions"</span>;
        Map<String, Object> body = new HashMap<>();
        body.put(<span>"chat_id"</span>, chatId);
        body.put(<span>"stream"</span>, <span>true</span>);
        body.put(<span>"llm_id"</span>, llmId);
        body.put(<span>"messages"</span>, messages);
        body.put(<span>"pass_all_history_messages"</span>, <span>false</span>); // 已传完整messages,避免RAGFlow叠加自身会话历史导致每轮翻倍

        <span>if</span> (StringUtils.isNotBlank(sessionId)) {
            body.put(<span>"session_id"</span>, sessionId);
        }

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        // 使用RestTemplate获取原生Response,不自动消费body
        ResponseEntity<org.springframework.core.io.Resource> respEntity = restTemplate.exchange(
                url,
                HttpMethod.POST,
                entity,
                org.springframework.core.io.Resource.class
        );

        HttpStatusCode statusCode = respEntity.getStatusCode();
        <span>if</span> (!statusCode.is2xxSuccessful()) {
            throw new RuntimeException(<span>"RAGFlow chatCompletionsV2Stream 调用异常,http状态码:"</span> + statusCode.value());
        }

        org.springframework.core.io.Resource resource = respEntity.getBody();
        <span>if</span> (resource == null) {
            throw new RuntimeException(<span>"RAGFlow chatCompletionsV2Stream 返回body为空"</span>);
        }
        try {
            // 包装输入流为BufferedReader,UTF-8
            <span>return</span> new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8));
        } catch (Exception e) {
            throw new RuntimeException(<span>"读取RAGFlow流式响应输入流失败"</span>, e);
        }
    }




    /**
     * 创建会话 POST /api/v1/chats/{chat_id}/sessions
     * @param chatId ragflow助手<span>id</span>
     * @param name 会话名称
     * @param userId 可选 用户自定义userId,可为null
     * @<span>return</span> 返回ragflow会话data对象,<span>id</span>字段为ragflow sessionId
     */
    public JSONObject createSession(String chatId, String name, String userId) {
        <span>if</span> (StringUtils.isBlank(chatId)) {
            throw new IllegalArgumentException(<span>"chatId不能为空"</span>);
        }
        <span>if</span> (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException(<span>"会话name不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats/"</span> + chatId + <span>"/sessions"</span>;
        Map<String, Object> body = new HashMap<>();
        body.put(<span>"name"</span>, name);
        <span>if</span> (StringUtils.isNotBlank(userId)) {
            body.put(<span>"user_id"</span>, userId);
        }
        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow创建会话失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }



    /**
     * 查询助手会话列表 GET /api/v1/chats/{chat_id}/sessions
     * @param chatId 助手<span>id</span>
     * @param page 页码 默认1
     * @param pageSize 页大小 默认30
     * @param orderby 排序字段 create_time / update_time
     * @param desc 是否降序 默认<span>true</span>
     * @param name 会话名称过滤
     * @param sessionId 会话<span>id</span>精确匹配
     * @param userId 用户自定义userId过滤
     * @<span>return</span> JSONArray 会话数组
     */
    public JSONArray listSession(String chatId,
                                 Integer page,
                                 Integer pageSize,
                                 String orderby,
                                 Boolean desc,
                                 String name,
                                 String sessionId,
                                 String userId) {
        <span>if</span> (StringUtils.isBlank(chatId)) {
            throw new IllegalArgumentException(<span>"chatId不能为空"</span>);
        }
        StringBuilder sb = new StringBuilder(baseUrl + <span>"/api/v1/chats/"</span> + chatId + <span>"/sessions?"</span>);
        <span>if</span> (page != null) sb.append(<span>"page="</span>).append(page).append(<span>"&"</span>);
        <span>if</span> (pageSize != null) sb.append(<span>"page_size="</span>).append(pageSize).append(<span>"&"</span>);
        <span>if</span> (StringUtils.isNotBlank(orderby))
            sb.append(<span>"orderby="</span>).append(URLEncoder.encode(orderby, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        <span>if</span> (desc != null) sb.append(<span>"desc="</span>).append(desc).append(<span>"&"</span>);
        <span>if</span> (StringUtils.isNotBlank(name))
            sb.append(<span>"name="</span>).append(URLEncoder.encode(name, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        <span>if</span> (StringUtils.isNotBlank(sessionId))
            sb.append(<span>"id="</span>).append(URLEncoder.encode(sessionId, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        <span>if</span> (StringUtils.isNotBlank(userId))
            sb.append(<span>"user_id="</span>).append(URLEncoder.encode(userId, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        String url = sb.toString();
        <span>if</span> (url.endsWith(<span>"&"</span>)) {
            url = url.substring(0, url.length() - 1);
        }
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow查询会话列表失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONArray(<span>"data"</span>);
    }

    /**
     * 获取单个会话详情 GET /api/v1/chats/{chat_id}/sessions/{sessionId}
     * @param chatId 助手<span>id</span>
     * @param sessionId ragflow会话<span>id</span>
     * @<span>return</span> 会话data对象,包含messages历史消息
     */
    public JSONObject getSessionDetail(String chatId, String sessionId) {
        <span>if</span> (StringUtils.isAnyBlank(chatId, sessionId)) {
            throw new IllegalArgumentException(<span>"chatId、sessionId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats/"</span> + chatId + <span>"/sessions/"</span> + sessionId;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow获取会话详情失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }

    /**
     * 更新消息点赞反馈 PUT /api/v1/chats/{chat_id}/sessions/{sessionId}/messages/{msgId}/feedback
     * @param chatId 助手<span>id</span>
     * @param sessionId 会话<span>id</span>
     * @param msgId ragflow消息<span>id</span>
     * @param thumbup <span>true</span>点赞 / <span>false</span>点踩
     * @param feedback 反馈文本,可为null
     * @<span>return</span> 更新后会话对象
     */
    public JSONObject messageFeedback(String chatId, String sessionId, String msgId, Boolean thumbup, String feedback) {
        <span>if</span> (StringUtils.isAnyBlank(chatId, sessionId, msgId) || thumbup == null) {
            throw new IllegalArgumentException(<span>"参数不全"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats/"</span> + chatId + <span>"/sessions/"</span> + sessionId + <span>"/messages/"</span> + msgId + <span>"/feedback"</span>;
        Map<String, Object> body = new HashMap<>();
        body.put(<span>"thumbup"</span>, thumbup);
        <span>if</span> (StringUtils.isNotBlank(feedback)) {
            body.put(<span>"feedback"</span>, feedback);
        }
        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow设置消息反馈失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }

    /**
     * 批量删除会话 DELETE /api/v1/chats/{chat_id}/sessions
     * @param chatId 助手<span>id</span>
     * @param ids 需要删除的sessionId列表;可为null
     * @param deleteAll 是否删除全部会话  为<span>true</span>时  ids写null
     */
    public void batchDeleteSession(String chatId, List<String> ids, Boolean deleteAll) {
        <span>if</span> (StringUtils.isBlank(chatId)) {
            throw new IllegalArgumentException(<span>"chatId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/chats/"</span> + chatId + <span>"/sessions"</span>;
        Map<String, Object> body = new HashMap<>();
        <span>if</span> (ids != null && !ids.isEmpty()) {
            body.put(<span>"ids"</span>, ids);
        }
        <span>if</span> (deleteAll != null) {
            body.put(<span>"delete_all"</span>, deleteAll);
        }
        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow批量删除会话失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
    }

    /**
     * 向量检索接口 POST /api/v1/retrieval
     * @param question 用户问题
     * @param datasetIds 知识库<span>id</span>列表,二选一 datasetIds / documentIds
     * @param documentIds 文档<span>id</span>列表
     * @param page 页码 默认1
     * @param pageSize 每页数量 默认30
     * @param similarityThreshold 相似度阈值
     * @param vectorSimilarityWeight 向量权重
     * @param topK topK
     * @param rerankId 重排模型<span>id</span>
     * @param keyword 是否开启关键词检索
     * @param highlight 是否高亮
     * @param crossLanguages 跨语言列表
     * @param metadataCondition 元数据过滤条件,可为null
     * @param useKg 是否启用知识图谱
     * @param tocEnhance 是否目录增强
     * @<span>return</span> 返回检索data对象,包含chunks、doc_aggs、total
     */
    public JSONObject retrievalChunks(String question,
                                      List<String> datasetIds,
                                      List<String> documentIds,
                                      Integer page,
                                      Integer pageSize,
                                      Float similarityThreshold,
                                      Float vectorSimilarityWeight,
                                      Integer topK,
                                      String rerankId,
                                      Boolean keyword,
                                      Boolean highlight,
                                      List<String> crossLanguages,
                                      Map<String, Object> metadataCondition,
                                      Boolean useKg,
                                      Boolean tocEnhance) {
        <span>if</span> (StringUtils.isBlank(question)) {
            throw new IllegalArgumentException(<span>"question不能为空"</span>);
        }
        <span>if</span> ((datasetIds == null || datasetIds.isEmpty()) && (documentIds == null || documentIds.isEmpty())) {
            throw new IllegalArgumentException(<span>"datasetIds 和 documentIds至少传一组"</span>);
        }
        String url = baseUrl + <span>"/api/v1/retrieval"</span>;
        Map<String, Object> body = new HashMap<>();
        body.put(<span>"question"</span>, question);
        <span>if</span> (datasetIds != null) body.put(<span>"dataset_ids"</span>, datasetIds);
        <span>if</span> (documentIds != null) body.put(<span>"document_ids"</span>, documentIds);
        <span>if</span> (page != null) body.put(<span>"page"</span>, page);
        <span>if</span> (pageSize != null) body.put(<span>"page_size"</span>, pageSize);
        <span>if</span> (similarityThreshold != null) body.put(<span>"similarity_threshold"</span>, similarityThreshold);
        <span>if</span> (vectorSimilarityWeight != null) body.put(<span>"vector_similarity_weight"</span>, vectorSimilarityWeight);
        <span>if</span> (topK != null) body.put(<span>"top_k"</span>, topK);
        <span>if</span> (StringUtils.isNotBlank(rerankId)) body.put(<span>"rerank_id"</span>, rerankId);
        <span>if</span> (keyword != null) body.put(<span>"keyword"</span>, keyword);
        <span>if</span> (highlight != null) body.put(<span>"highlight"</span>, highlight);
        <span>if</span> (crossLanguages != null) body.put(<span>"cross_languages"</span>, crossLanguages);
        <span>if</span> (metadataCondition != null) body.put(<span>"metadata_condition"</span>, metadataCondition);
        <span>if</span> (useKg != null) body.put(<span>"use_kg"</span>, useKg);
        <span>if</span> (tocEnhance != null) body.put(<span>"toc_enhance"</span>, tocEnhance);

        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(body), getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        Integer code = json.getInteger(<span>"code"</span>);
        <span>if</span> (!Objects.equals(code, 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow向量检索失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        <span>return</span> json.getJSONObject(<span>"data"</span>);
    }

    /**
     * 生成相关推荐问题 POST /api/v1/chat/recommandation
     * 注意:该接口需要login‑token,不是api‑key;本客户端暂不封装鉴权逻辑,业务层按需调用
     * @param question 用户原始问题
     * @param searchId 搜索配置<span>id</span>,可为null
     * @<span>return</span> 推荐问题字符串数组
     */
    public JSONArray generateRelatedQuestions(String question, String searchId) {
        throw new UnsupportedOperationException(<span>"generateRelatedQuestions需要登录token,不是api‑key,业务层自行处理header鉴权"</span>);
    }

    /**
     * 下载附件 GET /api/v1/agents/attachments/{attachmentId}/download
     * @param attachmentId 附件<span>id</span>
     * @param ext 输出格式 markdown/html/pdf/docx/xlsx/csv
     * @<span>return</span> byte[] 文件二进制
     */
    public byte[] downloadAttachment(String attachmentId, String ext) {
        <span>if</span> (StringUtils.isBlank(attachmentId)) {
            throw new IllegalArgumentException(<span>"attachmentId不能为空"</span>);
        }
        StringBuilder sb = new StringBuilder(baseUrl + <span>"/api/v1/agents/attachments/"</span> + attachmentId + <span>"/download?"</span>);
        <span>if</span> (StringUtils.isNotBlank(ext)) {
            sb.append(<span>"ext="</span>).append(URLEncoder.encode(ext, StandardCharsets.UTF_8));
        }
        String url = sb.toString();
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<byte[]> resp = restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
        <span>return</span> resp.getBody();
    }


    /**
     * 获取模型列表 GET /api/v1/models【DTO强类型版本】
     * @param modelType 可选过滤:embedding / chat / rerank / tts / vision,传null返回全部
     * @<span>return</span> RagFlowModelListResp
     */
    public RagFlowModelListResp listModels(String modelType,String instanceName,String providerName,String name) {
        StringBuilder sb = new StringBuilder(baseUrl + <span>"/api/v1/models?t=1"</span>);

        <span>if</span> (StringUtils.isNotBlank(modelType)) {
            sb.append(<span>"&model_type="</span>).append(URLEncoder.encode(modelType, StandardCharsets.UTF_8));
        }
        <span>if</span> (StringUtils.isNotBlank(instanceName)) {
            sb.append(<span>"&instance_name="</span>).append(URLEncoder.encode(instanceName, StandardCharsets.UTF_8));
        }
        <span>if</span> (StringUtils.isNotBlank(providerName)) {
            sb.append(<span>"&provider_name="</span>).append(URLEncoder.encode(providerName, StandardCharsets.UTF_8));
        }
        <span>if</span> (StringUtils.isNotBlank(name)) {
            sb.append(<span>"&name="</span>).append(URLEncoder.encode(name, StandardCharsets.UTF_8));
        }
        String url = sb.toString();

        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        RagFlowModelListResp result = JSON.parseObject(resp.getBody(), RagFlowModelListResp.class);

        <span>if</span> (!Objects.equals(result.getCode(), 0)) {
            String msg = result.getMessage();
            throw new RuntimeException(<span>"RAGFlow查询模型列表失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        <span>return</span> result;
    }
    
    /**
     * 查询ragFlow中用户配置的llm模型<span>id</span>
     * @<span>return</span>
     */
    public String <span><span>getUserLLMId</span></span>() {
        RagFlowUserMeModels po = getUserMeModelsDto();
        RagFlowUserMeModels.UserMeModelData data = po.getData();
        <span>return</span> data.getTenantLlmId();
    }

    //==================== 引用展示:切片图片 / 文档缩略图 / 文档预览 / 原文件下载 ====================

    /**
     * 获取切片图片(也用于文档缩略图,二者都是 image_id 形态)
     * GET /api/v1/documents/images/{imageId}
     * 说明:imageId 形如 <span>"{dataset_id}-{chunk_id}"</span> 或 <span>"{dataset_id}-thumbnail_{doc_id}.png"</span>
     * @param imageId ragflow image_id
     * @<span>return</span> 图片字节
     */
    public byte[] getChunkImage(String imageId) {
        <span>if</span> (StringUtils.isBlank(imageId)) {
            throw new IllegalArgumentException(<span>"imageId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/documents/images/"</span> + imageId;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<byte[]> resp = restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
        <span>return</span> resp.getBody();
    }

    /**
     * 批量查询文档缩略图 GET /api/v1/thumbnails?doc_ids=a&doc_ids=b
     * RAGFlow返回:{<span>"code"</span>:0,<span>"data"</span>:{<span>"docId"</span>:<span>"/api/v1/documents/images/{dataset_id}-thumbnail_{doc_id}.png"</span>或<span>""</span>}}
     * 本方法把缩略图URL收敛成 image_id(最后一个路径段),空串表示该文档无缩略图(如doc/xlsx)
     * @param docIds 文档<span>id</span>列表
     * @<span>return</span> docId -> thumbnailImageId(可能为空串)
     */
    public Map<String, String> getDocumentThumbnails(List<String> docIds) {
        <span>if</span> (docIds == null || docIds.isEmpty()) {
            <span>return</span> new HashMap<>();
        }
        StringBuilder sb = new StringBuilder(baseUrl + <span>"/api/v1/thumbnails?"</span>);
        <span>for</span> (String docId : docIds) {
            <span>if</span> (StringUtils.isBlank(docId)) {
                <span>continue</span>;
            }
            sb.append(<span>"doc_ids="</span>).append(URLEncoder.encode(docId, StandardCharsets.UTF_8)).append(<span>"&"</span>);
        }
        String url = sb.substring(0, sb.length() - 1);

        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        JSONObject json = JSON.parseObject(resp.getBody());
        <span>if</span> (!Objects.equals(json.getInteger(<span>"code"</span>), 0)) {
            String msg = json.getString(<span>"message"</span>);
            throw new RuntimeException(<span>"RAGFlow查询文档缩略图失败:"</span> + Objects.toString(msg, <span>"未知错误"</span>));
        }
        JSONObject dataObj = json.getJSONObject(<span>"data"</span>);
        Map<String, String> result = new HashMap<>();
        <span>if</span> (dataObj != null) {
            <span>for</span> (String docId : dataObj.keySet()) {
                String thumbUrl = dataObj.getString(docId);
                <span>if</span> (StringUtils.isBlank(thumbUrl)) {
                    result.put(docId, <span>""</span>);
                    <span>continue</span>;
                }
                // 形如 /api/v1/documents/images/xxx-thumbnail_yyy.png → 取最后一段作为image_id
                int idx = thumbUrl.lastIndexOf(<span>'/'</span>);
                result.put(docId, idx >= 0 ? thumbUrl.substring(idx + 1) : thumbUrl);
            }
        }
        <span>return</span> result;
    }

    /**
     * 获取文档预览流(PDF返回application/pdf可直接渲染;doc/xlsx/ppt返回原始文件字节)
     * GET /api/v1/documents/{documentId}/preview
     * @param documentId ragflow文档<span>id</span>
     * @<span>return</span> 原始响应(含Content-Type),业务层按类型决定 inline / attachment
     */
    public ResponseEntity<byte[]> getDocumentPreviewResponse(String documentId) {
        <span>if</span> (StringUtils.isBlank(documentId)) {
            throw new IllegalArgumentException(<span>"documentId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/documents/"</span> + documentId + <span>"/preview"</span>;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        <span>return</span> restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
    }

    /**
     * GET /api/v1/users/me/models
     * 获取当前API Key所属租户的模型配置信息【DTO强类型版本】
     * @<span>return</span> RagFlowUserMeModelResp 完整返回实体
     */
    public RagFlowUserMeModels <span><span>getUserMeModelsDto</span></span>() {
        String url = baseUrl + <span>"/api/v1/users/me/models"</span>;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        // fastjson2 直接转DTO
        RagFlowUserMeModels result = JSON.parseObject(resp.getBody(), RagFlowUserMeModels.class);
        <span>if</span> (!Objects.equals(result.getCode(), 0)) {
            String msg = result.getMessage();
            throw new RuntimeException(<span>"RAGFlow获取租户模型配置失败:"</span> + Objects.toString(msg,<span>"未知错误"</span>));
        }
        <span>return</span> result;
    }

    /**
     * 下载原文档 GET /api/v1/datasets/{datasetId}/documents/{documentId}
     * @param datasetId 知识库<span>id</span>
     * @param documentId 文档<span>id</span>
     * @<span>return</span> 原始响应(含Content-Type),业务层按 attachment 处理
     */
    public ResponseEntity<byte[]> downloadDocumentResponse(String datasetId, String documentId) {
        <span>if</span> (StringUtils.isAnyBlank(datasetId, documentId)) {
            throw new IllegalArgumentException(<span>"datasetId、documentId不能为空"</span>);
        }
        String url = baseUrl + <span>"/api/v1/datasets/"</span> + datasetId + <span>"/documents/"</span> + documentId;
        HttpEntity<Void> entity = new HttpEntity<>(null, getHeader());
        <span>return</span> restTemplate.exchange(url, HttpMethod.GET, entity, byte[].class);
    }
}

3.4 Controller 层:权限校验(参考代码)

/**
     * 查询RAG知识库管理列表(本人创建 + 开放授权可见,非本人不可改删由 isOwner/canEdit 标记控制)
     */
    @PreAuthorize(<span>"@ss.hasPermi('rag:knowledgeBase:list')"</span>)
    @GetMapping(<span>"/list"</span>)
    public TableDataInfo list(RagKnowledgeBase ragKnowledgeBase)
    {
        // 可见范围过滤(核心权限方法)
        ragKnowledgeBase.setVisibleIds(ragKnowledgeBaseService.selectVisibleKbIds());
        startPage();
        List<RagKnowledgeBase> list = ragKnowledgeBaseService.selectRagKnowledgeBaseList(ragKnowledgeBase);

        <span>if</span>(list != null){
            RagFlowUserMeModels po = ragFlowApiClient.getUserMeModelsDto();
            Map<String, String> modelItems = getModelItems(po);

            String currentUser = SecurityUtils.getUsername();
            <span>for</span> (RagKnowledgeBase base : list) {
                base.setEmbeddingModelName(modelItems.get(base.getEmbeddingModel()));
                // 权限标记:本人创建的可改删,授权可见的仅查看
                boolean owner = currentUser.equals(base.getCreateBy());
                base.setIsOwner(owner);
                base.setCanEdit(owner);
            }
        }

        <span>return</span> getDataTable(list);
    }

关键设计:浏览器不直接访问 RAGFlow,所有请求经若依后端代理——既解决跨域,又避免 RAGFlow 的 Token 暴露到前端,权限校验也统一在若依这一层完成。

四、总结

本在开发层面介绍了若依集成ragFlow的设计实现思路:

  1. RAGFlow 私有化部署:无 GPU 环境可跑,模型走 OpenAI 兼容 API 可插拔,生产可平滑替换本地模型;
  2. 8 个功能模块:模型、知识库、文档、提示词、助手、会话、问答,职责清晰、表结构完整;
  3. 五种开放范围权限:私有 / 公开 / 指定用户 / 指定部门 / 指定角色,配合若依数据权限,真正实精准的权限管控;
  4. Java 对接 RAGFlow:安全与权限统一收敛在若依层,我贴了一些有参考价值的代码,主要为了展示设计思路,具体实现那就见仁见智了。