feat(impl): 添加百炼模型服务实现

- 新增 BaiLianModelService 接口及其实现类 BaiLianModelServiceImpl- 添加与阿里云 DashScope API 交互的适配器 DashScopeAdapter
- 新增百炼提供商 BaiLianProvider
- 更新 ChatController 以支持流式聊天- 添加必要的配置类和属性文件
This commit is contained in:
gewuyou 2025-04-27 17:08:12 +08:00
parent 3c9796524f
commit 05a2a78dfd
28 changed files with 792 additions and 22 deletions

View File

@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.plugin.spring) alias(libs.plugins.kotlin.plugin.spring)
alias(libs.plugins.spring.boot) alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management) alias(libs.plugins.spring.dependency.management)
alias(libs.plugins.jibLocalPlugin)
} }
group = "org.jcnc" group = "org.jcnc"
@ -91,7 +92,7 @@ subprojects {
// springCloudBom // springCloudBom
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_CLOUD_BOM)) { if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_CLOUD_BOM)) {
dependencies { dependencies {
implementation(libs.springCloudDependencies.bom) implementation(platform(libs.springCloudDependencies.bom))
} }
} }
} }
@ -99,6 +100,7 @@ subprojects {
apply { apply {
plugin(libs.plugins.java.get().pluginId) plugin(libs.plugins.java.get().pluginId)
plugin(libs.plugins.kotlin.jvm.get().pluginId) plugin(libs.plugins.kotlin.jvm.get().pluginId)
plugin(libs.plugins.jibLocalPlugin.get().pluginId)
} }
println(project.name + ":" + project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)) println(project.name + ":" + project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT))
@ -107,6 +109,13 @@ subprojects {
freeCompilerArgs.addAll("-Xjsr305=strict") freeCompilerArgs.addAll("-Xjsr305=strict")
} }
} }
jibConfig{
project{
projectName = "llmhub-core-service"
ports = listOf("9002")
environment = mapOf("SPRING_PROFILES_ACTIVE" to "prod")
}
}
} }
tasks.test { tasks.test {

View File

@ -6,10 +6,17 @@ plugins {
alias(libs.plugins.javaGradle.plugin) alias(libs.plugins.javaGradle.plugin)
} }
dependencies { dependencies {
// 导入 jib 插件依赖
implementation(libs.jib.gradlePlugin)
} }
gradlePlugin { gradlePlugin {
plugins { plugins {
register("jib-plugin") {
id = "org.jcnc.llmhub.plugin.jib"
implementationClass = "org.jcnc.llmhub.plugin.jib.JibPlugin"
description =
"提供简单的配置构建镜像"
}
} }
} }

View File

@ -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<Project> {
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<JibProject>()
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<JibProject>) {
val jibProject = jibProjects.find { it.projectName == project.name }
jibProject?.let {
extensions.configure<JibExtension> {
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")
}
}
}
}
}
}

View File

@ -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<String> = listOf("8080"),
var environment: Map<String, String> = mapOf("SPRING_PROFILES_ACTIVE" to "prod"),
var entrypoint: List<String> = emptyList(),
var paths: List<String> = listOf("llmhub-base/scripts/entrypoint.sh"),
var imageName: String = "",
var version: String = "latest",
var permissions: Map<String, String> = mapOf("/scripts/entrypoint.sh" to "755"),
var baseImage: String = "docker://bellsoft/liberica-openjdk-debian:21"
) {
init {
if (imageName.isEmpty()) {
imageName = projectName
}
}
}

View File

@ -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:

View File

