소스 검색

feat: 新增产品库根据产品条目,同步至dify平台

lihao16 4 달 전
부모
커밋
91bcca9423

+ 12 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbDifyDatasetsController.java

@@ -1,5 +1,6 @@
 package com.inspur.device.controller;
 
+import java.io.IOException;
 import java.util.List;
 
 import lombok.RequiredArgsConstructor;
@@ -84,6 +85,17 @@ public class SmsbDifyDatasetsController extends BaseController {
         return toAjax(smsbDifyDatasetsService.insertByBo(bo));
     }
 
+    /**
+     * 下发产品库
+     */
+    @SaCheckPermission("device:datasets:edit")
+    @Log(title = "下发产品库", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PostMapping("/push/product")
+    public R<Void> pushProduct(@RequestBody SmsbDifyDatasetsBo bo) throws IOException {
+        return toAjax(smsbDifyDatasetsService.pushProduct(bo));
+    }
+
     /**
      * 修改知识库管理
      */

+ 5 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/bo/SmsbDifyDatasetsBo.java

@@ -6,6 +6,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import org.dromara.common.mybatis.core.domain.BaseEntity;
 
+import java.util.List;
+
 /**
  * 知识库管理业务对象 smsb_dify_datasets
  *
@@ -77,5 +79,8 @@ public class SmsbDifyDatasetsBo extends BaseEntity {
      */
     private Integer tag;
 
+    /** 问答\产品条目下发Ids */
+    private List<String> entryIds;
+
 
 }

+ 4 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDifyDatasetsProductMapper.java

@@ -2,8 +2,11 @@ package com.inspur.device.mapper;
 
 import com.inspur.device.domain.SmsbDifyDatasetsProduct;
 import com.inspur.device.domain.vo.SmsbDifyDatasetsProductVo;
+import org.apache.ibatis.annotations.Param;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 
+import java.util.List;
+
 /**
  * Dify产品推荐Mapper接口
  *
@@ -12,4 +15,5 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
  */
 public interface SmsbDifyDatasetsProductMapper extends BaseMapperPlus<SmsbDifyDatasetsProduct, SmsbDifyDatasetsProductVo> {
 
+    List<SmsbDifyDatasetsProductVo> selectVoListByIds(@Param("ids") List<String> ids);
 }

+ 8 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDifyDatasetsService.java

@@ -5,6 +5,7 @@ import com.inspur.device.domain.vo.SmsbDifyDatasetsVo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 
@@ -71,4 +72,11 @@ public interface ISmsbDifyDatasetsService {
      * @return
      */
     boolean syncFromDify();
+
+    /**
+     * 产品条目下发
+     * @param bo
+     * @return
+     */
+    boolean pushProduct(SmsbDifyDatasetsBo bo) throws IOException;
 }

+ 139 - 8
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDifyDatasetsServiceImpl.java

@@ -2,6 +2,7 @@ package com.inspur.device.service.impl;
 
 import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONObject;
@@ -9,12 +10,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.inspur.device.domain.SmsbDifyDatasets;
+import com.inspur.device.domain.SmsbDifyDatasetsFile;
 import com.inspur.device.domain.bo.SmsbDifyDatasetsBo;
 import com.inspur.device.domain.vo.DifyDatasetsRspData;
+import com.inspur.device.domain.vo.SmsbDifyDatasetsFileVo;
+import com.inspur.device.domain.vo.SmsbDifyDatasetsProductVo;
 import com.inspur.device.domain.vo.SmsbDifyDatasetsVo;
+import com.inspur.device.mapper.SmsbDifyDatasetsFileMapper;
 import com.inspur.device.mapper.SmsbDifyDatasetsMapper;
+import com.inspur.device.mapper.SmsbDifyDatasetsProductMapper;
+import com.inspur.device.service.ISmsbDifyDatasetsFileService;
 import com.inspur.device.service.ISmsbDifyDatasetsService;
 import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
@@ -25,11 +33,16 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.*;
 
 /**
  * 知识库管理Service业务层处理
@@ -43,15 +56,16 @@ public class SmsbDifyDatasetsServiceImpl implements ISmsbDifyDatasetsService {
 
     @Value("${dify.url}")
     private String difyUrl;
-
     @Value("${dify.datasets.apiKey}")
     private String datasetsApiKey;
-
+    @Value("${server.tempDir}")
+    private String tempDir;
     /** 知识库API */
     private final static String API_DATASETS_COMMON = "/v1/datasets";
