1.AI助手模版、样式修改成谢提供的对话模版(有特效)。
2.点击AI分析后,对话弹出类似商品链接引入,用户点击确认后进行分析。 3.AI分析对话新增提示词编辑,根据组件名称:模块编号进行获取提示词进行模块分析。 4.商品大盘首页字体调整,数据卡片中的字体调小一点,文字描述超长不显示,鼠标放上去显示全量。
This commit is contained in:
4
sql/mysql/ai_image_chat_menu.sql
Normal file
4
sql/mysql/ai_image_chat_menu.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- AI 图片对话菜单(插入到 AI 大模型 2758 下)
|
||||
-- 执行前请确认 id 5100 未被占用,若冲突可改为其他值
|
||||
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
|
||||
VALUES (5100, 'AI 图片对话', '', 2, 0, 2758, 'image-chat', 'ep:chat-dot-round', 'ydoyun/aichat/index.vue', 'AiImageChat', 0, '1', '1', '1', '1', NOW(), '1', NOW(), '0');
|
||||
24
sql/mysql/ydoyun_ai_module_prompt.sql
Normal file
24
sql/mysql/ydoyun_ai_module_prompt.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- AI 模块提示词配置表:存储各组件模块的 AI 分析提示词,key 为 组件名称:模块编号
|
||||
CREATE TABLE IF NOT EXISTS `ydoyun_ai_module_prompt` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`component_name` varchar(64) NOT NULL COMMENT '组件名称,如 ProductDashboard',
|
||||
`module_name` varchar(128) NOT NULL COMMENT '模块名称,如 KPI指标、供应商表现',
|
||||
`module_key` varchar(128) NOT NULL COMMENT '组件:模块编号,如 ProductDashboard:kpi',
|
||||
`prompt` text COMMENT 'AI 提示词,用于定义任务并传入 Dify 分析',
|
||||
`creator` varchar(64) DEFAULT '' NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
|
||||
`updater` varchar(64) DEFAULT '' NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` bit(1) DEFAULT b'0' NOT NULL COMMENT '是否删除',
|
||||
`tenant_id` bigint DEFAULT 0 NULL COMMENT '租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tenant_module_key` (`tenant_id`, `module_key`) COMMENT '租户内组件:模块编号唯一',
|
||||
KEY `idx_component_name` (`component_name`) COMMENT '按组件名称查询',
|
||||
KEY `idx_tenant_id` (`tenant_id`) COMMENT '按租户查询'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI 模块提示词配置表';
|
||||
|
||||
-- ProductDashboard 可分析模块说明(供 AI 定义任务时列举):
|
||||
-- ProductDashboard:kpi - KPI 指标卡片
|
||||
-- ProductDashboard:supplier - 供应商表现 (Top 10)
|
||||
-- ProductDashboard:pie - 中类销售排名 Top 5
|
||||
-- ProductDashboard:productList - 商品明细列表
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aichat;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.ydoyun.service.aichat.DifyAiChatService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* AI 对话(流式响应)
|
||||
* 前端直接上传图片文件,后端上传到 Dify 后对话
|
||||
* 使用登录用户 ID 作为 Dify user 参数,实现各用户对话隔离
|
||||
*/
|
||||
@Tag(name = "管理后台 - AI 图片对话")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ydoyun/ai-chat")
|
||||
@RequiredArgsConstructor
|
||||
public class AiChatController {
|
||||
|
||||
private final DifyAiChatService difyAiChatService;
|
||||
|
||||
@Operation(summary = "流式图片对话")
|
||||
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public void streamChat(
|
||||
@RequestParam("query") String query,
|
||||
@RequestParam(value = "file", required = false) MultipartFile file,
|
||||
@RequestParam(value = "task_desc", required = false) String taskDesc,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
String user = "system";
|
||||
try {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (userId != null) {
|
||||
user = "user_" + userId;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("获取登录用户失败,使用默认 user: {}", e.getMessage());
|
||||
}
|
||||
response.setContentType("text/event-stream;charset=UTF-8");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Connection", "keep-alive");
|
||||
response.setHeader("X-Accel-Buffering", "no");
|
||||
|
||||
try (OutputStream out = response.getOutputStream()) {
|
||||
difyAiChatService.streamChat(query, file, taskDesc, user, out);
|
||||
} catch (Exception e) {
|
||||
log.error("AI 图片对话流式响应异常", e);
|
||||
try {
|
||||
String err = "data: {\"error\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}\n\n";
|
||||
response.getOutputStream().write(err.getBytes(StandardCharsets.UTF_8));
|
||||
response.getOutputStream().flush();
|
||||
} catch (Exception ex) {
|
||||
log.warn("写入错误响应失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aichat.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "AI 对话流式请求")
|
||||
@Data
|
||||
public class AiChatStreamReqVO {
|
||||
|
||||
@Schema(description = "用户输入/问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
private String query;
|
||||
|
||||
@Schema(description = "图片 URL(上传或粘贴后获取的完整访问地址)")
|
||||
private String pic;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt.vo.AiModulePromptRespVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt.vo.AiModulePromptSaveReqVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aimoduleprompt.AiModulePromptDO;
|
||||
import cn.iocoder.yudao.module.ydoyun.service.aimoduleprompt.AiModulePromptService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* AI 模块提示词配置 Controller
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@Tag(name = "管理后台 - AI 模块提示词")
|
||||
@RestController
|
||||
@RequestMapping("/ydoyun/ai-module-prompt")
|
||||
@Validated
|
||||
public class AiModulePromptController {
|
||||
|
||||
@Resource
|
||||
private AiModulePromptService aiModulePromptService;
|
||||
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "保存提示词")
|
||||
public CommonResult<Boolean> savePrompt(@Valid @RequestBody AiModulePromptSaveReqVO reqVO) {
|
||||
aiModulePromptService.savePrompt(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/list-by-component")
|
||||
@Operation(summary = "按组件名称查询提示词列表")
|
||||
@Parameter(name = "componentName", description = "组件名称", required = true, example = "ProductDashboard")
|
||||
public CommonResult<List<AiModulePromptRespVO>> listByComponent(@RequestParam("componentName") String componentName) {
|
||||
List<AiModulePromptDO> list = aiModulePromptService.getListByComponent(componentName);
|
||||
return success(BeanUtils.toBean(list, AiModulePromptRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/map-by-component")
|
||||
@Operation(summary = "按组件名称查询提示词映射(moduleKey -> prompt)")
|
||||
@Parameter(name = "componentName", description = "组件名称", required = true, example = "ProductDashboard")
|
||||
public CommonResult<Map<String, String>> getPromptMapByComponent(@RequestParam("componentName") String componentName) {
|
||||
return success(aiModulePromptService.getPromptMapByComponent(componentName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - AI 模块提示词 Response VO")
|
||||
@Data
|
||||
public class AiModulePromptRespVO {
|
||||
|
||||
@Schema(description = "主键ID", example = "1")
|
||||
private Long id;
|
||||
@Schema(description = "组件名称", example = "ProductDashboard")
|
||||
private String componentName;
|
||||
@Schema(description = "模块名称", example = "KPI指标")
|
||||
private String moduleName;
|
||||
@Schema(description = "组件:模块编号", example = "ProductDashboard:kpi")
|
||||
private String moduleKey;
|
||||
@Schema(description = "AI 提示词")
|
||||
private String prompt;
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "管理后台 - AI 模块提示词保存 Request VO")
|
||||
@Data
|
||||
public class AiModulePromptSaveReqVO {
|
||||
|
||||
@Schema(description = "主键ID,更新时必填", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "组件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ProductDashboard")
|
||||
@NotBlank(message = "组件名称不能为空")
|
||||
private String componentName;
|
||||
|
||||
@Schema(description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "KPI指标")
|
||||
@NotBlank(message = "模块名称不能为空")
|
||||
private String moduleName;
|
||||
|
||||
@Schema(description = "组件:模块编号,唯一键", requiredMode = Schema.RequiredMode.REQUIRED, example = "ProductDashboard:kpi")
|
||||
@NotBlank(message = "模块编号不能为空")
|
||||
private String moduleKey;
|
||||
|
||||
@Schema(description = "AI 提示词")
|
||||
private String prompt;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.dal.dataobject.aimoduleprompt;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* AI 模块提示词配置 DO
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@TableName("ydoyun_ai_module_prompt")
|
||||
@KeySequence("ydoyun_ai_module_prompt_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiModulePromptDO extends TenantBaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
/** 组件名称,如 ProductDashboard */
|
||||
private String componentName;
|
||||
/** 模块名称,如 KPI指标、供应商表现 */
|
||||
private String moduleName;
|
||||
/** 组件:模块编号,唯一键,如 ProductDashboard:kpi */
|
||||
private String moduleKey;
|
||||
/** AI 提示词 */
|
||||
private String prompt;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.dal.mysql.aimoduleprompt;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aimoduleprompt.AiModulePromptDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 模块提示词配置 Mapper
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiModulePromptMapper extends BaseMapperX<AiModulePromptDO> {
|
||||
|
||||
default AiModulePromptDO selectByModuleKey(String moduleKey) {
|
||||
return selectOne(AiModulePromptDO::getModuleKey, moduleKey);
|
||||
}
|
||||
|
||||
default List<AiModulePromptDO> selectListByComponentName(String componentName) {
|
||||
return selectList(AiModulePromptDO::getComponentName, componentName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aichat;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* Dify AI 对话服务(流式响应)
|
||||
*/
|
||||
public interface DifyAiChatService {
|
||||
|
||||
/**
|
||||
* 流式发送对话请求,逐字返回 AI 响应
|
||||
*
|
||||
* @param query 用户问题
|
||||
* @param file 图片文件(可选,前端直接上传)
|
||||
* @param taskDesc 任务描述(可选,传给 Dify task_desc 做 AI 分析)
|
||||
* @param user Dify 用户标识,用于对话隔离(不同用户对话互不污染)
|
||||
* @param output 输出流,用于写入 SSE 格式的响应
|
||||
*/
|
||||
void streamChat(String query, MultipartFile file, String taskDesc, String user, java.io.OutputStream output) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aichat;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Dify AI 对话服务实现(流式响应)
|
||||
* 前端直接上传图片文件,后端上传到 Dify 后对话
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DifyAiChatServiceImpl implements DifyAiChatService {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Value("${ydoyun.dify.ai-chat.base-url:http://118.253.178.8:8888/v1}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${ydoyun.dify.ai-chat.api-key:app-uch2HPKpicnPgNpJCnQJTehq}")
|
||||
private String apiKey;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Override
|
||||
public void streamChat(String query, MultipartFile file, String taskDesc, String user, java.io.OutputStream output) throws Exception {
|
||||
// 构建请求体
|
||||
String base = baseUrl.replaceAll("/$", "");
|
||||
String chatUrl = base + "/chat-messages";
|
||||
|
||||
List<Map<String, String>> files = new ArrayList<>();
|
||||
if (file != null && !file.isEmpty()) {
|
||||
String uploadFileId = uploadImageToDify(file.getBytes(), file.getOriginalFilename(), user);
|
||||
if (uploadFileId != null) {
|
||||
Map<String, String> fileObj = new HashMap<>();
|
||||
fileObj.put("type", "image");
|
||||
fileObj.put("transfer_method", "local_file");
|
||||
fileObj.put("upload_file_id", uploadFileId);
|
||||
files.add(fileObj);
|
||||
} else {
|
||||
log.warn("图片上传到 Dify 失败,将不携带图片继续请求");
|
||||
}
|
||||
}
|
||||
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
|
||||
objectObjectHashMap.put("pic", files);
|
||||
if (taskDesc != null && !taskDesc.trim().isEmpty()) {
|
||||
objectObjectHashMap.put("task_desc", taskDesc.trim());
|
||||
}
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("query", query);
|
||||
requestBody.put("inputs", objectObjectHashMap);
|
||||
if (!files.isEmpty()) {
|
||||
requestBody.put("files", files);
|
||||
}
|
||||
requestBody.put("response_mode", "streaming");
|
||||
requestBody.put("user", user);
|
||||
|
||||
String jsonBody = OBJECT_MAPPER.writeValueAsString(requestBody);
|
||||
log.info("Dify 请求体:{}", jsonBody);
|
||||
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = new URL(chatUrl);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(300000); // 5 分钟
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(jsonBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
if (code != 200) {
|
||||
String err = readStream(conn.getErrorStream());
|
||||
log.error("Dify 请求失败: {} {}", code, err);
|
||||
writeSseError(output, "Dify 请求失败: " + code);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 读取 Dify SSE 流并转发给前端
|
||||
int chunkCount = 0;
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6).trim();
|
||||
if ("[DONE]".equals(data)) break;
|
||||
try {
|
||||
JsonNode node = OBJECT_MAPPER.readTree(data);
|
||||
String event = node.path("event").asText("");
|
||||
if (chunkCount < 3) {
|
||||
log.info("[Dify] SSE event: event={}, hasAnswer={}", event, node.has("answer"));
|
||||
}
|
||||
// 兼容 message / agent_message 事件,均含 answer 增量
|
||||
if (node.has("answer")) {
|
||||
String answer = node.get("answer").asText();
|
||||
if (answer != null && !answer.isEmpty()) {
|
||||
Map<String, Object> chunk = new HashMap<String, Object>();
|
||||
chunk.put("text", answer);
|
||||
writeSseData(output, chunk);
|
||||
chunkCount++;
|
||||
}
|
||||
}
|
||||
if ("message_end".equals(event)) {
|
||||
log.info("[Dify] 流式结束, 共转发 {} 个 chunk", chunkCount);
|
||||
break;
|
||||
}
|
||||
if (node.has("code")) {
|
||||
String errMsg = node.path("message").asText("未知错误");
|
||||
log.warn("[Dify] 流式响应错误: code={}, message={}", node.path("code").asText(), errMsg);
|
||||
writeSseError(output, errMsg);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("解析 SSE 行: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片到 Dify,返回 upload_file_id
|
||||
*/
|
||||
private String uploadImageToDify(byte[] imageBytes, String originalFilename, String user) {
|
||||
try {
|
||||
String ext = "png";
|
||||
if (originalFilename != null) {
|
||||
int dot = originalFilename.lastIndexOf('.');
|
||||
if (dot >= 0 && dot < originalFilename.length() - 1) {
|
||||
ext = originalFilename.substring(dot + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
if (!ext.matches("png|jpg|jpeg|webp|gif")) ext = "png";
|
||||
|
||||
String uploadUrl = baseUrl.replaceAll("/$", "") + "/files/upload";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
final String fileExt = ext;
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new ByteArrayResource(imageBytes) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "image." + fileExt;
|
||||
}
|
||||
});
|
||||
body.add("user", user);
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<String> resp = restTemplate.postForEntity(uploadUrl, entity, String.class);
|
||||
// Dify 上传成功返回 201 CREATED,非 200 OK
|
||||
if ((resp.getStatusCode() == HttpStatus.OK || resp.getStatusCode() == HttpStatus.CREATED) && resp.getBody() != null) {
|
||||
JsonNode node = OBJECT_MAPPER.readTree(resp.getBody());
|
||||
JsonNode idNode = node.has("id") ? node.get("id") : (node.has("data") && node.get("data").has("id") ? node.get("data").get("id") : null);
|
||||
if (idNode != null) {
|
||||
return idNode.asText();
|
||||
}
|
||||
log.warn("[Dify] 上传响应无 id 字段: {}", resp.getBody());
|
||||
} else {
|
||||
log.warn("[Dify] 上传失败: status={}, body={}", resp.getStatusCode(), resp.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Dify] 上传图片到 Dify 异常", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String readStream(InputStream is) throws IOException {
|
||||
if (is == null) return "";
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) sb.append(line);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSseData(OutputStream out, Map<String, Object> data) throws IOException {
|
||||
try {
|
||||
String json = OBJECT_MAPPER.writeValueAsString(data);
|
||||
out.write(("data: " + json + "\n\n").getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
} catch (Exception e) {
|
||||
log.debug("写入 SSE: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSseError(OutputStream out, String msg) throws IOException {
|
||||
Map<String, Object> errData = new HashMap<String, Object>();
|
||||
errData.put("error", msg);
|
||||
writeSseData(out, errData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aimoduleprompt;
|
||||
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt.vo.AiModulePromptSaveReqVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aimoduleprompt.AiModulePromptDO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 模块提示词配置 Service
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
public interface AiModulePromptService {
|
||||
|
||||
/**
|
||||
* 保存提示词(按 moduleKey 存在则更新,否则新增)
|
||||
*/
|
||||
void savePrompt(@Valid AiModulePromptSaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 按组件名称查询所有模块提示词,返回 moduleKey -> prompt 映射
|
||||
*/
|
||||
Map<String, String> getPromptMapByComponent(String componentName);
|
||||
|
||||
/**
|
||||
* 按组件名称查询所有模块提示词列表
|
||||
*/
|
||||
List<AiModulePromptDO> getListByComponent(String componentName);
|
||||
|
||||
/**
|
||||
* 按 moduleKey 查询
|
||||
*/
|
||||
AiModulePromptDO getByModuleKey(String moduleKey);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cn.iocoder.yudao.module.ydoyun.service.aimoduleprompt;
|
||||
|
||||
import cn.iocoder.yudao.module.ydoyun.controller.admin.aimoduleprompt.vo.AiModulePromptSaveReqVO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.dataobject.aimoduleprompt.AiModulePromptDO;
|
||||
import cn.iocoder.yudao.module.ydoyun.dal.mysql.aimoduleprompt.AiModulePromptMapper;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI 模块提示词配置 Service 实现类
|
||||
*
|
||||
* @author 衣朵云
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class AiModulePromptServiceImpl implements AiModulePromptService {
|
||||
|
||||
@Resource
|
||||
private AiModulePromptMapper aiModulePromptMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void savePrompt(AiModulePromptSaveReqVO reqVO) {
|
||||
AiModulePromptDO existing = aiModulePromptMapper.selectByModuleKey(reqVO.getModuleKey());
|
||||
AiModulePromptDO entity = BeanUtils.toBean(reqVO, AiModulePromptDO.class);
|
||||
if (existing != null) {
|
||||
entity.setId(existing.getId());
|
||||
aiModulePromptMapper.updateById(entity);
|
||||
} else {
|
||||
aiModulePromptMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getPromptMapByComponent(String componentName) {
|
||||
List<AiModulePromptDO> list = aiModulePromptMapper.selectListByComponentName(componentName);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
if (list != null) {
|
||||
for (AiModulePromptDO d : list) {
|
||||
if (d.getPrompt() != null && !d.getPrompt().isEmpty()) {
|
||||
map.put(d.getModuleKey(), d.getPrompt());
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiModulePromptDO> getListByComponent(String componentName) {
|
||||
return aiModulePromptMapper.selectListByComponentName(componentName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiModulePromptDO getByModuleKey(String moduleKey) {
|
||||
return aiModulePromptMapper.selectByModuleKey(moduleKey);
|
||||
}
|
||||
}
|
||||
@@ -363,4 +363,11 @@ ydoyun:
|
||||
base-url: http://118.253.178.8:5001/v1/chat-messages
|
||||
api-key1: app-a6GWULbcg8zkv8g3VVLPARPz
|
||||
api-key2: app-xc2JwxgwqTRZWybnkAgyLZQV
|
||||
# AI 图片对话(独立应用,流式响应)
|
||||
ai-chat:
|
||||
base-url: http://118.253.178.8:5001/v1
|
||||
api-key: app-uch2HPKpicnPgNpJCnQJTehq
|
||||
pic-param: pic # Dify 应用中的图片参数名
|
||||
pic-format: object # object=单对象 | array=数组 | id=仅 upload_file_id 字符串,若 AI 看不到图可切换尝试
|
||||
image-payload-mode: both # both=同时传 inputs+files | inputs=只传 inputs.pic | files=只传顶层 files
|
||||
|
||||
|
||||
Reference in New Issue
Block a user