admin
2025-05-29 ef4c38330371547b66bc0c5b7ebc02d13c81cb2c
0529
11个文件已修改
2个文件已添加
365 ■■■■■ 已修改文件
aiflowy-commons/aiflowy-common-web/src/main/java/tech/aiflowy/common/web/controller/BaseCurdController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/config/RestTemplateConfig.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiBotController.java 152 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiFirstMenuController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiSecondMenuController.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiSecondMenu.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiBotBase.java 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiSecondMenuBase.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/mapper/AiSecondMenuMapper.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/AiSecondMenuService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/impl/AiSecondMenuServiceImpl.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/vo/ModelConfig.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-commons/aiflowy-common-web/src/main/java/tech/aiflowy/common/web/controller/BaseCurdController.java
@@ -135,13 +135,6 @@
     *
     * @return 所有数据
     */
    @GetMapping("sysDept")
    public Result sysDept(M entity, Boolean asTree, String sortKey, String sortType) {
        QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
        queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
        List<M> list = Tree.tryToTree(service.list(queryWrapper), asTree);
        return Result.success(list);
    }
    @GetMapping("list")
    public Result list(M entity, Boolean asTree, String sortKey, String sortType) {
        QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
aiflowy-modules/aiflowy-module-ai/pom.xml
@@ -64,5 +64,11 @@
            <groupId>tech.aiflowy</groupId>
            <artifactId>aiflowy-module-system</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.14</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/config/RestTemplateConfig.java
New file
@@ -0,0 +1,14 @@
package tech.aiflowy.ai.config;
import cn.hutool.json.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiBotController.java
@@ -2,6 +2,8 @@
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.agentsflex.core.llm.ChatContext;
import com.agentsflex.core.llm.Llm;
import com.agentsflex.core.llm.StreamResponseListener;
@@ -16,24 +18,31 @@
import com.agentsflex.core.util.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.jfinal.template.stat.ast.Break;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.table.TableInfo;
import com.mybatisflex.core.table.TableInfoFactory;
import io.milvus.param.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.aiflowy.ai.entity.*;
import tech.aiflowy.ai.mapper.AiBotConversationMessageMapper;
import tech.aiflowy.ai.service.*;
import tech.aiflowy.ai.vo.ModelConfig;
import tech.aiflowy.common.ai.ChatManager;
import tech.aiflowy.common.ai.MySseEmitter;
import tech.aiflowy.common.domain.Result;
@@ -48,7 +57,9 @@
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigInteger;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -77,6 +88,8 @@
    private AiBotConversationMessageMapper aiBotConversationMessageMapper;
    private static final Logger logger = LoggerFactory.getLogger(AiBotController.class);
    @Autowired
    private RestTemplate restTemplate;
    public AiBotController(AiBotService service, AiLlmService aiLlmService, AiBotWorkflowService aiBotWorkflowService, AiBotKnowledgeService aiBotKnowledgeService, AiBotMessageService aiBotMessageService) {
        super(service);
@@ -132,6 +145,10 @@
     * @param response
     * @return
     */
    @Autowired
    private ObjectMapper objectMapper;
    @PostMapping("chat")
    public SseEmitter chat(@JsonBody(value = "prompt", required = true) String prompt,
                           @JsonBody(value = "botId", required = true) BigInteger botId,
@@ -143,86 +160,77 @@
        if (aiBot == null) {
            return ChatManager.getInstance().sseEmitterForContent("机器人不存在");
        }
        if (StringUtil.hasText(aiBot.getApiEndpoint())){
            // 情况1:aiBot自带大模型信息
            try {
                // 从aiBot构建自定义LLM实现
                Llm llm = null;
                if (llm == null) {
                    return ChatManager.getInstance().sseEmitterForContent("LLM获取为空");
                }
        if (StringUtil.hasText(aiBot.getModelAPI())){
            // 使用Dify模型的逻辑
            String apiUrl = aiBot.getModelAPI();
            String bearerToken = aiBot.getModelKey();
                AiBotMessageMemory memory = new AiBotMessageMemory(botId, SaTokenUtil.getLoginAccount().getId(),
                        sessionId, isExternalMsg, aiBotMessageService, aiBotConversationMessageMapper,
                        aiBotConversationMessageService);
            // 构建请求JSON
            JSONObject jsonBody = new JSONObject();
            jsonBody.put("inputs", new JSONObject());
            jsonBody.put("query", prompt);
            jsonBody.put("response_mode", "blocking");
            jsonBody.put("conversation_id", "");
            jsonBody.put("user", userId.toString());
                final HistoriesPrompt historiesPrompt = new HistoriesPrompt();
            JSONArray files = new JSONArray();
            JSONObject fileObj = new JSONObject();
            fileObj.put("type", "");
            fileObj.put("transfer_method", "");
            fileObj.put("url", "");
            files.put(fileObj);
            jsonBody.put("files", files);
                historiesPrompt.setMemory(memory);
            // 发送HTTP请求
            HttpClient client = HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(30))
                    .build();
                HumanMessage humanMessage = new HumanMessage(prompt);
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(apiUrl))
                    .header("Content-Type", "application/json")
                    .header("Authorization", "Bearer " + bearerToken)
                    .POST(HttpRequest.BodyPublishers.ofString(jsonBody.toString()))
                    .build();
                // 添加插件相关的function calling
                appendPluginToolFunction(botId, humanMessage);
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                //添加工作流相关的 Function Calling
                appendWorkflowFunctions(botId, humanMessage);
            // 处理成功响应
            JSONObject responseJson = new JSONObject(response.body());
                //添加知识库相关的 Function Calling
                appendKnowledgeFunctions(botId, humanMessage);
            // 保存用户消息到历史记录
            ChatHistory userChat = new ChatHistory();
            userChat.setContent(prompt);
            userChat.setRole("user");
            userChat.setUserId(userId);
            userChat.setBotId(botId.intValue());
            userChat.setModel("dify");
            userChat.setChatId(responseJson.optString("conversation_id", ""));
            Integer userHistoryId = chatHistoryService.saveChatHistory(userChat);
                historiesPrompt.addMessage(humanMessage);
            // 保存AI回复到历史记录
            ChatHistory assistantChat = new ChatHistory();
            assistantChat.setContent(responseJson.getString("answer"));
            assistantChat.setRole("assistant");
            assistantChat.setUserId(userId);
            assistantChat.setBotId(botId.intValue());
            assistantChat.setModel("dify");
            assistantChat.setChatId(userChat.getChatId());
            Integer assistantHistoryId = chatHistoryService.saveChatHistory(assistantChat);
                MySseEmitter emitter = new MySseEmitter((long) (1000 * 60 * 2));
            // 构建响应数据
            int count = chatHistoryService.getChatHistoryCount(userId, botId.intValue());
            JSONObject result = new JSONObject();
            result.put("Count", count);
            result.put("Data", responseJson.getString("answer"));
            result.put("UserId", userHistoryId);
            result.put("AssistantId", assistantHistoryId);
                final Boolean[] needClose = {true};
                ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                // 统一使用流式处理,无论是否有 Function Calling
                llm.chatStream(historiesPrompt, new StreamResponseListener() {
                    @Override
                    public void onMessage(ChatContext context, AiMessageResponse response) {
                        try {
                            RequestContextHolder.setRequestAttributes(sra, true);
                            if (response != null) {
                                // 检查是否需要触发 Function Calling
                                if (response.getFunctionCallers() != null && CollectionUtil.hasItems(response.getFunctionCallers())) {
                                    needClose[0] = false;
                                    function_call(response, emitter, needClose, historiesPrompt, llm, prompt, false);
                                } else {
                                    // 强制流式返回,即使有 Function Calling 也先返回部分结果
                                    if (response.getMessage() != null) {
                                        String content = response.getMessage().getContent();
                                        if (StringUtil.hasText(content)) {
                                            emitter.send(JSON.toJSONString(response.getMessage()));
                                        }
                                    }
                                }
                            }
                        } catch (Exception e) {
                            emitter.completeWithError(e);
                        }
                    }
                    @Override
                    public void onStop(ChatContext context) {
                        if (needClose[0]) {
                            emitter.complete();
                        }
                    }
                    @Override
                    public void onFailure(ChatContext context, Throwable throwable) {
                        emitter.completeWithError(throwable);
                    }
                });
                return emitter;
            } catch (Exception e) {
                return ChatManager.getInstance().sseEmitterForContent("自定义LLM配置错误");
            }
            // 发送SSE响应
            emitter.send(SseEmitter.event()
                    .name("message")
                    .data(result.toString()));
            emitter.complete();
        }else{
            Map<String, Object> llmOptions = aiBot.getLlmOptions();
            String systemPrompt = llmOptions != null ? (String) llmOptions.get("systemPrompt") : null;
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiFirstMenuController.java
@@ -28,4 +28,12 @@
    AiFirstMenuService aiFirstMenuService;
    public Result list(AiFirstMenu entity, Boolean asTree, String sortKey, String sortType) {
        QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
        queryWrapper.orderBy(AiFirstMenu::getId);
        List<AiFirstMenu> list = Tree.tryToTree(super.service.list(queryWrapper), asTree);
        return Result.success(list);
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiSecondMenuController.java
@@ -1,11 +1,23 @@
package tech.aiflowy.ai.controller;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.table.TableInfo;
import com.mybatisflex.core.table.TableInfoFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import tech.aiflowy.ai.entity.AiSecondMenu;
import tech.aiflowy.ai.service.AiSecondMenuService;
import tech.aiflowy.common.domain.Result;
import tech.aiflowy.common.entity.LoginAccount;
import tech.aiflowy.common.satoken.util.SaTokenUtil;
import tech.aiflowy.common.tree.Tree;
import tech.aiflowy.common.web.controller.BaseCurdController;
import tech.aiflowy.common.web.jsonbody.JsonBody;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@RestController
@RequestMapping("/api/v1/aiMenu/SecondMenu")
public class AiSecondMenuController extends BaseCurdController<AiSecondMenuService, AiSecondMenu> {
@@ -14,5 +26,16 @@
    }
    @Autowired
    AiSecondMenuService AiSecondMenuService;
    AiSecondMenuService aiSecondMenuService;
    @Override
    public Result list(AiSecondMenu entity, Boolean asTree, String sortKey, String sortType) {
        QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
        queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
        List<AiSecondMenu> list = Tree.tryToTree(aiSecondMenuService.findAll(queryWrapper), asTree);
        return Result.success(list);
    }
    protected Page<AiSecondMenu> queryPage(Page<AiSecondMenu> page, QueryWrapper queryWrapper) {
        return service.page(page, queryWrapper);
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiSecondMenu.java
@@ -1,7 +1,25 @@
package tech.aiflowy.ai.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import tech.aiflowy.ai.entity.base.AiSecondMenuBase;
import java.math.BigInteger;
@Table(value ="ai_second_menu")
public class AiSecondMenu extends AiSecondMenuBase {
    @Column(ignore = true)
    public String firstMenuName;
    public String getFirstMenuName() {
        return firstMenuName;
    }
    public void setFirstMenuName(String firstMenuName) {
        this.firstMenuName = firstMenuName;
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiBotBase.java
@@ -94,17 +94,12 @@
    @Column(comment = "修改者ID")
    private BigInteger modifiedBy;
    @Column(comment = "API品牌")
    private String apiProvider;
    @Column(comment = "API")
    private String modelAPI;
    @Column(comment = "API地址")
    private String apiEndpoint;
    @Column(comment = "KEY")
    private String modelKey;
    @Column(comment = "API密钥")
    private String apiKey;
    @Column(comment = "API额外配置")
    private String apiVersion;
    @Column(comment = "一级菜单编号")
    private Integer firstMenuId;
@@ -120,37 +115,20 @@
        this.firstMenuId = firstMenuId;
    }
    public String getApiProvider() {
        return apiProvider;
    public String getModelAPI() {
        return modelAPI;
    }
    public void setApiProvider(String apiProvider) {
        this.apiProvider = apiProvider;
    public void setModelAPI(String modelAPI) {
        this.modelAPI = modelAPI;
    }
    public String getApiEndpoint() {
        return apiEndpoint;
    public String getModelKey() {
        return modelKey;
    }
    public void setApiEndpoint(String apiEndpoint) {
        this.apiEndpoint = apiEndpoint;
    }
    public String getApiKey() {
        return apiKey;
    }
    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }
    public String getApiVersion() {
        return apiVersion;
    }
    public void setApiVersion(String apiVersion) {
        this.apiVersion = apiVersion;
    public void setModelKey(String modelKey) {
        this.modelKey = modelKey;
    }
    public Integer getSecondMenuId() {
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiSecondMenuBase.java
@@ -22,11 +22,20 @@
    @Id(keyType = KeyType.Auto, value = "secondMenuId", comment = "二级菜单编号")
    private BigInteger id;
    /**
     * 一级菜单编号
     */
    @Column(comment = "一级菜单编号")
    @Column("first_menu_id")
    private BigInteger firstMenuId;
    public BigInteger getFirstMenuId() {
        return firstMenuId;
    }
    public void setFirstMenuId(BigInteger firstMenuId) {
        this.firstMenuId = firstMenuId;
    }
    /**
     * 二级菜单名称
@@ -40,14 +49,6 @@
    public void setId(BigInteger id) {
        this.id = id;
    }
    public BigInteger getFirstMenuId() {
        return firstMenuId;
    }
    public void setFirstMenuId(BigInteger firstMenuId) {
        this.firstMenuId = firstMenuId;
    }
    public String getSecondMenuName() {
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/mapper/AiSecondMenuMapper.java
@@ -1,8 +1,12 @@
package tech.aiflowy.ai.mapper;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.apache.ibatis.annotations.Select;
import tech.aiflowy.ai.entity.AiSecondMenu;
import tech.aiflowy.ai.entity.base.AiSecondMenuBase;
import java.math.BigInteger;
import java.util.List;
/**
* @author admin
@@ -11,7 +15,8 @@
* @Entity tech.aiflowy.ai.entity.base.AiSecondMenu
*/
public interface AiSecondMenuMapper extends BaseMapper<AiSecondMenu> {
    @Select("select first_menu_name from ai_first_menu where id = #{firstMenuId}")
    String getFMN(BigInteger firstMenuId);
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/AiSecondMenuService.java
@@ -1,8 +1,12 @@
package tech.aiflowy.ai.service;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.service.IService;
import tech.aiflowy.ai.entity.AiSecondMenu;
import tech.aiflowy.ai.entity.base.AiSecondMenuBase;
import java.util.List;
/**
* @author admin
@@ -10,5 +14,8 @@
* @createDate 2025-05-28 15:26:35
*/
public interface AiSecondMenuService extends IService<AiSecondMenu> {
    public List<AiSecondMenu> findAll(QueryWrapper query);
    Page<AiSecondMenu> page(Page<AiSecondMenu> page, QueryWrapper query) ;
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/impl/AiSecondMenuServiceImpl.java
@@ -1,10 +1,17 @@
package tech.aiflowy.ai.service.impl;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.aiflowy.ai.entity.AiSecondMenu;
import tech.aiflowy.ai.mapper.AiSecondMenuMapper;
import tech.aiflowy.ai.service.AiSecondMenuService;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import java.util.List;
/**
* @author admin
* @description 针对表【ai_second_menu(ai机器人二级菜单表)】的数据库操作Service实现
@@ -13,7 +20,24 @@
@Service
public class AiSecondMenuServiceImpl extends ServiceImpl<AiSecondMenuMapper, AiSecondMenu>
    implements AiSecondMenuService {
    @Autowired
    private AiSecondMenuMapper aiSecondMenuMapper;
    @Override
    public List<AiSecondMenu> findAll(QueryWrapper query) {
        List<AiSecondMenu> list = this.list(query);
        for (AiSecondMenu aiSecondMenu : list) {
            aiSecondMenu.setFirstMenuName(aiSecondMenuMapper.getFMN(aiSecondMenu.getFirstMenuId()));
        }
        return list;
    }
    public Page<AiSecondMenu> page(Page<AiSecondMenu> page, QueryWrapper query){
        Page page1 = this.<AiSecondMenu>pageAs(page, query, (Class) null);
        List<AiSecondMenu> records = page1.getRecords();
        for (AiSecondMenu record : records) {
            record.setFirstMenuName(aiSecondMenuMapper.getFMN(record.getFirstMenuId()));
        }
        return page1;
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/vo/ModelConfig.java
New file
@@ -0,0 +1,24 @@
package tech.aiflowy.ai.vo;
public class ModelConfig {
    private String modelApi;
    private String modelKey;
    // getters and setters
    public String getModelApi() {
        return modelApi;
    }
    public void setModelApi(String modelApi) {
        this.modelApi = modelApi;
    }
    public String getModelKey() {
        return modelKey;
    }
    public void setModelKey(String modelKey) {
        this.modelKey = modelKey;
    }
}