-
-
     private final SmsbDifyDatasetsMapper baseMapper;
+    private final SmsbDifyDatasetsProductMapper smsbDifyDatasetsProductMapper;
+    private final SmsbDifyDatasetsFileMapper smsbDifyDatasetsFileMapper;
+    private final ISmsbDifyDatasetsFileService smsbDifyDatasetsFileService;
 
 
     /**
@@ -109,6 +123,123 @@ public class SmsbDifyDatasetsServiceImpl implements ISmsbDifyDatasetsService {
         return true;
     }
 
+    @Override
+    public boolean pushProduct(SmsbDifyDatasetsBo bo) throws IOException {
+        Long datasetsId = bo.getId();
+        SmsbDifyDatasetsVo datasetsVo = baseMapper.selectVoById(datasetsId);
+        List<String> productIds = bo.getEntryIds();
+        if (CollectionUtils.isEmpty(productIds)) {
+            throw new ServiceException("产品条目ID不能为空!");
+        }
+        List<SmsbDifyDatasetsProductVo> productVoList = smsbDifyDatasetsProductMapper.selectVoListByIds(productIds);
+        // 1 生成新的内容
+        StringBuffer sb = new StringBuffer();
+        for (SmsbDifyDatasetsProductVo productVo : productVoList) {
+            sb.append("###");
+            sb.append("产品名称:" + productVo.getName()).append("\n");
+            sb.append("产品简介:" + productVo.getNote()).append("\n");
+            sb.append("产品图片:" + productVo.getImgUrl()).append("\n");
+        }
+        // 2 查询当前产品库关联的文件
+        SmsbDifyDatasetsFileVo difyDatasetsFileVo = smsbDifyDatasetsFileMapper.selectVoOne(new LambdaQueryWrapper<SmsbDifyDatasetsFile>()
+            .eq(SmsbDifyDatasetsFile::getDatasetsId,datasetsId).last("limit 1"));
+        // 3 根据记录先删除dify平台的文件
+        if (null != difyDatasetsFileVo) {
+            List<Long> ids = new ArrayList<>();
+            ids.add(difyDatasetsFileVo.getId());
+            smsbDifyDatasetsFileService.deleteWithValidByIds(ids,false);
+        }
+        // 4 生成新的文件上传至dify平台
+        String filePath = createTempFile(sb.toString());
+        boolean uploadResult = upload2Dify(datasetsVo,filePath);
+
+        return uploadResult;
+    }
+
+    private boolean upload2Dify(SmsbDifyDatasetsVo datasetsVo,String filePath) {
+        // 根据
+        // 2、 调用dify接口,将文件上传至dify
+        String requestUrl = difyUrl + API_DATASETS_COMMON + "/" + datasetsVo.getDifyId() + "/document/create-by-file";
+        String requestBody = createAddFileRequestBody();
+
+        // 发送请求
+        HttpResponse response = HttpRequest.post(requestUrl)
+            .header("Authorization", "Bearer " + datasetsApiKey)
+            .header("User-Agent","PostmanRuntime/7.26.8")
+            .form("data", requestBody)
+            .form("file", new File(filePath))
+            .execute();
+        // 处理响应
+        if (!response.isOk()) {
+            return false;
+        }
+        // 3、 获取接口返回文件ID
+        cn.hutool.json.JSONObject document = JSONUtil.parseObj(response.body()).get("document", cn.hutool.json.JSONObject.class);
+        String difyId = document.getStr("id");
+        // 4、 保存至数据库
+        SmsbDifyDatasetsFile add = new SmsbDifyDatasetsFile();
+        add.setDatasetsDifyId(datasetsVo.getDifyId());
+        add.setDifyId(difyId);
+        // 获取知识库信息
+        add.setDatasetsId(datasetsVo.getId());
+        add.setDatasetsName(datasetsVo.getName());
+        add.setName(filePath);
+        add.setIndexingStatus(document.getStr("indexing_status"));
+        add.setPosition(document.getInt("position"));
+        add.setTokens(document.getInt("tokens"));
+        add.setWordCount(document.getInt("word_count"));
+        add.setFilePath(filePath);
+        smsbDifyDatasetsFileMapper.insert(add);
+        return true;
+    }
+
+    private String createAddFileRequestBody() {
+        // 构建 JSON 格式的 data 参数
+        cn.hutool.json.JSONObject dataJson = new cn.hutool.json.JSONObject();
+        // 1-索引方式
+        dataJson.put("indexing_technique", "high_quality");
+
+        // 2-处理规则
+        cn.hutool.json.JSONObject processRule = new cn.hutool.json.JSONObject();
+        // 2-1 清洗、分段模式
+        processRule.put("mode", "custom");
+        // 2-2 自定义规则
+        cn.hutool.json.JSONObject rules = new cn.hutool.json.JSONObject();
+        // 2-2-1 预处理规则
+        JSONArray preProcessingRules = new JSONArray();
+        cn.hutool.json.JSONObject rule1 = new cn.hutool.json.JSONObject();
+        rule1.put("id", "remove_extra_spaces");
+        rule1.put("enabled", true);
+        preProcessingRules.add(rule1);
+        cn.hutool.json.JSONObject rule2 = new cn.hutool.json.JSONObject();
+        rule2.put("id", "remove_urls_emails");
+        rule2.put("enabled", false);
+        preProcessingRules.add(rule2);
+        // 2-2-1 分段规则
+        cn.hutool.json.JSONObject segmentation = new cn.hutool.json.JSONObject();
+        segmentation.put("separator", "###");
+        segmentation.put("max_tokens", 800);
+        rules.put("pre_processing_rules", preProcessingRules);
+        rules.put("segmentation", segmentation);
+        processRule.put("rules", rules);
+
+        dataJson.put("process_rule", processRule);
+        return dataJson.toString();
+    }
+
+    private String createTempFile(String entryContent) throws IOException {
+        // 创建一个临时路径
+        String localPath = tempDir + "/" + System.currentTimeMillis();
+        Path path = Paths.get(localPath);
+        Files.createDirectories(path);
+        // 将entryContent保存到临时路径
+        String filePath = localPath + "/" + System.currentTimeMillis() + ".txt";
+        Path filePathObj = Paths.get(filePath);
+        byte[] contentBytes = entryContent.getBytes(StandardCharsets.UTF_8);
+        Files.write(filePathObj, contentBytes);
+        return filePath;
+    }
+
     private SmsbDifyDatasets buildDatasetByDifyData(DifyDatasetsRspData dataset) {
         SmsbDifyDatasets smsbDifyDatasets = new SmsbDifyDatasets();
         BeanUtils.copyProperties(dataset, smsbDifyDatasets);

+ 12 - 0
smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDifyDatasetsProductMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.inspur.device.mapper.SmsbDifyDatasetsProductMapper">
+    <select id="selectVoListByIds" resultType="com.inspur.device.domain.vo.SmsbDifyDatasetsProductVo">
+        select * from smsb_dify_datasets_product where id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+</mapper>

+ 0 - 7
smsb-modules/smsb-system/src/main/resources/mapper/system/SmsbDifyDatasetsProductMapper.xml

@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper
-PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.inspur.device.mapper.SmsbDifyDatasetsProductMapper">
-
-</mapper>

+ 7 - 0
smsb-plus-ui/src/api/smsb/device/datasets.ts

@@ -46,6 +46,13 @@ export const addDifyDatasets = (data: DifyDatasetsForm) => {
   });
 };
 
+export const pushProductEntry = (data: any) => {
+  return request({
+    url: '/smsb/device/datasets/push/product',
+    method: 'post',
+    data: data
+  });
+};
 /**
  * 修改知识库管理
  * @param data

+ 86 - 11
smsb-plus-ui/src/views/smsb/datasets/product.vue

@@ -17,9 +17,9 @@
               <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
                          v-hasPermi="['device:datasets:remove']">删除
               </el-button>
-              <el-button type="warning" plain icon="Refresh" @click="handleSync()"
+<!--              <el-button type="warning" plain icon="Refresh" @click="handleSync()"
                          v-hasPermi="['device:datasets:add']">同步
-              </el-button>
+              </el-button>-->
             </el-form-item>
           </el-form>
         </el-card>
@@ -53,9 +53,13 @@
           <el-table-column label="更新时间" align="left" prop="updateTime" width="160"/>
           <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
             <template #default="scope">
-              <el-tooltip content="文件列表" placement="top">
-                <el-button link type="primary" icon="Document" @click="handleToFileList(scope.row)"
-                           v-hasPermi="['device:datasetsFile:list']"></el-button>
+              <!--              <el-tooltip content="文件列表" placement="top">
+                              <el-button link type="primary" icon="Document" @click="handleToFileList(scope.row)"
+                                         v-hasPermi="['device:datasetsFile:list']"></el-button>
+                            </el-tooltip>-->
+              <el-tooltip content="下发" placement="top">
+                <el-button link type="primary" icon="Download" @click="handlePush(scope.row)"
+                           v-hasPermi="['device:datasets:edit']"></el-button>
               </el-tooltip>
               <el-tooltip content="删除" placement="top">
                 <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
@@ -86,6 +90,28 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 产品条目下发 -->
+    <el-dialog :title="pushDialog.title" v-model="pushDialog.visible" width="900px" append-to-body>
+      <div>
+        <el-transfer
+          v-model="selectedEntry"
+          filterable
+          :filter-method="filterEntry"
+          filter-placeholder="请输入产品名称"
+          :data="entryList"
+          :props="{ key: 'key', label: 'label' }"
+          :titles="['可选条目', '已选条目']"
+          :button-texts="['移除', '添加']"
+        />
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitEntry">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -94,10 +120,12 @@ import {
   addDifyDatasets,
   delDifyDatasets,
   getDifyDatasets,
-  listDifyDatasets, syncDifyDatasets,
+  listDifyDatasets, pushProductEntry,
+  syncDifyDatasets,
   updateDifyDatasets
 } from '@/api/smsb/device/datasets';
 import {DifyDatasetsForm, DifyDatasetsQuery, DifyDatasetsVO} from '@/api/smsb/device/datasets_type';
+import {listAllProduct} from "@/api/smsb/device/difyDatasetsProduct";
 
 const {proxy} = getCurrentInstance() as ComponentInternalInstance;
 
@@ -109,15 +137,22 @@ const ids = ref<Array<string | number>>([]);
 const single = ref(true);
 const multiple = ref(true);
 const total = ref(0);
-
+const selectedEntry = ref([]);
+const entryList = ref([]);
 const queryFormRef = ref<ElFormInstance>();
 const difyDatasetsFormRef = ref<ElFormInstance>();
+const datasetsId = ref<number | string>();
 
 const dialog = reactive<DialogOption>({
   visible: false,
   title: ''
 });
 
+const pushDialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
 const initFormData: DifyDatasetsForm = {
   id: undefined,
   name: undefined,
@@ -161,6 +196,8 @@ const getList = async () => {
 const cancel = () => {
   reset();
   dialog.visible = false;
+  pushDialog.visible = false;
+  selectedEntry.value = [];
 }
 
 /** 表单重置 */
