diff --git a/llmhub-base/build.gradle.kts b/llmhub-base/build.gradle.kts index 3936242..747a5d0 100644 --- a/llmhub-base/build.gradle.kts +++ b/llmhub-base/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.kotlin.plugin.spring) alias(libs.plugins.spring.boot) alias(libs.plugins.spring.dependency.management) + alias(libs.plugins.jibLocalPlugin) } group = "org.jcnc" @@ -91,7 +92,7 @@ subprojects { // springCloudBom if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_CLOUD_BOM)) { dependencies { - implementation(libs.springCloudDependencies.bom) + implementation(platform(libs.springCloudDependencies.bom)) } } } @@ -99,6 +100,7 @@ subprojects { apply { plugin(libs.plugins.java.get().pluginId) plugin(libs.plugins.kotlin.jvm.get().pluginId) + plugin(libs.plugins.jibLocalPlugin.get().pluginId) } println(project.name + ":" + project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)) @@ -107,6 +109,13 @@ subprojects { freeCompilerArgs.addAll("-Xjsr305=strict") } } + jibConfig{ + project{ + projectName = "llmhub-core-service" + ports = listOf("9002") + environment = mapOf("SPRING_PROFILES_ACTIVE" to "prod") + } + } } tasks.test { diff --git a/llmhub-base/buildSrc/build.gradle.kts b/llmhub-base/buildSrc/build.gradle.kts index a729ddd..aec6e88 100644 --- a/llmhub-base/buildSrc/build.gradle.kts +++ b/llmhub-base/buildSrc/build.gradle.kts @@ -6,10 +6,17 @@ plugins { alias(libs.plugins.javaGradle.plugin) } dependencies { + // 导入 jib 插件依赖 + implementation(libs.jib.gradlePlugin) } gradlePlugin { plugins { - + register("jib-plugin") { + id = "org.jcnc.llmhub.plugin.jib" + implementationClass = "org.jcnc.llmhub.plugin.jib.JibPlugin" + description = + "提供简单的配置构建镜像" + } } } diff --git a/llmhub-base/buildSrc/src/main/kotlin/org/jcnc/llmhub/plugin/jib/JibPlugin.kt b/llmhub-base/buildSrc/src/main/kotlin/org/jcnc/llmhub/plugin/jib/JibPlugin.kt new file mode 100644 index 0000000..db87bbe --- /dev/null +++ b/llmhub-base/buildSrc/src/main/kotlin/org/jcnc/llmhub/plugin/jib/JibPlugin.kt @@ -0,0 +1,83 @@ +package org.jcnc.llmhub.plugin.jib + + +import org.jcnc.llmhub.plugin.jib.entity.JibProject +import com.google.cloud.tools.jib.gradle.JibExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +/** + *JIB插件 + * + * @since 2025-03-30 12:11:23 + * @author gewuyou + */ +class JibPlugin : Plugin { + override fun apply(project: Project) { + // 创建扩展 + val extension = project.extensions.create("jibConfig", JibPluginExtension::class.java) + project.afterEvaluate { + // 只有匹配的模块才会实际应用JIB插件 + if (extension.projects.any { it.projectName == project.name }) { + project.plugins.apply("com.google.cloud.tools.jib") + // 调用配置逻辑 + project.configureJib(extension.projects) + } + } + } + + // 新增扩展类 + open class JibPluginExtension { + val projects = mutableListOf() + fun project(configure: JibProject.() -> Unit) { + projects.add(JibProject().apply(configure)) + } + } + + /** + *项目扩展 + * + * @since 2025-03-30 01:40:10 + * @author gewuyou + */ + private fun Project.configureJib(jibProjects: List) { + val jibProject = jibProjects.find { it.projectName == project.name } + jibProject?.let { + extensions.configure { + from { + image = jibProject.baseImage + } + to { + image = + "${System.getenv("LUKE_SERVER_DOCKER_REGISTRY_URL")}/${jibProject.imageName}:${jibProject.version}" + auth { + username = "root" + password = System.getenv("LUKE_SERVER_DOCKER_REGISTRY_PASSWORD") + } + } + // 动态配置容器参数 + container { + ports = jibProject.ports + environment = jibProject.environment + if (jibProject.entrypoint.isNotEmpty()) { + entrypoint = jibProject.entrypoint + } + } + + // 动态配置额外目录 + extraDirectories { + setPaths(jibProject.paths) + permissions.putAll(jibProject.permissions) + } + // 将动态部分移到任务配置中 + tasks.named("jib").configure { + doFirst { + // 只有在实际执行jib任务时才会打印日志 + println("jibProject: $jibProject") + } + } + } + } + } +} \ No newline at end of file diff --git a/llmhub-base/buildSrc/src/main/kotlin/org/jcnc/llmhub/plugin/jib/entity/JibProject.kt b/llmhub-base/buildSrc/src/main/kotlin/org/jcnc/llmhub/plugin/jib/entity/JibProject.kt new file mode 100644 index 0000000..a4f7c0d --- /dev/null +++ b/llmhub-base/buildSrc/src/main/kotlin/org/jcnc/llmhub/plugin/jib/entity/JibProject.kt @@ -0,0 +1,33 @@ +package org.jcnc.llmhub.plugin.jib.entity + + +/** + * Jib 项目配置类 + * @author gewuyou + * @date 2025/03/30 + * @constructor 创建[JibProject] + * @param [projectName] 项目名称 + * @param [ports] 监听端口 + * @param [environment] 环境 + * @param [entrypoint] 入口点(用于自定义启动命令) + * @param [imageName] 图像名称 + * @param [version] 版本 + * @param [permissions] 权限 + */ +data class JibProject( + var projectName: String = "", + var ports: List = listOf("8080"), + var environment: Map = mapOf("SPRING_PROFILES_ACTIVE" to "prod"), + var entrypoint: List = emptyList(), + var paths: List = listOf("llmhub-base/scripts/entrypoint.sh"), + var imageName: String = "", + var version: String = "latest", + var permissions: Map = mapOf("/scripts/entrypoint.sh" to "755"), + var baseImage: String = "docker://bellsoft/liberica-openjdk-debian:21" +) { + init { + if (imageName.isEmpty()) { + imageName = projectName + } + } +} diff --git a/llmhub-base/docker/docker-compose.master.yml b/llmhub-base/docker/docker-compose.master.yml new file mode 100644 index 0000000..55d8212 --- /dev/null +++ b/llmhub-base/docker/docker-compose.master.yml @@ -0,0 +1,54 @@ +services: + nacos: + image: nacos/nacos-server:latest + container_name: nacos-server + ports: + - "9001:8848" # Nacos控制台 + - "9848:9848" # gRPC (nacos2.0以后内部使用) + - "9849:9849" # gRPC (nacos2.0以后内部使用) + environment: + - NACOS_AUTH_ENABLE=true + - NACOS_AUTH_IDENTITY_KEY=serverIdentity + - NACOS_AUTH_IDENTITY_VALUE=security + - NACOS_AUTH_TOKEN=L4s6f9y3 + - MODE=standalone + - SPRING_DATASOURCE_PLATFORM=empty + - JVM_XMS=256m + - JVM_XMX=512m + networks: + - llmhub-net + volumes: + - nacos-conf-volume:/home/nacos/conf + - nacos-data-volume:/home/nacos/data + - nacos-logs-volume:/home/nacos/logs + restart: always + + llmhub-core-service: + image: ${49.235.96.75:5000}/llmhub-core-service + container_name: llmhub-core-service + ports: + - "9002:9002" + networks: + - llmhub-net + volumes: + - llmhub-core-service-volume:/app/volume + restart: always + llmhub-impl-baiLian: + image: ${49.235.96.75:5000}/llmhub-impl-baiLian + container_name: llmhub-impl-baiLian + ports: + - "9002:9002" + networks: + - llmhub-net + volumes: + - llmhub-impl-baiLian-volume:/app/volume + restart: always +networks: + llmhub-net: + driver: bridge +volumes: + nacos-conf-volume: + nacos-data-volume: + nacos-logs-volume: + llmhub-core-service-volume: + llmhub-impl-baiLian-volume: \ No newline at end of file diff --git a/llmhub-base/gradle/libs.versions.toml b/llmhub-base/gradle/libs.versions.toml index 7a089a8..3bff539 100644 --- a/llmhub-base/gradle/libs.versions.toml +++ b/llmhub-base/gradle/libs.versions.toml @@ -1,12 +1,13 @@ [versions] kotlin-version = "2.0.0" -spring-cloud-version = "2024.0.1" -spring-cloud-starter-version = "4.2.1" -spring-boot-version = "3.4.4" +spring-cloud-version = "2023.0.5" +spring-boot-version = "3.2.4" spring-dependency-management-version = "1.1.7" aliyun-bailian-version = "2.0.0" spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.3.2" forgeBoot-version = "1.0.0" +okHttp-version = "4.12.0" +jib-version = "3.4.2" [plugins] # 应用 Java 插件,提供基本的 Java 代码编译和构建能力 java = { id = "java" } @@ -24,19 +25,22 @@ spring-dependency-management = { id = "io.spring.dependency-management", version # 应用 Spring Boot 插件,提供 Spring Boot 应用的开发和运行能力 spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-version" } +jib = { id = "com.google.cloud.tools.jib", version.ref = "jib-version" } +jibLocalPlugin = { id = "org.jcnc.llmhub.plugin.jib" } [libraries] +jib-gradlePlugin = { module = "com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin", version.ref = "jib-version" } # bom -springCloudDependencies-bom = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud-version" } +springCloudDependencies-bom = { group ="org.springframework.cloud",name = "spring-cloud-dependencies", version.ref = "spring-cloud-version" } # kotlinx # 响应式协程库 -kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor"} +kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" } # 阿里云百炼 aliyun-bailian = { group = "com.aliyun", name = "bailian20231229", version.ref = "aliyun-bailian-version" } # SrpingCloud springCloudStarter-alibaba-nacos-discovery = { group = "com.alibaba.cloud", name = "spring-cloud-starter-alibaba-nacos-discovery", version.ref = "spring-cloud-starter-alibaba-nacos-discovery-version" } -springCloudStarter-loadbalancer = { group = "org.springframework.cloud", name = "spring-cloud-starter-loadbalancer" ,version.ref="spring-cloud-starter-version"} +springCloudStarter-loadbalancer = { group = "org.springframework.cloud", name = "spring-cloud-starter-loadbalancer" } # SpringBootStarter springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" } @@ -45,7 +49,9 @@ springBootStarter-test = { group = "org.springframework.boot", name = "spring-bo junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } +# OkHttp +okHttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp-version" } # forgeBoot -forgeBoot-webmvc-version-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-webmvc-version-spring-boot-starter",version.ref="forgeBoot-version" } -forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension",version.ref="forgeBoot-version" } +forgeBoot-webmvc-version-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-webmvc-version-spring-boot-starter", version.ref = "forgeBoot-version" } +forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension", version.ref = "forgeBoot-version" } [bundles] diff --git a/llmhub-base/llmhub-core/llmhub-core-service/build.gradle.kts b/llmhub-base/llmhub-core/llmhub-core-service/build.gradle.kts index fd6b045..c64a8e4 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/build.gradle.kts +++ b/llmhub-base/llmhub-core/llmhub-core-service/build.gradle.kts @@ -1,6 +1,7 @@ extra { // 开启springboot setProperty(ProjectFlags.USE_SPRING_BOOT, true) + setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true) } dependencies { val libs = rootProject.libs @@ -11,6 +12,7 @@ dependencies { implementation(libs.springBootStarter.webflux) implementation(libs.springCloudStarter.loadbalancer) + implementation(project(Modules.Core.SPI)) // Kotlin Coroutines diff --git a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/LlmhubCoreServiceApplication.kt b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/LlmhubCoreServiceApplication.kt index ed67a2e..133d495 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/LlmhubCoreServiceApplication.kt +++ b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/LlmhubCoreServiceApplication.kt @@ -1,9 +1,12 @@ package org.jcnc.llmhub.core.service +import org.springframework.boot.autoconfigure.AutoConfiguration import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.cloud.client.discovery.EnableDiscoveryClient @SpringBootApplication +@EnableDiscoveryClient class LlmhubCoreServiceApplication /** diff --git a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/config/ModelProperties.kt b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/config/ModelProperties.kt index c07bfe9..76b2eed 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/config/ModelProperties.kt +++ b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/config/ModelProperties.kt @@ -1,6 +1,7 @@ package org.jcnc.llmhub.core.service.config import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.cloud.context.config.annotation.RefreshScope /** *模型属性 @@ -9,7 +10,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties * @author gewuyou */ @ConfigurationProperties(prefix = "llmhub.model-route") -class ModelProperties { +@RefreshScope +open class ModelProperties { /** * 模型名前缀 -> 服务名映射 * 该映射表存储了模型名前缀与服务名的对应关系,用于快速查找模型对应的服务 diff --git a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/controller/ChatController.kt b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/controller/ChatController.kt index 31cbb5f..31e65ea 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/controller/ChatController.kt +++ b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/controller/ChatController.kt @@ -1,7 +1,11 @@ package org.jcnc.llmhub.core.service.controller import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion +import kotlinx.coroutines.flow.Flow import org.jcnc.llmhub.core.service.service.impl.LLMServiceImpl +import org.jcnc.llmhub.core.spi.entities.request.ChatRequest +import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -17,5 +21,18 @@ import org.springframework.web.bind.annotation.RestController class ChatController( private val llmServiceImpl: LLMServiceImpl ) { - + /** + * 聊天功能入口 + * + * 该函数负责调用语言模型服务实现类中的聊天方法,为用户提供与AI聊天的功能 + * 它接收一个ChatRequest对象作为参数,该对象包含了聊天所需的参数和用户信息 + * 函数返回一个Flow流,流中包含了部分聊天响应,这种设计允许用户逐步接收聊天结果,提高用户体验 + * + * @param request 聊天请求对象,包含了发起聊天所需的各种参数和用户信息 + * @return 返回一个Flow流,流中依次提供了聊天响应的部分数据,允许异步处理和逐步消费响应内容 + */ + @PostMapping("/stream") + fun chat(request: ChatRequest): Flow { + return llmServiceImpl.chat(request) + } } \ No newline at end of file diff --git a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/manager/ModelRouteManager.kt b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/manager/ModelRouteManager.kt index 17dcf5b..530d965 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/manager/ModelRouteManager.kt +++ b/llmhub-base/llmhub-core/llmhub-core-service/src/main/kotlin/org/jcnc/llmhub/core/service/manager/ModelRouteManager.kt @@ -1,7 +1,6 @@ package org.jcnc.llmhub.core.service.manager import org.jcnc.llmhub.core.service.config.ModelProperties -import org.springframework.cloud.context.config.annotation.RefreshScope import org.springframework.stereotype.Component /** @@ -14,7 +13,6 @@ import org.springframework.stereotype.Component * @author gewuyou */ @Component -@RefreshScope // 支持Nacos热刷新,使得配置更新时无需重启应用即可生效 class ModelRouteManager( private val modelProperties: ModelProperties ) { diff --git a/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application-dev.yml b/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application-dev.yml index a16b1e1..4c5ecd0 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application-dev.yml +++ b/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application-dev.yml @@ -3,11 +3,20 @@ server: spring: cloud: nacos: + access-key: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc0NTc1NjkwOH0.86TmeN27gXbpw55jMmOVNz42B9u8dXtGwCvyGlWbYVo + username: nacos + password: L4s6f9y3 + server-addr: 49.235.96.75:8848 discovery: - server-addr: 49.235.96.75:8848 # Nacos 服务地址 + server-addr: ${spring.cloud.nacos.server-addr} + access-key: ${spring.cloud.nacos.access-key} + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} llmhub: model-route: modelServiceMap: - openai: llmhub-impl-openai - anotherModel: llmhub-impl-another + qwen-turbo: llmhub-impl-baiLian + qwen-max: llmhub-impl-baiLian + qwen-plus: llmhub-impl-baiLian + diff --git a/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application.yml b/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application.yml index 22cad4a..da53d98 100644 --- a/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application.yml +++ b/llmhub-base/llmhub-core/llmhub-core-service/src/main/resources/application.yml @@ -1,5 +1,25 @@ spring: application: name: llmhub-core-service - profiles: - active: dev \ No newline at end of file + cloud: + nacos: + access-key: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc0NTc1NjkwOH0.86TmeN27gXbpw55jMmOVNz42B9u8dXtGwCvyGlWbYVo + username: nacos + password: L4s6f9y3 + server-addr: 49.235.96.75:8848 + discovery: + server-addr: ${spring.cloud.nacos.server-addr} + access-key: ${spring.cloud.nacos.access-key} + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} +llmhub: + model-route: + modelServiceMap: + qwen-turbo: llmhub-impl-baiLian + qwen-max: llmhub-impl-baiLian + qwen-plus: llmhub-impl-baiLian + + # profiles: + # active: dev +server: + port: 8081 \ No newline at end of file diff --git a/llmhub-base/llmhub-core/llmhub-core-spi/build.gradle.kts b/llmhub-base/llmhub-core/llmhub-core-spi/build.gradle.kts index 540aa86..923d5d7 100644 --- a/llmhub-base/llmhub-core/llmhub-core-spi/build.gradle.kts +++ b/llmhub-base/llmhub-core/llmhub-core-spi/build.gradle.kts @@ -4,7 +4,6 @@ apply { plugin(libs.plugins.kotlin.plugin.spring.get().pluginId) } dependencies { - val libs = rootProject.libs compileOnly(libs.kotlinx.coruntes.reactor) compileOnly(libs.springBootStarter.web) } \ No newline at end of file diff --git a/llmhub-base/llmhub-core/llmhub-core-spi/src/main/kotlin/org/jcnc/llmhub/core/spi/entities/request/ChatRequest.kt b/llmhub-base/llmhub-core/llmhub-core-spi/src/main/kotlin/org/jcnc/llmhub/core/spi/entities/request/ChatRequest.kt index 55fc319..9a022d9 100644 --- a/llmhub-base/llmhub-core/llmhub-core-spi/src/main/kotlin/org/jcnc/llmhub/core/spi/entities/request/ChatRequest.kt +++ b/llmhub-base/llmhub-core/llmhub-core-spi/src/main/kotlin/org/jcnc/llmhub/core/spi/entities/request/ChatRequest.kt @@ -13,5 +13,5 @@ package org.jcnc.llmhub.core.spi.entities.request data class ChatRequest( val prompt: String, val model: String, - val options: Map? = null + val options: Map = mapOf() ) diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/build.gradle.kts b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/build.gradle.kts index e8b7647..c4a9aa4 100644 --- a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/build.gradle.kts +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/build.gradle.kts @@ -1,7 +1,19 @@ // 开启springboot extra[ProjectFlags.USE_SPRING_BOOT] = true +setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true) dependencies { + // Nacos 服务发现和配置 + implementation(libs.springCloudStarter.alibaba.nacos.discovery) + implementation(project(Modules.Core.SPI)) + + implementation(libs.kotlinx.coruntes.reactor) + + implementation(libs.aliyun.bailian) + + implementation(libs.okHttp) + + implementation(libs.forgeBoot.core.extension) } diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/LlmhubImplBailianApplication.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/LlmhubImplBailianApplication.kt index 521bd95..55b78de 100644 --- a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/LlmhubImplBailianApplication.kt +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/LlmhubImplBailianApplication.kt @@ -2,9 +2,11 @@ package org.jcnc.llmhub.impl.baiLian import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.cloud.client.discovery.EnableDiscoveryClient @SpringBootApplication +@EnableDiscoveryClient class LlmhubImplBaiLianApplication -fun main(args: Array) { + fun main(args: Array) { runApplication(*args) } diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/adapter/DashScopeAdapter.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/adapter/DashScopeAdapter.kt new file mode 100644 index 0000000..682f027 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/adapter/DashScopeAdapter.kt @@ -0,0 +1,107 @@ +package org.jcnc.llmhub.impl.baiLian.adapter + +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import okhttp3.Headers.Companion.toHeaders +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart +import org.springframework.stereotype.Component + +/** + * 百炼适配器 + * + * 该类负责与百炼API进行交互,提供流式聊天功能。 + * 它使用OkHttpClient发送请求,并通过Jackson库处理JSON数据。 + * + * @param okHttpClient 用于发送HTTP请求的客户端 + * @param objectMapper 用于序列化和反序列化JSON数据的映射器 + * @since 2025-04-27 10:31:47 + * @author gewuyou + */ +@Component +class DashScopeAdapter( + private val okHttpClient: OkHttpClient, + private val objectMapper: ObjectMapper +) { + /** + * 发送流式聊天请求 + * + * 本函数构建并发送一个聊天请求,然后以流的形式接收和处理响应。 + * 它主要用于与DashScope API进行交互,提取并发布聊天响应的部分内容。 + * + * @param url 请求的URL + * @param headers 请求的头部信息 + * @param requestBody 请求的主体内容 + * @param extractContent 一个函数,用于从JSON响应中提取内容 + * @param dispatcher 协程调度器,默认为IO调度器 + * @return 返回一个Flow,发布聊天响应的部分内容 + */ + fun sendStreamChat( + url: String, + headers: Map, + requestBody: Any, + extractContent: (String) -> String, + dispatcher: CoroutineDispatcher = Dispatchers.IO + ): Flow = flow { + // 将请求体序列化为JSON格式 + val requestJson = objectMapper.writeValueAsString(requestBody) + // 构建HTTP请求 + val request = Request.Builder() + .url(url) + .headers(headers.toHeaders()) + .post(requestJson.toRequestBody("application/json".toMediaType())) + .build() + + // 发送HTTP请求 + val call = okHttpClient.newCall(request) + + // 使用指定的调度器执行请求 + val response = withContext(dispatcher) { + call.execute() + } + + // 检查HTTP响应是否成功 + if (!response.isSuccessful) { + throw RuntimeException("DashScope request failed: ${response.code}") + } + + // 获取响应体 + val responseBody = response.body ?: throw RuntimeException("Empty response body from DashScope") + // 获取响应体的源 + val source = responseBody.source().buffer + + try { + // 读取响应体,直到数据结束或协程被取消 + while (!source.exhausted() && currentCoroutineContext().isActive) { + // 读取一行数据 + val line = source.readUtf8Line() + // 忽略空行 + if (line.isNullOrBlank()) { + continue + } + // 处理以"data:"开头的行 + if (line.startsWith("data:")) { + // 提取JSON部分 + val jsonPart = line.removePrefix("data:").trim() + // 提取内容 + val content = extractContent(jsonPart) + + // 发布聊天响应的部分内容 + emit(ChatResponsePart(content = content, done = false)) + } + } + + // 发布结束信号 + emit(ChatResponsePart(content = "[END]", done = true)) + } finally { + // 关闭资源 + source.close() + response.close() + } + } +} diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/ClientConfig.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/ClientConfig.kt new file mode 100644 index 0000000..fae2c74 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/ClientConfig.kt @@ -0,0 +1,28 @@ +package org.jcnc.llmhub.impl.baiLian.config + +import okhttp3.OkHttpClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.util.concurrent.TimeUnit + + +/** + *客户端配置 + * + * @since 2025-03-28 17:03:48 + * @author gewuyou + */ +@Configuration +class ClientConfig { + /** + * OkHttpClient + */ + @Bean + fun createOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } +} \ No newline at end of file diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/DashScopeConfig.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/DashScopeConfig.kt new file mode 100644 index 0000000..5462c71 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/DashScopeConfig.kt @@ -0,0 +1,15 @@ +package org.jcnc.llmhub.impl.baiLian.config + +import org.jcnc.llmhub.impl.baiLian.config.entities.DashScopeProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Configuration + +/** + *仪表范围配置 + * + * @since 2025-04-27 10:53:55 + * @author gewuyou + */ +@Configuration +@EnableConfigurationProperties(DashScopeProperties::class) +class DashScopeConfig \ No newline at end of file diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/entities/DashScopeProperties.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/entities/DashScopeProperties.kt new file mode 100644 index 0000000..7c01870 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/config/entities/DashScopeProperties.kt @@ -0,0 +1,48 @@ +package org.jcnc.llmhub.impl.baiLian.config.entities + +import org.springframework.boot.context.properties.ConfigurationProperties + +/** + * 仪表范围属性 + * + * @author gewuyou + * @since 2025-03-08 15:39:34 + */ +@ConfigurationProperties(prefix = "aliyun.dash.scope") +class DashScopeProperties { + /** + * 访问密钥ID,用于身份认证。 + */ + var accessKeyId: String = "" + + /** + * 访问密钥,与访问密钥ID配合使用完成身份认证。 + */ + var accessKeySecret: String = "" + + /** + * 服务访问端点,例如 https://dash.aliyun.com。 + */ + var endpoint: String = "" + + /** + * 工作空间ID,标识具体的业务工作空间。 + */ + var workspaceId: String = "" + + /** + * API密钥,用于调用API的身份验证。 + */ + var apiKey: String = "" + + /** + * 应用ID,标识具体的应用程序。 + */ + var appId: String = "" + + /** + * 应用调用基路径,例如 /api/v1。 + */ + var baseUrl: String = "" + +} diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/controller/BaiLianProvider.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/controller/BaiLianProvider.kt new file mode 100644 index 0000000..f3c622b --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/controller/BaiLianProvider.kt @@ -0,0 +1,45 @@ +package org.jcnc.llmhub.impl.baiLian.controller + +import kotlinx.coroutines.flow.Flow +import org.jcnc.llmhub.core.spi.entities.request.ChatRequest +import org.jcnc.llmhub.core.spi.entities.request.EmbeddingRequest +import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart +import org.jcnc.llmhub.core.spi.entities.response.EmbeddingResponse +import org.jcnc.llmhub.core.spi.provider.LLMProvider +import org.jcnc.llmhub.impl.baiLian.service.BaiLianModelService +import org.springframework.web.bind.annotation.RestController + +/** + *百炼提供商 + * + * @since 2025-04-27 10:12:12 + * @author gewuyou + */ +@RestController +class BaiLianProvider( + private val baiLianModelService: BaiLianModelService +): LLMProvider { + /** + * 初始化与聊天服务的连接,以处理聊天请求 + * + * 此函数接收一个聊天请求对象,并返回一个Flow流,用于接收聊天响应的部分数据 + * 它主要用于建立聊天通信的通道,而不是发送具体的消息 + * + * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 + * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 + */ + override fun chat(request: ChatRequest): Flow { + return baiLianModelService.streamChat(request) + } + + /** + * 嵌入功能方法 + * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 + * + * @param request 嵌入请求对象,包含需要进行嵌入处理的数据 + * @return EmbeddingResponse 嵌入响应对象,包含生成的嵌入向量信息 + */ + override fun embedding(request: EmbeddingRequest): EmbeddingResponse { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/service/BaiLianModelService.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/service/BaiLianModelService.kt new file mode 100644 index 0000000..4691834 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/service/BaiLianModelService.kt @@ -0,0 +1,22 @@ +package org.jcnc.llmhub.impl.baiLian.service + +import kotlinx.coroutines.flow.Flow +import org.jcnc.llmhub.core.spi.entities.request.ChatRequest +import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart + +/** + * 百炼模型服务接口 + * 用于定义与百炼AI模型交互的方法 + * + * @since 2025-04-27 10:45:42 + * @author gewuyou + */ +interface BaiLianModelService { + /** + * 使用流式聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 + */ + fun streamChat(request: ChatRequest): Flow +} diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt new file mode 100644 index 0000000..dd6f459 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/kotlin/org/jcnc/llmhub/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt @@ -0,0 +1,91 @@ +package org.jcnc.llmhub.impl.baiLian.service.impl + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.flow.Flow +import org.jcnc.llmhub.core.spi.entities.request.ChatRequest +import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart +import org.jcnc.llmhub.impl.baiLian.adapter.DashScopeAdapter +import org.jcnc.llmhub.impl.baiLian.config.entities.DashScopeProperties +import org.jcnc.llmhub.impl.baiLian.service.BaiLianModelService +import org.springframework.stereotype.Service +import org.springframework.util.CollectionUtils +import org.springframework.util.StringUtils +/** + * 百炼模型服务接口实现 + * + * @since 2025-04-27 10:47:07 + * @author gewuyou + */ +@Service +class BaiLianModelServiceImpl( + private val dashScopeAdapter: DashScopeAdapter, + private val dashScopeProperties: DashScopeProperties, + private val objectMapper: ObjectMapper +) : BaiLianModelService { + /** + * 使用流式聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 + */ + override fun streamChat(request: ChatRequest): Flow { + // 构造请求URL + val url = "${dashScopeProperties.baseUrl}${dashScopeProperties.appId}/completion" + // 构造请求头,包括授权信息和内容类型 + val headers = mapOf( + "Authorization" to "Bearer ${dashScopeProperties.apiKey}", + "Content-Type" to "application/json", + "X-DashScope-SSE" to "enable" + ) + // 构造输入参数,主要包括用户的prompt + val inputMap = mutableMapOf("prompt" to request.prompt) + // 获取会话ID,如果存在,则添加到输入参数中 + val sessionId = (request.options["session_id"] ?: "").toString() + if (StringUtils.hasText(sessionId)) { + inputMap["session_id"] = sessionId + } + // 构造参数地图,包括是否包含思考、模型ID、增量输出等 + val parametersMap = mutableMapOf( + "has_thoughts" to (request.options["has_thoughts"] ?: false), + "model_id" to request.model, + "incremental_output" to true, + "rag_options" to emptyMap() + ) + // 构造RAG选项参数,包括管道ID和文件ID + val ragOptionsMap = mutableMapOf() + // 解析并添加管道ID + val pipelineIdsJson = request.options["pipelineIds"] + pipelineIdsJson?.let { + val pipelineIds = objectMapper.readValue(it, object : TypeReference>() {}) + if (!CollectionUtils.isEmpty(pipelineIds)) { + ragOptionsMap["pipeline_ids"] = pipelineIds + } + } + // 解析并添加文件ID + val fileIdsJson = request.options["fileIds"] + fileIdsJson?.let { + val fileIds = objectMapper.readValue(it, object : TypeReference>() {}) + if (!CollectionUtils.isEmpty(fileIds)) { + ragOptionsMap["session_file_ids"] = fileIds + } + } + // 将RAG选项参数添加到参数地图中 + parametersMap["rag_options"] = ragOptionsMap + // 构造请求体,包括输入参数、构造的参数地图和调试信息 + val body = mapOf( + "input" to inputMap, + "parameters" to parametersMap, + "debug" to emptyMap() + ) + // 发送流式聊天请求,并处理响应 + return dashScopeAdapter.sendStreamChat( + url, headers, body, + { json: String -> + // 解析响应JSON,提取输出文本 + val node = objectMapper.readTree(json) + node["output"]?.get("text")?.asText() ?: "" + } + ) + } +} diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application-dev.yml b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application-dev.yml new file mode 100644 index 0000000..c964936 --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application-dev.yml @@ -0,0 +1,22 @@ +server: + port: 8082 +spring: + application: + name: llmhub-impl-baiLian + cloud: + nacos: + discovery: + server-addr: 49.235.96.75:8848 # Nacos 服务地址 +# 阿里云配置 +aliyun: + # DashScope服务配置 + dash: + # 访问凭证配置 + scope: + access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID + access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥 + endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点 + workspace-id: llm-axfkuqft05uzbjpi # 工作区ID + api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥 + app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID + base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application-prod.yml b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application-prod.yml new file mode 100644 index 0000000..b39b48c --- /dev/null +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application-prod.yml @@ -0,0 +1,22 @@ +server: + port: 9003 +spring: + application: + name: llmhub-impl-baiLian + cloud: + nacos: + discovery: + server-addr: 49.235.96.75:9001 # Nacos 服务地址 +# 阿里云配置 +aliyun: + # DashScope服务配置 + dash: + # 访问凭证配置 + scope: + access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID + access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥 + endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点 + workspace-id: llm-axfkuqft05uzbjpi # 工作区ID + api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥 + app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID + base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL diff --git a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application.yml b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application.yml index 83f9112..b9fc61b 100644 --- a/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application.yml +++ b/llmhub-base/llmhub-impl/llmhub-impl-baiLian/src/main/resources/application.yml @@ -1,3 +1,18 @@ spring: application: name: llmhub-impl-baiLian + profiles: + active: dev +# 阿里云配置 +aliyun: + # DashScope服务配置 + dash: + # 访问凭证配置 + scope: + access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID + access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥 + endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点 + workspace-id: llm-axfkuqft05uzbjpi # 工作区ID + api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥 + app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID + base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL \ No newline at end of file diff --git a/llmhub-base/scripts/entrypoint.sh b/llmhub-base/scripts/entrypoint.sh new file mode 100644 index 0000000..0378a7e --- /dev/null +++ b/llmhub-base/scripts/entrypoint.sh @@ -0,0 +1,99 @@ +#!/bin/bash +#set -x # 调试模式,可以启用以打印每行脚本的执行情况 + +# 设置默认的等待时间间隔,默认值为2秒 +: "${SLEEP_SECOND:=2}" +# 设置默认的超时时间,默认值为60秒 +: "${TIMEOUT:=60}" + +# 带时间戳+emoji的小型日志函数 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +# 等待服务函数 +wait_for() { + local host="$1" + local port="$2" + local timeout="$TIMEOUT" + local start_time + local current_time + local elapsed_time + local attempt=0 + + start_time=$(date +%s) + log "🔄 开始等待依赖服务 $host:$port 可用 (超时时间: ${timeout}s, 间隔: ${SLEEP_SECOND}s)" + + while ! nc -z "$host" "$port" 2>/dev/null; do + current_time=$(date +%s) + elapsed_time=$((current_time - start_time)) + attempt=$((attempt + 1)) + + if [ "$elapsed_time" -ge "$timeout" ]; then + log "🛑 [ERROR] 等待超时!依赖服务 $host:$port 在 ${timeout}s 内未启动" + return 1 + fi + + log "🔄 第 ${attempt} 次检测:$host:$port 未就绪,已等待 ${elapsed_time}s..." + sleep "$SLEEP_SECOND" + done + + total_time=$(( $(date +%s) - start_time )) + log "✅ [SUCCESS] 依赖服务 $host:$port 已启动,耗时 ${total_time}s" + return 0 +} + +# 声明变量 +declare DEPENDS +declare CMD + +# 解析参数 +while getopts "d:c:" arg; do + case "$arg" in + d) + DEPENDS="$OPTARG" + ;; + c) + CMD="$OPTARG" + ;; + ?) + log "🛑 [ERROR] 未知参数" + exit 1 + ;; + esac +done + +# 检查依赖 +if [ -n "$DEPENDS" ]; then + log "📦 检测到依赖服务列表: $DEPENDS" + for var in ${DEPENDS//,/ }; do + host=${var%:*} + port=${var#*:} + if [ -z "$host" ] || [ -z "$port" ]; then + log "🛑 [ERROR] 依赖项格式错误: $var,应为 host:port" + exit 1 + fi + + if ! wait_for "$host" "$port"; then + log "❌ [ERROR] 依赖服务 $host:$port 启动失败,终止执行" + exit 1 + fi + done +else + log "⚡️ 未配置依赖服务,跳过依赖检测" +fi + +# 执行命令 +if [ -n "$CMD" ]; then + log "🚀 准备执行命令: $CMD" + eval "$CMD" + cmd_exit_code=$? + if [ $cmd_exit_code -eq 0 ]; then + log "✅ [SUCCESS] 命令执行完成,退出码: $cmd_exit_code" + else + log "❌ [ERROR] 命令执行失败,退出码: $cmd_exit_code" + exit $cmd_exit_code + fi +else + log "⚠️ [WARNING] 未指定要执行的命令,脚本结束" +fi