@ -1,12 +1,13 @@
[versions] [versions]
kotlin-version = "2.0.0" kotlin-version = "2.0.0"
spring-cloud-version = "2024.0.1" spring-cloud-version = "2023.0.5"
spring-cloud-starter-version = "4.2.1" spring-boot-version = "3.2.4"
spring-boot-version = "3.4.4"
spring-dependency-management-version = "1.1.7" spring-dependency-management-version = "1.1.7"
aliyun-bailian-version = "2.0.0" aliyun-bailian-version = "2.0.0"
spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.3.2" spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.3.2"
forgeBoot-version = "1.0.0" forgeBoot-version = "1.0.0"
okHttp-version = "4.12.0"
jib-version = "3.4.2"
[plugins] [plugins]
# 应用 Java 插件,提供基本的 Java 代码编译和构建能力 # 应用 Java 插件,提供基本的 Java 代码编译和构建能力
java = { id = "java" } java = { id = "java" }
@ -24,9 +25,12 @@ spring-dependency-management = { id = "io.spring.dependency-management", version
# 应用 Spring Boot 插件,提供 Spring Boot 应用的开发和运行能力 # 应用 Spring Boot 插件,提供 Spring Boot 应用的开发和运行能力
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-version" } 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] [libraries]
jib-gradlePlugin = { module = "com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin", version.ref = "jib-version" }
# bom # 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
# 响应式协程库 # 响应式协程库
kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" } kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" }
@ -36,7 +40,7 @@ aliyun-bailian = { group = "com.aliyun", name = "bailian20231229", version.ref =
# SrpingCloud # 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-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
springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" } springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
@ -45,6 +49,8 @@ springBootStarter-test = { group = "org.springframework.boot", name = "spring-bo
junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
# OkHttp
okHttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp-version" }
# forgeBoot # forgeBoot
forgeBoot-webmvc-version-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-webmvc-version-spring-boot-starter", 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" } forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension", version.ref = "forgeBoot-version" }

View File

@ -1,6 +1,7 @@
extra { extra {
// 开启springboot // 开启springboot
setProperty(ProjectFlags.USE_SPRING_BOOT, true) setProperty(ProjectFlags.USE_SPRING_BOOT, true)
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
} }
dependencies { dependencies {
val libs = rootProject.libs val libs = rootProject.libs
@ -11,6 +12,7 @@ dependencies {
implementation(libs.springBootStarter.webflux) implementation(libs.springBootStarter.webflux)
implementation(libs.springCloudStarter.loadbalancer) implementation(libs.springCloudStarter.loadbalancer)
implementation(project(Modules.Core.SPI)) implementation(project(Modules.Core.SPI))
// Kotlin Coroutines // Kotlin Coroutines

View File

@ -1,9 +1,12 @@
package org.jcnc.llmhub.core.service package org.jcnc.llmhub.core.service
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication @SpringBootApplication
@EnableDiscoveryClient
class LlmhubCoreServiceApplication class LlmhubCoreServiceApplication
/** /**

View File

@ -1,6 +1,7 @@
package org.jcnc.llmhub.core.service.config package org.jcnc.llmhub.core.service.config
import org.springframework.boot.context.properties.ConfigurationProperties 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 * @author gewuyou
*/ */
@ConfigurationProperties(prefix = "llmhub.model-route") @ConfigurationProperties(prefix = "llmhub.model-route")
class ModelProperties { @RefreshScope
open class ModelProperties {
/** /**
* 模型名前缀 -> 服务名映射 * 模型名前缀 -> 服务名映射
* 该映射表存储了模型名前缀与服务名的对应关系用于快速查找模型对应的服务 * 该映射表存储了模型名前缀与服务名的对应关系用于快速查找模型对应的服务

View File

@ -1,7 +1,11 @@
package org.jcnc.llmhub.core.service.controller package org.jcnc.llmhub.core.service.controller
import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion 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.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.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@ -17,5 +21,18 @@ import org.springframework.web.bind.annotation.RestController
class ChatController( class ChatController(
private val llmServiceImpl: LLMServiceImpl private val llmServiceImpl: LLMServiceImpl
) { ) {
/**
* 聊天功能入口
*
* 该函数负责调用语言模型服务实现类中的聊天方法为用户提供与AI聊天的功能
* 它接收一个ChatRequest对象作为参数该对象包含了聊天所需的参数和用户信息
* 函数返回一个Flow流流中包含了部分聊天响应这种设计允许用户逐步接收聊天结果提高用户体验
*
* @param request 聊天请求对象包含了发起聊天所需的各种参数和用户信息
* @return 返回一个Flow流流中依次提供了聊天响应的部分数据允许异步处理和逐步消费响应内容
*/
@PostMapping("/stream")
fun chat(request: ChatRequest): Flow<ChatResponsePart> {
return llmServiceImpl.chat(request)
}
} }

View File

@ -1,7 +1,6 @@
package org.jcnc.llmhub.core.service.manager package org.jcnc.llmhub.core.service.manager
import org.jcnc.llmhub.core.service.config.ModelProperties import org.jcnc.llmhub.core.service.config.ModelProperties
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
/** /**
@ -14,7 +13,6 @@ import org.springframework.stereotype.Component
* @author gewuyou * @author gewuyou
*/ */
@Component @Component
@RefreshScope // 支持Nacos热刷新使得配置更新时无需重启应用即可生效
class ModelRouteManager( class ModelRouteManager(
private val modelProperties: ModelProperties private val modelProperties: ModelProperties
) { ) {

View File

@ -3,11 +3,20 @@ server:
spring: spring:
cloud: cloud:
nacos: nacos:
access-key: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTc0NTc1NjkwOH0.86TmeN27gXbpw55jMmOVNz42B9u8dXtGwCvyGlWbYVo
username: nacos
password: L4s6f9y3
server-addr: 49.235.96.75:8848
discovery: 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: llmhub:
model-route: model-route:
modelServiceMap: modelServiceMap:
openai: llmhub-impl-openai qwen-turbo: llmhub-impl-baiLian
anotherModel: llmhub-impl-another qwen-max: llmhub-impl-baiLian
qwen-plus: llmhub-impl-baiLian

View File

@ -1,5 +1,25 @@
spring: spring:
application: application:
name: llmhub-core-service name: llmhub-core-service
profiles: cloud:
active: dev 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

View File

@ -4,7 +4,6 @@ apply {
plugin(libs.plugins.kotlin.plugin.spring.get().pluginId) plugin(libs.plugins.kotlin.plugin.spring.get().pluginId)
} }
dependencies { dependencies {
val libs = rootProject.libs
compileOnly(libs.kotlinx.coruntes.reactor) compileOnly(libs.kotlinx.coruntes.reactor)
compileOnly(libs.springBootStarter.web) compileOnly(libs.springBootStarter.web)
} }

View File

@ -13,5 +13,5 @@ package org.jcnc.llmhub.core.spi.entities.request
data class ChatRequest( data class ChatRequest(
val prompt: String, val prompt: String,
val model: String, val model: String,
val options: Map<String, Any>? = null val options: Map<String, String> = mapOf()
) )

View File

@ -1,7 +1,19 @@
// 开启springboot // 开启springboot
extra[ProjectFlags.USE_SPRING_BOOT] = true extra[ProjectFlags.USE_SPRING_BOOT] = true
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
dependencies { dependencies {
// Nacos 服务发现和配置
implementation(libs.springCloudStarter.alibaba.nacos.discovery)
implementation(project(Modules.Core.SPI)) implementation(project(Modules.Core.SPI))
implementation(libs.kotlinx.coruntes.reactor)
implementation(libs.aliyun.bailian)
implementation(libs.okHttp)
implementation(libs.forgeBoot.core.extension)
} }

View File

@ -2,8 +2,10 @@ package org.jcnc.llmhub.impl.baiLian
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication @SpringBootApplication
@EnableDiscoveryClient
class LlmhubImplBaiLianApplication class LlmhubImplBaiLianApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {
runApplication<LlmhubImplBaiLianApplication>(*args) runApplication<LlmhubImplBaiLianApplication>(*args)

View File

@ -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<String, String>,
requestBody: Any,
extractContent: (String) -> String,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Flow<ChatResponsePart> = 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()
}
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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 = ""
}

View File

@ -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<ChatResponsePart> {
return baiLianModelService.streamChat(request)
}
/**
* 嵌入功能方法
* 该方法允许用户发送嵌入请求以获取LLM生成的嵌入向量
*
* @param request 嵌入请求对象包含需要进行嵌入处理的数据
* @return EmbeddingResponse 嵌入响应对象包含生成的嵌入向量信息
*/
override fun embedding(request: EmbeddingRequest): EmbeddingResponse {
TODO("Not yet implemented")
}
}

View File

@ -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<ChatResponsePart>
}

View File

@ -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<ChatResponsePart> {
// 构造请求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<String, Any>()
)
// 构造RAG选项参数包括管道ID和文件ID
val ragOptionsMap = mutableMapOf<String, Any>()
// 解析并添加管道ID
val pipelineIdsJson = request.options["pipelineIds"]
pipelineIdsJson?.let {
val pipelineIds = objectMapper.readValue(it, object : TypeReference<List<String>>() {})
if (!CollectionUtils.isEmpty(pipelineIds)) {
ragOptionsMap["pipeline_ids"] = pipelineIds
}
}
// 解析并添加文件ID
val fileIdsJson = request.options["fileIds"]
fileIdsJson?.let {
val fileIds = objectMapper.readValue(it, object : TypeReference<List<String>>() {})
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() ?: ""
}
)
}
}

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,18 @@
spring: spring:
application: application:
name: llmhub-impl-baiLian 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

View File

@ -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