@@ -214,12 +251,43 @@ const handleUpdate = async (row?: DifyDatasetsVO) => {
   dialog.visible = true;
   dialog.title = "修改产品库";
 }
+const handlePush = async (row?: DifyDatasetsVO) => {
+  datasetsId.value = row.id;
+  console.log("datasetsId1 = " + datasetsId.value);
 
-/** 跳转至文件列表页面  */
-const handleToFileList = (row?: DifyDatasetsVO) =>{
-  proxy.$router.push('/device/datasetsFile?datasetsId=' + row.difyId);
+  pushDialog.visible = true;
+  pushDialog.title = "产品条目下发";
+  const res = await listAllProduct();
+  const rspList = res.data || [];
+  // 清空现有数据
+  entryList.value = [];
+  selectedEntry.value = [];
+  // 使用map更简洁地转换数据格式
+  entryList.value = rspList.map(item => ({
+    key: item.id,
+    label: item.name,
+    disabled: false // 如果需要禁用某些项,可以在这里设置
+  }));
+}
+const filterEntry = (query, item) => {
+  if (!query) return true; // 没有查询条件时返回所有项
+  return item.label.toLowerCase().includes(query.toLowerCase());
+}
+const submitEntry = async () => {
+  buttonLoading.value = true;
+  console.log("datasetsId = " + datasetsId.value);
+  console.log("selectedEntry = " + selectedEntry.value)
+  if (selectedEntry.value.length < 1) {
+    proxy?.$modal.msgError("请至少选择一个产品!");
+  }
+  const pushProductParam = {
+    id: datasetsId.value,
+    entryIds: selectedEntry.value
+  }
+  await pushProductEntry(pushProductParam).finally(() => buttonLoading.value = false);
+  proxy?.$modal.msgSuccess("操作成功");
+  pushDialog.visible = false;
 }
-
 /** 提交按钮 */
 const submitForm = () => {
   difyDatasetsFormRef.value?.validate(async (valid: boolean) => {
@@ -257,3 +325,10 @@ onMounted(() => {
   getList();
 });
 </script>
+<style lang="scss" scoped>
+/* 自定义穿梭框样式 */
+:deep(.el-transfer-panel) {
+  width: 300px !important;
+}
+
+</style>