Commit d608a9a8423637e42e6ec53ac55f6f8701279299
Merge remote-tracking branch 'origin/master-jdk17' into master-jdk17
Showing
413 changed files
with
30507 additions
and
2033 deletions
Too many changes.
To preserve performance only 100 of 413 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-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
| 1 | package cn.iocoder.yudao.framework.common.util.json.databind; | 1 | package cn.iocoder.yudao.framework.common.util.json.databind; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.date.DateUtil; | ||
| 4 | +import cn.hutool.core.date.LocalDateTimeUtil; | ||
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 3 | import com.fasterxml.jackson.core.JsonParser; | 6 | import com.fasterxml.jackson.core.JsonParser; |
| 4 | import com.fasterxml.jackson.databind.DeserializationContext; | 7 | import com.fasterxml.jackson.databind.DeserializationContext; |
| 5 | import com.fasterxml.jackson.databind.JsonDeserializer; | 8 | import com.fasterxml.jackson.databind.JsonDeserializer; |
| @@ -7,10 +10,11 @@ import com.fasterxml.jackson.databind.JsonDeserializer; | @@ -7,10 +10,11 @@ import com.fasterxml.jackson.databind.JsonDeserializer; | ||
| 7 | import java.io.IOException; | 10 | import java.io.IOException; |
| 8 | import java.time.Instant; | 11 | import java.time.Instant; |
| 9 | import java.time.LocalDateTime; | 12 | import java.time.LocalDateTime; |
| 10 | -import java.time.ZoneId; | 13 | +import java.time.format.DateTimeFormatter; |
| 14 | +import java.util.regex.Pattern; | ||
| 11 | 15 | ||
| 12 | /** | 16 | /** |
| 13 | - * 基于时间戳的 LocalDateTime 反序列化器 | 17 | + * LocalDateTime 反序列化器,支持毫秒时间戳、常见日期时间字符串和 ISO-8601 日期。 |
| 14 | * | 18 | * |
| 15 | * @author 老五 | 19 | * @author 老五 |
| 16 | */ | 20 | */ |
| @@ -18,10 +22,26 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa | @@ -18,10 +22,26 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa | ||
| 18 | 22 | ||
| 19 | public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); | 23 | public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); |
| 20 | 24 | ||
| 25 | + private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("-?\\d+"); | ||
| 26 | + | ||
| 21 | @Override | 27 | @Override |
| 22 | public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { | 28 | public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { |
| 23 | - // 将 Long 时间戳,转换为 LocalDateTime 对象 | ||
| 24 | - return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); | 29 | + String text = p.getText(); |
| 30 | + if (StrUtil.isBlank(text)) { | ||
| 31 | + return null; | ||
| 32 | + } | ||
| 33 | + text = text.trim(); | ||
| 34 | + // 情况一:毫秒时间戳 | ||
| 35 | + if (TIMESTAMP_PATTERN.matcher(text).matches()) { | ||
| 36 | + return LocalDateTimeUtil.of(Long.parseLong(text)); | ||
| 37 | + } | ||
| 38 | + // 情况二:ISO-8601 日期;标准解析保留时区偏移与纳秒精度 | ||
| 39 | + if (text.contains("T")) { | ||
| 40 | + return LocalDateTimeUtil.of(DateTimeFormatter.ISO_DATE_TIME.parseBest( | ||
| 41 | + text, Instant::from, LocalDateTime::from)); | ||
| 42 | + } | ||
| 43 | + // 情况三:常见日期时间字符串 | ||
| 44 | + return LocalDateTimeUtil.of(DateUtil.parse(text).toInstant()); | ||
| 25 | } | 45 | } |
| 26 | 46 | ||
| 27 | } | 47 | } |
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializerTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.common.util.json.databind; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | +import com.fasterxml.jackson.databind.module.SimpleModule; | ||
| 5 | +import org.junit.jupiter.api.Test; | ||
| 6 | + | ||
| 7 | +import java.time.Instant; | ||
| 8 | +import java.time.LocalDateTime; | ||
| 9 | +import java.time.OffsetDateTime; | ||
| 10 | +import java.time.ZoneId; | ||
| 11 | + | ||
| 12 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 13 | +import static org.junit.jupiter.api.Assertions.assertNull; | ||
| 14 | +import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * {@link TimestampLocalDateTimeDeserializer} 的单元测试 | ||
| 18 | + * | ||
| 19 | + * @author 芋道源码 | ||
| 20 | + */ | ||
| 21 | +public class TimestampLocalDateTimeDeserializerTest { | ||
| 22 | + | ||
| 23 | + private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
| 24 | + | ||
| 25 | + static { | ||
| 26 | + SimpleModule module = new SimpleModule(); | ||
| 27 | + module.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); | ||
| 28 | + MAPPER.registerModule(module); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + private LocalDateTime parse(String json) throws Exception { | ||
| 32 | + return MAPPER.readValue(json, LocalDateTime.class); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @Test | ||
| 36 | + public void testEpochMilliNumber() throws Exception { | ||
| 37 | + // 1700000000000 = 2023-11-14T22:13:20Z(本地时区换算后) | ||
| 38 | + LocalDateTime expected = LocalDateTime.ofInstant(Instant.ofEpochMilli(1700000000000L), | ||
| 39 | + ZoneId.systemDefault()); | ||
| 40 | + assertEquals(expected, parse("1700000000000")); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @Test | ||
| 44 | + public void testEpochMilliString() throws Exception { | ||
| 45 | + LocalDateTime expected = LocalDateTime.ofInstant(Instant.ofEpochMilli(1700000000000L), | ||
| 46 | + ZoneId.systemDefault()); | ||
| 47 | + assertEquals(expected, parse("\"1700000000000\"")); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @Test | ||
| 51 | + public void testSpaceFormat() throws Exception { | ||
| 52 | + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0), parse("\"2026-09-11 08:00:00\"")); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + @Test | ||
| 56 | + public void testSpaceFormatWithMillis() throws Exception { | ||
| 57 | + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0, 123000000), parse("\"2026-09-11 08:00:00.123\"")); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + @Test | ||
| 61 | + public void testIsoLocal() throws Exception { | ||
| 62 | + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0), parse("\"2026-09-11T08:00:00\"")); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + @Test | ||
| 66 | + public void testIsoLocalWithNanos() throws Exception { | ||
| 67 | + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0, 123456789), | ||
| 68 | + parse("\"2026-09-11T08:00:00.123456789\"")); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + @Test | ||
| 72 | + public void testIsoWithOffset() throws Exception { | ||
| 73 | + // 前端 Date 对象序列化:带 Z(UTC)。2026-09-11T00:00:00Z 换算到系统时区 | ||
| 74 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T00:00:00Z") | ||
| 75 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 76 | + assertEquals(expected, parse("\"2026-09-11T00:00:00Z\"")); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + @Test | ||
| 80 | + public void testIsoWithOffsetMillis() throws Exception { | ||
| 81 | + // JSON.stringify(Date) 实际产物:带毫秒 + Z(无 value-format 的 Date 对象控件提交格式) | ||
| 82 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T00:00:00.000Z") | ||
| 83 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 84 | + assertEquals(expected, parse("\"2026-09-11T00:00:00.000Z\"")); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @Test | ||
| 88 | + public void testIsoWithOffsetPlus() throws Exception { | ||
| 89 | + // 带 +08:00 时区偏移的形式 | ||
| 90 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T08:00:00.000+08:00") | ||
| 91 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 92 | + assertEquals(expected, parse("\"2026-09-11T08:00:00.000+08:00\"")); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + @Test | ||
| 96 | + public void testDateOnly() throws Exception { | ||
| 97 | + assertEquals(LocalDateTime.of(2026, 9, 11, 0, 0, 0), parse("\"2026-09-11\"")); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @Test | ||
| 101 | + public void testIsoWithNegativeOffsetAndMillis() throws Exception { | ||
| 102 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T23:30:00.123-05:30") | ||
| 103 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 104 | + assertEquals(expected, parse("\"2026-09-11T23:30:00.123-05:30\"")); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + @Test | ||
| 108 | + public void testZeroAndNegativeEpochMilli() throws Exception { | ||
| 109 | + for (long millis : new long[]{0L, -1L}) { | ||
| 110 | + LocalDateTime expected = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), | ||
| 111 | + ZoneId.systemDefault()); | ||
| 112 | + assertEquals(expected, parse(Long.toString(millis))); | ||
| 113 | + assertEquals(expected, parse("\"" + millis + "\"")); | ||
| 114 | + } | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + @Test | ||
| 118 | + public void testSurroundingWhitespace() throws Exception { | ||
| 119 | + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0, 0), parse("\" 2026-09-11 08:00:00 \"")); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + @Test | ||
| 123 | + public void testNullAndEmpty() throws Exception { | ||
| 124 | + assertNull(parse("null")); | ||
| 125 | + assertNull(parse("\"\"")); | ||
| 126 | + assertNull(parse("\" \"")); | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + @Test | ||
| 130 | + public void testHutoolDateFormats() throws Exception { | ||
| 131 | + assertEquals(LocalDateTime.of(2026, 9, 11, 0, 0), parse("\"2026/09/11\"")); | ||
| 132 | + assertEquals(LocalDateTime.of(2026, 9, 11, 8, 0), parse("\"2026/09/11 08:00:00\"")); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + @Test | ||
| 136 | + public void testInvalidThrows() { | ||
| 137 | + // 调用,并断言异常 | ||
| 138 | + assertThrows(Exception.class, () -> parse("\"not-a-date\"")); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | +} |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/translate/config/YudaoTranslateAutoConfiguration.java
| 1 | package cn.iocoder.yudao.framework.translate.config; | 1 | package cn.iocoder.yudao.framework.translate.config; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.translate.core.TranslateUtils; | 3 | import cn.iocoder.yudao.framework.translate.core.TranslateUtils; |
| 4 | +import org.dromara.trans.config.EasyTransMybatisPlusConfig; | ||
| 5 | +import org.dromara.trans.config.TransServiceConfig; | ||
| 4 | import org.dromara.trans.service.impl.TransService; | 6 | import org.dromara.trans.service.impl.TransService; |
| 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; | 7 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
| 7 | import org.springframework.context.annotation.Bean; | 9 | import org.springframework.context.annotation.Bean; |
| 8 | 10 | ||
| 9 | -@AutoConfiguration | 11 | +// 解决 Easy-Trans 自动配置顺序问题,详见 https://t.zsxq.com/nX3vi |
| 12 | +@AutoConfiguration(after = { | ||
| 13 | + EasyTransMybatisPlusConfig.class, | ||
| 14 | + TransServiceConfig.class | ||
| 15 | +}) | ||
| 10 | public class YudaoTranslateAutoConfiguration { | 16 | public class YudaoTranslateAutoConfiguration { |
| 11 | 17 | ||
| 12 | @Bean | 18 | @Bean |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
| @@ -21,6 +21,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; | @@ -21,6 +21,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; | ||
| 21 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; | 21 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; |
| 22 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; | 22 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; |
| 23 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; | 23 | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; |
| 24 | +import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction; | ||
| 25 | +import cn.iocoder.yudao.module.ai.tool.function.UserProfileQueryToolFunction; | ||
| 26 | +import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction; | ||
| 24 | import cn.iocoder.yudao.module.ai.tool.method.PersonService; | 27 | import cn.iocoder.yudao.module.ai.tool.method.PersonService; |
| 25 | import io.micrometer.observation.ObservationRegistry; | 28 | import io.micrometer.observation.ObservationRegistry; |
| 26 | import lombok.extern.slf4j.Slf4j; | 29 | import lombok.extern.slf4j.Slf4j; |
| @@ -38,6 +41,7 @@ import org.springframework.ai.support.ToolCallbacks; | @@ -38,6 +41,7 @@ import org.springframework.ai.support.ToolCallbacks; | ||
| 38 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; | 41 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; |
| 39 | import org.springframework.ai.tokenizer.TokenCountEstimator; | 42 | import org.springframework.ai.tokenizer.TokenCountEstimator; |
| 40 | import org.springframework.ai.tool.ToolCallback; | 43 | import org.springframework.ai.tool.ToolCallback; |
| 44 | +import org.springframework.ai.tool.function.FunctionToolCallback; | ||
| 41 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; | 45 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; |
| 42 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; | 46 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; |
| 43 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; | 47 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; |
| @@ -424,7 +428,7 @@ public class AiAutoConfiguration { | @@ -424,7 +428,7 @@ public class AiAutoConfiguration { | ||
| 424 | return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); | 428 | return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); |
| 425 | } | 429 | } |
| 426 | 430 | ||
| 427 | - // ========== MCP 相关 ========== | 431 | + // ========== MCP & 工具相关 ========== |
| 428 | 432 | ||
| 429 | /** | 433 | /** |
| 430 | * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</> | 434 | * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</> |
| @@ -434,4 +438,25 @@ public class AiAutoConfiguration { | @@ -434,4 +438,25 @@ public class AiAutoConfiguration { | ||
| 434 | return List.of(ToolCallbacks.from(personService)); | 438 | return List.of(ToolCallbacks.from(personService)); |
| 435 | } | 439 | } |
| 436 | 440 | ||
| 441 | + @Bean | ||
| 442 | + public ToolCallback directoryListToolCallback(DirectoryListToolFunction function) { | ||
| 443 | + return FunctionToolCallback.builder("directory_list", function) | ||
| 444 | + .description("列出指定目录的文件列表") | ||
| 445 | + .inputType(DirectoryListToolFunction.Request.class).build(); | ||
| 446 | + } | ||
| 447 | + | ||
| 448 | + @Bean | ||
| 449 | + public ToolCallback weatherQueryToolCallback(WeatherQueryToolFunction function) { | ||
| 450 | + return FunctionToolCallback.builder("weather_query", function) | ||
| 451 | + .description("查询指定城市的天气信息") | ||
| 452 | + .inputType(WeatherQueryToolFunction.Request.class).build(); | ||
| 453 | + } | ||
| 454 | + | ||
| 455 | + @Bean | ||
| 456 | + public ToolCallback userProfileQueryToolCallback(UserProfileQueryToolFunction function) { | ||
| 457 | + return FunctionToolCallback.builder("user_profile_query", function) | ||
| 458 | + .description("用户信息查询") | ||
| 459 | + .inputType(UserProfileQueryToolFunction.Request.class).build(); | ||
| 460 | + } | ||
| 461 | + | ||
| 437 | } | 462 | } |
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.13.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.util.Map; | ||
| 7 | + | ||
| 8 | +import java.time.LocalDateTime; | ||
| 9 | + | ||
| 10 | +@Schema(description = "管理后台 - AI1 供应商 Response VO") | ||
| 11 | +@Data | ||
| 12 | +public class Ai1ProviderRespVO { | ||
| 13 | + | ||
| 14 | + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | ||
| 15 | + private Long id; | ||
| 16 | + | ||
| 17 | + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "DeepSeek") | ||
| 18 | + private String name; | ||
| 19 | + | ||
| 20 | + @Schema(description = "接口地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://api.deepseek.com/v1") | ||
| 21 | + private String baseUrl; | ||
| 22 | + | ||
| 23 | + @Schema(description = "API 密钥,脱敏展示", example = "sk-****abcd") | ||
| 24 | + private String apiKey; | ||
| 25 | + | ||
| 26 | + @Schema(description = "请求 Header,JSON 对象", example = "{\"X-Session\":\"{session}\"}") | ||
| 27 | + private Map<String, String> headers; | ||
| 28 | + | ||
| 29 | + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | ||
| 30 | + private Integer status; | ||
| 31 | + | ||
| 32 | + @Schema(description = "备注", example = "官方接口") | ||
| 33 | + private String remark; | ||
| 34 | + | ||
| 35 | + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | ||
| 36 | + private LocalDateTime createTime; | ||
| 37 | + | ||
| 38 | + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) | ||
| 39 | + private LocalDateTime updateTime; | ||
| 40 | + | ||
| 41 | +} |
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.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.common.validation.InEnum; | ||
| 5 | +import cn.iocoder.yudao.module.ai1.util.Ai1Utils; | ||
| 6 | +import com.fasterxml.jackson.annotation.JsonIgnore; | ||
| 7 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 8 | +import jakarta.validation.constraints.AssertTrue; | ||
| 9 | +import jakarta.validation.constraints.NotEmpty; | ||
| 10 | +import jakarta.validation.constraints.NotNull; | ||
| 11 | +import jakarta.validation.constraints.Size; | ||
| 12 | +import lombok.Data; | ||
| 13 | + | ||
| 14 | +import java.util.Map; | ||
| 15 | + | ||
| 16 | +@Schema(description = "管理后台 - AI1 供应商新增/修改 Request VO") | ||
| 17 | +@Data | ||
| 18 | +public class Ai1ProviderSaveReqVO { | ||
| 19 | + | ||
| 20 | + @Schema(description = "编号", example = "1024") | ||
| 21 | + private Long id; | ||
| 22 | + | ||
| 23 | + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "DeepSeek") | ||
| 24 | + @NotEmpty(message = "名称不能为空") | ||
| 25 | + @Size(max = 50, message = "名称长度不能超过 50 个字符") | ||
| 26 | + private String name; | ||
| 27 | + | ||
| 28 | + @Schema(description = "接口地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://api.deepseek.com/v1") | ||
| 29 | + @NotEmpty(message = "接口地址不能为空") | ||
| 30 | + @Size(max = 200, message = "接口地址长度不能超过 200 个字符") | ||
| 31 | + private String baseUrl; | ||
| 32 | + | ||
| 33 | + @Schema(description = "API 密钥;修改时为空表示保持原密钥不变", example = "sk-xxx") | ||
| 34 | + @Size(max = 200, message = "API 密钥长度不能超过 200 个字符") | ||
| 35 | + private String apiKey; | ||
| 36 | + | ||
| 37 | + @Schema(description = "请求 Header,JSON 对象", example = "{\"X-Session\":\"{session}\"}") | ||
| 38 | + private Map<String, 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 长度不能超过 2000 个字符") | ||
| 50 | + @JsonIgnore | ||
| 51 | + public boolean isHeadersValid() { | ||
| 52 | + return Ai1Utils.isHeadersValid(headers); | ||
| 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.TableField; | ||
| 8 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; | ||
| 9 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 10 | +import lombok.*; | ||
| 11 | + | ||
| 12 | +import java.util.Map; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * AI1 供应商 DO | ||
| 16 | + * | ||
| 17 | + * 供应商即模型提供方(OpenAI 兼容端点),维护接口地址与密钥 | ||
| 18 | + * | ||
| 19 | + * @author 芋道源码 | ||
| 20 | + */ | ||
| 21 | +@TableName(value = "ai1_provider", autoResultMap = true) | ||
| 22 | +@KeySequence("ai1_provider_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 | ||
| 23 | +@Data | ||
| 24 | +@EqualsAndHashCode(callSuper = true) | ||
| 25 | +@ToString(callSuper = true) | ||
| 26 | +@Builder | ||
| 27 | +@NoArgsConstructor | ||
| 28 | +@AllArgsConstructor | ||
| 29 | +public class Ai1ProviderDO extends TenantBaseDO { | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 编号 | ||
| 33 | + */ | ||
| 34 | + @TableId | ||
| 35 | + private Long id; | ||
| 36 | + /** | ||
| 37 | + * 名称 | ||
| 38 | + */ | ||
| 39 | + private String name; | ||
| 40 | + /** | ||
| 41 | + * 接口地址 | ||
| 42 | + */ | ||
| 43 | + private String baseUrl; | ||
| 44 | + /** | ||
| 45 | + * API 密钥 | ||
| 46 | + */ | ||
| 47 | + private String apiKey; | ||
| 48 | + /** | ||
| 49 | + * 请求 Header | ||
| 50 | + * | ||
| 51 | + * JSON 对象,格式为 {"X-Session":"{session}"};value 可包含 {session} 占位符,请求时替换为会话编号 | ||
| 52 | + */ | ||
| 53 | + @TableField(typeHandler = JacksonTypeHandler.class) | ||
| 54 | + private Map<String, String> headers; | ||
| 55 | + /** | ||
| 56 | + * 状态 | ||
| 57 | + * | ||
| 58 | + * 枚举 {@link CommonStatusEnum} | ||
| 59 | + */ | ||
| 60 | + private Integer status; | ||
| 61 | + /** | ||
| 62 | + * 备注 | ||
| 63 | + */ | ||
| 64 | + private String remark; | ||
| 65 | + | ||
| 66 | +} |
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 | + ErrorCode PROVIDER_HEADERS_INVALID = new ErrorCode(1_041_000_007, "请求 Header 格式不正确或名称重复,请检查供应商配置"); | ||
| 20 | + | ||
| 21 | + // ========== 模型 1-041-001-000 ========== | ||
| 22 | + ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_041_001_000, "模型不存在"); | ||
| 23 | + ErrorCode MODEL_DISABLE = new ErrorCode(1_041_001_001, "模型({}) 已禁用"); | ||
| 24 | + ErrorCode MODEL_DUPLICATE = new ErrorCode(1_041_001_002, "模型标识({}) 已存在"); | ||
| 25 | + ErrorCode MODEL_USED_BY_AGENT = new ErrorCode(1_041_001_003, "模型已被 Agent 使用,禁止删除"); | ||
| 26 | + ErrorCode MODEL_NOT_BELONG_PROVIDER = new ErrorCode(1_041_001_004, "模型不属于该供应商"); | ||
| 27 | + ErrorCode MODEL_TYPE_NOT_CHAT = new ErrorCode(1_041_001_005, "所选模型不是对话模型"); | ||
| 28 | + ErrorCode MODEL_TYPE_NOT_EMBEDDING = new ErrorCode(1_041_001_006, "所选模型不是嵌入模型"); | ||
| 29 | + ErrorCode MODEL_IMPORT_ALL_EXISTS = new ErrorCode(1_041_001_007, "所选模型均已导入,无需重复导入"); | ||
| 30 | + ErrorCode MODEL_USED_BY_KNOWLEDGE_BASE = new ErrorCode(1_041_001_008, "模型已被知识库使用,禁止删除"); | ||
| 31 | + ErrorCode MODEL_TYPE_USED_BY_AGENT = new ErrorCode(1_041_001_009, "模型已被 Agent 使用,禁止修改类型"); | ||
| 32 | + ErrorCode MODEL_TYPE_USED_BY_KNOWLEDGE_BASE = new ErrorCode(1_041_001_010, "模型已被知识库使用,禁止修改类型"); | ||
| 33 | + | ||
| 34 | + // ========== 知识库 1-041-002-000 ========== | ||
| 35 | + ErrorCode KNOWLEDGE_BASE_NOT_EXISTS = new ErrorCode(1_041_002_000, "知识库不存在"); | ||
| 36 | + ErrorCode KNOWLEDGE_BASE_DOCUMENT_EMPTY = new ErrorCode(1_041_002_001, "知识库下暂无文档"); | ||
| 37 | + ErrorCode KNOWLEDGE_BASE_SEARCH_FAIL = new ErrorCode(1_041_002_002, "向量检索失败:{}"); | ||
| 38 | + ErrorCode KNOWLEDGE_BASE_USED_BY_AGENT = new ErrorCode(1_041_002_003, "知识库已被 Agent 使用,禁止删除"); | ||
| 39 | + | ||
| 40 | + // ========== 知识文档 1-041-003-000 ========== | ||
| 41 | + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_041_003_000, "文档不存在"); | ||
| 42 | + ErrorCode KNOWLEDGE_DOCUMENT_CONTENT_EMPTY = new ErrorCode(1_041_003_001, "文档内容为空,无法向量化"); | ||
| 43 | + ErrorCode KNOWLEDGE_DOCUMENT_FILE_TYPE_INVALID = new ErrorCode(1_041_003_002, "仅支持 .txt / .md 文本文件"); | ||
| 44 | + ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_041_003_003, "文件内容为空"); | ||
| 45 | + ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_041_003_004, "文件读取失败:{}"); | ||
| 46 | + ErrorCode KNOWLEDGE_DOCUMENT_VECTORIZE_FAIL = new ErrorCode(1_041_003_005, "向量化失败:{}"); | ||
| 47 | + | ||
| 48 | + // ========== MCP 1-041-004-000 ========== | ||
| 49 | + ErrorCode MCP_NOT_EXISTS = new ErrorCode(1_041_004_000, "MCP 不存在"); | ||
| 50 | + ErrorCode MCP_CONFIG_INVALID = new ErrorCode(1_041_004_001, "config 配置需为合法的 JSON 对象"); | ||
| 51 | + ErrorCode MCP_COMMAND_REQUIRED = new ErrorCode(1_041_004_002, "本地(stdio)类型必须在 config 中配置 command 命令"); | ||
| 52 | + ErrorCode MCP_URL_REQUIRED = new ErrorCode(1_041_004_003, "远程(http)类型必须配置服务地址"); | ||
| 53 | + | ||
| 54 | + // ========== SKILL 1-041-005-000 ========== | ||
| 55 | + ErrorCode SKILL_NOT_EXISTS = new ErrorCode(1_041_005_000, "SKILL 不存在"); | ||
| 56 | + ErrorCode SKILL_USED_BY_AGENT = new ErrorCode(1_041_005_001, "SKILL 已被 Agent 使用,禁止删除"); | ||
| 57 | + ErrorCode SKILL_NAME_DUPLICATE = new ErrorCode(1_041_005_002, "SKILL 名称({}) 已存在"); | ||
| 58 | + | ||
| 59 | + // ========== SKILL 内容文件 1-041-006-000 ========== | ||
| 60 | + ErrorCode SKILL_FILE_NOT_EXISTS = new ErrorCode(1_041_006_000, "文件或目录不存在"); | ||
| 61 | + ErrorCode SKILL_FILE_NAME_INVALID = new ErrorCode(1_041_006_001, "名称格式不合法(不能包含路径分隔符、控制字符,且不能为 . 或 ..)"); | ||
| 62 | + ErrorCode SKILL_FILE_NAME_DUPLICATE = new ErrorCode(1_041_006_002, "同级下已存在同名节点({})"); | ||
| 63 | + ErrorCode SKILL_FILE_PARENT_INVALID = new ErrorCode(1_041_006_003, "父节点必须是同一 SKILL 下的目录"); | ||
| 64 | + ErrorCode SKILL_FILE_LOCKED = new ErrorCode(1_041_006_004, "固定文件或目录禁止{}"); | ||
| 65 | + ErrorCode SKILL_FILE_MOVE_TO_SELF = new ErrorCode(1_041_006_005, "不能移入自身"); | ||
| 66 | + ErrorCode SKILL_FILE_NOT_FILE = new ErrorCode(1_041_006_006, "目录不支持保存内容"); | ||
| 67 | + ErrorCode SKILL_FILE_MOVE_TO_DESCENDANT = new ErrorCode(1_041_006_007, "不能移入自身的子目录"); | ||
| 68 | + ErrorCode SKILL_FILE_PARENT_NOT_EXISTS = new ErrorCode(1_041_006_008, "父目录不存在"); | ||
| 69 | + | ||
| 70 | + // ========== Agent 1-041-007-000 ========== | ||
| 71 | + ErrorCode AGENT_NOT_EXISTS = new ErrorCode(1_041_007_000, "Agent 不存在"); | ||
| 72 | + ErrorCode AGENT_DISABLE = new ErrorCode(1_041_007_001, "Agent({}) 已关闭"); | ||
| 73 | + | ||
| 74 | + // ========== 会话 1-041-008-000 ========== | ||
| 75 | + ErrorCode SESSION_NOT_EXISTS = new ErrorCode(1_041_008_000, "会话不存在"); | ||
| 76 | + | ||
| 77 | + // ========== 会话消息 1-041-009-000 ========== | ||
| 78 | + ErrorCode SESSION_MESSAGE_NOT_EXISTS = new ErrorCode(1_041_009_000, "会话消息不存在"); | ||
| 79 | + | ||
| 80 | +} |
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.util.StrUtil; | ||
| 4 | +import cn.iocoder.yudao.module.ai1.service.model.bo.Ai1ModelRespBO; | ||
| 5 | +import com.google.common.cache.Cache; | ||
| 6 | +import com.google.common.cache.CacheBuilder; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.ai.chat.client.ChatClient; | ||
| 9 | +import org.springframework.ai.document.MetadataMode; | ||
| 10 | +import org.springframework.ai.embedding.EmbeddingModel; | ||
| 11 | +import org.springframework.ai.openai.OpenAiChatModel; | ||
| 12 | +import org.springframework.ai.openai.OpenAiChatOptions; | ||
| 13 | +import org.springframework.ai.openai.OpenAiEmbeddingModel; | ||
| 14 | +import org.springframework.ai.openai.OpenAiEmbeddingOptions; | ||
| 15 | +import org.springframework.ai.openai.api.OpenAiApi; | ||
| 16 | +import org.springframework.stereotype.Component; | ||
| 17 | +import org.springframework.util.LinkedMultiValueMap; | ||
| 18 | +import org.springframework.util.MultiValueMap; | ||
| 19 | + | ||
| 20 | +import java.net.URI; | ||
| 21 | +import java.time.Duration; | ||
| 22 | +import java.util.LinkedHashMap; | ||
| 23 | +import java.util.Map; | ||
| 24 | +import java.util.Objects; | ||
| 25 | +import java.util.function.Predicate; | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * AI1 LLM 模型工厂 | ||
| 29 | + * | ||
| 30 | + * 按 {@link Ai1ModelRespBO} 程序化构建 Spring AI 模型: | ||
| 31 | + * 1. 对话模型:OpenAiChatModel(OpenAI 兼容协议,覆盖 DeepSeek、GLM、Ollama(/v1) 等) | ||
| 32 | + * 2. 嵌入模型:OpenAiEmbeddingModel(向量化) | ||
| 33 | + * | ||
| 34 | + * 模型按 providerId:modelId 缓存复用(编号全局唯一,无需再区分租户),缓存使用 Guava Cache 按容量淘汰最久未访问的模型; | ||
| 35 | + * Provider、模型的配置变更时,由对应 Service 调用 {@link #evictByProviderId}、{@link #evictByModelId} 显式失效 | ||
| 36 | + * | ||
| 37 | + * @author 芋道源码 | ||
| 38 | + */ | ||
| 39 | +@Component | ||
| 40 | +@Slf4j | ||
| 41 | +public class Ai1LlmModelFactory { | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 请求 Header 会话占位符:构建模型时替换为当前会话编号 | ||
| 45 | + */ | ||
| 46 | + public static final String SESSION_PLACEHOLDER = "{session}"; | ||
| 47 | + /** | ||
| 48 | + * 缓存上限,达到后逐出最久未访问的模型 | ||
| 49 | + */ | ||
| 50 | + private static final int CACHE_MAX = 128; | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 缓存写入后的过期时间:修改 Provider / 模型时只能失效本节点,其他节点最迟在该时间后按新配置重建 | ||
| 54 | + */ | ||
| 55 | + private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10); | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 缓存 key 分隔符 | ||
| 59 | + */ | ||
| 60 | + private static final String KEY_SEPARATOR = ":"; | ||
| 61 | + | ||
| 62 | + /** | ||
| 63 | + * 对话模型缓存:providerId:modelId:sessionKey → ChatModel | ||
| 64 | + */ | ||
| 65 | + private final Cache<String, OpenAiChatModel> chatModelCache = CacheBuilder.newBuilder() | ||
| 66 | + .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); | ||
| 67 | + /** | ||
| 68 | + * 嵌入模型缓存:providerId:modelId → EmbeddingModel | ||
| 69 | + */ | ||
| 70 | + private final Cache<String, OpenAiEmbeddingModel> embeddingModelCache = CacheBuilder.newBuilder() | ||
| 71 | + .maximumSize(CACHE_MAX).expireAfterWrite(CACHE_EXPIRE).build(); | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 获取(或构建)对话模型,并包装为 ChatClient | ||
| 75 | + * | ||
| 76 | + * @param model 模型运行时快照 | ||
| 77 | + * @param sessionId 会话编号,用于替换请求 Header 中的 {session} 占位符,可为空 | ||
| 78 | + * @return ChatClient | ||
| 79 | + */ | ||
| 80 | + public ChatClient buildChatClient(Ai1ModelRespBO model, Long sessionId) { | ||
| 81 | + return ChatClient.builder(getOrCreateChatModel(model, sessionId)).build(); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 获取(或构建)嵌入模型 | ||
| 86 | + * | ||
| 87 | + * @param model 模型运行时快照 | ||
| 88 | + * @return 嵌入模型 | ||
| 89 | + */ | ||
| 90 | + public EmbeddingModel getOrCreateEmbeddingModel(Ai1ModelRespBO model) { | ||
| 91 | + return embeddingModelCache.asMap().computeIfAbsent(buildCacheKey(model), key -> { | ||
| 92 | + OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder() | ||
| 93 | + .model(model.getModel()) | ||
| 94 | + .build(); | ||
| 95 | + log.debug("[getOrCreateEmbeddingModel][providerId({}) modelId({}) 嵌入模型构建完成]", model.getProviderId(), model.getModelId()); | ||
| 96 | + return new OpenAiEmbeddingModel(buildOpenAiApi(model, null), MetadataMode.EMBED, options); | ||
| 97 | + }); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * 失效指定 Provider 下的全部模型缓存(Provider 的地址、密钥、Header、状态变更或删除时调用) | ||
| 102 | + * | ||
| 103 | + * @param providerId Provider 编号 | ||
| 104 | + */ | ||
| 105 | + public void evictByProviderId(Long providerId) { | ||
| 106 | + String prefix = providerId + KEY_SEPARATOR; | ||
| 107 | + evict(key -> key.startsWith(prefix)); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + /** | ||
| 111 | + * 失效指定模型的缓存(模型标识、类型、状态变更或删除时调用) | ||
| 112 | + * | ||
| 113 | + * @param modelId 模型编号 | ||
| 114 | + */ | ||
| 115 | + public void evictByModelId(Long modelId) { | ||
| 116 | + String modelIdText = String.valueOf(modelId); | ||
| 117 | + evict(key -> modelIdText.equals(StrUtil.split(key, KEY_SEPARATOR).get(1))); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * 按 key 条件失效对话模型、嵌入模型缓存 | ||
| 122 | + * | ||
| 123 | + * @param keyMatcher key 匹配条件 | ||
| 124 | + */ | ||
| 125 | + private void evict(Predicate<String> keyMatcher) { | ||
| 126 | + chatModelCache.asMap().keySet().removeIf(keyMatcher); | ||
| 127 | + embeddingModelCache.asMap().keySet().removeIf(keyMatcher); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + /** | ||
| 131 | + * 获取(或构建)对话模型 | ||
| 132 | + * | ||
| 133 | + * 仅当请求 Header 使用 {session} 占位符(需按会话隔离)时,才把会话编号纳入缓存 key; | ||
| 134 | + * 否则同一模型跨会话复用,避免每个会话都构建一个模型实例 | ||
| 135 | + */ | ||
| 136 | + private OpenAiChatModel getOrCreateChatModel(Ai1ModelRespBO model, Long sessionId) { | ||
| 137 | + // 1. 计算缓存 key:Header 含 {session} 占位符时按会话隔离,否则跨会话共享 | ||
| 138 | + String sessionKey = usesSessionHeader(model.getHeaders()) && sessionId != null | ||
| 139 | + ? String.valueOf(sessionId) : ""; | ||
| 140 | + String cacheKey = buildCacheKey(model) + KEY_SEPARATOR + sessionKey; | ||
| 141 | + | ||
| 142 | + // 2. 命中缓存直接复用;未命中时按运行时快照构建 OpenAI 兼容对话模型并缓存 | ||
| 143 | + return chatModelCache.asMap().computeIfAbsent(cacheKey, key -> { | ||
| 144 | + OpenAiChatOptions options = OpenAiChatOptions.builder() | ||
| 145 | + .model(model.getModel()) | ||
| 146 | + .build(); | ||
| 147 | + log.debug("[getOrCreateChatModel][providerId({}) modelId({}) 对话模型构建完成]", model.getProviderId(), model.getModelId()); | ||
| 148 | + return OpenAiChatModel.builder().openAiApi(buildOpenAiApi(model, sessionId)).defaultOptions(options).build(); | ||
| 149 | + }); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * Spring AI 1.x 将连接参数放在 OpenAiApi,而非模型 Options;仅在模型构建边界适配。 | ||
| 154 | + * normalizeBaseUrl 已包含 /v1 或供应商的自定义前缀,因此端点不再重复添加 /v1。 | ||
| 155 | + */ | ||
| 156 | + private static OpenAiApi buildOpenAiApi(Ai1ModelRespBO model, Long sessionId) { | ||
| 157 | + MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); | ||
| 158 | + buildHeaders(model.getHeaders(), sessionId).forEach(headers::add); | ||
| 159 | + return OpenAiApi.builder() | ||
| 160 | + .baseUrl(normalizeBaseUrl(model.getBaseUrl())) | ||
| 161 | + .apiKey(StrUtil.nullToEmpty(model.getApiKey())) | ||
| 162 | + .headers(headers) | ||
| 163 | + .completionsPath("/chat/completions") | ||
| 164 | + .embeddingsPath("/embeddings") | ||
| 165 | + .build(); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + /** | ||
| 169 | + * 构建模型缓存 key:providerId:modelId:配置指纹 | ||
| 170 | + * | ||
| 171 | + * 带上配置指纹:配置变化后新请求自然命中新 key;并发下按旧快照构建的实例只会写入旧 key,不会再被读到 | ||
| 172 | + * | ||
| 173 | + * @param model 模型运行时快照 | ||
| 174 | + * @return 缓存 key | ||
| 175 | + */ | ||
| 176 | + private static String buildCacheKey(Ai1ModelRespBO model) { | ||
| 177 | + return model.getProviderId() + KEY_SEPARATOR + model.getModelId() + KEY_SEPARATOR | ||
| 178 | + + Objects.hash(model.getBaseUrl(), model.getApiKey(), model.getModel(), model.getModelType(), model.getHeaders()); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + /** | ||
| 182 | + * 归一化接口地址:去掉结尾斜杠;裸地址(无路径)自动补 /v1, | ||
| 183 | + * 保证连通测试、模型拉取与实际会话访问同一个 OpenAI 兼容端点 | ||
| 184 | + * | ||
| 185 | + * 例如说:Ollama 的 <a href="http://127.0.0.1:11434">http://127.0.0.1:11434</a> | ||
| 186 | + * 归一化为 <a href="http://127.0.0.1:11434/v1">http://127.0.0.1:11434/v1</a> | ||
| 187 | + * | ||
| 188 | + * @param baseUrl 接口地址 | ||
| 189 | + * @return 归一化后的地址 | ||
| 190 | + */ | ||
| 191 | + public static String normalizeBaseUrl(String baseUrl) { | ||
| 192 | + String url = StrUtil.removeSuffix(StrUtil.trim(baseUrl), "/"); | ||
| 193 | + try { | ||
| 194 | + String path = URI.create(url).getPath(); | ||
| 195 | + if (StrUtil.isBlank(path) || "/".equals(path)) { | ||
| 196 | + url = url + "/v1"; | ||
| 197 | + } | ||
| 198 | + } catch (Exception e) { | ||
| 199 | + // 非法 URL 按原值使用,交由后续请求报错 | ||
| 200 | + log.warn("[normalizeBaseUrl][接口地址({}) 解析失败,按原值使用]", url, e); | ||
| 201 | + } | ||
| 202 | + return url; | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + /** | ||
| 206 | + * 请求 Header 是否使用 {session} 占位符 | ||
| 207 | + */ | ||
| 208 | + private static boolean usesSessionHeader(Map<String, String> headers) { | ||
| 209 | + return headers != null && headers.values().stream().anyMatch(value -> StrUtil.contains(value, SESSION_PLACEHOLDER)); | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + /** | ||
| 213 | + * 构建请求 Header:{session} 占位符替换为会话编号;会话编号为空时,跳过带占位符的 Header | ||
| 214 | + */ | ||
| 215 | + private static Map<String, String> buildHeaders(Map<String, String> headers, Long sessionId) { | ||
| 216 | + Map<String, String> result = new LinkedHashMap<>(); | ||
| 217 | + if (headers == null || headers.isEmpty()) { | ||
| 218 | + return result; | ||
| 219 | + } | ||
| 220 | + for (Map.Entry<String, String> header : headers.entrySet()) { | ||
| 221 | + String key = header.getKey(); | ||
| 222 | + String value = header.getValue(); | ||
| 223 | + if (StrUtil.isBlank(key)) { | ||
| 224 | + continue; | ||
| 225 | + } | ||
| 226 | + if (StrUtil.contains(value, SESSION_PLACEHOLDER)) { | ||
| 227 | + if (sessionId == null) { | ||
| 228 | + continue; | ||
| 229 | + } | ||
| 230 | + value = value.replace(SESSION_PLACEHOLDER, String.valueOf(sessionId)); | ||
| 231 | + } | ||
| 232 | + result.put(key.trim(), StrUtil.nullToEmpty(value)); | ||
| 233 | + } | ||
| 234 | + return result; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | +} |
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 | +} |