Commit c9201c30f914554587c4e7a22eb23206052283e5
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.
pom.xml
| @@ -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
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
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
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/framework/package-info.java
0 → 100644
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
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
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 | +} |