Commit c9201c30f914554587c4e7a22eb23206052283e5

Authored by YunaiV
1 parent 0caf6464

feat(ai1):新增 ai agent,基于 spring ai 实现

Showing 159 changed files with 15361 additions and 0 deletions

Too many changes.

To preserve performance only 100 of 159 files are displayed.

@@ -33,6 +33,7 @@ @@ -33,6 +33,7 @@
33 <!-- <module>yudao-module-im</module>--> 33 <!-- <module>yudao-module-im</module>-->
34 <!-- 请参考 https://doc.iocoder.cn/ai/build/ 文档,完成 AI 模块的启动!!! --> 34 <!-- 请参考 https://doc.iocoder.cn/ai/build/ 文档,完成 AI 模块的启动!!! -->
35 <!-- <module>yudao-module-ai</module>--> 35 <!-- <module>yudao-module-ai</module>-->
  36 +<!-- <module>yudao-module-ai1-springai</module>-->
36 </modules> 37 </modules>
37 38
38 <name>${project.artifactId}</name> 39 <name>${project.artifactId}</name>
yudao-module-ai1-springai/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <groupId>cn.iocoder.boot</groupId>
  7 + <artifactId>yudao</artifactId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 + <packaging>jar</packaging>
  12 + <artifactId>yudao-module-ai1-springai</artifactId>
  13 +
  14 + <name>${project.artifactId}</name>
  15 + <description>
  16 + ai1 模块,参考 XXL-AI 优化实现的 AI Agent 能力,基于 Spring AI 接入芋道:
  17 + 1、Agent 编排:模型、系统指令、知识库、MCP、SKILL 组合成 Agent,资源支持多选绑定
  18 + 2、多供应商:统一接入 OpenAI 兼容协议的对话与嵌入模型,支持连通测试和远程模型导入
  19 + 3、MCP、SKILL、RAG:远程与本地 MCP、SKILL.md 技能物化执行、Milvus 知识库检索并注入对话
  20 + 4、流式对话:SSE 输出思考过程与回复,Redis Stream 支持集群部署和断线续传
  21 + </description>
  22 + <properties>
  23 + <!-- 以下依赖不在 spring-ai-bom 中,需显式声明版本 -->
  24 + <mcp-sdk.version>2.0.1</mcp-sdk.version>
  25 + <spring-ai-agent-utils.version>0.12.0</spring-ai-agent-utils.version>
  26 + <milvus-sdk.version>3.0.10</milvus-sdk.version>
  27 + </properties>
  28 +
  29 + <dependencies>
  30 + <dependency>
  31 + <groupId>cn.iocoder.boot</groupId>
  32 + <artifactId>yudao-module-system</artifactId>
  33 + <version>${revision}</version>
  34 + </dependency>
  35 + <dependency>
  36 + <groupId>cn.iocoder.boot</groupId>
  37 + <artifactId>yudao-module-infra</artifactId>
  38 + <version>${revision}</version>
  39 + </dependency>
  40 +
  41 + <!-- 业务组件 -->
  42 + <dependency>
  43 + <groupId>cn.iocoder.boot</groupId>
  44 + <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
  45 + </dependency>
  46 +
  47 + <!-- Web 相关 -->
  48 + <dependency>
  49 + <groupId>cn.iocoder.boot</groupId>
  50 + <artifactId>yudao-spring-boot-starter-security</artifactId>
  51 + </dependency>
  52 +
  53 + <!-- DB 相关 -->
  54 + <dependency>
  55 + <groupId>cn.iocoder.boot</groupId>
  56 + <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
  57 + </dependency>
  58 + <dependency>
  59 + <groupId>cn.iocoder.boot</groupId>
  60 + <artifactId>yudao-spring-boot-starter-redis</artifactId>
  61 + </dependency>
  62 +
  63 + <!-- Test 测试相关 -->
  64 + <dependency>
  65 + <groupId>cn.iocoder.boot</groupId>
  66 + <artifactId>yudao-spring-boot-starter-test</artifactId>
  67 + </dependency>
  68 +
  69 + <!-- Spring AI:模型(chat / embedding,OpenAI 兼容协议) -->
  70 + <dependency>
  71 + <groupId>org.springframework.ai</groupId>
  72 + <artifactId>spring-ai-openai</artifactId>
  73 + <exclusions>
  74 + <exclusion>
  75 + <groupId>io.swagger.core.v3</groupId>
  76 + <artifactId>swagger-annotations-jakarta</artifactId>
  77 + </exclusion>
  78 + </exclusions>
  79 + </dependency>
  80 + <!-- agent-utils 的 Spring AI 依赖为 provided,显式使用基础项目 BOM 管理的 1.x ChatClient -->
  81 + <dependency>
  82 + <groupId>org.springframework.ai</groupId>
  83 + <artifactId>spring-ai-client-chat</artifactId>
  84 + </dependency>
  85 + <!-- Spring AI:RAG Advisor(QuestionAnswerAdvisor) -->
  86 + <dependency>
  87 + <groupId>org.springframework.ai</groupId>
  88 + <artifactId>spring-ai-advisors-vector-store</artifactId>
  89 + </dependency>
  90 + <!-- Spring AI:Milvus 向量存储(非 starter 构件) -->
  91 + <dependency>
  92 + <groupId>org.springframework.ai</groupId>
  93 + <artifactId>spring-ai-milvus-store</artifactId>
  94 + <exclusions>
  95 + <!-- 解决和 logback 的日志冲突 -->
  96 + <exclusion>
  97 + <groupId>org.slf4j</groupId>
  98 + <artifactId>slf4j-reload4j</artifactId>
  99 + </exclusion>
  100 + </exclusions>
  101 + </dependency>
  102 + <dependency>
  103 + <groupId>io.milvus</groupId>
  104 + <artifactId>milvus-sdk-java</artifactId>
  105 + <version>${milvus-sdk.version}</version>
  106 + <exclusions>
  107 + <!-- 解决和 logback 的日志冲突 -->
  108 + <exclusion>
  109 + <groupId>org.slf4j</groupId>
  110 + <artifactId>slf4j-reload4j</artifactId>
  111 + </exclusion>
  112 + </exclusions>
  113 + </dependency>
  114 + <!-- spring-ai-agent-utils:SkillsTool 技能系统(SKILL.md)及 Shell / FileSystem 等执行工具 -->
  115 + <dependency>
  116 + <groupId>org.springaicommunity</groupId>
  117 + <artifactId>spring-ai-agent-utils</artifactId>
  118 + <version>${spring-ai-agent-utils.version}</version>
  119 + </dependency>
  120 + <!-- MCP:官方 Java MCP SDK(stdio / Streamable HTTP 客户端) -->
  121 + <dependency>
  122 + <groupId>io.modelcontextprotocol.sdk</groupId>
  123 + <artifactId>mcp-json-jackson2</artifactId>
  124 + <version>${mcp-sdk.version}</version>
  125 + </dependency>
  126 + </dependencies>
  127 +
  128 +</project>
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/agent/Ai1AgentController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.agent;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.agent.vo.Ai1AgentPageReqVO;
  10 +import cn.iocoder.yudao.module.ai1.controller.admin.agent.vo.Ai1AgentRespVO;
  11 +import cn.iocoder.yudao.module.ai1.controller.admin.agent.vo.Ai1AgentSaveReqVO;
  12 +import cn.iocoder.yudao.module.ai1.controller.admin.agent.vo.Ai1AgentUpdateStatusReqVO;
  13 +import cn.iocoder.yudao.module.ai1.dal.dataobject.agent.Ai1AgentDO;
  14 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ModelDO;
  15 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ProviderDO;
  16 +import cn.iocoder.yudao.module.ai1.service.agent.Ai1AgentService;
  17 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ModelService;
  18 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ProviderService;
  19 +import io.swagger.v3.oas.annotations.Operation;
  20 +import io.swagger.v3.oas.annotations.Parameter;
  21 +import io.swagger.v3.oas.annotations.tags.Tag;
  22 +import jakarta.annotation.Resource;
  23 +import jakarta.validation.Valid;
  24 +import org.springframework.security.access.prepost.PreAuthorize;
  25 +import org.springframework.validation.annotation.Validated;
  26 +import org.springframework.web.bind.annotation.*;
  27 +
  28 +import java.util.Collections;
  29 +import java.util.List;
  30 +import java.util.Map;
  31 +
  32 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  35 +
  36 +@Tag(name = "管理后台 - AI1 Agent")
  37 +@RestController
  38 +@RequestMapping("/ai1/agent")
  39 +@Validated
  40 +public class Ai1AgentController {
  41 +
  42 + @Resource
  43 + private Ai1AgentService agentService;
  44 + @Resource
  45 + private Ai1ProviderService providerService;
  46 + @Resource
  47 + private Ai1ModelService modelService;
  48 +
  49 + @PostMapping("/create")
  50 + @Operation(summary = "创建 Agent")
  51 + @PreAuthorize("@ss.hasPermission('ai1:agent:create')")
  52 + public CommonResult<Long> createAgent(@Valid @RequestBody Ai1AgentSaveReqVO createReqVO) {
  53 + return success(agentService.createAgent(createReqVO));
  54 + }
  55 +
  56 + @PutMapping("/update")
  57 + @Operation(summary = "更新 Agent")
  58 + @PreAuthorize("@ss.hasPermission('ai1:agent:update')")
  59 + public CommonResult<Boolean> updateAgent(@Valid @RequestBody Ai1AgentSaveReqVO updateReqVO) {
  60 + agentService.updateAgent(updateReqVO);
  61 + return success(true);
  62 + }
  63 +
  64 + @DeleteMapping("/delete")
  65 + @Operation(summary = "删除 Agent", description = "级联删除会话与消息")
  66 + @Parameter(name = "id", description = "编号", required = true)
  67 + @PreAuthorize("@ss.hasPermission('ai1:agent:delete')")
  68 + public CommonResult<Boolean> deleteAgent(@RequestParam("id") Long id) {
  69 + agentService.deleteAgent(id);
  70 + return success(true);
  71 + }
  72 +
  73 + @DeleteMapping("/delete-list")
  74 + @Operation(summary = "批量删除 Agent", description = "级联删除会话与消息")
  75 + @Parameter(name = "ids", description = "编号列表", required = true)
  76 + @PreAuthorize("@ss.hasPermission('ai1:agent:delete')")
  77 + public CommonResult<Boolean> deleteAgentList(@RequestParam("ids") List<Long> ids) {
  78 + agentService.deleteAgentListByIds(ids);
  79 + return success(true);
  80 + }
  81 +
  82 + @GetMapping("/get")
  83 + @Operation(summary = "获得 Agent")
  84 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  85 + @PreAuthorize("@ss.hasPermission('ai1:agent:query')")
  86 + public CommonResult<Ai1AgentRespVO> getAgent(@RequestParam("id") Long id) {
  87 + Ai1AgentDO agent = agentService.getAgent(id);
  88 + return success(buildAgentRespVO(agent));
  89 + }
  90 +
  91 + @GetMapping("/page")
  92 + @Operation(summary = "获得 Agent 分页")
  93 + @PreAuthorize("@ss.hasPermission('ai1:agent:query')")
  94 + public CommonResult<PageResult<Ai1AgentRespVO>> getAgentPage(@Valid Ai1AgentPageReqVO pageReqVO) {
  95 + PageResult<Ai1AgentDO> pageResult = agentService.getAgentPage(pageReqVO);
  96 + return success(new PageResult<>(buildAgentRespVOList(pageResult.getList()), pageResult.getTotal()));
  97 + }
  98 +
  99 + @GetMapping("/simple-list")
  100 + @Operation(summary = "获得已开启的 Agent 精简列表", description = "用于会话页选择 Agent,仅返回已开启的 Agent")
  101 + public CommonResult<List<Ai1AgentRespVO>> getAgentSimpleList() {
  102 + List<Ai1AgentDO> list = agentService.getAgentListByStatus(CommonStatusEnum.ENABLE.getStatus());
  103 + Map<Long, Ai1ModelDO> modelMap = modelService.getModelMap(convertSet(list, Ai1AgentDO::getModelId));
  104 + return success(convertList(list, agent -> {
  105 + Ai1AgentRespVO respVO = new Ai1AgentRespVO().setId(agent.getId()).setName(agent.getName())
  106 + .setIntroduction(agent.getIntroduction()).setModelId(agent.getModelId());
  107 + MapUtils.findAndThen(modelMap, agent.getModelId(), model -> respVO.setModelName(model.getName()));
  108 + return respVO;
  109 + }));
  110 + }
  111 +
  112 + @PutMapping("/update-status")
  113 + @Operation(summary = "修改 Agent 状态")
  114 + @PreAuthorize("@ss.hasPermission('ai1:agent:update')")
  115 + public CommonResult<Boolean> updateAgentStatus(@Valid @RequestBody Ai1AgentUpdateStatusReqVO reqVO) {
  116 + agentService.updateAgentStatus(reqVO.getId(), reqVO.getStatus());
  117 + return success(true);
  118 + }
  119 +
  120 + // ==================== 拼接 VO ====================
  121 +
  122 + private Ai1AgentRespVO buildAgentRespVO(Ai1AgentDO agent) {
  123 + if (agent == null) {
  124 + return null;
  125 + }
  126 + return CollUtil.getFirst(buildAgentRespVOList(Collections.singletonList(agent)));
  127 + }
  128 +
  129 + private List<Ai1AgentRespVO> buildAgentRespVOList(List<Ai1AgentDO> list) {
  130 + Map<Long, Ai1ProviderDO> providerMap = providerService.getProviderMap(convertSet(list, Ai1AgentDO::getProviderId));
  131 + Map<Long, Ai1ModelDO> modelMap = modelService.getModelMap(convertSet(list, Ai1AgentDO::getModelId));
  132 + return BeanUtils.toBean(list, Ai1AgentRespVO.class, respVO -> {
  133 + MapUtils.findAndThen(providerMap, respVO.getProviderId(), provider -> respVO.setProviderName(provider.getName()));
  134 + MapUtils.findAndThen(modelMap, respVO.getModelId(), model -> respVO.setModelName(model.getName()));
  135 + });
  136 + }
  137 +
  138 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/agent/vo/Ai1AgentPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.agent.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import lombok.Data;
  8 +import lombok.EqualsAndHashCode;
  9 +import lombok.ToString;
  10 +
  11 +@Schema(description = "管理后台 - AI1 Agent 分页 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class Ai1AgentPageReqVO extends PageParam {
  16 +
  17 + @Schema(description = "名称,模糊匹配", example = "客服助手")
  18 + private String name;
  19 +
  20 + @Schema(description = "状态", example = "0")
  21 + @InEnum(CommonStatusEnum.class)
  22 + private Integer status;
  23 +
  24 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/agent/vo/Ai1AgentRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.agent.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - AI1 Agent Response VO")
  10 +@Data
  11 +public class Ai1AgentRespVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客服助手")
  17 + private String name;
  18 +
  19 + @Schema(description = "介绍", example = "解答产品使用问题")
  20 + private String introduction;
  21 +
  22 + @Schema(description = "模型所属 Provider 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  23 + private Long providerId;
  24 +
  25 + @Schema(description = "模型所属 Provider 名称", example = "DeepSeek")
  26 + private String providerName;
  27 +
  28 + @Schema(description = "对话模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  29 + private Long modelId;
  30 +
  31 + @Schema(description = "对话模型名称", example = "DeepSeek Chat")
  32 + private String modelName;
  33 +
  34 + @Schema(description = "系统指令", example = "你是一名专业的客服")
  35 + private String systemPrompt;
  36 +
  37 + @Schema(description = "知识库编号集合", example = "[1, 2]")
  38 + private List<Long> knowledgeBaseIds;
  39 +
  40 + @Schema(description = "MCP 编号集合", example = "[1]")
  41 + private List<Long> mcpIds;
  42 +
  43 + @Schema(description = "SKILL 编号集合", example = "[1]")
  44 + private List<Long> skillIds;
  45 +
  46 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  47 + private Integer status;
  48 +
  49 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  50 + private LocalDateTime createTime;
  51 +
  52 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  53 + private LocalDateTime updateTime;
  54 +
  55 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/agent/vo/Ai1AgentSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.agent.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +import java.util.List;
  10 +
  11 +@Schema(description = "管理后台 - AI1 Agent 新增/修改 Request VO")
  12 +@Data
  13 +public class Ai1AgentSaveReqVO {
  14 +
  15 + @Schema(description = "编号", example = "1024")
  16 + private Long id;
  17 +
  18 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "客服助手")
  19 + @NotEmpty(message = "名称不能为空")
  20 + @Size(max = 100, message = "名称长度不能超过 100 个字符")
  21 + private String name;
  22 +
  23 + @Schema(description = "介绍", example = "解答产品使用问题")
  24 + @Size(max = 500, message = "介绍长度不能超过 500 个字符")
  25 + private String introduction;
  26 +
  27 + @Schema(description = "模型所属 Provider 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  28 + @NotNull(message = "Provider 不能为空")
  29 + private Long providerId;
  30 +
  31 + @Schema(description = "对话模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + @NotNull(message = "对话模型不能为空")
  33 + private Long modelId;
  34 +
  35 + @Schema(description = "系统指令", example = "你是一名专业的客服")
  36 + private String systemPrompt;
  37 +
  38 + @Schema(description = "知识库编号集合", example = "[1, 2]")
  39 + private List<Long> knowledgeBaseIds;
  40 +
  41 + @Schema(description = "MCP 编号集合", example = "[1]")
  42 + private List<Long> mcpIds;
  43 +
  44 + @Schema(description = "SKILL 编号集合", example = "[1]")
  45 + private List<Long> skillIds;
  46 +
  47 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/agent/vo/Ai1AgentUpdateStatusReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.agent.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotNull;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - AI1 Agent 修改状态 Request VO")
  10 +@Data
  11 +public class Ai1AgentUpdateStatusReqVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + @NotNull(message = "编号不能为空")
  15 + private Long id;
  16 +
  17 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  18 + @NotNull(message = "状态不能为空")
  19 + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}")
  20 + private Integer status;
  21 +
  22 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/home/Ai1HomeController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.home;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  5 +import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeMessageSummaryByAgentRespVO;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeMessageSummaryByDateRespVO;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeSummaryRespVO;
  8 +import cn.iocoder.yudao.module.ai1.dal.dataobject.agent.Ai1AgentDO;
  9 +import cn.iocoder.yudao.module.ai1.service.agent.Ai1AgentService;
  10 +import cn.iocoder.yudao.module.ai1.service.home.Ai1HomeService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.constraints.Max;
  16 +import jakarta.validation.constraints.Min;
  17 +import org.springframework.security.access.prepost.PreAuthorize;
  18 +import org.springframework.validation.annotation.Validated;
  19 +import org.springframework.web.bind.annotation.GetMapping;
  20 +import org.springframework.web.bind.annotation.RequestMapping;
  21 +import org.springframework.web.bind.annotation.RequestParam;
  22 +import org.springframework.web.bind.annotation.RestController;
  23 +
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  29 +
  30 +@Tag(name = "管理后台 - AI1 首页")
  31 +@RestController
  32 +@RequestMapping("/ai1/home")
  33 +@Validated
  34 +public class Ai1HomeController {
  35 +
  36 + @Resource
  37 + private Ai1HomeService homeService;
  38 + @Resource
  39 + private Ai1AgentService agentService;
  40 +
  41 + @GetMapping("/summary")
  42 + @Operation(summary = "获得总量统计")
  43 + @PreAuthorize("@ss.hasPermission('ai1:home:query')")
  44 + public CommonResult<Ai1HomeSummaryRespVO> getSummary() {
  45 + return success(homeService.getSummary());
  46 + }
  47 +
  48 + @GetMapping("/message-summary-by-date")
  49 + @Operation(summary = "获得按日消息统计", description = "近 days 个自然日(含今天)每日消息数量,没有消息的日期补 0")
  50 + @Parameter(name = "days", description = "天数,1 ~ 30,默认 30", example = "30")
  51 + @PreAuthorize("@ss.hasPermission('ai1:home:query')")
  52 + public CommonResult<List<Ai1HomeMessageSummaryByDateRespVO>> getMessageSummaryByDate(
  53 + @RequestParam(value = "days", defaultValue = "30") @Min(value = 1, message = "天数不能小于 1")
  54 + @Max(value = 30, message = "天数不能大于 30") Integer days) {
  55 + return success(homeService.getMessageSummaryByDate(days));
  56 + }
  57 +
  58 + @GetMapping("/message-summary-by-agent")
  59 + @Operation(summary = "获得按 Agent 消息统计", description = "近 days 个自然日(含今天)各 Agent 的消息数量")
  60 + @Parameter(name = "days", description = "天数,1 ~ 30,默认 30", example = "30")
  61 + @PreAuthorize("@ss.hasPermission('ai1:home:query')")
  62 + public CommonResult<List<Ai1HomeMessageSummaryByAgentRespVO>> getMessageSummaryByAgent(
  63 + @RequestParam(value = "days", defaultValue = "30") @Min(value = 1, message = "天数不能小于 1")
  64 + @Max(value = 30, message = "天数不能大于 30") Integer days) {
  65 + return success(buildMessageSummaryByAgentRespVOList(homeService.getMessageSummaryByAgent(days)));
  66 + }
  67 +
  68 + // ==================== 拼接 VO ====================
  69 +
  70 + private List<Ai1HomeMessageSummaryByAgentRespVO> buildMessageSummaryByAgentRespVOList(List<Ai1HomeMessageSummaryByAgentRespVO> list) {
  71 + Map<Long, Ai1AgentDO> agentMap = agentService.getAgentMap(convertSet(list, Ai1HomeMessageSummaryByAgentRespVO::getAgentId));
  72 + list.forEach(item ->
  73 + MapUtils.findAndThen(agentMap, item.getAgentId(), agent -> item.setAgentName(agent.getName())));
  74 + return list;
  75 + }
  76 +
  77 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/home/vo/Ai1HomeMessageSummaryByAgentRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.home.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - AI1 首页按 Agent 消息统计 Response VO")
  7 +@Data
  8 +public class Ai1HomeMessageSummaryByAgentRespVO {
  9 +
  10 + @Schema(description = "Agent 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  11 + private Long agentId;
  12 +
  13 + @Schema(description = "Agent 名称,Agent 已删除时为空", example = "Hi Agent")
  14 + private String agentName;
  15 +
  16 + @Schema(description = "消息数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "36")
  17 + private Long count;
  18 +
  19 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/home/vo/Ai1HomeMessageSummaryByDateRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.home.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - AI1 首页按日消息统计 Response VO")
  7 +@Data
  8 +public class Ai1HomeMessageSummaryByDateRespVO {
  9 +
  10 + @Schema(description = "日期,格式 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-09-29")
  11 + private String date;
  12 +
  13 + @Schema(description = "当日消息数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
  14 + private Long count;
  15 +
  16 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/home/vo/Ai1HomeSummaryRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.home.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - AI1 首页总量统计 Response VO")
  7 +@Data
  8 +public class Ai1HomeSummaryRespVO {
  9 +
  10 + @Schema(description = "Agent 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
  11 + private Long agentCount;
  12 +
  13 + @Schema(description = "SKILL 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  14 + private Long skillCount;
  15 +
  16 + @Schema(description = "MCP 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
  17 + private Long mcpCount;
  18 +
  19 + @Schema(description = "模型数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
  20 + private Long modelCount;
  21 +
  22 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/Ai1KnowledgeBaseController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base.Ai1KnowledgeBasePageReqVO;
  10 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base.Ai1KnowledgeBaseRespVO;
  11 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base.Ai1KnowledgeBaseSaveReqVO;
  12 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base.Ai1KnowledgeSearchRespVO;
  13 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeBaseDO;
  14 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeDocumentDO;
  15 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ModelDO;
  16 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ProviderDO;
  17 +import cn.iocoder.yudao.module.ai1.service.knowledge.Ai1KnowledgeBaseService;
  18 +import cn.iocoder.yudao.module.ai1.service.knowledge.Ai1KnowledgeDocumentService;
  19 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ModelService;
  20 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ProviderService;
  21 +import io.swagger.v3.oas.annotations.Operation;
  22 +import io.swagger.v3.oas.annotations.Parameter;
  23 +import io.swagger.v3.oas.annotations.tags.Tag;
  24 +import jakarta.annotation.Resource;
  25 +import jakarta.validation.Valid;
  26 +import jakarta.validation.constraints.NotEmpty;
  27 +import org.springframework.security.access.prepost.PreAuthorize;
  28 +import org.springframework.validation.annotation.Validated;
  29 +import org.springframework.web.bind.annotation.*;
  30 +
  31 +import java.util.Collections;
  32 +import java.util.List;
  33 +import java.util.Map;
  34 +
  35 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  36 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
  37 +
  38 +@Tag(name = "管理后台 - AI1 知识库")
  39 +@RestController
  40 +@RequestMapping("/ai1/knowledge")
  41 +@Validated
  42 +public class Ai1KnowledgeBaseController {
  43 +
  44 + @Resource
  45 + private Ai1KnowledgeBaseService knowledgeBaseService;
  46 + @Resource
  47 + private Ai1KnowledgeDocumentService knowledgeDocumentService;
  48 + @Resource
  49 + private Ai1ProviderService providerService;
  50 + @Resource
  51 + private Ai1ModelService modelService;
  52 +
  53 + @PostMapping("/create")
  54 + @Operation(summary = "创建知识库")
  55 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:create')")
  56 + public CommonResult<Long> createKnowledgeBase(@Valid @RequestBody Ai1KnowledgeBaseSaveReqVO createReqVO) {
  57 + return success(knowledgeBaseService.createKnowledgeBase(createReqVO));
  58 + }
  59 +
  60 + @PutMapping("/update")
  61 + @Operation(summary = "更新知识库")
  62 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:update')")
  63 + public CommonResult<Boolean> updateKnowledgeBase(@Valid @RequestBody Ai1KnowledgeBaseSaveReqVO updateReqVO) {
  64 + knowledgeBaseService.updateKnowledgeBase(updateReqVO);
  65 + return success(true);
  66 + }
  67 +
  68 + @DeleteMapping("/delete")
  69 + @Operation(summary = "删除知识库", description = "连带删除文档与向量集合")
  70 + @Parameter(name = "id", description = "编号", required = true)
  71 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:delete')")
  72 + public CommonResult<Boolean> deleteKnowledgeBase(@RequestParam("id") Long id) {
  73 + knowledgeBaseService.deleteKnowledgeBase(id);
  74 + return success(true);
  75 + }
  76 +
  77 + @DeleteMapping("/delete-list")
  78 + @Operation(summary = "批量删除知识库", description = "连带删除文档与向量集合")
  79 + @Parameter(name = "ids", description = "编号列表", required = true)
  80 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:delete')")
  81 + public CommonResult<Boolean> deleteKnowledgeBaseList(@RequestParam("ids") List<Long> ids) {
  82 + knowledgeBaseService.deleteKnowledgeBaseListByIds(ids);
  83 + return success(true);
  84 + }
  85 +
  86 + @GetMapping("/get")
  87 + @Operation(summary = "获得知识库")
  88 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  89 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:query')")
  90 + public CommonResult<Ai1KnowledgeBaseRespVO> getKnowledgeBase(@RequestParam("id") Long id) {
  91 + Ai1KnowledgeBaseDO knowledgeBase = knowledgeBaseService.getKnowledgeBase(id);
  92 + return success(buildKnowledgeBaseRespVO(knowledgeBase));
  93 + }
  94 +
  95 + @GetMapping("/page")
  96 + @Operation(summary = "获得知识库分页")
  97 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:query')")
  98 + public CommonResult<PageResult<Ai1KnowledgeBaseRespVO>> getKnowledgeBasePage(@Valid Ai1KnowledgeBasePageReqVO pageReqVO) {
  99 + PageResult<Ai1KnowledgeBaseDO> pageResult = knowledgeBaseService.getKnowledgeBasePage(pageReqVO);
  100 + return success(new PageResult<>(buildKnowledgeBaseRespVOList(pageResult.getList()), pageResult.getTotal()));
  101 + }
  102 +
  103 + @GetMapping("/simple-list")
  104 + @Operation(summary = "获得知识库精简列表", description = "只包含开启状态,用于 Agent 绑定知识库的下拉选择")
  105 + public CommonResult<List<Ai1KnowledgeBaseRespVO>> getKnowledgeBaseSimpleList() {
  106 + List<Ai1KnowledgeBaseDO> list = knowledgeBaseService.getKnowledgeBaseListByStatus(CommonStatusEnum.ENABLE.getStatus());
  107 + return success(convertList(list, knowledgeBase -> new Ai1KnowledgeBaseRespVO()
  108 + .setId(knowledgeBase.getId()).setName(knowledgeBase.getName())));
  109 + }
  110 +
  111 + @GetMapping("/search")
  112 + @Operation(summary = "知识库检索测试")
  113 + @Parameter(name = "id", description = "知识库编号", required = true, example = "1024")
  114 + @Parameter(name = "query", description = "查询文本", required = true, example = "三体舰队")
  115 + @Parameter(name = "topK", description = "检索数量,为空时使用知识库配置", example = "5")
  116 + @PreAuthorize("@ss.hasPermission('ai1:knowledge:search')")
  117 + public CommonResult<List<Ai1KnowledgeSearchRespVO>> searchKnowledgeBase(
  118 + @RequestParam("id") Long id,
  119 + @RequestParam("query") @NotEmpty(message = "检索内容不能为空") String query,
  120 + @RequestParam(value = "topK", required = false) Integer topK) {
  121 + List<Ai1KnowledgeSearchRespVO> list = knowledgeBaseService.searchKnowledgeBase(id, query, topK);
  122 + return success(buildKnowledgeSearchRespVOList(list));
  123 + }
  124 +
  125 + // ==================== 拼接 VO ====================
  126 +
  127 + private Ai1KnowledgeBaseRespVO buildKnowledgeBaseRespVO(Ai1KnowledgeBaseDO knowledgeBase) {
  128 + if (knowledgeBase == null) {
  129 + return null;
  130 + }
  131 + return CollUtil.getFirst(buildKnowledgeBaseRespVOList(Collections.singletonList(knowledgeBase)));
  132 + }
  133 +
  134 + private List<Ai1KnowledgeBaseRespVO> buildKnowledgeBaseRespVOList(List<Ai1KnowledgeBaseDO> list) {
  135 + Map<Long, Ai1ProviderDO> providerMap = providerService.getProviderMap(
  136 + convertSet(list, Ai1KnowledgeBaseDO::getEmbeddingProviderId));
  137 + Map<Long, Ai1ModelDO> modelMap = modelService.getModelMap(
  138 + convertSet(list, Ai1KnowledgeBaseDO::getEmbeddingModelId));
  139 + return BeanUtils.toBean(list, Ai1KnowledgeBaseRespVO.class, respVO -> {
  140 + MapUtils.findAndThen(providerMap, respVO.getEmbeddingProviderId(),
  141 + provider -> respVO.setEmbeddingProviderName(provider.getName()));
  142 + MapUtils.findAndThen(modelMap, respVO.getEmbeddingModelId(),
  143 + model -> respVO.setEmbeddingModelName(model.getName()));
  144 + });
  145 + }
  146 +
  147 + private List<Ai1KnowledgeSearchRespVO> buildKnowledgeSearchRespVOList(List<Ai1KnowledgeSearchRespVO> list) {
  148 + Map<Long, Ai1KnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
  149 + convertSet(list, Ai1KnowledgeSearchRespVO::getDocumentId));
  150 + list.forEach(respVO -> MapUtils.findAndThen(documentMap, respVO.getDocumentId(),
  151 + document -> respVO.setDocumentName(document.getName())));
  152 + return list;
  153 + }
  154 +
  155 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/Ai1KnowledgeDocumentController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document.Ai1KnowledgeDocumentPageReqVO;
  8 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document.Ai1KnowledgeDocumentRespVO;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document.Ai1KnowledgeDocumentSaveReqVO;
  10 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document.Ai1KnowledgeDocumentVectorizeRespVO;
  11 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeDocumentDO;
  12 +import cn.iocoder.yudao.module.ai1.service.knowledge.Ai1KnowledgeDocumentService;
  13 +import io.swagger.v3.oas.annotations.Operation;
  14 +import io.swagger.v3.oas.annotations.Parameter;
  15 +import io.swagger.v3.oas.annotations.tags.Tag;
  16 +import jakarta.annotation.Resource;
  17 +import jakarta.validation.Valid;
  18 +import org.springframework.security.access.prepost.PreAuthorize;
  19 +import org.springframework.validation.annotation.Validated;
  20 +import org.springframework.web.bind.annotation.*;
  21 +import org.springframework.web.multipart.MultipartFile;
  22 +
  23 +import java.util.List;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  26 +
  27 +@Tag(name = "管理后台 - AI1 知识文档")
  28 +@RestController
  29 +@RequestMapping("/ai1/knowledge/document")
  30 +@Validated
  31 +public class Ai1KnowledgeDocumentController {
  32 +
  33 + @Resource
  34 + private Ai1KnowledgeDocumentService knowledgeDocumentService;
  35 +
  36 + @PostMapping("/create")
  37 + @Operation(summary = "创建文档(粘贴文本)")
  38 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:create')")
  39 + public CommonResult<Long> createKnowledgeDocument(@Valid @RequestBody Ai1KnowledgeDocumentSaveReqVO createReqVO) {
  40 + return success(knowledgeDocumentService.createKnowledgeDocument(createReqVO));
  41 + }
  42 +
  43 + @PostMapping("/upload")
  44 + @Operation(summary = "上传文档", description = "仅支持 .txt / .md 文本文件")
  45 + @Parameter(name = "knowledgeBaseId", description = "知识库编号", required = true, example = "1")
  46 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:create')")
  47 + public CommonResult<Long> uploadKnowledgeDocument(@RequestParam("knowledgeBaseId") Long knowledgeBaseId,
  48 + @RequestParam("file") MultipartFile file) {
  49 + return success(knowledgeDocumentService.uploadKnowledgeDocument(knowledgeBaseId, file));
  50 + }
  51 +
  52 + @PutMapping("/update")
  53 + @Operation(summary = "更新文档", description = "内容变更后需要重新向量化")
  54 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:update')")
  55 + public CommonResult<Boolean> updateKnowledgeDocument(@Valid @RequestBody Ai1KnowledgeDocumentSaveReqVO updateReqVO) {
  56 + knowledgeDocumentService.updateKnowledgeDocument(updateReqVO);
  57 + return success(true);
  58 + }
  59 +
  60 + @DeleteMapping("/delete")
  61 + @Operation(summary = "删除文档")
  62 + @Parameter(name = "id", description = "编号", required = true)
  63 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:delete')")
  64 + public CommonResult<Boolean> deleteKnowledgeDocument(@RequestParam("id") Long id) {
  65 + knowledgeDocumentService.deleteKnowledgeDocument(id);
  66 + return success(true);
  67 + }
  68 +
  69 + @DeleteMapping("/delete-list")
  70 + @Operation(summary = "批量删除文档")
  71 + @Parameter(name = "ids", description = "编号列表", required = true)
  72 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:delete')")
  73 + public CommonResult<Boolean> deleteKnowledgeDocumentList(@RequestParam("ids") List<Long> ids) {
  74 + knowledgeDocumentService.deleteKnowledgeDocumentListByIds(ids);
  75 + return success(true);
  76 + }
  77 +
  78 + @GetMapping("/get")
  79 + @Operation(summary = "获得文档", description = "包含完整内容")
  80 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  81 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:query')")
  82 + public CommonResult<Ai1KnowledgeDocumentRespVO> getKnowledgeDocument(@RequestParam("id") Long id) {
  83 + Ai1KnowledgeDocumentDO document = knowledgeDocumentService.getKnowledgeDocument(id);
  84 + return success(BeanUtils.toBean(document, Ai1KnowledgeDocumentRespVO.class,
  85 + respVO -> respVO.setContentLength(StrUtil.length(respVO.getContent()))));
  86 + }
  87 +
  88 + @GetMapping("/page")
  89 + @Operation(summary = "获得文档分页", description = "不返回文档内容")
  90 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:query')")
  91 + public CommonResult<PageResult<Ai1KnowledgeDocumentRespVO>> getKnowledgeDocumentPage(@Valid Ai1KnowledgeDocumentPageReqVO pageReqVO) {
  92 + PageResult<Ai1KnowledgeDocumentDO> pageResult = knowledgeDocumentService.getKnowledgeDocumentPage(pageReqVO);
  93 + return success(BeanUtils.toBean(pageResult, Ai1KnowledgeDocumentRespVO.class,
  94 + respVO -> respVO.setContentLength(StrUtil.length(respVO.getContent())).setContent(null)));
  95 + }
  96 +
  97 + @PostMapping("/vectorize")
  98 + @Operation(summary = "文档向量化", description = "返回分片数量")
  99 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  100 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:vectorize')")
  101 + public CommonResult<Integer> vectorizeKnowledgeDocument(@RequestParam("id") Long id) {
  102 + return success(knowledgeDocumentService.vectorizeKnowledgeDocument(id));
  103 + }
  104 +
  105 + @PostMapping("/vectorize-all")
  106 + @Operation(summary = "知识库批量向量化")
  107 + @Parameter(name = "knowledgeBaseId", description = "知识库编号", required = true, example = "1")
  108 + @PreAuthorize("@ss.hasPermission('ai1:knowledge-document:vectorize')")
  109 + public CommonResult<Ai1KnowledgeDocumentVectorizeRespVO> vectorizeKnowledgeDocumentAll(
  110 + @RequestParam("knowledgeBaseId") Long knowledgeBaseId) {
  111 + return success(knowledgeDocumentService.vectorizeKnowledgeDocumentListByKnowledgeBaseId(knowledgeBaseId));
  112 + }
  113 +
  114 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/base/Ai1KnowledgeBasePageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "管理后台 - AI1 知识库分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class Ai1KnowledgeBasePageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "名称,模糊匹配", example = "产品手册")
  16 + private String name;
  17 +
  18 + @Schema(description = "状态", example = "0")
  19 + private Integer status;
  20 +
  21 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/base/Ai1KnowledgeBaseRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 知识库 Response VO")
  9 +@Data
  10 +public class Ai1KnowledgeBaseRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品手册")
  16 + private String name;
  17 +
  18 + @Schema(description = "描述", example = "产品使用说明")
  19 + private String description;
  20 +
  21 + @Schema(description = "向量化 Provider 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  22 + private Long embeddingProviderId;
  23 +
  24 + @Schema(description = "向量化 Provider 名称", example = "Ollama")
  25 + private String embeddingProviderName;
  26 +
  27 + @Schema(description = "向量化模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  28 + private Long embeddingModelId;
  29 +
  30 + @Schema(description = "向量化模型名称", example = "bge-m3")
  31 + private String embeddingModelName;
  32 +
  33 + @Schema(description = "分片大小,单位:字符", requiredMode = Schema.RequiredMode.REQUIRED, example = "500")
  34 + private Integer chunkSize;
  35 +
  36 + @Schema(description = "分片重叠,单位:字符", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
  37 + private Integer chunkOverlap;
  38 +
  39 + @Schema(description = "检索数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
  40 + private Integer topK;
  41 +
  42 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  43 + private Integer status;
  44 +
  45 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  46 + private LocalDateTime createTime;
  47 +
  48 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  49 + private LocalDateTime updateTime;
  50 +
  51 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/base/Ai1KnowledgeBaseSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import com.fasterxml.jackson.annotation.JsonIgnore;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.*;
  8 +import lombok.Data;
  9 +
  10 +@Schema(description = "管理后台 - AI1 知识库新增/修改 Request VO")
  11 +@Data
  12 +public class Ai1KnowledgeBaseSaveReqVO {
  13 +
  14 + @Schema(description = "编号", example = "1024")
  15 + private Long id;
  16 +
  17 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品手册")
  18 + @NotEmpty(message = "名称不能为空")
  19 + @Size(max = 100, message = "名称长度不能超过 100 个字符")
  20 + private String name;
  21 +
  22 + @Schema(description = "描述", example = "产品使用说明")
  23 + @Size(max = 500, message = "描述长度不能超过 500 个字符")
  24 + private String description;
  25 +
  26 + @Schema(description = "向量化 Provider 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  27 + @NotNull(message = "向量化 Provider 不能为空")
  28 + private Long embeddingProviderId;
  29 +
  30 + @Schema(description = "向量化模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  31 + @NotNull(message = "向量化模型不能为空")
  32 + private Long embeddingModelId;
  33 +
  34 + @Schema(description = "分片大小,单位:字符", requiredMode = Schema.RequiredMode.REQUIRED, example = "500")
  35 + @NotNull(message = "分片大小不能为空")
  36 + @Min(value = 100, message = "分片大小不能小于 100")
  37 + private Integer chunkSize;
  38 +
  39 + @Schema(description = "分片重叠,单位:字符", requiredMode = Schema.RequiredMode.REQUIRED, example = "50")
  40 + @NotNull(message = "分片重叠不能为空")
  41 + @Min(value = 0, message = "分片重叠不能小于 0")
  42 + private Integer chunkOverlap;
  43 +
  44 + @Schema(description = "检索数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
  45 + @NotNull(message = "检索数量不能为空")
  46 + @Min(value = 1, message = "检索数量必须大于 0")
  47 + private Integer topK;
  48 +
  49 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  50 + @NotNull(message = "状态不能为空")
  51 + @InEnum(CommonStatusEnum.class)
  52 + private Integer status;
  53 +
  54 + @AssertTrue(message = "分片重叠必须小于分片大小")
  55 + @JsonIgnore
  56 + public boolean isChunkOverlapValid() {
  57 + return chunkSize == null || chunkOverlap == null || chunkOverlap < chunkSize;
  58 + }
  59 +
  60 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/base/Ai1KnowledgeSearchRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - AI1 知识库检索命中 Response VO")
  7 +@Data
  8 +public class Ai1KnowledgeSearchRespVO {
  9 +
  10 + @Schema(description = "分片文本", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体舰队距离地球还有四光年")
  11 + private String text;
  12 +
  13 + @Schema(description = "文档编号", example = "1")
  14 + private Long documentId;
  15 +
  16 + @Schema(description = "文档名称", example = "三体.txt")
  17 + private String documentName;
  18 +
  19 + @Schema(description = "分片序号", example = "0")
  20 + private Integer chunkIndex;
  21 +
  22 + @Schema(description = "相似度得分", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.86")
  23 + private Double score;
  24 +
  25 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/document/Ai1KnowledgeDocumentPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +import lombok.ToString;
  9 +
  10 +@Schema(description = "管理后台 - AI1 知识文档分页 Request VO")
  11 +@Data
  12 +@EqualsAndHashCode(callSuper = true)
  13 +@ToString(callSuper = true)
  14 +public class Ai1KnowledgeDocumentPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + @NotNull(message = "知识库编号不能为空")
  18 + private Long knowledgeBaseId;
  19 +
  20 + @Schema(description = "名称,模糊匹配", example = "三体")
  21 + private String name;
  22 +
  23 + @Schema(description = "向量化状态", example = "1")
  24 + private Integer status;
  25 +
  26 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/document/Ai1KnowledgeDocumentRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 知识文档 Response VO")
  9 +@Data
  10 +public class Ai1KnowledgeDocumentRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long knowledgeBaseId;
  17 +
  18 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体.txt")
  19 + private String name;
  20 +
  21 + @Schema(description = "内容;分页列表不返回,编辑时通过获取接口加载", example = "三体舰队距离地球还有四光年")
  22 + private String content;
  23 +
  24 + @Schema(description = "内容长度,单位:字符", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  25 + private Integer contentLength;
  26 +
  27 + @Schema(description = "分片数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
  28 + private Integer chunkCount;
  29 +
  30 + @Schema(description = "向量化状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  31 + private Integer status;
  32 +
  33 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private LocalDateTime createTime;
  35 +
  36 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  37 + private LocalDateTime updateTime;
  38 +
  39 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/document/Ai1KnowledgeDocumentSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - AI1 知识文档新增/修改 Request VO")
  10 +@Data
  11 +public class Ai1KnowledgeDocumentSaveReqVO {
  12 +
  13 + @Schema(description = "编号", example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "知识库编号;修改时不允许变更", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + @NotNull(message = "知识库编号不能为空")
  18 + private Long knowledgeBaseId;
  19 +
  20 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体.txt")
  21 + @NotEmpty(message = "名称不能为空")
  22 + @Size(max = 255, message = "名称长度不能超过 255 个字符")
  23 + private String name;
  24 +
  25 + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体舰队距离地球还有四光年")
  26 + @NotEmpty(message = "内容不能为空")
  27 + @Size(max = 16_777_215, message = "内容长度不能超过 16777215 个字符")
  28 + private String content;
  29 +
  30 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/knowledge/vo/document/Ai1KnowledgeDocumentVectorizeRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +@Schema(description = "管理后台 - AI1 知识库批量向量化 Response VO")
  9 +@Data
  10 +@NoArgsConstructor
  11 +@AllArgsConstructor
  12 +public class Ai1KnowledgeDocumentVectorizeRespVO {
  13 +
  14 + @Schema(description = "成功数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
  15 + private Integer successCount;
  16 +
  17 + @Schema(description = "失败数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  18 + private Integer failureCount;
  19 +
  20 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/mcp/Ai1McpController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.mcp;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo.Ai1McpConnectRespVO;
  8 +import cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo.Ai1McpPageReqVO;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo.Ai1McpRespVO;
  10 +import cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo.Ai1McpSaveReqVO;
  11 +import cn.iocoder.yudao.module.ai1.dal.dataobject.mcp.Ai1McpDO;
  12 +import cn.iocoder.yudao.module.ai1.service.mcp.Ai1McpService;
  13 +import io.swagger.v3.oas.annotations.Operation;
  14 +import io.swagger.v3.oas.annotations.Parameter;
  15 +import io.swagger.v3.oas.annotations.tags.Tag;
  16 +import jakarta.annotation.Resource;
  17 +import jakarta.validation.Valid;
  18 +import org.springframework.security.access.prepost.PreAuthorize;
  19 +import org.springframework.validation.annotation.Validated;
  20 +import org.springframework.web.bind.annotation.*;
  21 +
  22 +import java.util.List;
  23 +
  24 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  26 +
  27 +@Tag(name = "管理后台 - AI1 MCP")
  28 +@RestController
  29 +@RequestMapping("/ai1/mcp")
  30 +@Validated
  31 +public class Ai1McpController {
  32 +
  33 + @Resource
  34 + private Ai1McpService mcpService;
  35 +
  36 + @PostMapping("/create")
  37 + @Operation(summary = "创建 MCP")
  38 + @PreAuthorize("@ss.hasPermission('ai1:mcp:create')")
  39 + public CommonResult<Long> createMcp(@Valid @RequestBody Ai1McpSaveReqVO createReqVO) {
  40 + return success(mcpService.createMcp(createReqVO));
  41 + }
  42 +
  43 + @PutMapping("/update")
  44 + @Operation(summary = "更新 MCP")
  45 + @PreAuthorize("@ss.hasPermission('ai1:mcp:update')")
  46 + public CommonResult<Boolean> updateMcp(@Valid @RequestBody Ai1McpSaveReqVO updateReqVO) {
  47 + mcpService.updateMcp(updateReqVO);
  48 + return success(true);
  49 + }
  50 +
  51 + @DeleteMapping("/delete")
  52 + @Operation(summary = "删除 MCP")
  53 + @Parameter(name = "id", description = "编号", required = true)
  54 + @PreAuthorize("@ss.hasPermission('ai1:mcp:delete')")
  55 + public CommonResult<Boolean> deleteMcp(@RequestParam("id") Long id) {
  56 + mcpService.deleteMcp(id);
  57 + return success(true);
  58 + }
  59 +
  60 + @DeleteMapping("/delete-list")
  61 + @Operation(summary = "批量删除 MCP")
  62 + @Parameter(name = "ids", description = "编号列表", required = true)
  63 + @PreAuthorize("@ss.hasPermission('ai1:mcp:delete')")
  64 + public CommonResult<Boolean> deleteMcpList(@RequestParam("ids") List<Long> ids) {
  65 + mcpService.deleteMcpListByIds(ids);
  66 + return success(true);
  67 + }
  68 +
  69 + @GetMapping("/get")
  70 + @Operation(summary = "获得 MCP")
  71 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  72 + @PreAuthorize("@ss.hasPermission('ai1:mcp:query')")
  73 + public CommonResult<Ai1McpRespVO> getMcp(@RequestParam("id") Long id) {
  74 + Ai1McpDO mcp = mcpService.getMcp(id);
  75 + return success(BeanUtils.toBean(mcp, Ai1McpRespVO.class));
  76 + }
  77 +
  78 + @GetMapping("/page")
  79 + @Operation(summary = "获得 MCP 分页")
  80 + @PreAuthorize("@ss.hasPermission('ai1:mcp:query')")
  81 + public CommonResult<PageResult<Ai1McpRespVO>> getMcpPage(@Valid Ai1McpPageReqVO pageReqVO) {
  82 + PageResult<Ai1McpDO> pageResult = mcpService.getMcpPage(pageReqVO);
  83 + return success(BeanUtils.toBean(pageResult, Ai1McpRespVO.class));
  84 + }
  85 +
  86 + @GetMapping("/simple-list")
  87 + @Operation(summary = "获得 MCP 精简列表", description = "只包含开启状态,用于 Agent 绑定 MCP 的下拉选择")
  88 + public CommonResult<List<Ai1McpRespVO>> getMcpSimpleList() {
  89 + List<Ai1McpDO> list = mcpService.getMcpListByStatus(CommonStatusEnum.ENABLE.getStatus());
  90 + return success(convertList(list, mcp -> new Ai1McpRespVO()
  91 + .setId(mcp.getId()).setName(mcp.getName()).setTransport(mcp.getTransport())));
  92 + }
  93 +
  94 + @PostMapping("/test")
  95 + @Operation(summary = "MCP 连通测试", description = "initialize + tools/list")
  96 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  97 + @PreAuthorize("@ss.hasPermission('ai1:mcp:test')")
  98 + public CommonResult<Ai1McpConnectRespVO> testMcp(@RequestParam("id") Long id) {
  99 + return success(mcpService.testMcpConnect(id));
  100 + }
  101 +
  102 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/mcp/vo/Ai1McpConnectRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +
  8 +@Schema(description = "管理后台 - AI1 MCP 连通测试 Response VO")
  9 +@Data
  10 +public class Ai1McpConnectRespVO {
  11 +
  12 + @Schema(description = "是否连通", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  13 + private Boolean connectable;
  14 +
  15 + @Schema(description = "服务名称", example = "weather-server")
  16 + private String serverName;
  17 +
  18 + @Schema(description = "服务版本", example = "1.0.0")
  19 + private String serverVersion;
  20 +
  21 + @Schema(description = "服务说明", example = "提供城市天气查询")
  22 + private String instructions;
  23 +
  24 + @Schema(description = "可用工具数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  25 + private Integer toolCount;
  26 +
  27 + @Schema(description = "测试耗时,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "320")
  28 + private Long elapsedMs;
  29 +
  30 + @Schema(description = "测试过程描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "连接成功,发现 2 个工具")
  31 + private String message;
  32 +
  33 + @Schema(description = "可用工具明细")
  34 + private List<Tool> tools;
  35 +
  36 + @Schema(description = "MCP 工具")
  37 + @Data
  38 + public static class Tool {
  39 +
  40 + @Schema(description = "工具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "get_weather")
  41 + private String name;
  42 +
  43 + @Schema(description = "工具标题", example = "天气查询")
  44 + private String title;
  45 +
  46 + @Schema(description = "工具描述", example = "查询指定城市的天气")
  47 + private String description;
  48 +
  49 + }
  50 +
  51 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/mcp/vo/Ai1McpPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "管理后台 - AI1 MCP 分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class Ai1McpPageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "名称,模糊匹配", example = "天气")
  16 + private String name;
  17 +
  18 + @Schema(description = "状态", example = "0")
  19 + private Integer status;
  20 +
  21 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/mcp/vo/Ai1McpRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.Map;
  8 +
  9 +@Schema(description = "管理后台 - AI1 MCP Response VO")
  10 +@Data
  11 +public class Ai1McpRespVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "天气查询")
  17 + private String name;
  18 +
  19 + @Schema(description = "传输方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "http")
  20 + private String transport;
  21 +
  22 + @Schema(description = "服务地址", example = "http://127.0.0.1:8081/mcp")
  23 + private String url;
  24 +
  25 + @Schema(description = "请求头", example = "{\"Authorization\":\"Bearer xxx\"}")
  26 + private Map<String, String> headers;
  27 +
  28 + @Schema(description = "完整 MCP 配置,JSON 对象", example = "{\"transport\":\"http\",\"url\":\"http://127.0.0.1:8081/mcp\"}")
  29 + private String config;
  30 +
  31 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  32 + private Integer status;
  33 +
  34 + @Schema(description = "备注", example = "查询城市天气")
  35 + private String remark;
  36 +
  37 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  38 + private LocalDateTime createTime;
  39 +
  40 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  41 + private LocalDateTime updateTime;
  42 +
  43 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/mcp/vo/Ai1McpSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  7 +import cn.iocoder.yudao.module.ai1.enums.mcp.Ai1McpTransportEnum;
  8 +import com.fasterxml.jackson.annotation.JsonIgnore;
  9 +import io.swagger.v3.oas.annotations.media.Schema;
  10 +import jakarta.validation.constraints.AssertTrue;
  11 +import jakarta.validation.constraints.NotEmpty;
  12 +import jakarta.validation.constraints.NotNull;
  13 +import jakarta.validation.constraints.Size;
  14 +import lombok.Data;
  15 +
  16 +import java.util.Map;
  17 +
  18 +@Schema(description = "管理后台 - AI1 MCP 新增/修改 Request VO")
  19 +@Data
  20 +public class Ai1McpSaveReqVO {
  21 +
  22 + @Schema(description = "编号", example = "1024")
  23 + private Long id;
  24 +
  25 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "天气查询")
  26 + @NotEmpty(message = "名称不能为空")
  27 + @Size(max = 100, message = "名称长度不能超过 100 个字符")
  28 + private String name;
  29 +
  30 + @Schema(description = "传输方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "http")
  31 + @NotEmpty(message = "传输方式不能为空")
  32 + @InEnum(Ai1McpTransportEnum.class)
  33 + private String transport;
  34 +
  35 + @Schema(description = "服务地址,远程必填;为空时取 config 中的 url", example = "http://127.0.0.1:8081/mcp")
  36 + @Size(max = 200, message = "服务地址长度不能超过 200 个字符")
  37 + private String url;
  38 +
  39 + @Schema(description = "请求头,远程配置以此字段为准;为空时清空请求头", example = "{\"Authorization\":\"Bearer xxx\"}")
  40 + private Map<String, String> headers;
  41 +
  42 + @Schema(description = "完整 MCP 配置,JSON 对象;本地必填 command", example = "{\"transport\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"xxx\"]}")
  43 + private String config;
  44 +
  45 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  46 + @NotNull(message = "状态不能为空")
  47 + @InEnum(CommonStatusEnum.class)
  48 + private Integer status;
  49 +
  50 + @Schema(description = "备注", example = "查询城市天气")
  51 + @Size(max = 500, message = "备注长度不能超过 500 个字符")
  52 + private String remark;
  53 +
  54 + @AssertTrue(message = "请求头长度不能超过 500 个字符")
  55 + @JsonIgnore
  56 + public boolean isHeadersValid() {
  57 + return MapUtil.isEmpty(headers) || JsonUtils.toJsonString(headers).length() <= 500;
  58 + }
  59 +
  60 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/Ai1ModelController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model.*;
  8 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ModelDO;
  9 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ModelService;
  10 +import io.swagger.v3.oas.annotations.Operation;
  11 +import io.swagger.v3.oas.annotations.Parameter;
  12 +import io.swagger.v3.oas.annotations.tags.Tag;
  13 +import jakarta.annotation.Resource;
  14 +import jakarta.validation.Valid;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +
  19 +import java.util.List;
  20 +import java.util.Set;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  25 +
  26 +@Tag(name = "管理后台 - AI1 模型")
  27 +@RestController
  28 +@RequestMapping("/ai1/model")
  29 +@Validated
  30 +public class Ai1ModelController {
  31 +
  32 + @Resource
  33 + private Ai1ModelService modelService;
  34 +
  35 + @PostMapping("/create")
  36 + @Operation(summary = "创建模型")
  37 + @PreAuthorize("@ss.hasPermission('ai1:model:create')")
  38 + public CommonResult<Long> createModel(@Valid @RequestBody Ai1ModelSaveReqVO createReqVO) {
  39 + return success(modelService.createModel(createReqVO));
  40 + }
  41 +
  42 + @PutMapping("/update")
  43 + @Operation(summary = "更新模型")
  44 + @PreAuthorize("@ss.hasPermission('ai1:model:update')")
  45 + public CommonResult<Boolean> updateModel(@Valid @RequestBody Ai1ModelSaveReqVO updateReqVO) {
  46 + modelService.updateModel(updateReqVO);
  47 + return success(true);
  48 + }
  49 +
  50 + @DeleteMapping("/delete")
  51 + @Operation(summary = "删除模型")
  52 + @Parameter(name = "id", description = "编号", required = true)
  53 + @PreAuthorize("@ss.hasPermission('ai1:model:delete')")
  54 + public CommonResult<Boolean> deleteModel(@RequestParam("id") Long id) {
  55 + modelService.deleteModel(id);
  56 + return success(true);
  57 + }
  58 +
  59 + @DeleteMapping("/delete-list")
  60 + @Operation(summary = "批量删除模型")
  61 + @Parameter(name = "ids", description = "编号列表", required = true)
  62 + @PreAuthorize("@ss.hasPermission('ai1:model:delete')")
  63 + public CommonResult<Boolean> deleteModelList(@RequestParam("ids") List<Long> ids) {
  64 + modelService.deleteModelListByIds(ids);
  65 + return success(true);
  66 + }
  67 +
  68 + @GetMapping("/get")
  69 + @Operation(summary = "获得模型")
  70 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  71 + @PreAuthorize("@ss.hasPermission('ai1:model:query')")
  72 + public CommonResult<Ai1ModelRespVO> getModel(@RequestParam("id") Long id) {
  73 + Ai1ModelDO model = modelService.getModel(id);
  74 + return success(BeanUtils.toBean(model, Ai1ModelRespVO.class));
  75 + }
  76 +
  77 + @GetMapping("/page")
  78 + @Operation(summary = "获得模型分页")
  79 + @PreAuthorize("@ss.hasPermission('ai1:model:query')")
  80 + public CommonResult<PageResult<Ai1ModelRespVO>> getModelPage(@Valid Ai1ModelPageReqVO pageReqVO) {
  81 + PageResult<Ai1ModelDO> pageResult = modelService.getModelPage(pageReqVO);
  82 + return success(BeanUtils.toBean(pageResult, Ai1ModelRespVO.class));
  83 + }
  84 +
  85 + @GetMapping("/simple-list")
  86 + @Operation(summary = "获得模型精简列表", description = "只包含开启状态,用于 Agent 模型、知识库向量化模型的下拉选择")
  87 + @Parameter(name = "providerId", description = "供应商编号", example = "1")
  88 + @Parameter(name = "type", description = "类型", example = "0")
  89 + public CommonResult<List<Ai1ModelRespVO>> getModelSimpleList(@RequestParam(value = "providerId", required = false) Long providerId,
  90 + @RequestParam(value = "type", required = false) Integer type) {
  91 + List<Ai1ModelDO> list = modelService.getModelListByProviderIdAndTypeAndStatus(
  92 + providerId, type, CommonStatusEnum.ENABLE.getStatus());
  93 + return success(convertList(list, model -> new Ai1ModelRespVO()
  94 + .setId(model.getId()).setProviderId(model.getProviderId())
  95 + .setName(model.getName()).setModel(model.getModel()).setType(model.getType())));
  96 + }
  97 +
  98 + @GetMapping("/remote-list")
  99 + @Operation(summary = "拉取供应商远程可用模型", description = "并标注是否已导入")
  100 + @Parameter(name = "providerId", description = "供应商编号", required = true, example = "1")
  101 + @PreAuthorize("@ss.hasPermission('ai1:model:import')")
  102 + public CommonResult<List<Ai1ModelRemoteRespVO>> getRemoteModelList(@RequestParam("providerId") Long providerId) {
  103 + List<String> remoteModels = modelService.getRemoteModelList(providerId);
  104 + List<Ai1ModelDO> existModelList = modelService.getModelListByProviderId(providerId);
  105 + Set<String> existModels = convertSet(existModelList, Ai1ModelDO::getModel);
  106 + return success(convertList(remoteModels, model -> new Ai1ModelRemoteRespVO(model, existModels.contains(model))));
  107 + }
  108 +
  109 + @PostMapping("/import")
  110 + @Operation(summary = "批量导入远程模型", description = "返回实际导入数量")
  111 + @PreAuthorize("@ss.hasPermission('ai1:model:import')")
  112 + public CommonResult<Integer> importRemoteModelList(@Valid @RequestBody Ai1ModelImportReqVO importReqVO) {
  113 + return success(modelService.importRemoteModelList(importReqVO));
  114 + }
  115 +
  116 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/Ai1ProviderController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model;
  2 +
  3 +import cn.hutool.core.util.ReUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  6 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  7 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider.Ai1ProviderConnectRespVO;
  10 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider.Ai1ProviderPageReqVO;
  11 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider.Ai1ProviderRespVO;
  12 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider.Ai1ProviderSaveReqVO;
  13 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ProviderDO;
  14 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ProviderService;
  15 +import io.swagger.v3.oas.annotations.Operation;
  16 +import io.swagger.v3.oas.annotations.Parameter;
  17 +import io.swagger.v3.oas.annotations.tags.Tag;
  18 +import jakarta.annotation.Resource;
  19 +import jakarta.validation.Valid;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
  21 +import org.springframework.validation.annotation.Validated;
  22 +import org.springframework.web.bind.annotation.*;
  23 +
  24 +import java.util.List;
  25 +
  26 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  28 +
  29 +@Tag(name = "管理后台 - AI1 供应商")
  30 +@RestController
  31 +@RequestMapping("/ai1/provider")
  32 +@Validated
  33 +public class Ai1ProviderController {
  34 +
  35 + @Resource
  36 + private Ai1ProviderService providerService;
  37 +
  38 + @PostMapping("/create")
  39 + @Operation(summary = "创建供应商")
  40 + @PreAuthorize("@ss.hasPermission('ai1:provider:create')")
  41 + public CommonResult<Long> createProvider(@Valid @RequestBody Ai1ProviderSaveReqVO createReqVO) {
  42 + return success(providerService.createProvider(createReqVO));
  43 + }
  44 +
  45 + @PutMapping("/update")
  46 + @Operation(summary = "更新供应商")
  47 + @PreAuthorize("@ss.hasPermission('ai1:provider:update')")
  48 + public CommonResult<Boolean> updateProvider(@Valid @RequestBody Ai1ProviderSaveReqVO updateReqVO) {
  49 + providerService.updateProvider(updateReqVO);
  50 + return success(true);
  51 + }
  52 +
  53 + @DeleteMapping("/delete")
  54 + @Operation(summary = "删除供应商")
  55 + @Parameter(name = "id", description = "编号", required = true)
  56 + @PreAuthorize("@ss.hasPermission('ai1:provider:delete')")
  57 + public CommonResult<Boolean> deleteProvider(@RequestParam("id") Long id) {
  58 + providerService.deleteProvider(id);
  59 + return success(true);
  60 + }
  61 +
  62 + @DeleteMapping("/delete-list")
  63 + @Operation(summary = "批量删除供应商")
  64 + @Parameter(name = "ids", description = "编号列表", required = true)
  65 + @PreAuthorize("@ss.hasPermission('ai1:provider:delete')")
  66 + public CommonResult<Boolean> deleteProviderList(@RequestParam("ids") List<Long> ids) {
  67 + providerService.deleteProviderListByIds(ids);
  68 + return success(true);
  69 + }
  70 +
  71 + @GetMapping("/get")
  72 + @Operation(summary = "获得供应商")
  73 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  74 + @PreAuthorize("@ss.hasPermission('ai1:provider:query')")
  75 + public CommonResult<Ai1ProviderRespVO> getProvider(@RequestParam("id") Long id) {
  76 + Ai1ProviderDO provider = providerService.getProvider(id);
  77 + return success(buildProviderRespVO(provider));
  78 + }
  79 +
  80 + @GetMapping("/page")
  81 + @Operation(summary = "获得供应商分页")
  82 + @PreAuthorize("@ss.hasPermission('ai1:provider:query')")
  83 + public CommonResult<PageResult<Ai1ProviderRespVO>> getProviderPage(@Valid Ai1ProviderPageReqVO pageReqVO) {
  84 + PageResult<Ai1ProviderDO> pageResult = providerService.getProviderPage(pageReqVO);
  85 + return success(new PageResult<>(convertList(pageResult.getList(), this::buildProviderRespVO), pageResult.getTotal()));
  86 + }
  87 +
  88 + @GetMapping("/simple-list")
  89 + @Operation(summary = "获得供应商精简列表", description = "只包含开启状态,用于 Agent 模型、知识库向量化模型的下拉选择")
  90 + public CommonResult<List<Ai1ProviderRespVO>> getProviderSimpleList() {
  91 + List<Ai1ProviderDO> list = providerService.getProviderListByStatus(CommonStatusEnum.ENABLE.getStatus());
  92 + return success(convertList(list, provider -> new Ai1ProviderRespVO().setId(provider.getId()).setName(provider.getName())));
  93 + }
  94 +
  95 + @PostMapping("/test")
  96 + @Operation(summary = "供应商连通测试", description = "GET /models 优先,失败时回退 POST /chat/completions")
  97 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  98 + @PreAuthorize("@ss.hasPermission('ai1:provider:test')")
  99 + public CommonResult<Ai1ProviderConnectRespVO> testProvider(@RequestParam("id") Long id) {
  100 + return success(providerService.testProviderConnect(id));
  101 + }
  102 +
  103 + // ==================== 拼接 VO ====================
  104 +
  105 + /**
  106 + * 构建供应商响应:API 密钥脱敏返回,避免查询权限直接获取完整密钥
  107 + */
  108 + private Ai1ProviderRespVO buildProviderRespVO(Ai1ProviderDO provider) {
  109 + if (provider == null) {
  110 + return null;
  111 + }
  112 + Ai1ProviderRespVO respVO = BeanUtils.toBean(provider, Ai1ProviderRespVO.class);
  113 + String apiKey = provider.getApiKey();
  114 + if (ReUtil.isMatch("^\\$\\{[a-zA-Z_][a-zA-Z0-9_.-]*\\}$", apiKey)) {
  115 + // 【安全性】仅完整的变量占位符原样返回;带默认值或拼接内容的密钥仍需脱敏
  116 + respVO.setApiKey(apiKey);
  117 + } else if (StrUtil.isNotEmpty(apiKey)) {
  118 + respVO.setApiKey(apiKey.length() <= 8 ? "****"
  119 + : StrUtil.hide(apiKey, 3, apiKey.length() - 4));
  120 + }
  121 + return respVO;
  122 + }
  123 +
  124 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/model/Ai1ModelImportReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Schema(description = "管理后台 - AI1 远程模型导入 Request VO")
  11 +@Data
  12 +public class Ai1ModelImportReqVO {
  13 +
  14 + @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  15 + @NotNull(message = "供应商编号不能为空")
  16 + private Long providerId;
  17 +
  18 + @Schema(description = "模型标识列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"deepseek-chat\"]")
  19 + @NotEmpty(message = "请选择要导入的模型")
  20 + private List<String> models;
  21 +
  22 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/model/Ai1ModelPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +import lombok.ToString;
  9 +
  10 +@Schema(description = "管理后台 - AI1 模型分页 Request VO")
  11 +@Data
  12 +@EqualsAndHashCode(callSuper = true)
  13 +@ToString(callSuper = true)
  14 +public class Ai1ModelPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + @NotNull(message = "供应商编号不能为空")
  18 + private Long providerId;
  19 +
  20 + @Schema(description = "展示名称,模糊匹配", example = "DeepSeek")
  21 + private String name;
  22 +
  23 + @Schema(description = "类型", example = "0")
  24 + private Integer type;
  25 +
  26 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/model/Ai1ModelRemoteRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +@Schema(description = "管理后台 - AI1 远程模型 Response VO")
  9 +@Data
  10 +@NoArgsConstructor
  11 +@AllArgsConstructor
  12 +public class Ai1ModelRemoteRespVO {
  13 +
  14 + @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "deepseek-chat")
  15 + private String model;
  16 +
  17 + @Schema(description = "是否已导入当前供应商", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
  18 + private Boolean imported;
  19 +
  20 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/model/Ai1ModelRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 模型 Response VO")
  9 +@Data
  10 +public class Ai1ModelRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long providerId;
  17 +
  18 + @Schema(description = "展示名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "DeepSeek Chat")
  19 + private String name;
  20 +
  21 + @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "deepseek-chat")
  22 + private String model;
  23 +
  24 + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  25 + private Integer type;
  26 +
  27 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  28 + private Integer status;
  29 +
  30 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private LocalDateTime createTime;
  32 +
  33 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/model/Ai1ModelSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.module.ai1.enums.model.Ai1ModelTypeEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.NotEmpty;
  8 +import jakarta.validation.constraints.NotNull;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +@Schema(description = "管理后台 - AI1 模型新增/修改 Request VO")
  13 +@Data
  14 +public class Ai1ModelSaveReqVO {
  15 +
  16 + @Schema(description = "编号", example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  20 + @NotNull(message = "供应商编号不能为空")
  21 + private Long providerId;
  22 +
  23 + @Schema(description = "展示名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "DeepSeek Chat")
  24 + @NotEmpty(message = "展示名称不能为空")
  25 + @Size(max = 50, message = "展示名称长度不能超过 50 个字符")
  26 + private String name;
  27 +
  28 + @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "deepseek-chat")
  29 + @NotEmpty(message = "模型标识不能为空")
  30 + @Size(max = 100, message = "模型标识长度不能超过 100 个字符")
  31 + private String model;
  32 +
  33 + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  34 + @NotNull(message = "类型不能为空")
  35 + @InEnum(Ai1ModelTypeEnum.class)
  36 + private Integer type;
  37 +
  38 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  39 + @NotNull(message = "状态不能为空")
  40 + @InEnum(CommonStatusEnum.class)
  41 + private Integer status;
  42 +
  43 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/provider/Ai1ProviderConnectRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +@Schema(description = "管理后台 - AI1 供应商连通测试 Response VO")
  7 +@Data
  8 +public class Ai1ProviderConnectRespVO {
  9 +
  10 + @Schema(description = "是否连通", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  11 + private Boolean connectable;
  12 +
  13 + @Schema(description = "最近一次请求的 HTTP 状态码,0 表示网络异常未收到响应", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
  14 + private Integer httpCode;
  15 +
  16 + @Schema(description = "测试耗时,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "320")
  17 + private Long elapsedMs;
  18 +
  19 + @Schema(description = "测试过程描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "连通正常:GET /models 返回 HTTP 200")
  20 + private String message;
  21 +
  22 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/provider/Ai1ProviderPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "管理后台 - AI1 供应商分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class Ai1ProviderPageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "名称,模糊匹配", example = "DeepSeek")
  16 + private String name;
  17 +
  18 + @Schema(description = "状态", example = "0")
  19 + private Integer status;
  20 +
  21 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/provider/Ai1ProviderRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 供应商 Response VO")
  9 +@Data
  10 +public class Ai1ProviderRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "DeepSeek")
  16 + private String name;
  17 +
  18 + @Schema(description = "接口地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://api.deepseek.com/v1")
  19 + private String baseUrl;
  20 +
  21 + @Schema(description = "API 密钥,脱敏展示", example = "sk-****abcd")
  22 + private String apiKey;
  23 +
  24 + @Schema(description = "请求附属 Header,JSON 数组", example = "[{\"key\":\"X-Session\",\"value\":\"{session}\"}]")
  25 + private String headers;
  26 +
  27 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  28 + private Integer status;
  29 +
  30 + @Schema(description = "备注", example = "官方接口")
  31 + private String remark;
  32 +
  33 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private LocalDateTime createTime;
  35 +
  36 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  37 + private LocalDateTime updateTime;
  38 +
  39 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/model/vo/provider/Ai1ProviderSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  6 +import cn.iocoder.yudao.module.ai1.util.Ai1Utils;
  7 +import com.fasterxml.jackson.annotation.JsonIgnore;
  8 +import io.swagger.v3.oas.annotations.media.Schema;
  9 +import jakarta.validation.constraints.AssertTrue;
  10 +import jakarta.validation.constraints.NotEmpty;
  11 +import jakarta.validation.constraints.NotNull;
  12 +import jakarta.validation.constraints.Size;
  13 +import lombok.Data;
  14 +
  15 +@Schema(description = "管理后台 - AI1 供应商新增/修改 Request VO")
  16 +@Data
  17 +public class Ai1ProviderSaveReqVO {
  18 +
  19 + @Schema(description = "编号", example = "1024")
  20 + private Long id;
  21 +
  22 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "DeepSeek")
  23 + @NotEmpty(message = "名称不能为空")
  24 + @Size(max = 50, message = "名称长度不能超过 50 个字符")
  25 + private String name;
  26 +
  27 + @Schema(description = "接口地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://api.deepseek.com/v1")
  28 + @NotEmpty(message = "接口地址不能为空")
  29 + @Size(max = 200, message = "接口地址长度不能超过 200 个字符")
  30 + private String baseUrl;
  31 +
  32 + @Schema(description = "API 密钥;修改时为空表示保持原密钥不变", example = "sk-xxx")
  33 + @Size(max = 200, message = "API 密钥长度不能超过 200 个字符")
  34 + private String apiKey;
  35 +
  36 + @Schema(description = "请求附属 Header,JSON 数组", example = "[{\"key\":\"X-Session\",\"value\":\"{session}\"}]")
  37 + @Size(max = 2000, message = "请求附属 Header 长度不能超过 2000 个字符")
  38 + private String headers;
  39 +
  40 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  41 + @NotNull(message = "状态不能为空")
  42 + @InEnum(CommonStatusEnum.class)
  43 + private Integer status;
  44 +
  45 + @Schema(description = "备注", example = "官方接口")
  46 + @Size(max = 255, message = "备注长度不能超过 255 个字符")
  47 + private String remark;
  48 +
  49 + @AssertTrue(message = "请求附属 Header 格式不正确,应为 JSON 数组:[{\"key\":\"...\",\"value\":\"...\"}]")
  50 + @JsonIgnore
  51 + public boolean isHeadersValid() {
  52 + return StrUtil.isBlank(headers) || Ai1Utils.parseHeaders(headers) != null;
  53 + }
  54 +
  55 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/Ai1SessionController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session.Ai1SessionCreateMyReqVO;
  8 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session.Ai1SessionPageReqVO;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session.Ai1SessionRespVO;
  10 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session.Ai1SessionUpdateMyReqVO;
  11 +import cn.iocoder.yudao.module.ai1.dal.dataobject.session.Ai1SessionDO;
  12 +import cn.iocoder.yudao.module.ai1.service.session.Ai1SessionService;
  13 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  14 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  15 +import io.swagger.v3.oas.annotations.Operation;
  16 +import io.swagger.v3.oas.annotations.Parameter;
  17 +import io.swagger.v3.oas.annotations.tags.Tag;
  18 +import jakarta.annotation.Resource;
  19 +import jakarta.validation.Valid;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
  21 +import org.springframework.validation.annotation.Validated;
  22 +import org.springframework.web.bind.annotation.*;
  23 +
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +
  27 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  28 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  29 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  30 +
  31 +@Tag(name = "管理后台 - AI1 会话")
  32 +@RestController
  33 +@RequestMapping("/ai1/session")
  34 +@Validated
  35 +public class Ai1SessionController {
  36 +
  37 + @Resource
  38 + private Ai1SessionService sessionService;
  39 +
  40 + @Resource
  41 + private AdminUserApi adminUserApi;
  42 +
  43 + // ==================== 我的 ====================
  44 +
  45 + @PostMapping("/create-my")
  46 + @Operation(summary = "创建【我的】会话")
  47 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  48 + public CommonResult<Long> createSessionMy(@Valid @RequestBody Ai1SessionCreateMyReqVO createReqVO) {
  49 + return success(sessionService.createSessionMy(getLoginUserId(), createReqVO));
  50 + }
  51 +
  52 + @PutMapping("/update-my")
  53 + @Operation(summary = "修改【我的】会话标题")
  54 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  55 + public CommonResult<Boolean> updateSessionMy(@Valid @RequestBody Ai1SessionUpdateMyReqVO updateReqVO) {
  56 + sessionService.updateSessionMy(getLoginUserId(), updateReqVO);
  57 + return success(true);
  58 + }
  59 +
  60 + @DeleteMapping("/delete-my")
  61 + @Operation(summary = "删除【我的】会话", description = "连带删除消息")
  62 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  63 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  64 + public CommonResult<Boolean> deleteSessionMy(@RequestParam("id") Long id) {
  65 + sessionService.deleteSessionMy(getLoginUserId(), id);
  66 + return success(true);
  67 + }
  68 +
  69 + @GetMapping("/my-list")
  70 + @Operation(summary = "获得【我的】会话列表", description = "指定 Agent 下,按创建时间倒序")
  71 + @Parameter(name = "agentId", description = "Agent 编号", required = true, example = "1")
  72 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  73 + public CommonResult<List<Ai1SessionRespVO>> getSessionMyList(@RequestParam("agentId") Long agentId) {
  74 + List<Ai1SessionDO> list = sessionService.getSessionListByAgentIdAndUserId(agentId, getLoginUserId());
  75 + return success(BeanUtils.toBean(list, Ai1SessionRespVO.class));
  76 + }
  77 +
  78 + // ==================== 管理 ====================
  79 +
  80 + @GetMapping("/page")
  81 + @Operation(summary = "获得会话分页", description = "管理端查看 Agent 下全部用户的会话")
  82 + @PreAuthorize("@ss.hasPermission('ai1:session:query')")
  83 + public CommonResult<PageResult<Ai1SessionRespVO>> getSessionPage(@Valid Ai1SessionPageReqVO pageReqVO) {
  84 + PageResult<Ai1SessionDO> pageResult = sessionService.getSessionPage(pageReqVO);
  85 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(pageResult.getList(), Ai1SessionDO::getUserId));
  86 + return success(BeanUtils.toBean(pageResult, Ai1SessionRespVO.class, respVO ->
  87 + MapUtils.findAndThen(userMap, respVO.getUserId(), user -> respVO.setUserNickname(user.getNickname()))));
  88 + }
  89 +
  90 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/Ai1SessionMessageController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message.Ai1SessionMessageRespVO;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message.Ai1SessionMessageSendReqVO;
  7 +import cn.iocoder.yudao.module.ai1.service.session.Ai1SessionService;
  8 +import cn.iocoder.yudao.module.ai1.service.session.Ai1SessionMessageService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.http.MediaType;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
  19 +
  20 +import java.util.List;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  24 +
  25 +@Tag(name = "管理后台 - AI1 会话消息")
  26 +@RestController
  27 +@RequestMapping("/ai1/session/message")
  28 +@Validated
  29 +public class Ai1SessionMessageController {
  30 +
  31 + @Resource
  32 + private Ai1SessionMessageService sessionMessageService;
  33 + @Resource
  34 + private Ai1SessionService sessionService;
  35 +
  36 + // ==================== 我的 ====================
  37 +
  38 + @GetMapping("/my-list")
  39 + @Operation(summary = "获得【我的】会话消息列表")
  40 + @Parameter(name = "sessionId", description = "会话编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  42 + public CommonResult<List<Ai1SessionMessageRespVO>> getSessionMessageMyList(@RequestParam("sessionId") Long sessionId) {
  43 + sessionService.validateSessionMy(getLoginUserId(), sessionId);
  44 + return success(BeanUtils.toBean(sessionMessageService.getSessionMessageListBySessionId(sessionId),
  45 + Ai1SessionMessageRespVO.class));
  46 + }
  47 +
  48 + @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  49 + @Operation(summary = "发送消息(流式)", description = "SSE 事件:stream(助手消息编号)、thinking、message、ping、done、error;"
  50 + + "事件 data 均为 JSON 字符串,事件 id 为结果流条目编号,可用于续传")
  51 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  52 + public SseEmitter sendSessionMessageStream(@Valid @RequestBody Ai1SessionMessageSendReqVO sendReqVO) {
  53 + return sessionMessageService.sendSessionMessageStream(getLoginUserId(), sendReqVO);
  54 + }
  55 +
  56 + @PostMapping(value = "/resume-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  57 + @Operation(summary = "断线续传(流式)", description = "从结果流 lastEventId 之后继续转发,不重新生成;事件格式同发送消息")
  58 + @Parameter(name = "messageId", description = "助手消息编号", required = true, example = "1024")
  59 + @Parameter(name = "lastEventId", description = "已收到的最后一个事件编号,为空时从头重放", example = "1727541234567-0")
  60 + @PreAuthorize("@ss.hasPermission('ai1:session:my')")
  61 + public SseEmitter resumeSessionMessageStream(@RequestParam("messageId") Long messageId,
  62 + @RequestParam(value = "lastEventId", required = false) String lastEventId) {
  63 + return sessionMessageService.resumeSessionMessageStream(getLoginUserId(), messageId, lastEventId);
  64 + }
  65 +
  66 + // ==================== 管理 ====================
  67 +
  68 + @GetMapping("/list")
  69 + @Operation(summary = "获得会话消息列表", description = "管理端查看任意用户的会话明细")
  70 + @Parameter(name = "sessionId", description = "会话编号", required = true, example = "1024")
  71 + @PreAuthorize("@ss.hasPermission('ai1:session:query')")
  72 + public CommonResult<List<Ai1SessionMessageRespVO>> getSessionMessageList(@RequestParam("sessionId") Long sessionId) {
  73 + return success(BeanUtils.toBean(sessionMessageService.getSessionMessageListBySessionId(sessionId),
  74 + Ai1SessionMessageRespVO.class));
  75 + }
  76 +
  77 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/vo/message/Ai1SessionMessageRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 会话消息 Response VO")
  9 +@Data
  10 +public class Ai1SessionMessageRespVO {
  11 +
  12 + @Schema(description = "编号;助手消息的编号同时作为续传的结果流标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long sessionId;
  17 +
  18 + @Schema(description = "角色", requiredMode = Schema.RequiredMode.REQUIRED, example = "assistant")
  19 + private String role;
  20 +
  21 + @Schema(description = "思考过程", example = "用户想了解……")
  22 + private String reasoning;
  23 +
  24 + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "四光年")
  25 + private String content;
  26 +
  27 + @Schema(description = "生成状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  28 + private Integer status;
  29 +
  30 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private LocalDateTime createTime;
  32 +
  33 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/vo/message/Ai1SessionMessageSendReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +@Schema(description = "管理后台 - AI1 会话消息发送 Request VO")
  9 +@Data
  10 +public class Ai1SessionMessageSendReqVO {
  11 +
  12 + @Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + @NotNull(message = "会话编号不能为空")
  14 + private Long sessionId;
  15 +
  16 + @Schema(description = "提问内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体舰队还有多久到达地球?")
  17 + @NotBlank(message = "请输入内容")
  18 + private String content;
  19 +
  20 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/vo/session/Ai1SessionCreateMyReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI1 我的会话创建 Request VO")
  8 +@Data
  9 +public class Ai1SessionCreateMyReqVO {
  10 +
  11 + @Schema(description = "Agent 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  12 + @NotNull(message = "Agent 编号不能为空")
  13 + private Long agentId;
  14 +
  15 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/vo/session/Ai1SessionPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +import lombok.ToString;
  9 +
  10 +@Schema(description = "管理后台 - AI1 会话分页 Request VO")
  11 +@Data
  12 +@EqualsAndHashCode(callSuper = true)
  13 +@ToString(callSuper = true)
  14 +public class Ai1SessionPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "Agent 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  17 + @NotNull(message = "Agent 编号不能为空")
  18 + private Long agentId;
  19 +
  20 + @Schema(description = "标题,模糊匹配", example = "三体")
  21 + private String title;
  22 +
  23 + @Schema(description = "用户编号", example = "1")
  24 + private Long userId;
  25 +
  26 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/vo/session/Ai1SessionRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 会话 Response VO")
  9 +@Data
  10 +public class Ai1SessionRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "Agent 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long agentId;
  17 +
  18 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + private Long userId;
  20 +
  21 + @Schema(description = "用户昵称", example = "芋道源码")
  22 + private String userNickname;
  23 +
  24 + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体舰队的航速")
  25 + private String title;
  26 +
  27 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + private LocalDateTime createTime;
  29 +
  30 + @Schema(description = "更新时间,即最近活跃时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private LocalDateTime updateTime;
  32 +
  33 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/session/vo/session/Ai1SessionUpdateMyReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - AI1 我的会话修改 Request VO")
  10 +@Data
  11 +public class Ai1SessionUpdateMyReqVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + @NotNull(message = "编号不能为空")
  15 + private Long id;
  16 +
  17 + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "三体舰队的航速")
  18 + @NotBlank(message = "标题不能为空")
  19 + @Size(max = 50, message = "标题长度不能超过 50 个字符")
  20 + private String title;
  21 +
  22 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/Ai1SkillController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill.Ai1SkillPageReqVO;
  8 +import cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill.Ai1SkillRespVO;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill.Ai1SkillSaveReqVO;
  10 +import cn.iocoder.yudao.module.ai1.dal.dataobject.skill.Ai1SkillDO;
  11 +import cn.iocoder.yudao.module.ai1.service.skill.Ai1SkillService;
  12 +import io.swagger.v3.oas.annotations.Operation;
  13 +import io.swagger.v3.oas.annotations.Parameter;
  14 +import io.swagger.v3.oas.annotations.tags.Tag;
  15 +import jakarta.annotation.Resource;
  16 +import jakarta.validation.Valid;
  17 +import org.springframework.security.access.prepost.PreAuthorize;
  18 +import org.springframework.validation.annotation.Validated;
  19 +import org.springframework.web.bind.annotation.*;
  20 +
  21 +import java.util.List;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +
  26 +@Tag(name = "管理后台 - AI1 SKILL")
  27 +@RestController
  28 +@RequestMapping("/ai1/skill")
  29 +@Validated
  30 +public class Ai1SkillController {
  31 +
  32 + @Resource
  33 + private Ai1SkillService skillService;
  34 +
  35 + @PostMapping("/create")
  36 + @Operation(summary = "创建 SKILL", description = "自动播种固定文件 SKILL.md、scripts/、reference/")
  37 + @PreAuthorize("@ss.hasPermission('ai1:skill:create')")
  38 + public CommonResult<Long> createSkill(@Valid @RequestBody Ai1SkillSaveReqVO createReqVO) {
  39 + return success(skillService.createSkill(createReqVO));
  40 + }
  41 +
  42 + @PutMapping("/update")
  43 + @Operation(summary = "更新 SKILL")
  44 + @PreAuthorize("@ss.hasPermission('ai1:skill:update')")
  45 + public CommonResult<Boolean> updateSkill(@Valid @RequestBody Ai1SkillSaveReqVO updateReqVO) {
  46 + skillService.updateSkill(updateReqVO);
  47 + return success(true);
  48 + }
  49 +
  50 + @DeleteMapping("/delete")
  51 + @Operation(summary = "删除 SKILL", description = "级联删除内容文件")
  52 + @Parameter(name = "id", description = "编号", required = true)
  53 + @PreAuthorize("@ss.hasPermission('ai1:skill:delete')")
  54 + public CommonResult<Boolean> deleteSkill(@RequestParam("id") Long id) {
  55 + skillService.deleteSkill(id);
  56 + return success(true);
  57 + }
  58 +
  59 + @DeleteMapping("/delete-list")
  60 + @Operation(summary = "批量删除 SKILL", description = "级联删除内容文件")
  61 + @Parameter(name = "ids", description = "编号列表", required = true)
  62 + @PreAuthorize("@ss.hasPermission('ai1:skill:delete')")
  63 + public CommonResult<Boolean> deleteSkillList(@RequestParam("ids") List<Long> ids) {
  64 + skillService.deleteSkillListByIds(ids);
  65 + return success(true);
  66 + }
  67 +
  68 + @GetMapping("/get")
  69 + @Operation(summary = "获得 SKILL")
  70 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  71 + @PreAuthorize("@ss.hasPermission('ai1:skill:query')")
  72 + public CommonResult<Ai1SkillRespVO> getSkill(@RequestParam("id") Long id) {
  73 + Ai1SkillDO skill = skillService.getSkill(id);
  74 + return success(BeanUtils.toBean(skill, Ai1SkillRespVO.class));
  75 + }
  76 +
  77 + @GetMapping("/page")
  78 + @Operation(summary = "获得 SKILL 分页")
  79 + @PreAuthorize("@ss.hasPermission('ai1:skill:query')")
  80 + public CommonResult<PageResult<Ai1SkillRespVO>> getSkillPage(@Valid Ai1SkillPageReqVO pageReqVO) {
  81 + PageResult<Ai1SkillDO> pageResult = skillService.getSkillPage(pageReqVO);
  82 + return success(BeanUtils.toBean(pageResult, Ai1SkillRespVO.class));
  83 + }
  84 +
  85 + @GetMapping("/simple-list")
  86 + @Operation(summary = "获得 SKILL 精简列表", description = "只包含开启状态,用于 Agent 绑定 SKILL 的下拉选择")
  87 + public CommonResult<List<Ai1SkillRespVO>> getSkillSimpleList() {
  88 + List<Ai1SkillDO> list = skillService.getSkillListByStatus(CommonStatusEnum.ENABLE.getStatus());
  89 + return success(convertList(list, skill -> new Ai1SkillRespVO()
  90 + .setId(skill.getId()).setName(skill.getName()).setDescription(skill.getDescription())));
  91 + }
  92 +
  93 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/Ai1SkillFileController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.file.*;
  6 +import cn.iocoder.yudao.module.ai1.dal.dataobject.skill.Ai1SkillFileDO;
  7 +import cn.iocoder.yudao.module.ai1.service.skill.Ai1SkillFileService;
  8 +import cn.iocoder.yudao.module.ai1.service.skill.Ai1SkillService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.*;
  17 +
  18 +import java.util.List;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  21 +
  22 +@Tag(name = "管理后台 - AI1 SKILL 内容文件")
  23 +@RestController
  24 +@RequestMapping("/ai1/skill/file")
  25 +@Validated
  26 +public class Ai1SkillFileController {
  27 +
  28 + @Resource
  29 + private Ai1SkillFileService skillFileService;
  30 + @Resource
  31 + private Ai1SkillService skillService;
  32 +
  33 + @GetMapping("/list")
  34 + @Operation(summary = "获得 SKILL 文件列表", description = "平铺返回,不返回文件内容,编辑时通过获取接口加载")
  35 + @Parameter(name = "skillId", description = "SKILL 编号", required = true, example = "1")
  36 + @PreAuthorize("@ss.hasPermission('ai1:skill:query')")
  37 + public CommonResult<List<Ai1SkillFileRespVO>> getSkillFileList(@RequestParam("skillId") Long skillId) {
  38 + skillService.validateSkillExists(skillId);
  39 + List<Ai1SkillFileDO> list = skillFileService.getSkillFileListBySkillId(skillId);
  40 + return success(BeanUtils.toBean(list, Ai1SkillFileRespVO.class, skillFile -> skillFile.setContent(null)));
  41 + }
  42 +
  43 + @GetMapping("/get")
  44 + @Operation(summary = "获得 SKILL 文件", description = "包含文件内容")
  45 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  46 + @PreAuthorize("@ss.hasPermission('ai1:skill:query')")
  47 + public CommonResult<Ai1SkillFileRespVO> getSkillFile(@RequestParam("id") Long id) {
  48 + Ai1SkillFileDO skillFile = skillFileService.getSkillFile(id);
  49 + return success(BeanUtils.toBean(skillFile, Ai1SkillFileRespVO.class));
  50 + }
  51 +
  52 + @PostMapping("/create")
  53 + @Operation(summary = "创建 SKILL 目录或文件")
  54 + @PreAuthorize("@ss.hasPermission('ai1:skill:update')")
  55 + public CommonResult<Long> createSkillFile(@Valid @RequestBody Ai1SkillFileCreateReqVO createReqVO) {
  56 + return success(skillFileService.createSkillFile(createReqVO));
  57 + }
  58 +
  59 + @PutMapping("/rename")
  60 + @Operation(summary = "重命名 SKILL 目录或文件")
  61 + @PreAuthorize("@ss.hasPermission('ai1:skill:update')")
  62 + public CommonResult<Boolean> renameSkillFile(@Valid @RequestBody Ai1SkillFileRenameReqVO renameReqVO) {
  63 + skillFileService.renameSkillFile(renameReqVO);
  64 + return success(true);
  65 + }
  66 +
  67 + @PutMapping("/move")
  68 + @Operation(summary = "移动 SKILL 目录或文件")
  69 + @PreAuthorize("@ss.hasPermission('ai1:skill:update')")
  70 + public CommonResult<Boolean> moveSkillFile(@Valid @RequestBody Ai1SkillFileMoveReqVO moveReqVO) {
  71 + skillFileService.moveSkillFile(moveReqVO);
  72 + return success(true);
  73 + }
  74 +
  75 + @PutMapping("/update-content")
  76 + @Operation(summary = "保存 SKILL 文件内容")
  77 + @PreAuthorize("@ss.hasPermission('ai1:skill:update')")
  78 + public CommonResult<Boolean> updateSkillFileContent(@Valid @RequestBody Ai1SkillFileContentReqVO contentReqVO) {
  79 + skillFileService.updateSkillFileContent(contentReqVO);
  80 + return success(true);
  81 + }
  82 +
  83 + @DeleteMapping("/delete")
  84 + @Operation(summary = "删除 SKILL 目录或文件", description = "目录递归删除后代")
  85 + @Parameter(name = "id", description = "编号", required = true)
  86 + @PreAuthorize("@ss.hasPermission('ai1:skill:delete')")
  87 + public CommonResult<Boolean> deleteSkillFile(@RequestParam("id") Long id) {
  88 + skillFileService.deleteSkillFile(id);
  89 + return success(true);
  90 + }
  91 +
  92 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/file/Ai1SkillFileContentReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.file;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import jakarta.validation.constraints.Size;
  6 +import lombok.Data;
  7 +
  8 +@Schema(description = "管理后台 - AI1 SKILL 内容文件保存内容 Request VO")
  9 +@Data
  10 +public class Ai1SkillFileContentReqVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + @NotNull(message = "编号不能为空")
  14 + private Long id;
  15 +
  16 + @Schema(description = "文件内容,允许为空字符串", requiredMode = Schema.RequiredMode.REQUIRED, example = "# SKILL")
  17 + @NotNull(message = "文件内容不能为空")
  18 + @Size(max = 16_777_215, message = "内容长度不能超过 16777215 个字符")
  19 + private String content;
  20 +
  21 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/file/Ai1SkillFileCreateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.file;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.ai1.enums.skill.Ai1SkillFileTypeEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotEmpty;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import lombok.Data;
  9 +
  10 +@Schema(description = "管理后台 - AI1 SKILL 内容文件新增 Request VO")
  11 +@Data
  12 +public class Ai1SkillFileCreateReqVO {
  13 +
  14 + @Schema(description = "SKILL 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  15 + @NotNull(message = "SKILL 编号不能为空")
  16 + private Long skillId;
  17 +
  18 + @Schema(description = "父目录编号,0 为根级", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  19 + @NotNull(message = "父目录编号不能为空")
  20 + private Long parentId;
  21 +
  22 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "main.py")
  23 + @NotEmpty(message = "名称不能为空")
  24 + private String name;
  25 +
  26 + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  27 + @NotNull(message = "类型不能为空")
  28 + @InEnum(Ai1SkillFileTypeEnum.class)
  29 + private Integer type;
  30 +
  31 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/file/Ai1SkillFileMoveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.file;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI1 SKILL 内容文件移动 Request VO")
  8 +@Data
  9 +public class Ai1SkillFileMoveReqVO {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  12 + @NotNull(message = "编号不能为空")
  13 + private Long id;
  14 +
  15 + @Schema(description = "目标父目录编号,0 为根级", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  16 + @NotNull(message = "目标父目录编号不能为空")
  17 + private Long parentId;
  18 +
  19 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/file/Ai1SkillFileRenameReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.file;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +@Schema(description = "管理后台 - AI1 SKILL 内容文件重命名 Request VO")
  9 +@Data
  10 +public class Ai1SkillFileRenameReqVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + @NotNull(message = "编号不能为空")
  14 + private Long id;
  15 +
  16 + @Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "main.py")
  17 + @NotEmpty(message = "名称不能为空")
  18 + private String name;
  19 +
  20 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/file/Ai1SkillFileRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.file;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 SKILL 内容文件 Response VO")
  9 +@Data
  10 +public class Ai1SkillFileRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "SKILL 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  16 + private Long skillId;
  17 +
  18 + @Schema(description = "父目录编号,0 为根级", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  19 + private Long parentId;
  20 +
  21 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "SKILL.md")
  22 + private String name;
  23 +
  24 + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  25 + private Integer type;
  26 +
  27 + @Schema(description = "文件类型,即扩展名;目录为空", example = "md")
  28 + private String fileType;
  29 +
  30 + @Schema(description = "文件内容;文件树中不返回,编辑时通过获取接口加载", example = "# SKILL")
  31 + private String content;
  32 +
  33 + @Schema(description = "是否固定:固定节点不可删除、改名、移动", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  34 + private Boolean locked;
  35 +
  36 + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  37 + private Integer sort;
  38 +
  39 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + private LocalDateTime createTime;
  41 +
  42 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  43 + private LocalDateTime updateTime;
  44 +
  45 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/skill/Ai1SkillPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  5 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import lombok.Data;
  8 +import lombok.EqualsAndHashCode;
  9 +import lombok.ToString;
  10 +
  11 +@Schema(description = "管理后台 - AI1 SKILL 分页 Request VO")
  12 +@Data
  13 +@EqualsAndHashCode(callSuper = true)
  14 +@ToString(callSuper = true)
  15 +public class Ai1SkillPageReqVO extends PageParam {
  16 +
  17 + @Schema(description = "名称,模糊匹配", example = "pdf")
  18 + private String name;
  19 +
  20 + @Schema(description = "状态", example = "0")
  21 + @InEnum(CommonStatusEnum.class)
  22 + private Integer status;
  23 +
  24 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/skill/Ai1SkillRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI1 SKILL Response VO")
  9 +@Data
  10 +public class Ai1SkillRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "pdf-reader")
  16 + private String name;
  17 +
  18 + @Schema(description = "描述", example = "读取并总结 PDF 文件")
  19 + private String description;
  20 +
  21 + @Schema(description = "版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  22 + private String version;
  23 +
  24 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  25 + private Integer status;
  26 +
  27 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + private LocalDateTime createTime;
  29 +
  30 + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private LocalDateTime updateTime;
  32 +
  33 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/admin/skill/vo/skill/Ai1SkillSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import jakarta.validation.constraints.NotEmpty;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import jakarta.validation.constraints.Pattern;
  9 +import jakarta.validation.constraints.Size;
  10 +import lombok.Data;
  11 +
  12 +@Schema(description = "管理后台 - AI1 SKILL 新增/修改 Request VO")
  13 +@Data
  14 +public class Ai1SkillSaveReqVO {
  15 +
  16 + @Schema(description = "编号", example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "名称,同时作为物化目录名,仅支持字母、数字、中划线", requiredMode = Schema.RequiredMode.REQUIRED, example = "pdf-reader")
  20 + @NotEmpty(message = "名称不能为空")
  21 + @Size(max = 100, message = "名称长度不能超过 100 个字符")
  22 + @Pattern(regexp = "^[A-Za-z0-9-]+$", message = "名称仅支持字母、数字、中划线")
  23 + private String name;
  24 +
  25 + @Schema(description = "描述", example = "读取并总结 PDF 文件")
  26 + @Size(max = 500, message = "描述长度不能超过 500 个字符")
  27 + private String description;
  28 +
  29 + @Schema(description = "版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  30 + @NotEmpty(message = "版本不能为空")
  31 + @Size(max = 20, message = "版本长度不能超过 20 个字符")
  32 + private String version;
  33 +
  34 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  35 + @NotNull(message = "状态不能为空")
  36 + @InEnum(CommonStatusEnum.class)
  37 + private Integer status;
  38 +
  39 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/controller/package-info.java 0 → 100644
  1 +/**
  2 + * 提供 RESTful API 给前端:
  3 + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目
  4 + */
  5 +package cn.iocoder.yudao.module.ai1.controller;
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/agent/Ai1AgentDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.agent;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
  5 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  6 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeBaseDO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.mcp.Ai1McpDO;
  8 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ModelDO;
  9 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ProviderDO;
  10 +import cn.iocoder.yudao.module.ai1.dal.dataobject.skill.Ai1SkillDO;
  11 +import com.baomidou.mybatisplus.annotation.KeySequence;
  12 +import com.baomidou.mybatisplus.annotation.TableField;
  13 +import com.baomidou.mybatisplus.annotation.TableId;
  14 +import com.baomidou.mybatisplus.annotation.TableName;
  15 +import lombok.*;
  16 +
  17 +import java.util.List;
  18 +
  19 +/**
  20 + * AI1 Agent DO
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@TableName(value = "ai1_agent", autoResultMap = true)
  25 +@KeySequence("ai1_agent_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  26 +@Data
  27 +@EqualsAndHashCode(callSuper = true)
  28 +@ToString(callSuper = true)
  29 +@Builder
  30 +@NoArgsConstructor
  31 +@AllArgsConstructor
  32 +public class Ai1AgentDO extends TenantBaseDO {
  33 +
  34 + /**
  35 + * 编号
  36 + */
  37 + @TableId
  38 + private Long id;
  39 + /**
  40 + * 名称
  41 + */
  42 + private String name;
  43 + /**
  44 + * 介绍
  45 + */
  46 + private String introduction;
  47 + /**
  48 + * 模型所属 Provider 编号
  49 + *
  50 + * 关联 {@link Ai1ProviderDO#getId()}
  51 + */
  52 + private Long providerId;
  53 + /**
  54 + * 对话模型编号
  55 + *
  56 + * 关联 {@link Ai1ModelDO#getId()}
  57 + */
  58 + private Long modelId;
  59 + /**
  60 + * 系统指令
  61 + */
  62 + private String systemPrompt;
  63 + /**
  64 + * 知识库编号集合
  65 + *
  66 + * 关联 {@link Ai1KnowledgeBaseDO#getId()}
  67 + */
  68 + @TableField(typeHandler = LongListTypeHandler.class)
  69 + private List<Long> knowledgeBaseIds;
  70 + /**
  71 + * MCP 编号集合
  72 + *
  73 + * 关联 {@link Ai1McpDO#getId()}
  74 + */
  75 + @TableField(typeHandler = LongListTypeHandler.class)
  76 + private List<Long> mcpIds;
  77 + /**
  78 + * SKILL 编号集合
  79 + *
  80 + * 关联 {@link Ai1SkillDO#getId()}
  81 + */
  82 + @TableField(typeHandler = LongListTypeHandler.class)
  83 + private List<Long> skillIds;
  84 + /**
  85 + * 状态
  86 + *
  87 + * 枚举 {@link CommonStatusEnum};开启后才可在「Agent 会话」中使用
  88 + */
  89 + private Integer status;
  90 +
  91 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/knowledge/Ai1KnowledgeBaseDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  5 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ModelDO;
  6 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ProviderDO;
  7 +import com.baomidou.mybatisplus.annotation.KeySequence;
  8 +import com.baomidou.mybatisplus.annotation.TableId;
  9 +import com.baomidou.mybatisplus.annotation.TableName;
  10 +import lombok.*;
  11 +
  12 +/**
  13 + * AI1 知识库 DO
  14 + *
  15 + * 向量存储在 Milvus 集合 {collectionPrefix}{id}(默认 knowledge_base_{id})中,按知识库隔离
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@TableName("ai1_knowledge_base")
  20 +@KeySequence("ai1_knowledge_base_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  21 +@Data
  22 +@EqualsAndHashCode(callSuper = true)
  23 +@ToString(callSuper = true)
  24 +@Builder
  25 +@NoArgsConstructor
  26 +@AllArgsConstructor
  27 +public class Ai1KnowledgeBaseDO extends TenantBaseDO {
  28 +
  29 + /**
  30 + * 编号
  31 + */
  32 + @TableId
  33 + private Long id;
  34 + /**
  35 + * 名称
  36 + */
  37 + private String name;
  38 + /**
  39 + * 描述
  40 + */
  41 + private String description;
  42 + /**
  43 + * 向量化 Provider 编号
  44 + *
  45 + * 关联 {@link Ai1ProviderDO#getId()}
  46 + */
  47 + private Long embeddingProviderId;
  48 + /**
  49 + * 向量化模型编号
  50 + *
  51 + * 关联 {@link Ai1ModelDO#getId()}
  52 + */
  53 + private Long embeddingModelId;
  54 + /**
  55 + * 分片大小,单位:字符
  56 + */
  57 + private Integer chunkSize;
  58 + /**
  59 + * 分片重叠,单位:字符
  60 + */
  61 + private Integer chunkOverlap;
  62 + /**
  63 + * 检索数量
  64 + */
  65 + private Integer topK;
  66 + /**
  67 + * 状态
  68 + *
  69 + * 枚举 {@link CommonStatusEnum}
  70 + */
  71 + private Integer status;
  72 +
  73 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/knowledge/Ai1KnowledgeDocumentDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.ai1.enums.knowledge.Ai1KnowledgeDocumentStatusEnum;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * AI1 知识文档 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("ai1_knowledge_document")
  16 +@KeySequence("ai1_knowledge_document_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class Ai1KnowledgeDocumentDO extends TenantBaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 知识库编号
  32 + *
  33 + * 关联 {@link Ai1KnowledgeBaseDO#getId()}
  34 + */
  35 + private Long knowledgeBaseId;
  36 + /**
  37 + * 名称
  38 + */
  39 + private String name;
  40 + /**
  41 + * 内容
  42 + */
  43 + private String content;
  44 + /**
  45 + * 分片数量
  46 + */
  47 + private Integer chunkCount;
  48 + /**
  49 + * 向量化状态
  50 + *
  51 + * 枚举 {@link Ai1KnowledgeDocumentStatusEnum}
  52 + */
  53 + private Integer status;
  54 +
  55 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/mcp/Ai1McpDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.mcp;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  5 +import cn.iocoder.yudao.module.ai1.enums.mcp.Ai1McpTransportEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableField;
  8 +import com.baomidou.mybatisplus.annotation.TableId;
  9 +import com.baomidou.mybatisplus.annotation.TableName;
  10 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
  11 +import lombok.*;
  12 +
  13 +import java.util.Map;
  14 +
  15 +/**
  16 + * AI1 MCP 服务 DO
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@TableName(value = "ai1_mcp", autoResultMap = true)
  21 +@KeySequence("ai1_mcp_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  22 +@Data
  23 +@EqualsAndHashCode(callSuper = true)
  24 +@ToString(callSuper = true)
  25 +@Builder
  26 +@NoArgsConstructor
  27 +@AllArgsConstructor
  28 +public class Ai1McpDO extends TenantBaseDO {
  29 +
  30 + /**
  31 + * 编号
  32 + */
  33 + @TableId
  34 + private Long id;
  35 + /**
  36 + * 名称
  37 + */
  38 + private String name;
  39 + /**
  40 + * 传输方式
  41 + *
  42 + * 枚举 {@link Ai1McpTransportEnum};以该列为准,保存时同步写回 {@link #config} 的 transport 字段
  43 + */
  44 + private String transport;
  45 + /**
  46 + * 服务地址,远程必填、本地为空
  47 + *
  48 + * 切换为本地时需要清空,更新时由 Ai1McpMapper#updateForSave 主动置空
  49 + */
  50 + private String url;
  51 + /**
  52 + * 请求头,例如说 {"Authorization":"Bearer xxx"}
  53 + */
  54 + @TableField(typeHandler = JacksonTypeHandler.class)
  55 + private Map<String, String> headers;
  56 + /**
  57 + * 完整 MCP 配置,JSON 对象
  58 + *
  59 + * 远程:{transport, url, headers};本地:{transport, command, args, env, cwd}
  60 + */
  61 + private String config;
  62 + /**
  63 + * 状态
  64 + *
  65 + * 枚举 {@link CommonStatusEnum}
  66 + */
  67 + private Integer status;
  68 + /**
  69 + * 备注
  70 + */
  71 + private String remark;
  72 +
  73 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/model/Ai1ModelDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  5 +import cn.iocoder.yudao.module.ai1.enums.model.Ai1ModelTypeEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableId;
  8 +import com.baomidou.mybatisplus.annotation.TableName;
  9 +import lombok.*;
  10 +
  11 +/**
  12 + * AI1 模型 DO
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@TableName("ai1_model")
  17 +@KeySequence("ai1_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +@Builder
  22 +@NoArgsConstructor
  23 +@AllArgsConstructor
  24 +public class Ai1ModelDO extends TenantBaseDO {
  25 +
  26 + /**
  27 + * 编号
  28 + */
  29 + @TableId
  30 + private Long id;
  31 + /**
  32 + * 供应商编号
  33 + *
  34 + * 关联 {@link Ai1ProviderDO#getId()}
  35 + */
  36 + private Long providerId;
  37 + /**
  38 + * 展示名称
  39 + */
  40 + private String name;
  41 + /**
  42 + * 模型标识,例如说 gpt-4o、deepseek-chat
  43 + */
  44 + private String model;
  45 + /**
  46 + * 类型
  47 + *
  48 + * 枚举 {@link Ai1ModelTypeEnum}
  49 + */
  50 + private Integer type;
  51 + /**
  52 + * 状态
  53 + *
  54 + * 枚举 {@link CommonStatusEnum}
  55 + */
  56 + private Integer status;
  57 +
  58 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/model/Ai1ProviderDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * AI1 供应商 DO
  12 + *
  13 + * 供应商即模型提供方(OpenAI 兼容端点),维护接口地址与密钥
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@TableName("ai1_provider")
  18 +@KeySequence("ai1_provider_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  19 +@Data
  20 +@EqualsAndHashCode(callSuper = true)
  21 +@ToString(callSuper = true)
  22 +@Builder
  23 +@NoArgsConstructor
  24 +@AllArgsConstructor
  25 +public class Ai1ProviderDO extends TenantBaseDO {
  26 +
  27 + /**
  28 + * 编号
  29 + */
  30 + @TableId
  31 + private Long id;
  32 + /**
  33 + * 名称
  34 + */
  35 + private String name;
  36 + /**
  37 + * 接口地址
  38 + */
  39 + private String baseUrl;
  40 + /**
  41 + * API 密钥
  42 + */
  43 + private String apiKey;
  44 + /**
  45 + * 请求附属 Header
  46 + *
  47 + * JSON 数组,格式为 [{"key":"...","value":"..."}];value 可包含 {session} 占位符,请求时替换为会话编号
  48 + */
  49 + private String headers;
  50 + /**
  51 + * 状态
  52 + *
  53 + * 枚举 {@link CommonStatusEnum}
  54 + */
  55 + private Integer status;
  56 + /**
  57 + * 备注
  58 + */
  59 + private String remark;
  60 +
  61 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/session/Ai1SessionDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.session;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.ai1.dal.dataobject.agent.Ai1AgentDO;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * AI1 会话 DO
  12 + *
  13 + * 管理后台登录用户与 Agent 的一次会话
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@TableName("ai1_session")
  18 +@KeySequence("ai1_session_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  19 +@Data
  20 +@EqualsAndHashCode(callSuper = true)
  21 +@ToString(callSuper = true)
  22 +@Builder
  23 +@NoArgsConstructor
  24 +@AllArgsConstructor
  25 +public class Ai1SessionDO extends TenantBaseDO {
  26 +
  27 + /**
  28 + * 默认标题:首条消息发送后,自动替换为提问内容
  29 + */
  30 + public static final String TITLE_DEFAULT = "新会话";
  31 + /**
  32 + * 自动生成的会话标题最大长度:首条提问内容超出时截断
  33 + */
  34 + public static final int TITLE_MAX_LENGTH = 50;
  35 +
  36 + /**
  37 + * 编号
  38 + */
  39 + @TableId
  40 + private Long id;
  41 + /**
  42 + * Agent 编号
  43 + *
  44 + * 关联 {@link Ai1AgentDO#getId()}
  45 + */
  46 + private Long agentId;
  47 + /**
  48 + * 用户编号
  49 + *
  50 + * 关联 AdminUserDO 的 id 编号
  51 + */
  52 + private Long userId;
  53 + /**
  54 + * 标题
  55 + */
  56 + private String title;
  57 +
  58 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/session/Ai1SessionMessageDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.session;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.ai1.enums.session.Ai1SessionMessageRoleEnum;
  5 +import cn.iocoder.yudao.module.ai1.enums.session.Ai1SessionMessageStatusEnum;
  6 +import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableId;
  8 +import com.baomidou.mybatisplus.annotation.TableName;
  9 +import lombok.*;
  10 +
  11 +/**
  12 + * AI1 会话消息 DO
  13 + *
  14 + * 助手消息的编号同时作为 Redis 结果流标识,用于断线续传
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@TableName("ai1_session_message")
  19 +@KeySequence("ai1_session_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  20 +@Data
  21 +@EqualsAndHashCode(callSuper = true)
  22 +@ToString(callSuper = true)
  23 +@Builder
  24 +@NoArgsConstructor
  25 +@AllArgsConstructor
  26 +public class Ai1SessionMessageDO extends TenantBaseDO {
  27 +
  28 + /**
  29 + * 编号
  30 + */
  31 + @TableId
  32 + private Long id;
  33 + /**
  34 + * 会话编号
  35 + *
  36 + * 关联 {@link Ai1SessionDO#getId()}
  37 + */
  38 + private Long sessionId;
  39 + /**
  40 + * 角色
  41 + *
  42 + * 枚举 {@link Ai1SessionMessageRoleEnum}
  43 + */
  44 + private String role;
  45 + /**
  46 + * 思考过程,即推理模型的 reasoning_content
  47 + */
  48 + private String reasoning;
  49 + /**
  50 + * 内容
  51 + */
  52 + private String content;
  53 + /**
  54 + * 生成状态
  55 + *
  56 + * 枚举 {@link Ai1SessionMessageStatusEnum}
  57 + */
  58 + private Integer status;
  59 +
  60 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/skill/Ai1SkillDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * AI1 SKILL DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("ai1_skill")
  16 +@KeySequence("ai1_skill_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class Ai1SkillDO extends TenantBaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 名称
  32 + *
  33 + * 同时作为物化目录名,租户内唯一,仅支持字母、数字、中划线
  34 + */
  35 + private String name;
  36 + /**
  37 + * 描述
  38 + */
  39 + private String description;
  40 + /**
  41 + * 版本
  42 + */
  43 + private String version;
  44 + /**
  45 + * 状态
  46 + *
  47 + * 枚举 {@link CommonStatusEnum}
  48 + */
  49 + private Integer status;
  50 +
  51 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/dataobject/skill/Ai1SkillFileDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.dataobject.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.ai1.enums.skill.Ai1SkillFileTypeEnum;
  5 +import com.baomidou.mybatisplus.annotation.KeySequence;
  6 +import com.baomidou.mybatisplus.annotation.TableId;
  7 +import com.baomidou.mybatisplus.annotation.TableName;
  8 +import lombok.*;
  9 +
  10 +/**
  11 + * AI1 SKILL 内容文件 DO
  12 + *
  13 + * 以 parentId 组织为文件树,物化时按树结构写入本地技能目录
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@TableName("ai1_skill_file")
  18 +@KeySequence("ai1_skill_file_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  19 +@Data
  20 +@EqualsAndHashCode(callSuper = true)
  21 +@ToString(callSuper = true)
  22 +@Builder
  23 +@NoArgsConstructor
  24 +@AllArgsConstructor
  25 +public class Ai1SkillFileDO extends TenantBaseDO {
  26 +
  27 + /**
  28 + * 父目录编号:根级
  29 + */
  30 + public static final Long PARENT_ID_ROOT = 0L;
  31 +
  32 + /**
  33 + * 固定文件名:SKILL 入口文件
  34 + */
  35 + public static final String NAME_SKILL = "SKILL.md";
  36 +
  37 + /**
  38 + * 编号
  39 + */
  40 + @TableId
  41 + private Long id;
  42 + /**
  43 + * SKILL 编号
  44 + *
  45 + * 关联 {@link Ai1SkillDO#getId()}
  46 + */
  47 + private Long skillId;
  48 + /**
  49 + * 父目录编号
  50 + *
  51 + * 关联 {@link Ai1SkillFileDO#getId()};根级为 {@link #PARENT_ID_ROOT}
  52 + */
  53 + private Long parentId;
  54 + /**
  55 + * 文件或目录名称
  56 + */
  57 + private String name;
  58 + /**
  59 + * 类型
  60 + *
  61 + * 枚举 {@link Ai1SkillFileTypeEnum}
  62 + */
  63 + private Integer type;
  64 + /**
  65 + * 文件类型,即扩展名;目录为空
  66 + */
  67 + private String fileType;
  68 + /**
  69 + * 文件内容;目录为空
  70 + */
  71 + private String content;
  72 + /**
  73 + * 是否固定
  74 + *
  75 + * 固定节点(SKILL.md、scripts/、reference/)不可删除、改名、移动
  76 + */
  77 + private Boolean locked;
  78 + /**
  79 + * 排序
  80 + */
  81 + private Integer sort;
  82 +
  83 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/agent/Ai1AgentMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.agent;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
  7 +import cn.iocoder.yudao.module.ai1.controller.admin.agent.vo.Ai1AgentPageReqVO;
  8 +import cn.iocoder.yudao.module.ai1.dal.dataobject.agent.Ai1AgentDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +
  11 +import java.util.Collection;
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * AI1 Agent Mapper
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Mapper
  20 +public interface Ai1AgentMapper extends BaseMapperX<Ai1AgentDO> {
  21 +
  22 + default PageResult<Ai1AgentDO> selectPage(Ai1AgentPageReqVO reqVO) {
  23 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1AgentDO>()
  24 + .likeIfPresent(Ai1AgentDO::getName, reqVO.getName())
  25 + .eqIfPresent(Ai1AgentDO::getStatus, reqVO.getStatus())
  26 + .orderByAsc(Ai1AgentDO::getId));
  27 + }
  28 +
  29 + default List<Ai1AgentDO> selectListByStatusOrderById(Integer status) {
  30 + return selectList(new LambdaQueryWrapperX<Ai1AgentDO>()
  31 + .eq(Ai1AgentDO::getStatus, status)
  32 + .orderByAsc(Ai1AgentDO::getId));
  33 + }
  34 +
  35 + default Long selectCountByModelIds(Collection<Long> modelIds) {
  36 + return selectCount(new LambdaQueryWrapperX<Ai1AgentDO>().in(Ai1AgentDO::getModelId, modelIds));
  37 + }
  38 +
  39 + default Long selectCountByKnowledgeBaseIds(Collection<Long> knowledgeBaseIds) {
  40 + return selectCount(new LambdaQueryWrapperX<Ai1AgentDO>()
  41 + .apply("(" + MyBatisUtils.findInSet("knowledge_base_ids", knowledgeBaseIds) + ")", knowledgeBaseIds.toArray()));
  42 + }
  43 +
  44 + default Long selectCountBySkillIds(Collection<Long> skillIds) {
  45 + return selectCount(new LambdaQueryWrapperX<Ai1AgentDO>()
  46 + .apply("(" + MyBatisUtils.findInSet("skill_ids", skillIds) + ")", skillIds.toArray()));
  47 + }
  48 +
  49 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.home;
  2 +
  3 +import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeMessageSummaryByAgentRespVO;
  4 +import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeMessageSummaryByDateRespVO;
  5 +import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeSummaryRespVO;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +import org.apache.ibatis.annotations.Param;
  8 +import org.apache.ibatis.annotations.Select;
  9 +
  10 +import java.time.LocalDateTime;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI1 首页统计 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface Ai1HomeMapper {
  20 +
  21 + @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = FALSE) AS agentCount, " +
  22 + "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = FALSE) AS skillCount, " +
  23 + "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = FALSE) AS mcpCount, " +
  24 + "(SELECT COUNT(1) FROM ai1_model WHERE deleted = FALSE) AS modelCount")
  25 + Ai1HomeSummaryRespVO selectSummary();
  26 +
  27 + /**
  28 + * 按天统计消息数量,仅返回有消息的日期;使用 CAST AS DATE 兼容 MySQL 与 H2
  29 + */
  30 + @Select("SELECT CAST(create_time AS DATE) AS `date`, COUNT(1) AS `count` " +
  31 + "FROM ai1_session_message " +
  32 + "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = FALSE " +
  33 + "GROUP BY CAST(create_time AS DATE) " +
  34 + "ORDER BY `date`")
  35 + List<Ai1HomeMessageSummaryByDateRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByDate(
  36 + @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime);
  37 +
  38 + /**
  39 + * 按 Agent 统计消息数量,按数量倒序,数量相同按 Agent 编号升序;已删除会话下的消息不计入
  40 + */
  41 + @Select("SELECT c.agent_id AS agentId, COUNT(1) AS `count` " +
  42 + "FROM ai1_session_message m " +
  43 + "INNER JOIN ai1_session c ON m.session_id = c.id AND c.deleted = FALSE " +
  44 + "WHERE m.create_time BETWEEN #{beginTime} AND #{endTime} AND m.deleted = FALSE " +
  45 + "GROUP BY c.agent_id " +
  46 + "ORDER BY `count` DESC, c.agent_id")
  47 + List<Ai1HomeMessageSummaryByAgentRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByAgentId(
  48 + @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime);
  49 +
  50 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/knowledge/Ai1KnowledgeBaseMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base.Ai1KnowledgeBasePageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeBaseDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI1 知识库 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface Ai1KnowledgeBaseMapper extends BaseMapperX<Ai1KnowledgeBaseDO> {
  20 +
  21 + default PageResult<Ai1KnowledgeBaseDO> selectPage(Ai1KnowledgeBasePageReqVO reqVO) {
  22 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1KnowledgeBaseDO>()
  23 + .likeIfPresent(Ai1KnowledgeBaseDO::getName, reqVO.getName())
  24 + .eqIfPresent(Ai1KnowledgeBaseDO::getStatus, reqVO.getStatus())
  25 + .orderByAsc(Ai1KnowledgeBaseDO::getId));
  26 + }
  27 +
  28 + default List<Ai1KnowledgeBaseDO> selectListByStatus(Integer status) {
  29 + return selectList(new LambdaQueryWrapperX<Ai1KnowledgeBaseDO>()
  30 + .eq(Ai1KnowledgeBaseDO::getStatus, status)
  31 + .orderByAsc(Ai1KnowledgeBaseDO::getId));
  32 + }
  33 +
  34 + default Long selectCountByEmbeddingModelIds(Collection<Long> modelIds) {
  35 + return selectCount(new LambdaQueryWrapperX<Ai1KnowledgeBaseDO>()
  36 + .in(Ai1KnowledgeBaseDO::getEmbeddingModelId, modelIds));
  37 + }
  38 +
  39 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/knowledge/Ai1KnowledgeDocumentMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.document.Ai1KnowledgeDocumentPageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeDocumentDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI1 知识文档 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface Ai1KnowledgeDocumentMapper extends BaseMapperX<Ai1KnowledgeDocumentDO> {
  20 +
  21 + default PageResult<Ai1KnowledgeDocumentDO> selectPage(Ai1KnowledgeDocumentPageReqVO reqVO) {
  22 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1KnowledgeDocumentDO>()
  23 + .eq(Ai1KnowledgeDocumentDO::getKnowledgeBaseId, reqVO.getKnowledgeBaseId())
  24 + .likeIfPresent(Ai1KnowledgeDocumentDO::getName, reqVO.getName())
  25 + .eqIfPresent(Ai1KnowledgeDocumentDO::getStatus, reqVO.getStatus())
  26 + .orderByAsc(Ai1KnowledgeDocumentDO::getId));
  27 + }
  28 +
  29 + default List<Ai1KnowledgeDocumentDO> selectListByKnowledgeBaseId(Long knowledgeBaseId) {
  30 + return selectList(new LambdaQueryWrapperX<Ai1KnowledgeDocumentDO>()
  31 + .eq(Ai1KnowledgeDocumentDO::getKnowledgeBaseId, knowledgeBaseId)
  32 + .orderByAsc(Ai1KnowledgeDocumentDO::getId));
  33 + }
  34 +
  35 + default int deleteByKnowledgeBaseIds(Collection<Long> knowledgeBaseIds) {
  36 + return delete(new LambdaQueryWrapperX<Ai1KnowledgeDocumentDO>()
  37 + .in(Ai1KnowledgeDocumentDO::getKnowledgeBaseId, knowledgeBaseIds));
  38 + }
  39 +
  40 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/mcp/Ai1McpMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.mcp;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo.Ai1McpPageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.mcp.Ai1McpDO;
  8 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI1 MCP 服务 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface Ai1McpMapper extends BaseMapperX<Ai1McpDO> {
  20 +
  21 + default PageResult<Ai1McpDO> selectPage(Ai1McpPageReqVO reqVO) {
  22 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1McpDO>()
  23 + .likeIfPresent(Ai1McpDO::getName, reqVO.getName())
  24 + .eqIfPresent(Ai1McpDO::getStatus, reqVO.getStatus())
  25 + .orderByAsc(Ai1McpDO::getId));
  26 + }
  27 +
  28 + default List<Ai1McpDO> selectListByStatus(Integer status) {
  29 + return selectList(new LambdaQueryWrapperX<Ai1McpDO>()
  30 + .eq(Ai1McpDO::getStatus, status)
  31 + .orderByAsc(Ai1McpDO::getId));
  32 + }
  33 +
  34 + default void updateForSave(Ai1McpDO mcp) {
  35 + update(mcp, new LambdaUpdateWrapper<Ai1McpDO>().eq(Ai1McpDO::getId, mcp.getId())
  36 + .set(mcp.getUrl() == null, Ai1McpDO::getUrl, null));
  37 + }
  38 +
  39 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/model/Ai1ModelMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.model.Ai1ModelPageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ModelDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI1 模型 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface Ai1ModelMapper extends BaseMapperX<Ai1ModelDO> {
  20 +
  21 + default PageResult<Ai1ModelDO> selectPage(Ai1ModelPageReqVO reqVO) {
  22 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1ModelDO>()
  23 + .eqIfPresent(Ai1ModelDO::getProviderId, reqVO.getProviderId())
  24 + .likeIfPresent(Ai1ModelDO::getName, reqVO.getName())
  25 + .eqIfPresent(Ai1ModelDO::getType, reqVO.getType())
  26 + .orderByAsc(Ai1ModelDO::getId));
  27 + }
  28 +
  29 + default Ai1ModelDO selectByProviderIdAndModel(Long providerId, String model) {
  30 + return selectOne(Ai1ModelDO::getProviderId, providerId, Ai1ModelDO::getModel, model);
  31 + }
  32 +
  33 + default List<Ai1ModelDO> selectListByProviderId(Long providerId) {
  34 + return selectList(new LambdaQueryWrapperX<Ai1ModelDO>()
  35 + .eq(Ai1ModelDO::getProviderId, providerId)
  36 + .orderByAsc(Ai1ModelDO::getId));
  37 + }
  38 +
  39 + default List<Ai1ModelDO> selectListByProviderIdAndTypeAndStatus(Long providerId, Integer type, Integer status) {
  40 + return selectList(new LambdaQueryWrapperX<Ai1ModelDO>()
  41 + .eqIfPresent(Ai1ModelDO::getProviderId, providerId)
  42 + .eqIfPresent(Ai1ModelDO::getType, type)
  43 + .eq(Ai1ModelDO::getStatus, status)
  44 + .orderByAsc(Ai1ModelDO::getId));
  45 + }
  46 +
  47 + default Long selectCountByProviderIds(Collection<Long> providerIds) {
  48 + return selectCount(new LambdaQueryWrapperX<Ai1ModelDO>().in(Ai1ModelDO::getProviderId, providerIds));
  49 + }
  50 +
  51 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/model/Ai1ProviderMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.model;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider.Ai1ProviderPageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.model.Ai1ProviderDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * AI1 供应商 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface Ai1ProviderMapper extends BaseMapperX<Ai1ProviderDO> {
  19 +
  20 + default PageResult<Ai1ProviderDO> selectPage(Ai1ProviderPageReqVO reqVO) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1ProviderDO>()
  22 + .likeIfPresent(Ai1ProviderDO::getName, reqVO.getName())
  23 + .eqIfPresent(Ai1ProviderDO::getStatus, reqVO.getStatus())
  24 + .orderByAsc(Ai1ProviderDO::getId));
  25 + }
  26 +
  27 + default List<Ai1ProviderDO> selectListByStatus(Integer status) {
  28 + return selectList(new LambdaQueryWrapperX<Ai1ProviderDO>()
  29 + .eq(Ai1ProviderDO::getStatus, status)
  30 + .orderByAsc(Ai1ProviderDO::getId));
  31 + }
  32 +
  33 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/package-info.java 0 → 100644
  1 +/**
  2 + * DAL = Data Access Layer 数据访问层
  3 + * 1. data object:数据对象
  4 + * 2. redis:Redis 的 CRUD 操作
  5 + * 3. mysql:MySQL 的 CRUD 操作
  6 + *
  7 + * 其中,MySQL 的表以 ai1_ 作为前缀
  8 + */
  9 +package cn.iocoder.yudao.module.ai1.dal.mysql;
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/session/Ai1SessionMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.session;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.session.Ai1SessionPageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.session.Ai1SessionDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI1 会话 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface Ai1SessionMapper extends BaseMapperX<Ai1SessionDO> {
  20 +
  21 + default PageResult<Ai1SessionDO> selectPage(Ai1SessionPageReqVO reqVO) {
  22 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1SessionDO>()
  23 + .eq(Ai1SessionDO::getAgentId, reqVO.getAgentId())
  24 + .likeIfPresent(Ai1SessionDO::getTitle, reqVO.getTitle())
  25 + .eqIfPresent(Ai1SessionDO::getUserId, reqVO.getUserId())
  26 + .orderByDesc(Ai1SessionDO::getId));
  27 + }
  28 +
  29 + default List<Ai1SessionDO> selectListByAgentIdAndUserId(Long agentId, Long userId) {
  30 + return selectList(new LambdaQueryWrapperX<Ai1SessionDO>()
  31 + .eq(Ai1SessionDO::getAgentId, agentId)
  32 + .eq(Ai1SessionDO::getUserId, userId)
  33 + .orderByDesc(Ai1SessionDO::getId));
  34 + }
  35 +
  36 + default List<Ai1SessionDO> selectListByAgentIds(Collection<Long> agentIds) {
  37 + return selectList(new LambdaQueryWrapperX<Ai1SessionDO>().in(Ai1SessionDO::getAgentId, agentIds));
  38 + }
  39 +
  40 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/session/Ai1SessionMessageMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.session;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.dal.dataobject.session.Ai1SessionMessageDO;
  7 +import org.apache.ibatis.annotations.Mapper;
  8 +
  9 +import java.util.Collection;
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * AI1 会话消息 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface Ai1SessionMessageMapper extends BaseMapperX<Ai1SessionMessageDO> {
  19 +
  20 + default List<Ai1SessionMessageDO> selectListBySessionId(Long sessionId) {
  21 + return selectList(new LambdaQueryWrapperX<Ai1SessionMessageDO>()
  22 + .eq(Ai1SessionMessageDO::getSessionId, sessionId)
  23 + .orderByAsc(Ai1SessionMessageDO::getId));
  24 + }
  25 +
  26 + /**
  27 + * 按编号倒序,取指定消息之前最近的 limit 条
  28 + */
  29 + default List<Ai1SessionMessageDO> selectListBySessionIdAndIdLessThan(Long sessionId, Long id, Integer limit) {
  30 + return selectList(new QueryWrapperX<Ai1SessionMessageDO>()
  31 + .limitN(limit)
  32 + .eq("session_id", sessionId)
  33 + .lt("id", id)
  34 + .orderByDesc("id"));
  35 + }
  36 +
  37 + default Long selectCountBySessionId(Long sessionId) {
  38 + return selectCount(Ai1SessionMessageDO::getSessionId, sessionId);
  39 + }
  40 +
  41 + default int deleteBySessionIds(Collection<Long> sessionIds) {
  42 + return delete(new LambdaQueryWrapperX<Ai1SessionMessageDO>().in(Ai1SessionMessageDO::getSessionId, sessionIds));
  43 + }
  44 +
  45 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/skill/Ai1SkillFileMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.ai1.dal.dataobject.skill.Ai1SkillFileDO;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +
  8 +import java.util.Collection;
  9 +import java.util.List;
  10 +
  11 +/**
  12 + * AI1 SKILL 内容文件 Mapper
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Mapper
  17 +public interface Ai1SkillFileMapper extends BaseMapperX<Ai1SkillFileDO> {
  18 +
  19 + /**
  20 + * 按目录优先、排序号、编号升序返回 SKILL 下的全部节点
  21 + */
  22 + default List<Ai1SkillFileDO> selectListBySkillId(Long skillId) {
  23 + return selectList(new LambdaQueryWrapperX<Ai1SkillFileDO>()
  24 + .eq(Ai1SkillFileDO::getSkillId, skillId)
  25 + .orderByAsc(Ai1SkillFileDO::getType)
  26 + .orderByAsc(Ai1SkillFileDO::getSort)
  27 + .orderByAsc(Ai1SkillFileDO::getId));
  28 + }
  29 +
  30 + default List<Ai1SkillFileDO> selectListBySkillIdAndParentId(Long skillId, Long parentId) {
  31 + return selectList(new LambdaQueryWrapperX<Ai1SkillFileDO>()
  32 + .eq(Ai1SkillFileDO::getSkillId, skillId)
  33 + .eq(Ai1SkillFileDO::getParentId, parentId));
  34 + }
  35 +
  36 + default Ai1SkillFileDO selectBySkillIdAndParentIdAndName(Long skillId, Long parentId, String name) {
  37 + return selectOne(new LambdaQueryWrapperX<Ai1SkillFileDO>()
  38 + .eq(Ai1SkillFileDO::getSkillId, skillId)
  39 + .eq(Ai1SkillFileDO::getParentId, parentId)
  40 + .eq(Ai1SkillFileDO::getName, name));
  41 + }
  42 +
  43 + default int deleteBySkillIds(Collection<Long> skillIds) {
  44 + return delete(new LambdaQueryWrapperX<Ai1SkillFileDO>().in(Ai1SkillFileDO::getSkillId, skillIds));
  45 + }
  46 +
  47 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/skill/Ai1SkillMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.mysql.skill;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai1.controller.admin.skill.vo.skill.Ai1SkillPageReqVO;
  7 +import cn.iocoder.yudao.module.ai1.dal.dataobject.skill.Ai1SkillDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * AI1 SKILL Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface Ai1SkillMapper extends BaseMapperX<Ai1SkillDO> {
  19 +
  20 + default PageResult<Ai1SkillDO> selectPage(Ai1SkillPageReqVO reqVO) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<Ai1SkillDO>()
  22 + .likeIfPresent(Ai1SkillDO::getName, reqVO.getName())
  23 + .eqIfPresent(Ai1SkillDO::getStatus, reqVO.getStatus())
  24 + .orderByAsc(Ai1SkillDO::getId));
  25 + }
  26 +
  27 + default Ai1SkillDO selectByName(String name) {
  28 + return selectOne(Ai1SkillDO::getName, name);
  29 + }
  30 +
  31 + default List<Ai1SkillDO> selectListByStatus(Integer status) {
  32 + return selectList(new LambdaQueryWrapperX<Ai1SkillDO>()
  33 + .eq(Ai1SkillDO::getStatus, status)
  34 + .orderByAsc(Ai1SkillDO::getId));
  35 + }
  36 +
  37 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/redis/Ai1RedisKeyConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.dal.redis;
  2 +
  3 +/**
  4 + * AI1 Redis Key 枚举类
  5 + *
  6 + * @author 芋道源码
  7 + */
  8 +public interface Ai1RedisKeyConstants {
  9 +
  10 + /**
  11 + * 会话生成任务队列
  12 + * <p>
  13 + * KEY 格式:ai1:session:tasks
  14 + * VALUE 数据类型:Stream 生成任务,字段为 tenantId、messageId、agentId、sessionId、content
  15 + * <p>
  16 + * 所有租户共用一个队列,KEY 不带 tenantId;worker 从任务的 tenantId 字段还原租户上下文
  17 + */
  18 + String SESSION_TASK_STREAM = "ai1:session:tasks";
  19 +
  20 + /**
  21 + * 会话生成任务队列的消费组
  22 + */
  23 + String SESSION_TASK_GROUP = "ai1-session-workers";
  24 +
  25 + /**
  26 + * 会话生成任务执行锁,按助手消息编号隔离;使用 Redisson 看门狗续期
  27 + * <p>
  28 + * KEY 格式:ai1:session:task-lock:{messageId}
  29 + */
  30 + String SESSION_TASK_LOCK = "ai1:session:task-lock:%d";
  31 +
  32 + /**
  33 + * 会话结果流,按助手消息编号隔离
  34 + * <p>
  35 + * KEY 格式:ai1:session:result:{messageId}
  36 + * VALUE 数据类型:Stream 生成事件,字段为 type、data
  37 + * <p>
  38 + * messageId 全局唯一,KEY 不带 tenantId;续传前已按租户、用户校验消息归属
  39 + */
  40 + String SESSION_RESULT_STREAM = "ai1:session:result:%d";
  41 +
  42 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/Ai1ErrorCodeConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.ErrorCode;
  4 +
  5 +/**
  6 + * AI1 错误码枚举类
  7 + * <p>
  8 + * ai1 系统,使用 1-041-000-000 段
  9 + */
  10 +public interface Ai1ErrorCodeConstants {
  11 +
  12 + // ========== 供应商 1-041-000-000 ==========
  13 + ErrorCode PROVIDER_NOT_EXISTS = new ErrorCode(1_041_000_000, "供应商不存在");
  14 + ErrorCode PROVIDER_DISABLE = new ErrorCode(1_041_000_001, "供应商({}) 已禁用");
  15 + ErrorCode PROVIDER_HAS_MODEL = new ErrorCode(1_041_000_003, "供应商下存在模型,禁止删除");
  16 + ErrorCode PROVIDER_REMOTE_MODEL_LOAD_FAIL = new ErrorCode(1_041_000_004, "模型拉取失败:请检查供应商地址与 API 密钥");
  17 + ErrorCode PROVIDER_REMOTE_MODEL_EMPTY = new ErrorCode(1_041_000_005, "远程未返回可用模型");
  18 + ErrorCode CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_041_000_006, "配置({})无法解析,请检查环境变量或配置项");
  19 +
  20 + // ========== 模型 1-041-001-000 ==========
  21 + ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_041_001_000, "模型不存在");
  22 + ErrorCode MODEL_DISABLE = new ErrorCode(1_041_001_001, "模型({}) 已禁用");
  23 + ErrorCode MODEL_DUPLICATE = new ErrorCode(1_041_001_002, "模型标识({}) 已存在");
  24 + ErrorCode MODEL_USED_BY_AGENT = new ErrorCode(1_041_001_003, "模型已被 Agent 使用,禁止删除");
  25 + ErrorCode MODEL_NOT_BELONG_PROVIDER = new ErrorCode(1_041_001_004, "模型不属于该供应商");
  26 + ErrorCode MODEL_TYPE_NOT_CHAT = new ErrorCode(1_041_001_005, "所选模型不是对话模型");
  27 + ErrorCode MODEL_TYPE_NOT_EMBEDDING = new ErrorCode(1_041_001_006, "所选模型不是嵌入模型");
  28 + ErrorCode MODEL_IMPORT_ALL_EXISTS = new ErrorCode(1_041_001_007, "所选模型均已导入,无需重复导入");
  29 + ErrorCode MODEL_USED_BY_KNOWLEDGE_BASE = new ErrorCode(1_041_001_008, "模型已被知识库使用,禁止删除");
  30 + ErrorCode MODEL_TYPE_USED_BY_AGENT = new ErrorCode(1_041_001_009, "模型已被 Agent 使用,禁止修改类型");
  31 + ErrorCode MODEL_TYPE_USED_BY_KNOWLEDGE_BASE = new ErrorCode(1_041_001_010, "模型已被知识库使用,禁止修改类型");
  32 +
  33 + // ========== 知识库 1-041-002-000 ==========
  34 + ErrorCode KNOWLEDGE_BASE_NOT_EXISTS = new ErrorCode(1_041_002_000, "知识库不存在");
  35 + ErrorCode KNOWLEDGE_BASE_DOCUMENT_EMPTY = new ErrorCode(1_041_002_001, "知识库下暂无文档");
  36 + ErrorCode KNOWLEDGE_BASE_SEARCH_FAIL = new ErrorCode(1_041_002_002, "向量检索失败:{}");
  37 + ErrorCode KNOWLEDGE_BASE_USED_BY_AGENT = new ErrorCode(1_041_002_003, "知识库已被 Agent 使用,禁止删除");
  38 +
  39 + // ========== 知识文档 1-041-003-000 ==========
  40 + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_041_003_000, "文档不存在");
  41 + ErrorCode KNOWLEDGE_DOCUMENT_CONTENT_EMPTY = new ErrorCode(1_041_003_001, "文档内容为空,无法向量化");
  42 + ErrorCode KNOWLEDGE_DOCUMENT_FILE_TYPE_INVALID = new ErrorCode(1_041_003_002, "仅支持 .txt / .md 文本文件");
  43 + ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_041_003_003, "文件内容为空");
  44 + ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_041_003_004, "文件读取失败:{}");
  45 + ErrorCode KNOWLEDGE_DOCUMENT_VECTORIZE_FAIL = new ErrorCode(1_041_003_005, "向量化失败:{}");
  46 +
  47 + // ========== MCP 1-041-004-000 ==========
  48 + ErrorCode MCP_NOT_EXISTS = new ErrorCode(1_041_004_000, "MCP 不存在");
  49 + ErrorCode MCP_CONFIG_INVALID = new ErrorCode(1_041_004_001, "config 配置需为合法的 JSON 对象");
  50 + ErrorCode MCP_COMMAND_REQUIRED = new ErrorCode(1_041_004_002, "本地(stdio)类型必须在 config 中配置 command 命令");
  51 + ErrorCode MCP_URL_REQUIRED = new ErrorCode(1_041_004_003, "远程(http)类型必须配置服务地址");
  52 +
  53 + // ========== SKILL 1-041-005-000 ==========
  54 + ErrorCode SKILL_NOT_EXISTS = new ErrorCode(1_041_005_000, "SKILL 不存在");
  55 + ErrorCode SKILL_USED_BY_AGENT = new ErrorCode(1_041_005_001, "SKILL 已被 Agent 使用,禁止删除");
  56 + ErrorCode SKILL_NAME_DUPLICATE = new ErrorCode(1_041_005_002, "SKILL 名称({}) 已存在");
  57 +
  58 + // ========== SKILL 内容文件 1-041-006-000 ==========
  59 + ErrorCode SKILL_FILE_NOT_EXISTS = new ErrorCode(1_041_006_000, "文件或目录不存在");
  60 + ErrorCode SKILL_FILE_NAME_INVALID = new ErrorCode(1_041_006_001, "名称格式不合法(不能包含路径分隔符、控制字符,且不能为 . 或 ..)");
  61 + ErrorCode SKILL_FILE_NAME_DUPLICATE = new ErrorCode(1_041_006_002, "同级下已存在同名节点({})");
  62 + ErrorCode SKILL_FILE_PARENT_INVALID = new ErrorCode(1_041_006_003, "父节点必须是同一 SKILL 下的目录");
  63 + ErrorCode SKILL_FILE_LOCKED = new ErrorCode(1_041_006_004, "固定文件或目录禁止{}");
  64 + ErrorCode SKILL_FILE_MOVE_TO_SELF = new ErrorCode(1_041_006_005, "不能移入自身");
  65 + ErrorCode SKILL_FILE_NOT_FILE = new ErrorCode(1_041_006_006, "目录不支持保存内容");
  66 + ErrorCode SKILL_FILE_MOVE_TO_DESCENDANT = new ErrorCode(1_041_006_007, "不能移入自身的子目录");
  67 + ErrorCode SKILL_FILE_PARENT_NOT_EXISTS = new ErrorCode(1_041_006_008, "父目录不存在");
  68 +
  69 + // ========== Agent 1-041-007-000 ==========
  70 + ErrorCode AGENT_NOT_EXISTS = new ErrorCode(1_041_007_000, "Agent 不存在");
  71 + ErrorCode AGENT_DISABLE = new ErrorCode(1_041_007_001, "Agent({}) 已关闭");
  72 +
  73 + // ========== 会话 1-041-008-000 ==========
  74 + ErrorCode SESSION_NOT_EXISTS = new ErrorCode(1_041_008_000, "会话不存在");
  75 +
  76 + // ========== 会话消息 1-041-009-000 ==========
  77 + ErrorCode SESSION_MESSAGE_NOT_EXISTS = new ErrorCode(1_041_009_000, "会话消息不存在");
  78 +
  79 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/knowledge/Ai1KnowledgeDocumentStatusEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.knowledge;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * AI1 知识文档向量化状态的枚举
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Getter
  15 +@AllArgsConstructor
  16 +public enum Ai1KnowledgeDocumentStatusEnum implements ArrayValuable<Integer> {
  17 +
  18 + UNPROCESSED(0, "未处理"),
  19 + VECTORIZED(1, "已向量化"),
  20 + FAILED(2, "失败");
  21 +
  22 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(Ai1KnowledgeDocumentStatusEnum::getStatus).toArray(Integer[]::new);
  23 +
  24 + /**
  25 + * 状态
  26 + */
  27 + private final Integer status;
  28 + /**
  29 + * 名称
  30 + */
  31 + private final String name;
  32 +
  33 + @Override
  34 + public Integer[] array() {
  35 + return ARRAYS;
  36 + }
  37 +
  38 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/mcp/Ai1McpTransportEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.mcp;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Getter;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * AI1 MCP 传输方式的枚举
  12 + *
  13 + * 取值与 MCP 配置 JSON 中的 transport 字段一致
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Getter
  18 +@AllArgsConstructor
  19 +public enum Ai1McpTransportEnum implements ArrayValuable<String> {
  20 +
  21 + HTTP("http", "远程(Streamable HTTP)"),
  22 + STDIO("stdio", "本地进程(stdio)");
  23 +
  24 + public static final String[] ARRAYS = Arrays.stream(values()).map(Ai1McpTransportEnum::getTransport).toArray(String[]::new);
  25 +
  26 + /**
  27 + * 传输方式
  28 + */
  29 + private final String transport;
  30 + /**
  31 + * 名称
  32 + */
  33 + private final String name;
  34 +
  35 + @Override
  36 + public String[] array() {
  37 + return ARRAYS;
  38 + }
  39 +
  40 + public static boolean isStdio(String transport) {
  41 + return ObjUtil.equal(STDIO.transport, transport);
  42 + }
  43 +
  44 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/model/Ai1ModelTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.model;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Getter;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * AI1 模型类型的枚举
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Getter
  16 +@AllArgsConstructor
  17 +public enum Ai1ModelTypeEnum implements ArrayValuable<Integer> {
  18 +
  19 + CHAT(0, "对话"),
  20 + EMBEDDING(1, "嵌入");
  21 +
  22 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(Ai1ModelTypeEnum::getType).toArray(Integer[]::new);
  23 +
  24 + /**
  25 + * 类型
  26 + */
  27 + private final Integer type;
  28 + /**
  29 + * 名称
  30 + */
  31 + private final String name;
  32 +
  33 + @Override
  34 + public Integer[] array() {
  35 + return ARRAYS;
  36 + }
  37 +
  38 + public static boolean isChat(Integer type) {
  39 + return ObjUtil.equal(CHAT.type, type);
  40 + }
  41 +
  42 + public static boolean isEmbedding(Integer type) {
  43 + return ObjUtil.equal(EMBEDDING.type, type);
  44 + }
  45 +
  46 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/session/Ai1SessionMessageRoleEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.session;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +/**
  8 + * AI1 会话消息角色的枚举
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Getter
  13 +@AllArgsConstructor
  14 +public enum Ai1SessionMessageRoleEnum {
  15 +
  16 + USER("user", "用户"),
  17 + ASSISTANT("assistant", "助手");
  18 +
  19 + /**
  20 + * 角色
  21 + */
  22 + private final String role;
  23 + /**
  24 + * 名称
  25 + */
  26 + private final String name;
  27 +
  28 + public static boolean isUser(String role) {
  29 + return ObjUtil.equal(USER.role, role);
  30 + }
  31 +
  32 + public static boolean isAssistant(String role) {
  33 + return ObjUtil.equal(ASSISTANT.role, role);
  34 + }
  35 +
  36 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/session/Ai1SessionMessageStatusEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.session;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Getter;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * AI1 会话消息生成状态的枚举
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Getter
  16 +@AllArgsConstructor
  17 +public enum Ai1SessionMessageStatusEnum implements ArrayValuable<Integer> {
  18 +
  19 + GENERATING(0, "生成中"), // 助手消息占位,可按消息编号续传
  20 + SUCCESS(1, "完成"),
  21 + FAILED(2, "失败");
  22 +
  23 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(Ai1SessionMessageStatusEnum::getStatus).toArray(Integer[]::new);
  24 +
  25 + /**
  26 + * 状态
  27 + */
  28 + private final Integer status;
  29 + /**
  30 + * 名称
  31 + */
  32 + private final String name;
  33 +
  34 + @Override
  35 + public Integer[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 + public static boolean isGenerating(Integer status) {
  40 + return ObjUtil.equal(GENERATING.status, status);
  41 + }
  42 +
  43 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/session/Ai1SessionStreamEventEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.session;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +/**
  8 + * AI1 会话流事件的枚举
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Getter
  13 +@AllArgsConstructor
  14 +public enum Ai1SessionStreamEventEnum {
  15 +
  16 + STREAM("stream", "结果流标识"), // 连接建立时下发,data 为助手消息编号
  17 + THINKING("thinking", "思考过程增量"),
  18 + MESSAGE("message", "回复内容增量"),
  19 + PING("ping", "空闲心跳"),
  20 + DONE("done", "生成结束"),
  21 + ERROR("error", "生成失败"); // data 为错误提示
  22 +
  23 + /**
  24 + * 事件
  25 + */
  26 + private final String event;
  27 + /**
  28 + * 名称
  29 + */
  30 + private final String name;
  31 +
  32 + public static boolean isTerminal(String event) {
  33 + return ObjUtil.equal(DONE.event, event) || ObjUtil.equal(ERROR.event, event);
  34 + }
  35 +
  36 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/enums/skill/Ai1SkillFileTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.enums.skill;
  2 +
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.AllArgsConstructor;
  6 +import lombok.Getter;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * AI1 SKILL 内容节点类型的枚举
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Getter
  16 +@AllArgsConstructor
  17 +public enum Ai1SkillFileTypeEnum implements ArrayValuable<Integer> {
  18 +
  19 + DIRECTORY(0, "目录"),
  20 + FILE(1, "文件");
  21 +
  22 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(Ai1SkillFileTypeEnum::getType).toArray(Integer[]::new);
  23 +
  24 + /**
  25 + * 类型
  26 + */
  27 + private final Integer type;
  28 + /**
  29 + * 名称
  30 + */
  31 + private final String name;
  32 +
  33 + @Override
  34 + public Integer[] array() {
  35 + return ARRAYS;
  36 + }
  37 +
  38 + public static boolean isDirectory(Integer type) {
  39 + return ObjUtil.equal(DIRECTORY.type, type);
  40 + }
  41 +
  42 + public static boolean isFile(Integer type) {
  43 + return ObjUtil.equal(FILE.type, type);
  44 + }
  45 +
  46 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/ai/config/Ai1AutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.framework.ai.config;
  2 +
  3 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  4 +import org.springframework.context.annotation.Configuration;
  5 +
  6 +/**
  7 + * 芋道 AI1 自动配置
  8 + *
  9 + * 模型、向量存储、MCP 客户端均由 framework/ai/core 与 tool 层按数据库配置程序化构建,这里只负责注册 {@link YudaoAi1Properties}
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Configuration(proxyBeanMethods = false)
  14 +@EnableConfigurationProperties(YudaoAi1Properties.class)
  15 +public class Ai1AutoConfiguration {
  16 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/ai/config/YudaoAi1Properties.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.framework.ai.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +
  6 +/**
  7 + * 芋道 AI1 配置类
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@ConfigurationProperties(prefix = "yudao.ai1")
  12 +@Data
  13 +public class YudaoAi1Properties {
  14 +
  15 + /**
  16 + * Milvus 向量数据库
  17 + */
  18 + private Milvus milvus = new Milvus();
  19 +
  20 + /**
  21 + * 会话流
  22 + */
  23 + private Session session = new Session();
  24 +
  25 + /**
  26 + * SKILL 技能
  27 + */
  28 + private Skill skill = new Skill();
  29 +
  30 + @Data
  31 + public static class Milvus {
  32 +
  33 + /**
  34 + * 连接地址
  35 + */
  36 + private String uri = "http://127.0.0.1:19530";
  37 + /**
  38 + * 用户名
  39 + */
  40 + private String username;
  41 + /**
  42 + * 密码
  43 + */
  44 + private String password;
  45 + /**
  46 + * 数据库
  47 + */
  48 + private String database = "default";
  49 + /**
  50 + * 知识库集合名前缀,实际集合名为 {collectionPrefix}{知识库编号}
  51 + */
  52 + private String collectionPrefix = "knowledge_base_";
  53 + /**
  54 + * 默认检索数量,知识库未配置检索数量时使用
  55 + */
  56 + private Integer defaultTopK = 5;
  57 +
  58 + }
  59 +
  60 + @Data
  61 + public static class Session {
  62 +
  63 + /**
  64 + * 默认系统指令中的 Agent 名称占位符
  65 + */
  66 + public static final String AGENT_NAME_PLACEHOLDER = "{agentName}";
  67 +
  68 + /**
  69 + * 默认系统指令:Agent 未配置系统指令时使用,{agentName} 占位符替换为 Agent 名称
  70 + */
  71 + private String defaultSystemPrompt = "你是 " + AGENT_NAME_PLACEHOLDER + " 的智能助手。";
  72 + /**
  73 + * 结果流
  74 + */
  75 + private Stream stream = new Stream();
  76 + /**
  77 + * 生成 worker
  78 + */
  79 + private Worker worker = new Worker();
  80 + /**
  81 + * SSE 连接
  82 + */
  83 + private Sse sse = new Sse();
  84 + /**
  85 + * 上下文历史
  86 + */
  87 + private History history = new History();
  88 +
  89 + }
  90 +
  91 + @Data
  92 + public static class Stream {
  93 +
  94 + /**
  95 + * 单次生成 / 单 SSE 连接最长时长,单位:毫秒
  96 + */
  97 + private Long timeoutMs = 180000L;
  98 + /**
  99 + * 结果流保留时长,单位:秒;决定断线、刷新后可续传的时间窗
  100 + */
  101 + private Long ttlSeconds = 3600L;
  102 +
  103 + }
  104 +
  105 + @Data
  106 + public static class Worker {
  107 +
  108 + /**
  109 + * 单节点生成 worker 线程数(多实例部署时为每个实例的数量)
  110 + */
  111 + private Integer count = 4;
  112 +
  113 + }
  114 +
  115 + @Data
  116 + public static class Sse {
  117 +
  118 + /**
  119 + * 单节点 SSE 最大并发连接数,超出时拒绝新连接
  120 + */
  121 + private Integer maxConnections = 64;
  122 +
  123 + }
  124 +
  125 + @Data
  126 + public static class History {
  127 +
  128 + /**
  129 + * 附加给模型的最近历史消息条数上限
  130 + */
  131 + private Integer limit = 20;
  132 +
  133 + }
  134 +
  135 + @Data
  136 + public static class Skill {
  137 +
  138 + /**
  139 + * 技能物化根目录,实际目录为 {root}/agent_{agentId}(Agent 编号全局唯一,无需按租户分层);
  140 + * 默认放在用户目录下,集群部署建议挂载共享持久卷
  141 + */
  142 + private String root = System.getProperty("user.home") + "/yudao-ai1/skills";
  143 +
  144 + }
  145 +
  146 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/ai/package-info.java 0 → 100644
  1 +/**
  2 + * ai1 模块的 ai 配置
  3 + */
  4 +package cn.iocoder.yudao.module.ai1.framework.ai;
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/package-info.java 0 → 100644
  1 +/**
  2 + * 属于 ai1 模块的 framework 封装
  3 + *
  4 + * @author 芋道源码
  5 + */
  6 +package cn.iocoder.yudao.module.ai1.framework;
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/web/config/Ai1WebConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.framework.web.config;
  2 +
  3 +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration;
  4 +import org.springdoc.core.models.GroupedOpenApi;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +
  8 +/**
  9 + * ai1 模块的 web 组件的 Configuration
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Configuration(proxyBeanMethods = false)
  14 +public class Ai1WebConfiguration {
  15 +
  16 + /**
  17 + * ai1 模块的 API 分组
  18 + */
  19 + @Bean
  20 + public GroupedOpenApi ai1GroupedOpenApi() {
  21 + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("ai1");
  22 + }
  23 +
  24 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/web/package-info.java 0 → 100644
  1 +/**
  2 + * ai1 模块的 web 配置
  3 + */
  4 +package cn.iocoder.yudao.module.ai1.framework.web;
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmChatTool.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.harness.llm;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.ai1.enums.session.Ai1SessionMessageRoleEnum;
  6 +import cn.iocoder.yudao.module.ai1.service.model.bo.Ai1ModelRespBO;
  7 +import jakarta.annotation.Resource;
  8 +import lombok.AllArgsConstructor;
  9 +import lombok.Data;
  10 +import org.springframework.ai.chat.client.ChatClient;
  11 +import org.springframework.ai.chat.client.advisor.api.Advisor;
  12 +import org.springframework.ai.chat.messages.AssistantMessage;
  13 +import org.springframework.ai.chat.messages.Message;
  14 +import org.springframework.ai.chat.messages.SystemMessage;
  15 +import org.springframework.ai.chat.messages.UserMessage;
  16 +import org.springframework.ai.chat.model.ChatResponse;
  17 +import org.springframework.ai.chat.model.Generation;
  18 +import org.springframework.ai.support.ToolCallbacks;
  19 +import org.springframework.ai.tool.ToolCallback;
  20 +import org.springframework.stereotype.Component;
  21 +
  22 +import java.util.ArrayList;
  23 +import java.util.List;
  24 +import java.util.function.Consumer;
  25 +
  26 +/**
  27 + * AI1 LLM 会话工具
  28 + *
  29 + * 只负责「按给定模型与消息执行一次流式会话」:接收已装配好的系统指令、历史消息、工具与 Advisor,
  30 + * 经 ChatClient 流式会话,增量通过回调输出;传输方式(SSE、Redis Stream)由调用方决定
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@Component
  35 +public class Ai1LlmChatTool {
  36 +
  37 + /**
  38 + * 思考过程的元数据键(Spring AI OpenAI 推理模型的 reasoning_content)
  39 + */
  40 + private static final String METADATA_REASONING = "reasoningContent";
  41 +
  42 + @Resource
  43 + private Ai1LlmModelFactory llmModelFactory;
  44 +
  45 + /**
  46 + * 流式会话:装配消息 → 附加工具、Advisor → 增量输出
  47 + *
  48 + * @param model 模型运行时快照(需为对话模型)
  49 + * @param systemPrompt 系统指令,可为空
  50 + * @param histories 历史消息,按时间升序;每项为 [role, content]
  51 + * @param content 当前用户消息
  52 + * @param tools 工具(ToolCallback 或 @Tool 对象),可为空
  53 + * @param advisors 会话 Advisor,可为空
  54 + * @param sessionId 会话编号,用于替换附属 Header 中的 {session} 占位符
  55 + * @param onThinking 思考过程增量回调,可为空
  56 + * @param onContent 回复内容增量回调,可为空
  57 + * @return 完整回复(内容 + 思考过程)
  58 + */
  59 + public ChatText chat(Ai1ModelRespBO model, String systemPrompt, List<String[]> histories, String content,
  60 + List<Object> tools, List<Advisor> advisors, Long sessionId,
  61 + Consumer<String> onThinking, Consumer<String> onContent) {
  62 + // 1. 装配会话请求:消息(系统指令 + 历史 + 当前)+ 工具 + Advisor
  63 + ChatClient.ChatClientRequestSpec spec = llmModelFactory.buildChatClient(model, sessionId).prompt()
  64 + .messages(buildMessages(systemPrompt, histories, content));
  65 + if (CollUtil.isNotEmpty(tools)) {
  66 + // Spring AI 1.x 的 tools 只接受 @Tool 对象,MCP / SkillsTool 的回调需直接注册。
  67 + List<ToolCallback> callbacks = new ArrayList<>();
  68 + for (Object tool : tools) {
  69 + if (tool instanceof ToolCallback callback) {
  70 + callbacks.add(callback);
  71 + } else {
  72 + callbacks.addAll(List.of(ToolCallbacks.from(tool)));
  73 + }
  74 + }
  75 + spec = spec.toolCallbacks(callbacks);
  76 + }
  77 + if (CollUtil.isNotEmpty(advisors)) {
  78 + spec = spec.advisors(advisors);
  79 + }
  80 +
  81 + // 2. 流式执行,逐段回调
  82 + StringBuilder contentText = new StringBuilder();
  83 + StringBuilder thinkingText = new StringBuilder();
  84 + for (ChatResponse chatResponse : spec.stream().chatResponse().toIterable()) {
  85 + Generation generation = chatResponse.getResult();
  86 + // 部分供应商在流末尾下发只含 usage、没有输出的分片,跳过避免 NPE
  87 + if (generation == null || generation.getOutput() == null) {
  88 + continue;
  89 + }
  90 + // 2.1 思考过程:Spring AI 1.x 的 reasoning_content 是增量分片,直接转发(重复文本也可能是有效增量)
  91 + Object reasoningValue = generation.getOutput().getMetadata().get(METADATA_REASONING);
  92 + String delta = reasoningValue instanceof String value ? value : null;
  93 + if (StrUtil.isNotBlank(delta)) {
  94 + thinkingText.append(delta);
  95 + if (onThinking != null) {
  96 + onThinking.accept(delta);
  97 + }
  98 + }
  99 + // 2.2 回复内容
  100 + String chunk = generation.getOutput().getText();
  101 + if (StrUtil.isNotBlank(chunk)) {
  102 + contentText.append(chunk);
  103 + if (onContent != null) {
  104 + onContent.accept(chunk);
  105 + }
  106 + }
  107 + }
  108 + return new ChatText(contentText.toString(), thinkingText.toString());
  109 + }
  110 +
  111 + /**
  112 + * 消息装配:系统指令 + 历史 + 当前用户消息;推理模型的思考过程不进入上下文
  113 + */
  114 + private static List<Message> buildMessages(String systemPrompt, List<String[]> histories, String content) {
  115 + List<Message> messages = new ArrayList<>();
  116 + if (StrUtil.isNotBlank(systemPrompt)) {
  117 + messages.add(new SystemMessage(systemPrompt));
  118 + }
  119 + if (CollUtil.isNotEmpty(histories)) {
  120 + for (String[] history : histories) {
  121 + String text = StrUtil.nullToEmpty(history[1]);
  122 + if (Ai1SessionMessageRoleEnum.isUser(history[0])) {
  123 + messages.add(new UserMessage(text));
  124 + } else if (Ai1SessionMessageRoleEnum.isAssistant(history[0])) {
  125 + messages.add(new AssistantMessage(text));
  126 + }
  127 + }
  128 + }
  129 + messages.add(new UserMessage(content));
  130 + return messages;
  131 + }
  132 +
  133 + /**
  134 + * LLM 会话结果
  135 + */
  136 + @Data
  137 + @AllArgsConstructor
  138 + public static class ChatText {
  139 +
  140 + /**
  141 + * 回复内容
  142 + */
  143 + private String content;
  144 + /**
  145 + * 思考过程,推理模型才有
  146 + */
  147 + private String thinking;
  148 +
  149 + }
  150 +
  151 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/llm/Ai1LlmModelFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.harness.llm;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.ai1.service.model.bo.Ai1ModelRespBO;
  6 +import com.google.common.cache.Cache;
  7 +import com.google.common.cache.CacheBuilder;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +import org.springframework.ai.chat.client.ChatClient;
  10 +import org.springframework.ai.document.MetadataMode;
  11 +import org.springframework.ai.embedding.EmbeddingModel;
  12 +import org.springframework.ai.openai.OpenAiChatModel;
  13 +import org.springframework.ai.openai.OpenAiChatOptions;
  14 +import org.springframework.ai.openai.OpenAiEmbeddingModel;
  15 +import org.springframework.ai.openai.OpenAiEmbeddingOptions;
  16 +import org.springframework.ai.openai.api.OpenAiApi;
  17 +import org.springframework.stereotype.Component;
  18 +import org.springframework.util.LinkedMultiValueMap;
  19 +import org.springframework.util.MultiValueMap;
  20 +
  21 +import java.net.URI;
  22 +import java.time.Duration;
  23 +import java.util.LinkedHashMap;
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +import java.util.Objects;
  27 +import java.util.function.Predicate;
  28 +
  29 +/**
  30 + * AI1 LLM 模型工厂
  31 + *
  32 + * 按 {@link Ai1ModelRespBO} 程序化构建 Spring AI 模型:
  33 + * 1. 对话模型:OpenAiChatModel(OpenAI 兼容协议,覆盖 DeepSeek、GLM、Ollama(/v1) 等)
  34 + * 2. 嵌入模型:OpenAiEmbeddingModel(向量化)
  35 + *
  36 + * 模型按 providerId:modelId 缓存复用(编号全局唯一,无需再区分租户),缓存使用 Guava Cache 按容量淘汰最久未访问的模型;
  37 + * Provider、模型的配置变更时,由对应 Service 调用 {@link #evictByProviderId}、{@link #evictByModelId} 显式失效
  38 + *
  39 + * @author 芋道源码
  40 + */
  41 +@Component
  42 +@Slf4j
  43 +public class Ai1LlmModelFactory {
  44 +
  45 + /**
  46 + * 附属 Header 会话占位符:构建模型时替换为当前会话编号
  47 + */
  48 + public static final String SESSION_PLACEHOLDER = "{session}";
  49 + /**
  50 + * 缓存上限,达到后逐出最久未访问的模型
  51 + */
  52 + private static final int CACHE_MAX = 128;
  53 +
  54 + /**
  55 + * 缓存写入后的过期时间:修改 Provider / 模型时只能失效本节点,其他节点最迟在该时间后按新配置重建
  56 + */
  57 + private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10);
  58 +
  59 + /**
  60 + * 缓存 key 分隔符
  61 + */
  62 + private static final String KEY_SEPARATOR = ":";
  63 +
  64 + /**
  65 + * 对话模型缓存:providerId:modelId:sessionKey → ChatModel
  66 + */
  67 + private final Cache<String, OpenAiChatModel> chatModelCache = CacheBuilder.newBuilder()
  68 + .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build();
  69 + /**
  70 + * 嵌入模型缓存:providerId:modelId → EmbeddingModel
  71 + */
  72 + private final Cache<String, OpenAiEmbeddingModel> embeddingModelCache = CacheBuilder.newBuilder()
  73 + .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build();
  74 +
  75 + /**
  76 + * 获取(或构建)对话模型,并包装为 ChatClient
  77 + *
  78 + * @param model 模型运行时快照
  79 + * @param sessionId 会话编号,用于替换附属 Header 中的 {session} 占位符,可为空
  80 + * @return ChatClient
  81 + */
  82 + public ChatClient buildChatClient(Ai1ModelRespBO model, Long sessionId) {
  83 + return ChatClient.builder(getOrCreateChatModel(model, sessionId)).build();
  84 + }
  85 +
  86 + /**
  87 + * 获取(或构建)嵌入模型
  88 + *
  89 + * @param model 模型运行时快照
  90 + * @return 嵌入模型
  91 + */
  92 + public EmbeddingModel getOrCreateEmbeddingModel(Ai1ModelRespBO model) {
  93 + return embeddingModelCache.asMap().computeIfAbsent(buildCacheKey(model), key -> {
  94 + OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
  95 + .model(model.getModel())
  96 + .build();
  97 + log.debug("[getOrCreateEmbeddingModel][providerId({}) modelId({}) 嵌入模型构建完成]", model.getProviderId(), model.getModelId());
  98 + return new OpenAiEmbeddingModel(buildOpenAiApi(model, null), MetadataMode.EMBED, options);
  99 + });
  100 + }
  101 +
  102 + /**
  103 + * 失效指定 Provider 下的全部模型缓存(Provider 的地址、密钥、Header、状态变更或删除时调用)
  104 + *
  105 + * @param providerId Provider 编号
  106 + */
  107 + public void evictByProviderId(Long providerId) {
  108 + String prefix = providerId + KEY_SEPARATOR;
  109 + evict(key -> key.startsWith(prefix));
  110 + }
  111 +
  112 + /**
  113 + * 失效指定模型的缓存(模型标识、类型、状态变更或删除时调用)
  114 + *
  115 + * @param modelId 模型编号
  116 + */
  117 + public void evictByModelId(Long modelId) {
  118 + String modelIdText = String.valueOf(modelId);
  119 + evict(key -> modelIdText.equals(StrUtil.split(key, KEY_SEPARATOR).get(1)));
  120 + }
  121 +
  122 + /**
  123 + * 按 key 条件失效对话模型、嵌入模型缓存
  124 + *
  125 + * @param keyMatcher key 匹配条件
  126 + */
  127 + private void evict(Predicate<String> keyMatcher) {
  128 + chatModelCache.asMap().keySet().removeIf(keyMatcher);
  129 + embeddingModelCache.asMap().keySet().removeIf(keyMatcher);
  130 + }
  131 +
  132 + /**
  133 + * 获取(或构建)对话模型
  134 + *
  135 + * 仅当附属 Header 使用 {session} 占位符(需按会话隔离)时,才把会话编号纳入缓存 key;
  136 + * 否则同一模型跨会话复用,避免每个会话都构建一个模型实例
  137 + */
  138 + private OpenAiChatModel getOrCreateChatModel(Ai1ModelRespBO model, Long sessionId) {
  139 + // 1. 计算缓存 key:Header 含 {session} 占位符时按会话隔离,否则跨会话共享
  140 + String sessionKey = usesSessionHeader(model.getHeaders()) && sessionId != null
  141 + ? String.valueOf(sessionId) : "";
  142 + String cacheKey = buildCacheKey(model) + KEY_SEPARATOR + sessionKey;
  143 +
  144 + // 2. 命中缓存直接复用;未命中时按运行时快照构建 OpenAI 兼容对话模型并缓存
  145 + return chatModelCache.asMap().computeIfAbsent(cacheKey, key -> {
  146 + OpenAiChatOptions options = OpenAiChatOptions.builder()
  147 + .model(model.getModel())
  148 + .build();
  149 + log.debug("[getOrCreateChatModel][providerId({}) modelId({}) 对话模型构建完成]", model.getProviderId(), model.getModelId());
  150 + return OpenAiChatModel.builder().openAiApi(buildOpenAiApi(model, sessionId)).defaultOptions(options).build();
  151 + });
  152 + }
  153 +
  154 + /**
  155 + * Spring AI 1.x 将连接参数放在 OpenAiApi,而非模型 Options;仅在模型构建边界适配。
  156 + * normalizeBaseUrl 已包含 /v1 或供应商的自定义前缀,因此端点不再重复添加 /v1。
  157 + */
  158 + private static OpenAiApi buildOpenAiApi(Ai1ModelRespBO model, Long sessionId) {
  159 + MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
  160 + buildHeaders(model.getHeaders(), sessionId).forEach(headers::add);
  161 + return OpenAiApi.builder()
  162 + .baseUrl(normalizeBaseUrl(model.getBaseUrl()))
  163 + .apiKey(StrUtil.nullToEmpty(model.getApiKey()))
  164 + .headers(headers)
  165 + .completionsPath("/chat/completions")
  166 + .embeddingsPath("/embeddings")
  167 + .build();
  168 + }
  169 +
  170 + /**
  171 + * 构建模型缓存 key:providerId:modelId:配置指纹
  172 + *
  173 + * 带上配置指纹:配置变化后新请求自然命中新 key;并发下按旧快照构建的实例只会写入旧 key,不会再被读到
  174 + *
  175 + * @param model 模型运行时快照
  176 + * @return 缓存 key
  177 + */
  178 + private static String buildCacheKey(Ai1ModelRespBO model) {
  179 + return model.getProviderId() + KEY_SEPARATOR + model.getModelId() + KEY_SEPARATOR
  180 + + Objects.hash(model.getBaseUrl(), model.getApiKey(), model.getModel(), model.getModelType(), model.getHeaders());
  181 + }
  182 +
  183 + /**
  184 + * 归一化接口地址:去掉结尾斜杠;裸地址(无路径)自动补 /v1,
  185 + * 保证连通测试、模型拉取与实际会话访问同一个 OpenAI 兼容端点
  186 + *
  187 + * 例如说:Ollama 的 <a href="http://127.0.0.1:11434">http://127.0.0.1:11434</a>
  188 + * 归一化为 <a href="http://127.0.0.1:11434/v1">http://127.0.0.1:11434/v1</a>
  189 + *
  190 + * @param baseUrl 接口地址
  191 + * @return 归一化后的地址
  192 + */
  193 + public static String normalizeBaseUrl(String baseUrl) {
  194 + String url = StrUtil.removeSuffix(StrUtil.trim(baseUrl), "/");
  195 + try {
  196 + String path = URI.create(url).getPath();
  197 + if (StrUtil.isBlank(path) || "/".equals(path)) {
  198 + url = url + "/v1";
  199 + }
  200 + } catch (Exception e) {
  201 + // 非法 URL 按原值使用,交由后续请求报错
  202 + log.warn("[normalizeBaseUrl][接口地址({}) 解析失败,按原值使用]", url, e);
  203 + }
  204 + return url;
  205 + }
  206 +
  207 + /**
  208 + * 附属 Header 是否使用 {session} 占位符
  209 + */
  210 + private static boolean usesSessionHeader(List<Map<String, String>> headers) {
  211 + return CollUtil.findOne(headers, header -> StrUtil.contains(header.get("value"), SESSION_PLACEHOLDER)) != null;
  212 + }
  213 +
  214 + /**
  215 + * 构建请求 Header:{session} 占位符替换为会话编号;会话编号为空时,跳过带占位符的 Header
  216 + */
  217 + private static Map<String, String> buildHeaders(List<Map<String, String>> headers, Long sessionId) {
  218 + Map<String, String> result = new LinkedHashMap<>();
  219 + if (CollUtil.isEmpty(headers)) {
  220 + return result;
  221 + }
  222 + for (Map<String, String> header : headers) {
  223 + String key = header.get("key");
  224 + String value = header.get("value");
  225 + if (StrUtil.isBlank(key)) {
  226 + continue;
  227 + }
  228 + if (StrUtil.contains(value, SESSION_PLACEHOLDER)) {
  229 + if (sessionId == null) {
  230 + continue;
  231 + }
  232 + value = value.replace(SESSION_PLACEHOLDER, String.valueOf(sessionId));
  233 + }
  234 + result.put(key.trim(), StrUtil.nullToEmpty(value));
  235 + }
  236 + return result;
  237 + }
  238 +
  239 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/mcp/Ai1McpClientTool.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.harness.mcp;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.convert.Convert;
  5 +import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  8 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  9 +import cn.iocoder.yudao.module.ai1.controller.admin.mcp.vo.Ai1McpConnectRespVO;
  10 +import cn.iocoder.yudao.module.ai1.dal.dataobject.mcp.Ai1McpDO;
  11 +import cn.iocoder.yudao.module.ai1.enums.mcp.Ai1McpTransportEnum;
  12 +import com.google.common.cache.Cache;
  13 +import com.google.common.cache.CacheBuilder;
  14 +import com.google.common.cache.RemovalListener;
  15 +import io.modelcontextprotocol.client.McpClient;
  16 +import io.modelcontextprotocol.client.McpSyncClient;
  17 +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
  18 +import io.modelcontextprotocol.client.transport.ServerParameters;
  19 +import io.modelcontextprotocol.client.transport.StdioClientTransport;
  20 +import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
  21 +import io.modelcontextprotocol.json.McpJsonDefaults;
  22 +import io.modelcontextprotocol.spec.McpClientTransport;
  23 +import io.modelcontextprotocol.spec.McpError;
  24 +import io.modelcontextprotocol.spec.McpSchema;
  25 +import jakarta.annotation.PreDestroy;
  26 +import lombok.Data;
  27 +import lombok.extern.slf4j.Slf4j;
  28 +import org.springframework.stereotype.Component;
  29 +
  30 +import java.net.URI;
  31 +import java.time.Duration;
  32 +import java.util.*;
  33 +import java.util.concurrent.ConcurrentHashMap;
  34 +
  35 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  36 +import static cn.iocoder.yudao.module.ai1.util.Ai1Utils.resolveSpringPlaceholders;
  37 +
  38 +/**
  39 + * AI1 MCP 客户端工具(官方 Java MCP SDK):连接、列举工具、调用工具、连通测试
  40 + *
  41 + * 1. 传输层:http(Streamable HTTP)/ stdio(本地进程);传输方式以 {@link Ai1McpDO#getTransport()} 列为准,
  42 + * 其余参数取自 config JSON(缺失时回退平铺列)
  43 + * 2. 客户端与工具列表按 MCP 编号 + 配置指纹缓存,配置变化时重建并关闭旧连接;闲置超时自动关闭
  44 + *
  45 + * 注意:stdio 会在服务器上启动本地进程,与源工程行为一致,多租户不可信部署需自行评估
  46 + *
  47 + * @author 芋道源码
  48 + */
  49 +@Component
  50 +@Slf4j
  51 +public class Ai1McpClientTool {
  52 +
  53 + /**
  54 + * 远程服务默认的消息端点
  55 + */
  56 + private static final String DEFAULT_HTTP_ENDPOINT = "/mcp";
  57 + /**
  58 + * 缓存 key 分隔符
  59 + */
  60 + private static final String KEY_SEPARATOR = ":";
  61 + /**
  62 + * 缓存的闲置过期时间:超过该时间未使用的客户端自动关闭,释放连接与子进程
  63 + *
  64 + * 多节点下,其他节点删除 MCP 后,本节点的残留客户端也依赖该过期释放
  65 + */
  66 + private static final Duration CACHE_EXPIRE_AFTER_ACCESS = Duration.ofMinutes(30);
  67 +
  68 + /**
  69 + * 客户端缓存:MCP 编号:配置指纹 → 客户端
  70 + *
  71 + * key 带上配置指纹,配置变化后自然命中不到旧客户端,不需要额外的缓存项对象
  72 + */
  73 + private final Cache<String, McpSyncClient> clientCache = CacheBuilder.newBuilder()
  74 + .expireAfterAccess(CACHE_EXPIRE_AFTER_ACCESS)
  75 + .removalListener((RemovalListener<String, McpSyncClient>) notification -> closeQuietly(notification.getValue()))
  76 + .build();
  77 + /**
  78 + * 工具列表缓存:MCP 编号:配置指纹 → 工具列表
  79 + */
  80 + private final Cache<String, List<McpSchema.Tool>> toolsCache = CacheBuilder.newBuilder()
  81 + .expireAfterAccess(CACHE_EXPIRE_AFTER_ACCESS)
  82 + .build();
  83 + /**
  84 + * 按 MCP 串行建连的锁,避免不同服务的初始化互相阻塞
  85 + */
  86 + private final Map<Long, Object> clientLocks = new ConcurrentHashMap<>();
  87 +
  88 + /**
  89 + * 获取 MCP 服务暴露的工具列表(带缓存,配置变化自动重建)
  90 + *
  91 + * 直接返回 SDK 的工具定义(含名称、描述、入参 JSON Schema),仅供 {@link Ai1McpToolFactory} 桥接 ToolCallback 使用
  92 + *
  93 + * @param mcp MCP 服务
  94 + * @return 工具列表;列举失败时返回空列表
  95 + */
  96 + public List<McpSchema.Tool> listTools(Ai1McpDO mcp) {
  97 + // 1. 命中缓存:配置指纹一致时,直接返回
  98 + String cacheKey = buildCacheKey(mcp);
  99 + List<McpSchema.Tool> cached = toolsCache.getIfPresent(cacheKey);
  100 + if (cached != null) {
  101 + return cached;
  102 + }
  103 +
  104 + // 2. 调用 tools/list 拉取工具列表;失败时记录日志,按空列表处理
  105 + try {
  106 + McpSchema.ListToolsResult result = getOrCreateClient(mcp).listTools();
  107 + List<McpSchema.Tool> tools = result != null && result.tools() != null
  108 + ? result.tools() : Collections.emptyList();
  109 + // 3. 仅缓存成功的结果(包含服务确实没有工具的空列表),失败后允许下次会话重试
  110 + toolsCache.put(cacheKey, tools);
  111 + return tools;
  112 + } catch (Exception e) {
  113 + log.warn("[listTools][MCP({}/{}) tools/list 失败]", mcp.getId(), mcp.getName(), e);
  114 + return Collections.emptyList();
  115 + }
  116 + }
  117 +
  118 + /**
  119 + * 调用 MCP 工具(阻塞),返回文本结果
  120 + *
  121 + * @param mcp MCP 服务
  122 + * @param toolName 工具名称(MCP 服务侧的原始名称)
  123 + * @param arguments 工具参数
  124 + * @return 文本结果
  125 + */
  126 + public String callTool(Ai1McpDO mcp, String toolName, Map<String, Object> arguments) {
  127 + McpSyncClient client = getOrCreateClient(mcp);
  128 + // 同一客户端的调用串行执行,避免 stdio 管道并发读写错乱
  129 + synchronized (client) {
  130 + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder(toolName)
  131 + .arguments(MapUtil.emptyIfNull(arguments)).build();
  132 + McpSchema.CallToolResult result;
  133 + try {
  134 + result = client.callTool(request);
  135 + } catch (McpError e) {
  136 + // 服务端 JSON-RPC 业务错误不表示连接失效
  137 + throw e;
  138 + } catch (Exception e) {
  139 + String cacheKey = buildCacheKey(mcp);
  140 + synchronized (clientLocks.computeIfAbsent(mcp.getId(), key -> new Object())) {
  141 + // 只清理本次失败的客户端,避免旧调用误删已经重建的连接
  142 + if (clientCache.asMap().remove(cacheKey, client)) {
  143 + toolsCache.invalidate(cacheKey);
  144 + }
  145 + }
  146 + // 不自动重跑有副作用的工具;下次调用重新建连
  147 + throw e;
  148 + }
  149 + return readText(result);
  150 + }
  151 + }
  152 +
  153 + /**
  154 + * 连通测试:initialize + tools/list,使用临时客户端,不影响缓存
  155 + *
  156 + * @param mcp MCP 服务
  157 + * @return 测试结果
  158 + */
  159 + public Ai1McpConnectRespVO testConnect(Ai1McpDO mcp) {
  160 + long startTime = System.currentTimeMillis();
  161 + McpSyncClient client = null;
  162 + try {
  163 + // 1. 建立连接
  164 + McpClientTransport transport = buildTransport(mcp);
  165 + if (transport == null) {
  166 + return buildConnectFailResult("配置不完整(缺少必要参数)", startTime);
  167 + }
  168 + client = McpClient.sync(transport)
  169 + .requestTimeout(Duration.ofSeconds(30))
  170 + .initializationTimeout(Duration.ofSeconds(15))
  171 + .build();
  172 + client.initialize();
  173 +
  174 + // 2. 读取服务信息与工具列表
  175 + Ai1McpConnectRespVO result = new Ai1McpConnectRespVO().setConnectable(true)
  176 + .setInstructions(client.getServerInstructions());
  177 + McpSchema.Implementation serverInfo = client.getServerInfo();
  178 + if (serverInfo != null) {
  179 + result.setServerName(serverInfo.name()).setServerVersion(serverInfo.version());
  180 + }
  181 + McpSchema.ListToolsResult listResult = client.listTools();
  182 + result.setTools(convertList(listResult != null ? listResult.tools() : null, tool -> new Ai1McpConnectRespVO.Tool()
  183 + .setName(tool.name()).setTitle(tool.title()).setDescription(tool.description())));
  184 + return result.setToolCount(result.getTools().size())
  185 + .setMessage("连接成功,发现 " + result.getTools().size() + " 个工具")
  186 + .setElapsedMs(System.currentTimeMillis() - startTime);
  187 + } catch (Exception e) {
  188 + log.warn("[testConnect][MCP({}/{}) 连通测试失败]", mcp.getId(), mcp.getName(), e);
  189 + return buildConnectFailResult(String.valueOf(e.getMessage()), startTime);
  190 + } finally {
  191 + closeQuietly(client);
  192 + }
  193 + }
  194 +
  195 + /**
  196 + * 移除并关闭指定 MCP 的客户端(删除、配置变更时调用,释放子进程与连接线程)
  197 + *
  198 + * @param mcpId MCP 编号
  199 + */
  200 + public void evict(Long mcpId) {
  201 + String prefix = mcpId + KEY_SEPARATOR;
  202 + toolsCache.asMap().keySet().removeIf(key -> key.startsWith(prefix));
  203 + clientCache.asMap().keySet().removeIf(key -> key.startsWith(prefix));
  204 + }
  205 +
  206 + /**
  207 + * 移除并关闭指定 MCP 除当前指纹外的客户端与工具缓存
  208 + */
  209 + private void evictExcept(Long mcpId, String currentKey) {
  210 + String prefix = mcpId + KEY_SEPARATOR;
  211 + toolsCache.asMap().keySet().removeIf(key -> key.startsWith(prefix) && !key.equals(currentKey));
  212 + clientCache.asMap().keySet().removeIf(key -> key.startsWith(prefix) && !key.equals(currentKey));
  213 + }
  214 +
  215 + @PreDestroy
  216 + public void destroy() {
  217 + toolsCache.invalidateAll();
  218 + clientCache.invalidateAll();
  219 + }
  220 +
  221 + // ==================== 连接 / 传输 ====================
  222 +
  223 + /**
  224 + * 获取(或构建)MCP 客户端:配置指纹变化时重建,旧连接由缓存的 RemovalListener 关闭
  225 + */
  226 + private McpSyncClient getOrCreateClient(Ai1McpDO mcp) {
  227 + // 1. 命中缓存:配置指纹一致时,直接复用
  228 + String cacheKey = buildCacheKey(mcp);
  229 + McpSyncClient cached = clientCache.getIfPresent(cacheKey);
  230 + if (cached != null) {
  231 + return cached;
  232 + }
  233 + synchronized (clientLocks.computeIfAbsent(mcp.getId(), key -> new Object())) {
  234 + cached = clientCache.getIfPresent(cacheKey);
  235 + if (cached != null) {
  236 + return cached;
  237 + }
  238 + // 2. 配置变化:构建并初始化新客户端,成功后再释放旧指纹的客户端(与源码一致,避免打断在途调用)
  239 + McpClientTransport transport = buildTransport(mcp);
  240 + if (transport == null) {
  241 + throw new IllegalStateException("MCP(" + mcp.getName() + ") 配置不完整(缺少必要参数)");
  242 + }
  243 + McpSyncClient client = McpClient.sync(transport)
  244 + .requestTimeout(Duration.ofSeconds(60))
  245 + .initializationTimeout(Duration.ofSeconds(15))
  246 + .build();
  247 + try {
  248 + client.initialize();
  249 + } catch (Exception e) {
  250 + // 初始化失败的客户端没有进入缓存,必须主动释放连接或 stdio 子进程
  251 + closeQuietly(client);
  252 + throw e;
  253 + }
  254 + clientCache.put(cacheKey, client);
  255 + evictExcept(mcp.getId(), cacheKey);
  256 + return client;
  257 + }
  258 + }
  259 +
  260 + /**
  261 + * 构建传输层:stdio 走本地进程,其余走 Streamable HTTP;参数缺失时返回 null
  262 + */
  263 + private McpClientTransport buildTransport(Ai1McpDO mcp) {
  264 + McpConfig config = resolveConfig(parseConfig(mcp));
  265 + try {
  266 + return Ai1McpTransportEnum.isStdio(config.getTransport()) ? buildStdioTransport(config) : buildHttpTransport(config);
  267 + } catch (ServiceException ex) {
  268 + throw ex;
  269 + } catch (Exception e) {
  270 + log.warn("[buildTransport][MCP({}) 传输层构建失败]", mcp.getId(), e);
  271 + return null;
  272 + }
  273 + }
  274 +
  275 + /**
  276 + * Streamable HTTP 传输:按「base = scheme://host:port、endpoint = raw path + query」构建,
  277 + * 保证配置的服务地址精确作为 MCP 消息端点(避免 resolve("/mcp") 覆盖路径导致 404)
  278 + */
  279 + private McpClientTransport buildHttpTransport(McpConfig config) {
  280 + if (StrUtil.isBlank(config.getUrl())) {
  281 + return null;
  282 + }
  283 + String baseUri = config.getUrl();
  284 + String endpoint = DEFAULT_HTTP_ENDPOINT;
  285 + try {
  286 + URI uri = URI.create(config.getUrl());
  287 + if (uri.getScheme() != null && uri.getAuthority() != null) {
  288 + baseUri = uri.getScheme() + "://" + uri.getAuthority();
  289 + endpoint = StrUtil.isEmpty(uri.getRawPath()) ? DEFAULT_HTTP_ENDPOINT : uri.getRawPath();
  290 + if (uri.getRawQuery() != null) {
  291 + endpoint += "?" + uri.getRawQuery();
  292 + }
  293 + }
  294 + } catch (IllegalArgumentException e) {
  295 + log.warn("[buildHttpTransport][服务地址({}) 解析失败,按原值使用]", config.getUrl(), e);
  296 + }
  297 + HttpClientStreamableHttpTransport.Builder builder = HttpClientStreamableHttpTransport.builder(baseUri)
  298 + .endpoint(endpoint)
  299 + .customizeClient(clientBuilder -> clientBuilder.connectTimeout(Duration.ofSeconds(10)));
  300 + if (MapUtil.isNotEmpty(config.getHeaders())) {
  301 + builder.httpRequestCustomizer(buildHeaderCustomizer(config.getHeaders()));
  302 + }
  303 + return builder.build();
  304 + }
  305 +
  306 + /**
  307 + * 本地进程传输:command + args + env
  308 + */
  309 + private McpClientTransport buildStdioTransport(McpConfig config) {
  310 + if (StrUtil.isBlank(config.getCommand())) {
  311 + return null;
  312 + }
  313 + ServerParameters.Builder params = ServerParameters.builder(config.getCommand());
  314 + if (CollUtil.isNotEmpty(config.getArgs())) {
  315 + params.args(config.getArgs());
  316 + }
  317 + if (MapUtil.isNotEmpty(config.getEnv())) {
  318 + params.env(config.getEnv());
  319 + }
  320 + return new StdioClientTransport(params.build(), McpJsonDefaults.getMapper());
  321 + }
  322 +
  323 + /**
  324 + * 请求头自定义器:为 MCP HTTP 请求统一追加 headers
  325 + */
  326 + private static McpSyncHttpClientRequestCustomizer buildHeaderCustomizer(Map<String, String> headers) {
  327 + return (builder, method, uri, protocolVersion, context) -> headers.forEach((key, value) -> {
  328 + if (StrUtil.isNotEmpty(key) && value != null) {
  329 + builder.header(key, value);
  330 + }
  331 + });
  332 + }
  333 +
  334 + private static void closeQuietly(McpSyncClient client) {
  335 + if (client == null) {
  336 + return;
  337 + }
  338 + try {
  339 + client.closeGracefully();
  340 + } catch (Exception e) {
  341 + log.warn("[closeQuietly][MCP 客户端关闭失败]", e);
  342 + }
  343 + }
  344 +
  345 + // ==================== 配置 / 结果解析 ====================
  346 +
  347 + /**
  348 + * 解析 MCP 配置:transport 以列为准;url、headers 优先取 config,缺失时回退平铺列
  349 + */
  350 + private static McpConfig parseConfig(Ai1McpDO mcp) {
  351 + Map<String, Object> configMap = MapUtil.emptyIfNull(JsonUtils.parseMap(mcp.getConfig()));
  352 + McpConfig config = new McpConfig();
  353 + config.setTransport(StrUtil.blankToDefault(mcp.getTransport(),
  354 + StrUtil.blankToDefault(MapUtil.getStr(configMap, "transport"), Ai1McpTransportEnum.HTTP.getTransport())));
  355 + config.setUrl(StrUtil.blankToDefault(MapUtil.getStr(configMap, "url"), mcp.getUrl()));
  356 + Map<String, String> headers = parseStringMap(configMap.get("headers"));
  357 + config.setHeaders(headers != null ? headers : mcp.getHeaders());
  358 + config.setCommand(MapUtil.getStr(configMap, "command"));
  359 + Object args = configMap.get("args");
  360 + config.setArgs(args instanceof List<?> ? Convert.toList(String.class, args) : null);
  361 + config.setEnv(parseStringMap(configMap.get("env")));
  362 + return config;
  363 + }
  364 +
  365 + /**
  366 + * 调用前解析地址、请求头、命令和环境变量里的 ${ENV},库中的原文不改
  367 + */
  368 + private static McpConfig resolveConfig(McpConfig config) {
  369 + config.setUrl(resolveSpringPlaceholders(config.getUrl()));
  370 + config.setCommand(resolveSpringPlaceholders(config.getCommand()));
  371 + config.setHeaders(resolveSpringPlaceholders(config.getHeaders()));
  372 + config.setEnv(resolveSpringPlaceholders(config.getEnv()));
  373 + config.setArgs(convertList(config.getArgs(), arg -> resolveSpringPlaceholders(arg)));
  374 + return config;
  375 + }
  376 +
  377 + /**
  378 + * 字符串 Map 解析(config 中的 headers、env):支持 JSON 对象,或 JSON 对象形式的字符串
  379 + */
  380 + private static Map<String, String> parseStringMap(Object value) {
  381 + if (value instanceof String) {
  382 + value = JsonUtils.parseMap((String) value);
  383 + }
  384 + return value instanceof Map ? MapUtil.removeNullValue(Convert.toMap(String.class, String.class, value)) : null;
  385 + }
  386 +
  387 + /**
  388 + * 工具调用结果文本提取:优先文本内容,其次结构化内容
  389 + */
  390 + private static String readText(McpSchema.CallToolResult result) {
  391 + if (result == null) {
  392 + return "";
  393 + }
  394 + String text = CollUtil.join(convertList(result.content(), content -> content instanceof McpSchema.TextContent
  395 + ? ((McpSchema.TextContent) content).text() : null), "");
  396 + if (StrUtil.isNotEmpty(text)) {
  397 + return text;
  398 + }
  399 + if (result.structuredContent() != null) {
  400 + return JsonUtils.toJsonString(result.structuredContent());
  401 + }
  402 + return String.valueOf(result);
  403 + }
  404 +
  405 + /**
  406 + * 缓存 key:MCP 编号 + 配置指纹,传输方式、config、平铺列任一变化即得到新的 key
  407 + *
  408 + * 多节点下,各节点加载到的 MCP 配置变化后指纹不同,自然重建客户端
  409 + */
  410 + private static String buildCacheKey(Ai1McpDO mcp) {
  411 + return mcp.getId() + KEY_SEPARATOR
  412 + + Objects.hash(mcp.getTransport(), mcp.getConfig(), mcp.getUrl(), mcp.getHeaders());
  413 + }
  414 +
  415 + /**
  416 + * 构建连通测试失败结果
  417 + */
  418 + private static Ai1McpConnectRespVO buildConnectFailResult(String message, long startTime) {
  419 + return new Ai1McpConnectRespVO().setConnectable(false).setToolCount(0).setTools(new ArrayList<>())
  420 + .setMessage(message).setElapsedMs(System.currentTimeMillis() - startTime);
  421 + }
  422 +
  423 + /**
  424 + * 解析后的 MCP 服务配置
  425 + */
  426 + @Data
  427 + private static class McpConfig {
  428 +
  429 + /**
  430 + * 传输方式
  431 + */
  432 + private String transport;
  433 + /**
  434 + * 服务地址
  435 + */
  436 + private String url;
  437 + /**
  438 + * 请求头
  439 + */
  440 + private Map<String, String> headers;
  441 + /**
  442 + * stdio 命令
  443 + */
  444 + private String command;
  445 + /**
  446 + * stdio 参数
  447 + */
  448 + private List<String> args;
  449 + /**
  450 + * stdio 环境变量
  451 + */
  452 + private Map<String, String> env;
  453 +
  454 + }
  455 +
  456 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/mcp/Ai1McpToolFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.harness.mcp;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.ReUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
  8 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  9 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  10 +import cn.iocoder.yudao.module.ai1.dal.dataobject.agent.Ai1AgentDO;
  11 +import cn.iocoder.yudao.module.ai1.dal.dataobject.mcp.Ai1McpDO;
  12 +import cn.iocoder.yudao.module.ai1.service.mcp.Ai1McpService;
  13 +import io.modelcontextprotocol.spec.McpSchema;
  14 +import jakarta.annotation.Resource;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +import org.springframework.ai.tool.ToolCallback;
  17 +import org.springframework.ai.tool.function.FunctionToolCallback;
  18 +import org.springframework.stereotype.Component;
  19 +
  20 +import java.util.*;
  21 +import java.util.function.Function;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  24 +
  25 +/**
  26 + * AI1 MCP 工具工厂:把 Agent 绑定的 MCP 服务暴露的工具,桥接为 Spring AI ToolCallback
  27 + *
  28 + * 1. 工具名:MCP 名称净化 + "__" + 原始工具名;无有效名称时使用 MCP 编号,超长时保留摘要
  29 + * 2. 入参:透传 MCP 工具的 JSON Schema,调用落地仍走 {@link Ai1McpClientTool}
  30 + *
  31 + * 工具挂到 ChatClient 后,由 Spring AI 的工具调用机制统一驱动;与 {@link Ai1McpClientTool} 同包
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Component
  36 +@Slf4j
  37 +public class Ai1McpToolFactory {
  38 +
  39 + /**
  40 + * MCP 名称与工具名称的分隔符
  41 + */
  42 + private static final String NAME_SEPARATOR = "__";
  43 +
  44 + @Resource
  45 + private Ai1McpService mcpService;
  46 +
  47 + @Resource
  48 + private Ai1McpClientTool mcpClientTool;
  49 +
  50 + /**
  51 + * 构建 Agent 绑定的 MCP 工具集合;单个服务失败时跳过,不影响会话
  52 + *
  53 + * tools/list 结果已由 {@link Ai1McpClientTool} 按配置指纹缓存,这里只做 ToolCallback 包装
  54 + *
  55 + * @param agent Agent
  56 + * @return 工具集合
  57 + */
  58 + public List<ToolCallback> buildTools(Ai1AgentDO agent) {
  59 + List<ToolCallback> tools = new ArrayList<>();
  60 + if (CollUtil.isEmpty(agent.getMcpIds())) {
  61 + return tools;
  62 + }
  63 + for (Ai1McpDO mcp : mcpService.getMcpList(agent.getMcpIds())) {
  64 + if (CommonStatusEnum.isDisable(mcp.getStatus())) {
  65 + continue;
  66 + }
  67 + tools.addAll(convertList(listTools(agent, mcp), tool -> buildToolCallback(mcp, tool)));
  68 + }
  69 + return tools;
  70 + }
  71 +
  72 + /**
  73 + * 列举 MCP 服务暴露的工具;失败时记录日志并返回空列表
  74 + *
  75 + * @param agent Agent
  76 + * @param mcp MCP 服务
  77 + * @return 工具列表
  78 + */
  79 + private List<McpSchema.Tool> listTools(Ai1AgentDO agent, Ai1McpDO mcp) {
  80 + try {
  81 + return mcpClientTool.listTools(mcp);
  82 + } catch (Exception e) {
  83 + log.warn("[listTools][Agent({}) 加载 MCP({}) 工具失败]", agent.getId(), mcp.getName(), e);
  84 + return Collections.emptyList();
  85 + }
  86 + }
  87 +
  88 + /**
  89 + * 构建单个工具回调:名称去冲突 + 入参 Schema 透传 + MCP 客户端调用落地
  90 + */
  91 + private ToolCallback buildToolCallback(Ai1McpDO mcp, McpSchema.Tool tool) {
  92 + Function<Map<String, Object>, String> function = arguments ->
  93 + mcpClientTool.callTool(mcp, tool.name(), arguments);
  94 + Map<String, Object> inputSchema = tool.inputSchema();
  95 + if (MapUtil.isEmpty(inputSchema)) {
  96 + inputSchema = new HashMap<>();
  97 + inputSchema.put("type", "object");
  98 + inputSchema.put("properties", new HashMap<>());
  99 + }
  100 + return FunctionToolCallback.builder(buildToolName(mcp, tool), function)
  101 + .description(StrUtil.nullToEmpty(tool.description()))
  102 + .inputType(Map.class)
  103 + .inputSchema(JsonUtils.toJsonString(inputSchema))
  104 + .build();
  105 + }
  106 +
  107 + /**
  108 + * 构建工具名:无有效 MCP 名称时使用编号;超过 64 字符时截短并追加摘要,避免截断后重名
  109 + *
  110 + * @param mcp MCP 服务
  111 + * @param tool MCP 工具
  112 + * @return 工具名
  113 + */
  114 + private static String buildToolName(Ai1McpDO mcp, McpSchema.Tool tool) {
  115 + String prefix = slugify(mcp.getName());
  116 + if (StrUtil.isEmpty(prefix)) {
  117 + prefix = "mcp_" + mcp.getId();
  118 + }
  119 + String name = prefix + NAME_SEPARATOR + tool.name();
  120 + // OpenAI 兼容接口的工具名最多 64 字符,保留 47 字符前缀 + "_" + 16 位摘要。
  121 + // 摘要由 MCP 编号和原始工具名生成,避免不同服务或工具截断后重名。
  122 + if (name.length() > 64) {
  123 + String suffix = DigestUtil.sha256Hex(mcp.getId() + ":" + tool.name()).substring(0, 16);
  124 + name = name.substring(0, 47) + "_" + suffix;
  125 + }
  126 + return name;
  127 + }
  128 +
  129 + /**
  130 + * 名称净化:只保留小写字母、数字、下划线(工具名需匹配 ^[a-zA-Z0-9_-]+$)
  131 + */
  132 + static String slugify(String name) {
  133 + if (StrUtil.isBlank(name)) {
  134 + return "";
  135 + }
  136 + String slug = ReUtil.replaceAll(name.toLowerCase(Locale.ROOT), "[^a-z0-9_]+", "_");
  137 + slug = ReUtil.replaceAll(slug, "_+", "_");
  138 + return ReUtil.replaceAll(slug, "^_+|_+$", "");
  139 + }
  140 +
  141 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/model/Ai1ProviderTool.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.harness.model;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.hutool.http.ContentType;
  7 +import cn.hutool.http.Header;
  8 +import cn.hutool.http.HttpRequest;
  9 +import cn.hutool.http.HttpResponse;
  10 +import cn.hutool.http.HttpStatus;
  11 +import cn.hutool.http.Method;
  12 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  13 +import cn.iocoder.yudao.module.ai1.controller.admin.model.vo.provider.Ai1ProviderConnectRespVO;
  14 +import cn.iocoder.yudao.module.ai1.harness.llm.Ai1LlmModelFactory;
  15 +import lombok.AllArgsConstructor;
  16 +import lombok.Data;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.springframework.stereotype.Component;
  19 +
  20 +import java.util.List;
  21 +import java.util.Map;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +import static cn.iocoder.yudao.module.ai1.enums.Ai1ErrorCodeConstants.PROVIDER_REMOTE_MODEL_EMPTY;
  26 +import static cn.iocoder.yudao.module.ai1.enums.Ai1ErrorCodeConstants.PROVIDER_REMOTE_MODEL_LOAD_FAIL;
  27 +import static cn.iocoder.yudao.module.ai1.harness.llm.Ai1LlmModelFactory.normalizeBaseUrl;
  28 +
  29 +/**
  30 + * AI1 供应商工具:面向 OpenAI 兼容接口的原始 HTTP 探测
  31 + *
  32 + * 1. {@link #testConnect}:GET /models 优先、回退 POST /chat/completions 的连通测试
  33 + * 2. {@link #listModels}:拉取 GET /models 返回的模型标识
  34 + *
  35 + * 入参为已解析 ${ENV} 占位符的地址、密钥、Header,不感知供应商的存储与租户归属
  36 + *
  37 + * @author 芋道源码
  38 + */
  39 +@Component
  40 +@Slf4j
  41 +public class Ai1ProviderTool {
  42 +
  43 + /**
  44 + * 请求的连接超时,单位:毫秒
  45 + */
  46 + private static final int CONNECT_TIMEOUT = 5000;
  47 + /**
  48 + * 请求的读取超时,单位:毫秒
  49 + */
  50 + private static final int READ_TIMEOUT = 8000;
  51 + /**
  52 + * 连通测试回退时的最小会话请求体:仅探测服务可达与鉴权,不做真实会话
  53 + */
  54 + private static final String CONNECT_CHAT_BODY = "{\"model\":\"test\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}";
  55 +
  56 + /**
  57 + * 连通测试:GET {baseUrl}/models 优先;失败(认证错误除外)时,回退 POST {baseUrl}/chat/completions
  58 + *
  59 + * @param baseUrl 接口地址
  60 + * @param apiKey API 密钥
  61 + * @param headers 请求附属 Header
  62 + * @return 测试结果
  63 + */
  64 + public Ai1ProviderConnectRespVO testConnect(String baseUrl, String apiKey, List<Map<String, String>> headers) {
  65 + long startTime = System.currentTimeMillis();
  66 + String url = normalizeBaseUrl(baseUrl);
  67 +
  68 + // 1. 主校验:GET /models(标准 OpenAI 兼容接口)
  69 + HttpOutcome models = executeRequest(Method.GET, url + "/models", apiKey, headers, null);
  70 + if (models.getHttpCode() == HttpStatus.HTTP_OK) {
  71 + return buildConnectResult(true, HttpStatus.HTTP_OK, "连通正常:GET /models 返回 HTTP 200", startTime);
  72 + }
  73 + if (isAuthFail(models.getHttpCode())) {
  74 + return buildConnectResult(false, models.getHttpCode(),
  75 + "认证失败:GET /models 返回 HTTP " + models.getHttpCode() + ",请检查 API 密钥", startTime);
  76 + }
  77 +
  78 + // 2. 回退:POST /chat/completions 最小请求(仅探测服务可达与鉴权,不做真实会话)
  79 + HttpOutcome chat = executeRequest(Method.POST, url + "/chat/completions", apiKey, headers, CONNECT_CHAT_BODY);
  80 + if (chat.getHttpCode() == HttpStatus.HTTP_OK) {
  81 + return buildConnectResult(true, HttpStatus.HTTP_OK, "GET /models 不可用(HTTP " + models.getHttpCode()
  82 + + "),回退 POST /chat/completions 连通正常:HTTP 200", startTime);
  83 + }
  84 + if (isAuthFail(chat.getHttpCode())) {
  85 + return buildConnectResult(false, chat.getHttpCode(),
  86 + "认证失败:POST /chat/completions 返回 HTTP " + chat.getHttpCode() + ",请检查 API 密钥", startTime);
  87 + }
  88 + if (chat.getHttpCode() > 0) {
  89 + return buildConnectResult(false, chat.getHttpCode(), "服务可达但接口异常:GET /models HTTP " + models.getHttpCode()
  90 + + ",POST /chat/completions HTTP " + chat.getHttpCode(), startTime);
  91 + }
  92 +
  93 + // 3. 两次均网络异常:合并报告
  94 + String modelsMessage = StrUtil.isNotBlank(models.getMessage()) ? "GET /models 失败:" + models.getMessage() : "GET /models 无响应";
  95 + String chatMessage = StrUtil.isNotBlank(chat.getMessage())
  96 + ? "POST /chat/completions 失败:" + chat.getMessage() : "POST /chat/completions 无响应";
  97 + return buildConnectResult(false, 0, "连接失败:" + modelsMessage + ";" + chatMessage, startTime);
  98 + }
  99 +
  100 + /**
  101 + * 拉取远程模型标识列表:GET {baseUrl}/models,解析 data[].id
  102 + *
  103 + * @param baseUrl 接口地址
  104 + * @param apiKey API 密钥
  105 + * @param headers 请求附属 Header
  106 + * @return 模型标识列表
  107 + */
  108 + public List<String> listModels(String baseUrl, String apiKey, List<Map<String, String>> headers) {
  109 + // 1. 请求 GET /models:非 HTTP 200 时,提示检查配置
  110 + String url = normalizeBaseUrl(baseUrl) + "/models";
  111 + HttpOutcome outcome = executeRequest(Method.GET, url, apiKey, headers, null);
  112 + if (outcome.getHttpCode() != HttpStatus.HTTP_OK) {
  113 + log.warn("[listModels][地址({}) 模型拉取失败,httpCode({}) message({})]", url, outcome.getHttpCode(), outcome.getMessage());
  114 + throw exception(PROVIDER_REMOTE_MODEL_LOAD_FAIL);
  115 + }
  116 +
  117 + // 2. 解析 data[].id:解析失败时,提示检查配置;解析结果为空时,提示无可用模型
  118 + List<String> models = parseModels(outcome.getBody());
  119 + if (models == null) {
  120 + log.warn("[listModels][地址({}) 模型数据解析失败,body({})]", url, StrUtil.maxLength(outcome.getBody(), 200));
  121 + throw exception(PROVIDER_REMOTE_MODEL_LOAD_FAIL);
  122 + }
  123 + if (CollUtil.isEmpty(models)) {
  124 + throw exception(PROVIDER_REMOTE_MODEL_EMPTY);
  125 + }
  126 + return models;
  127 + }
  128 +
  129 + /**
  130 + * 解析 GET /models 响应的 data[].id
  131 + *
  132 + * 响应不是 JSON 对象、或 data 存在但不是数组时,视为解析失败(与源码一致)
  133 + *
  134 + * @param body 响应体
  135 + * @return 模型标识列表;解析失败返回 null
  136 + */
  137 + static List<String> parseModels(String body) {
  138 + Map<String, Object> root = JsonUtils.parseMap(body);
  139 + if (root == null) {
  140 + return null;
  141 + }
  142 + Object data = root.get("data");
  143 + if (data != null && !(data instanceof List)) {
  144 + return null;
  145 + }
  146 + return convertList((List<?>) data, item -> item instanceof Map
  147 + ? StrUtil.blankToDefault(MapUtil.getStr((Map<?, ?>) item, "id"), null) : null);
  148 + }
  149 +
  150 + /**
  151 + * 执行 OpenAI 兼容接口请求,地址非法、网络异常时记录原因
  152 + *
  153 + * @param method 请求方法
  154 + * @param url 请求地址
  155 + * @param apiKey API 密钥
  156 + * @param headers 请求附属 Header
  157 + * @param body JSON 请求体,可为空
  158 + * @return 请求结果
  159 + */
  160 + private static HttpOutcome executeRequest(Method method, String url, String apiKey,
  161 + List<Map<String, String>> headers, String body) {
  162 + try {
  163 + HttpRequest request = buildOpenAiRequest(method, url, apiKey, headers);
  164 + if (body != null) {
  165 + request.body(body, ContentType.JSON.getValue());
  166 + }
  167 + try (HttpResponse response = request.execute()) {
  168 + return new HttpOutcome(response.getStatus(), null, response.body());
  169 + }
  170 + } catch (Exception e) {
  171 + return new HttpOutcome(0, e.getMessage(), null);
  172 + }
  173 + }
  174 +
  175 + /**
  176 + * 构建 OpenAI 兼容接口的 HTTP 请求:携带 API 密钥与静态附属 Header,包含 {session} 占位符的 Header 跳过
  177 + *
  178 + * @param method 请求方法
  179 + * @param url 请求地址
  180 + * @param apiKey API 密钥
  181 + * @param headers 请求附属 Header
  182 + * @return HTTP 请求
  183 + */
  184 + private static HttpRequest buildOpenAiRequest(Method method, String url, String apiKey, List<Map<String, String>> headers) {
  185 + HttpRequest request = HttpRequest.of(url).method(method)
  186 + .setConnectionTimeout(CONNECT_TIMEOUT).setReadTimeout(READ_TIMEOUT)
  187 + .header(Header.ACCEPT, ContentType.JSON.getValue());
  188 + if (StrUtil.isNotBlank(apiKey)) {
  189 + request.bearerAuth(apiKey);
  190 + }
  191 + if (CollUtil.isNotEmpty(headers)) {
  192 + for (Map<String, String> header : headers) {
  193 + String key = header.get("key");
  194 + String value = header.get("value");
  195 + if (StrUtil.isBlank(key) || StrUtil.contains(value, Ai1LlmModelFactory.SESSION_PLACEHOLDER)) {
  196 + continue;
  197 + }
  198 + request.header(key.trim(), StrUtil.nullToEmpty(value));
  199 + }
  200 + }
  201 + return request;
  202 + }
  203 +
  204 + /**
  205 + * 认证失败:HTTP 401 未授权、403 禁止访问
  206 + */
  207 + private static boolean isAuthFail(int httpCode) {
  208 + return httpCode == HttpStatus.HTTP_UNAUTHORIZED || httpCode == HttpStatus.HTTP_FORBIDDEN;
  209 + }
  210 +
  211 + private static Ai1ProviderConnectRespVO buildConnectResult(boolean connectable, int httpCode, String message, long startTime) {
  212 + return new Ai1ProviderConnectRespVO().setConnectable(connectable).setHttpCode(httpCode).setMessage(message)
  213 + .setElapsedMs(System.currentTimeMillis() - startTime);
  214 + }
  215 +
  216 + /**
  217 + * HTTP 请求结果
  218 + */
  219 + @Data
  220 + @AllArgsConstructor
  221 + private static class HttpOutcome {
  222 +
  223 + /**
  224 + * HTTP 状态码,0 表示网络异常未收到响应
  225 + */
  226 + private int httpCode;
  227 + /**
  228 + * 异常信息
  229 + */
  230 + private String message;
  231 + /**
  232 + * 响应体
  233 + */
  234 + private String body;
  235 +
  236 + }
  237 +
  238 +}
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/package-info.java 0 → 100644
  1 +/**
  2 + * Agent 运行时支撑:
  3 + *
  4 + * 这里是平台去调用大模型、MCP 服务、知识库和 SKILL 的客户端与工厂,
  5 + * 不是注册给模型调用的 function tool。模型可调用的工具,在会话时由 MCP ToolCallback 和 SKILL 另行装配。
  6 + */
  7 +package cn.iocoder.yudao.module.ai1.harness;
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/harness/rag/Ai1RagTool.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai1.harness.rag;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.convert.Convert;
  5 +import cn.hutool.core.lang.loader.LazyFunLoader;
  6 +import cn.hutool.core.util.ObjUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import cn.iocoder.yudao.module.ai1.controller.admin.knowledge.vo.base.Ai1KnowledgeSearchRespVO;
  9 +import cn.iocoder.yudao.module.ai1.dal.dataobject.knowledge.Ai1KnowledgeBaseDO;
  10 +import cn.iocoder.yudao.module.ai1.enums.model.Ai1ModelTypeEnum;
  11 +import cn.iocoder.yudao.module.ai1.framework.ai.config.YudaoAi1Properties;
  12 +import cn.iocoder.yudao.module.ai1.harness.llm.Ai1LlmModelFactory;
  13 +import cn.iocoder.yudao.module.ai1.service.model.Ai1ModelService;
  14 +import cn.iocoder.yudao.module.ai1.service.model.bo.Ai1ModelRespBO;
  15 +import com.google.common.cache.Cache;
  16 +import com.google.common.cache.CacheBuilder;
  17 +import io.milvus.client.MilvusServiceClient;
  18 +import io.milvus.param.ConnectParam;
  19 +import io.milvus.param.IndexType;
  20 +import io.milvus.param.MetricType;
  21 +import io.milvus.param.R;
  22 +import io.milvus.param.RpcStatus;
  23 +import io.milvus.param.collection.DropCollectionParam;
  24 +import jakarta.annotation.PreDestroy;
  25 +import jakarta.annotation.Resource;
  26 +import lombok.Data;
  27 +import lombok.extern.slf4j.Slf4j;
  28 +import org.springframework.ai.chat.client.advisor.api.Advisor;
  29 +import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
  30 +import org.springframework.ai.document.Document;
  31 +import org.springframework.ai.embedding.EmbeddingModel;
  32 +import org.springframework.ai.vectorstore.SearchRequest;
  33 +import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
  34 +import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
  35 +import org.springframework.stereotype.Component;
  36 +
  37 +import java.util.*;
  38 +import java.util.concurrent.ConcurrentHashMap;
  39 +
  40 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  41 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  42 +import static cn.iocoder.yudao.module.ai1.enums.Ai1ErrorCodeConstants.MODEL_TYPE_NOT_EMBEDDING;
  43 +
  44 +/**
  45 + * AI1 RAG 工具(Spring AI + Milvus)
  46 + *
  47 + * 知识库的向量化、检索、清理与 Advisor 装配:
  48 + * 1. 向量化:分片 → Document(含 documentId、chunkIndex 元数据)→ VectorStore.add(先清理旧向量)
  49 + * 2. 检索:VectorStore.similaritySearch
  50 + * 3. 清理:按 documentId 元数据删除向量;知识库删除时 drop 整个集合
  51 + * 4. Advisor:为会话装配 QuestionAnswerAdvisor,检索上下文自动注入
  52 + *
  53 + * 集合名为 {collectionPrefix}{知识库编号}(编号全局唯一,无需租户前缀);调用方必须先在当前租户下查出知识库,再传入本工具,
  54 + * 以此保证不会越权访问其他租户的集合
  55 + *
  56 + * @author 芋道源码
  57 + */
  58 +@Component
  59 +@Slf4j
  60 +public class Ai1RagTool {
  61 +
  62 + /**
  63 + * 向量存储缓存上限
  64 + */
  65 + private static final int CACHE_MAX = 128;
  66 + /**
  67 + * 向量元数据:文档编号
  68 + */
  69 + private static final String METADATA_DOCUMENT_ID = "documentId";
  70 + /**
  71 + * 向量元数据:分片序号
  72 + */
  73 + private static final String METADATA_CHUNK_INDEX = "chunkIndex";
  74 +
  75 + @Resource
  76 + private YudaoAi1Properties ai1Properties;
  77 +
  78 + @Resource
  79 + private Ai1ModelService modelService;
  80 +
  81 + @Resource
  82 + private Ai1LlmModelFactory llmModelFactory;
  83 +
  84 + /**
  85 + * 向量存储缓存:知识库编号 → 向量存储;按访问顺序淘汰,嵌入模型实例变化(配置变更后重建)时自动重建
  86 + */
  87 + private final Cache<Long, CachedVectorStore> vectorStoreCache = CacheBuilder.newBuilder()
  88 + .maximumSize(CACHE_MAX).build();
  89 + /**
  90 + * 按知识库串行初始化的锁,避免并发首访重复创建集合和索引
  91 + */
  92 + private final Map<Long, Object> vectorStoreLocks = new ConcurrentHashMap<>();
  93 + /**
  94 + * Milvus 客户端:首次使用时创建,全局复用一个连接
  95 + */
  96 + private final LazyFunLoader<MilvusServiceClient> milvusClientLoader = LazyFunLoader.on(this::createMilvusClient);
  97 +
  98 + /**
  99 + * 文档向量化:分片 → 嵌入 → 写入 Milvus;先清理旧向量再写入,支持重复向量化
  100 + *
  101 + * @param knowledgeBase 知识库(需已配置嵌入模型)
  102 + * @param documentId 文档编号
  103 + * @param content 文档内容
  104 + * @return 分片数量
  105 + */
  106 + public int vectorize(Ai1KnowledgeBaseDO knowledgeBase, Long documentId, String content) {
  107 + // 1. 分片
  108 + List<String> chunks = split(content, knowledgeBase.getChunkSize(), knowledgeBase.getChunkOverlap());
  109 + if (CollUtil.isEmpty(chunks)) {
  110 + throw new IllegalArgumentException("文档无可分片内容");
  111 + }
  112 +
  113 + // 2. 清理旧向量后写入
  114 + MilvusVectorStore vectorStore = getOrCreateVectorStore(knowledgeBase);
  115 + vectorStore.delete(new FilterExpressionBuilder().eq(METADATA_DOCUMENT_ID, documentId).build());
  116 + List<Document> documents = new ArrayList<>(chunks.size());
  117 + for (int i = 0; i < chunks.size(); i++) {
  118 + Map<String, Object> metadata = new LinkedHashMap<>();
  119 + metadata.put(METADATA_DOCUMENT_ID, documentId);
  120 + metadata.put(METADATA_CHUNK_INDEX, i);
  121 + documents.add(new Document(chunks.get(i), metadata));
  122 + }
  123 + vectorStore.add(documents);
  124 + return chunks.size();
  125 + }
  126 +
  127 + /**
  128 + * 向量检索:按查询文本召回知识库相关分片
  129 + *
  130 + * @param knowledgeBase 知识库
  131 + * @param query 查询文本
  132 + * @param topK 检索数量,小于等于 0 时使用知识库配置
  133 + * @return 命中分片;文档名称由调用方按文档编号补充
  134 + */
  135 + public List<Ai1KnowledgeSearchRespVO> search(Ai1KnowledgeBaseDO knowledgeBase, String query, Integer topK) {
  136 + // 1. 相似度检索:未指定检索数量时,使用知识库配置
  137 + MilvusVectorStore vectorStore = getOrCreateVectorStore(knowledgeBase);
  138 + List<Document> documents = vectorStore.similaritySearch(SearchRequest.builder()
  139 + .query(query).topK(topK != null && topK > 0 ? topK : getDefaultTopK(knowledgeBase)).build());
  140 +
  141 + // 2. 转换为命中分片:从向量元数据中还原文档编号、分片序号
  142 + return convertList(documents, document -> new Ai1KnowledgeSearchRespVO().setText(document.getText())
  143 + .setDocumentId(Convert.toLong(document.getMetadata().get(METADATA_DOCUMENT_ID)))
  144 + .setChunkIndex(Convert.toInt(document.getMetadata().get(METADATA_CHUNK_INDEX)))
  145 + .setScore(ObjUtil.defaultIfNull(document.getScore(), 0D)));
  146 + }
  147 +
  148 + /**
  149 + * 按文档编号清理向量(文档删除、内容变更时调用);失败时仅记录日志
  150 + *
  151 + * @param knowledgeBase 知识库
  152 + * @param documentIds 文档编号集合
  153 + */
  154 + public void deleteByDocumentIds(Ai1KnowledgeBaseDO knowledgeBase, Collection<Long> documentIds) {
  155 + if (CollUtil.isEmpty(documentIds)) {
  156 + return;
  157 + }
  158 + try {
  159 + getOrCreateVectorStore(knowledgeBase).delete(new FilterExpressionBuilder()
  160 + .in(METADATA_DOCUMENT_ID, documentIds.toArray()).build());
  161 + } catch (Exception e) {
  162 + log.warn("[deleteByDocumentIds][知识库({}) 文档({}) 向量清理失败]", knowledgeBase.getId(), documentIds, e);
  163 + }
  164 + }
  165 +
  166 + /**
  167 + * 删除知识库的整个向量集合(知识库删除时调用,避免遗留孤儿集合);失败时仅记录日志
  168 + *
  169 + * @param knowledgeBaseId 知识库编号
  170 + */
  171 + public void dropCollection(Long knowledgeBaseId) {
  172 + evict(knowledgeBaseId);
  173 + try {
  174 + R<RpcStatus> result = milvusClientLoader.get().dropCollection(DropCollectionParam.newBuilder()
  175 + .withDatabaseName(ai1Properties.getMilvus().getDatabase())
  176 + .withCollectionName(buildCollectionName(knowledgeBaseId))
  177 + .build());
  178 + if (result.getStatus() != R.Status.Success.getCode()) {
  179 + log.warn("[dropCollection][知识库({}) 向量集合删除失败:{}]", knowledgeBaseId, result.getMessage());
  180 + }
  181 + } catch (Exception e) {
  182 + log.warn("[dropCollection][知识库({}) 向量集合删除异常]", knowledgeBaseId, e);
  183 + }
  184 + }
  185 +
  186 + /**
  187 + * 为知识库构建 RAG Advisor:检索上下文自动注入会话
  188 + *
  189 + * @param knowledgeBase 知识库
  190 + * @return RAG Advisor
  191 + */
  192 + public Advisor buildAdvisor(Ai1KnowledgeBaseDO knowledgeBase) {
  193 + MilvusVectorStore vectorStore = getOrCreateVectorStore(knowledgeBase);
  194 + SearchRequest searchRequest = SearchRequest.builder().topK(getDefaultTopK(knowledgeBase)).build();
  195 + return QuestionAnswerAdvisor.builder(vectorStore).searchRequest(searchRequest).build();
  196 + }
  197 +
  198 + /**
  199 + * 失效知识库的向量存储缓存
  200 + *
  201 + * 只作用于当前节点;其他节点在嵌入模型变更后,会因嵌入模型实例不一致自然重建
  202 + *
  203 + * @param knowledgeBaseId 知识库编号
  204 + */
  205 + public void evict(Long knowledgeBaseId) {
  206 + vectorStoreCache.invalidate(knowledgeBaseId);
  207 + }
  208 +
  209 + @PreDestroy
  210 + public void destroy() {
  211 + milvusClientLoader.ifInitialized(MilvusServiceClient::close);
  212 + }
  213 +
  214 + // ==================== 内部实现:嵌入模型 / 向量存储 / 分片 ====================
  215 +
  216 + /**
  217 + * 获取(或构建)知识库向量存储:嵌入模型实例不变时复用;Provider、模型配置变更后模型工厂会重建实例,此处随之重建
  218 + */
  219 + private MilvusVectorStore getOrCreateVectorStore(Ai1KnowledgeBaseDO knowledgeBase) {
  220 + // 1. 缓存命中、且嵌入模型实例未变化时,直接复用
  221 + EmbeddingModel embeddingModel = getEmbeddingModel(knowledgeBase);
  222 + CachedVectorStore cached = vectorStoreCache.getIfPresent(knowledgeBase.getId());
  223 + if (cached != null && cached.getEmbeddingModel() == embeddingModel) {
  224 + return cached.getVectorStore();
  225 + }
  226 +
  227 + // 2. 缓存未命中或嵌入模型实例变化时,构建新的向量存储;按知识库串行初始化,避免并发首访重复创建集合和索引
  228 + synchronized (vectorStoreLocks.computeIfAbsent(knowledgeBase.getId(), key -> new Object())) {
  229 + cached = vectorStoreCache.getIfPresent(knowledgeBase.getId());
  230 + if (cached != null && cached.getEmbeddingModel() == embeddingModel) {
  231 + return cached.getVectorStore();
  232 + }
  233 + // 2.1 构建向量存储:集合不存在时自动创建
  234 + String collectionName = buildCollectionName(knowledgeBase.getId());
  235 + MilvusVectorStore vectorStore = MilvusVectorStore.builder(milvusClientLoader.get(), embeddingModel)
  236 + .databaseName(ai1Properties.getMilvus().getDatabase())
  237 + .collectionName(collectionName)
  238 + .metricType(MetricType.COSINE)
  239 + .indexType(IndexType.FLAT)
  240 + .initializeSchema(true)
  241 + .build();
  242 + // 2.2 手动构建的实例不会触发 Spring 生命周期回调,需主动初始化:建集合、建索引、加载
  243 + try {
  244 + vectorStore.afterPropertiesSet();
  245 + } catch (Exception e) {
  246 + throw new IllegalStateException("向量集合(" + collectionName + ") 初始化失败:" + e.getMessage(), e);
  247 + }
  248 + // 2.3 放入缓存
  249 + vectorStoreCache.put(knowledgeBase.getId(), new CachedVectorStore()
  250 + .setEmbeddingModel(embeddingModel).setVectorStore(vectorStore));
  251 + log.debug("[getOrCreateVectorStore][知识库({}) 向量存储构建完成]", knowledgeBase.getId());
  252 + return vectorStore;
  253 + }
  254 + }
  255 +
  256 + /**
  257 + * 解析知识库的嵌入模型:Provider、模型不存在或被禁用时抛出业务异常
  258 + */
  259 + private EmbeddingModel getEmbeddingModel(Ai1KnowledgeBaseDO knowledgeBase) {
  260 + Ai1ModelRespBO model = modelService.getModelRespBO(
  261 + knowledgeBase.getEmbeddingProviderId(), knowledgeBase.getEmbeddingModelId());
  262 + if (!Ai1ModelTypeEnum.isEmbedding(model.getModelType())) {
  263 + throw exception(MODEL_TYPE_NOT_EMBEDDING);
  264 + }
  265 + return llmModelFactory.getOrCreateEmbeddingModel(model);
  266 + }
  267 +
  268 + /**
  269 + * 创建 Milvus 客户端:配置了用户名时,开启鉴权
  270 + */
  271 + private MilvusServiceClient createMilvusClient() {
  272 + YudaoAi1Properties.Milvus milvus = ai1Properties.getMilvus();
  273 + ConnectParam.Builder builder = ConnectParam.newBuilder()
  274 + .withUri(milvus.getUri())
  275 + .withDatabaseName(milvus.getDatabase());
  276 + if (StrUtil.isNotBlank(milvus.getUsername())) {
  277 + builder.withAuthorization(milvus.getUsername(), milvus.getPassword());
  278 + }
  279 + return new MilvusServiceClient(builder.build());
  280 + }
  281 +
  282 + /**
  283 + * 构建知识库的向量集合名:一个知识库对应一个集合
  284 + *
  285 + * 不同知识库可能使用不同的嵌入模型,向量维度不同,无法共用一个集合;后续也可能落到不同的向量库,按知识库隔离更便于迁移和整体删除
  286 + */
  287 + private String buildCollectionName(Long knowledgeBaseId) {
  288 + return ai1Properties.getMilvus().getCollectionPrefix() + knowledgeBaseId;
  289 + }
  290 +
  291 + /**
  292 + * 获得知识库的检索数量:知识库未配置时,使用 yudao.ai1.milvus.default-top-k 配置
  293 + */
  294 + private int getDefaultTopK(Ai1KnowledgeBaseDO knowledgeBase) {
  295 + return knowledgeBase.getTopK() != null && knowledgeBase.getTopK() > 0 ? knowledgeBase.getTopK()
  296 + : ai1Properties.getMilvus().getDefaultTopK();
  297 + }
  298 +
  299 + /**
  300 + * 文本分片:按 chunkSize、chunkOverlap 做字符级分片
  301 + *
  302 + * @param content 文本
  303 + * @param chunkSize 分片大小
  304 + * @param chunkOverlap 分片重叠,限制在 [0, chunkSize - 1]
  305 + * @return 分片列表
  306 + */
  307 + @SuppressWarnings("MathClampMigration")
  308 + static List<String> split(String content, Integer chunkSize, Integer chunkOverlap) {
  309 + List<String> chunks = new ArrayList<>();
  310 + if (StrUtil.isEmpty(content)) {
  311 + return chunks;
  312 + }
  313 + int size = Math.max(ObjUtil.defaultIfNull(chunkSize, 0), 1);
  314 + int overlap = Math.min(Math.max(ObjUtil.defaultIfNull(chunkOverlap, 0), 0), size - 1);
  315 + int length = content.length();
  316 + int start = 0;
  317 + while (start < length) {
  318 + int end = Math.min(start + size, length);
  319 + chunks.add(content.substring(start, end));
  320 + if (end >= length) {
  321 + break;
  322 + }
  323 + start = end - overlap;
  324 + }
  325 + return chunks;
  326 + }
  327 +
  328 + /**
  329 + * 向量存储缓存项
  330 + */
  331 + @Data
  332 + private static class CachedVectorStore {
  333 +
  334 + /**
  335 + * 构建时使用的嵌入模型实例,用于判断是否需要重建
  336 + */
  337 + private EmbeddingModel embeddingModel;
  338 + /**
  339 + * 向量存储
  340 + */
  341 + private MilvusVectorStore vectorStore;
  342 +
  343 + }
  344 +
  345 +}