refactor(llmhub): 重构项目并添加 OpenAI 实现- 重命名核心服务模块为 llmhub-core-service

- 新增 OpenAI 实现模块 llmhub-impl-openAi
- 更新配置文件和端口设置
- 优化百炼模型服务实现- 调整项目结构和依赖
This commit is contained in:
gewuyou 2025-04-30 20:21:01 +08:00
parent 729a701d9f
commit c945e5920f
26 changed files with 449 additions and 114 deletions

View File

@ -0,0 +1,27 @@
services:
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:
- "9003:9003"
networks:
- llmhub-net
volumes:
- llmhub-impl-baiLian-volume:/app/volume
restart: always
networks:
llmhub-net-dev:
driver: bridge
volumes:
llmhub-core-service-volume:
llmhub-impl-baiLian-volume:

View File

@ -1,6 +1,6 @@
services:
nacos:
image: nacos/nacos-server:latest
image: nacos/nacos-server:v2.3.2
container_name: nacos-server
ports:
- "9001:8848" # Nacos控制台
@ -9,12 +9,16 @@ services:
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
- NACOS_AUTH_IDENTITY_VALUE=gRuycTvLGqWpkXyOHQeqWV+KSpkLZxLvtPyATXGJ00w=
- NACOS_AUTH_TOKEN=yCS3rQVXrnJPt7q3vc79wJl76mG3dY++O854NhxVj7g=
- MODE=cluster
- NACOS_SERVER_PORT=8848
- SPRING_DATASOURCE_PLATFORM=mysql
- MYSQL_SERVICE_HOST=mysql
- MYSQL_SERVICE_PORT=3306
- MYSQL_SERVICE_DB_NAME=nacos_config
- MYSQL_SERVICE_USER=root
- MYSQL_SERVICE_PASSWORD=root
networks:
- llmhub-net
volumes:
@ -22,27 +26,41 @@ services:
- 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
mysql:
image: mysql:8.0
container_name: nacos-mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: nacos_config
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
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
- ./mysql-data:/var/lib/mysql
- ./nacos-mysql.sql:/docker-entrypoint-initdb.d/nacos-mysql.sql
ports:
- "9002:9002"
networks:
- llmhub-net
volumes:
- llmhub-impl-baiLian-volume:/app/volume
- "3306:3306"
restart: always
networks:
- nacos-net
# 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
@ -50,5 +68,5 @@ volumes:
nacos-conf-volume:
nacos-data-volume:
nacos-logs-volume:
llmhub-core-service-volume:
llmhub-impl-baiLian-volume:
# llmhub-core-service-volume:
# llmhub-impl-baiLian-volume:

View File

@ -0,0 +1,178 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/******************************************/
/* 表名称 = config_info */
/******************************************/
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) DEFAULT NULL COMMENT 'group_id',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description',
`c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage',
`effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述',
`type` varchar(64) DEFAULT NULL COMMENT '配置的类型',
`c_schema` text COMMENT '配置的模式',
`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 表名称 = config_info since 2.5.0 */
/******************************************/
CREATE TABLE `config_info_gray` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`src_user` text COMMENT 'src_user',
`src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip',
`gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create',
`gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`gray_name` varchar(128) NOT NULL COMMENT 'gray_name',
`gray_rule` text NOT NULL COMMENT 'gray_rule',
`encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`),
KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`),
KEY `idx_gmt_modified` (`gmt_modified`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='config_info_gray';
/******************************************/
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识',
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限单位为字节0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限单位为字节0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(20) unsigned NOT NULL COMMENT 'id',
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`op_type` char(10) DEFAULT NULL COMMENT 'operation type',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥',
`publish_type` varchar(50) DEFAULT 'formal' COMMENT 'publish type gray or formal',
`gray_name` varchar(50) DEFAULT NULL COMMENT 'gray name',
`ext_info` longtext DEFAULT NULL COMMENT 'ext info',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限单位为字节0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限单位为字节0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username',
`password` varchar(500) NOT NULL COMMENT 'password',
`enabled` boolean NOT NULL COMMENT 'enabled'
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL COMMENT 'username',
`role` varchar(50) NOT NULL COMMENT 'role',
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL COMMENT 'role',
`resource` varchar(128) NOT NULL COMMENT 'resource',
`action` varchar(8) NOT NULL COMMENT 'action',
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

View File

@ -4,7 +4,7 @@ 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"
spring-cloud-starter-alibaba-nacos-discovery-version = "2023.0.1.0"
forgeBoot-version = "1.1.0-SNAPSHOT"
okHttp-version = "4.12.0"
jib-version = "3.4.2"
@ -53,5 +53,12 @@ junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-
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-i18n-springBootStarter = { group = "com.gewuyou.forgeboot", name = "forgeboot-i18n-spring-boot-starter", version.ref = "forgeBoot-version" }
forgeBoot-core-extension = { group = "com.gewuyou.forgeboot", name = "forgeboot-core-extension", version.ref = "forgeBoot-version" }
jackson-core={group="com.fasterxml.jackson.core", name="jackson-core"}
jackson-databind={group="com.fasterxml.jackson.core", name="jackson-databind"}
jackson-annotations={group="com.fasterxml.jackson.core", name="jackson-annotations"}
jackson-datatype-jsr310={group="com.fasterxml.jackson.datatype", name="jackson-datatype-jsr310"}
jackson-module-kotlin={group="com.fasterxml.jackson.module", name="jackson-module-kotlin"}
[bundles]

View File

@ -21,4 +21,6 @@ dependencies {
implementation(libs.forgeBoot.webmvc.version.springBootStarter)
implementation(libs.forgeBoot.core.extension)
implementation(libs.jackson.module.kotlin)
}

View File

@ -1,6 +1,5 @@
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

View File

@ -6,6 +6,7 @@ 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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@ -32,7 +33,7 @@ class ChatController(
* @return 返回一个Flow流流中依次提供了聊天响应的部分数据允许异步处理和逐步消费响应内容
*/
@PostMapping("/stream")
fun chat(request: ChatRequest): Flow<ChatResponsePart> {
fun chat(@RequestBody request: ChatRequest): Flow<ChatResponsePart> {
return llmServiceImpl.chat(request)
}
}

View File

@ -33,7 +33,7 @@ class LLMServiceImpl(
val serviceName = modelRouteManager.resolveServiceName(request.model)
val webClient = webClientBuilder.build()
return webClient.post()
.uri("http://$serviceName/provider/chat/stream")
.uri("http://$serviceName/provider/chat")
.bodyValue(request)
.retrieve()
.bodyToFlux(ChatResponsePart::class.java)

View File

@ -1,17 +1,8 @@
server:
port: 8081
spring:
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}
config:
import: classpath:bootstrap-dev.yml
llmhub:
model-route:
modelServiceMap:

View File

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

View File

@ -0,0 +1,12 @@
spring:
cloud:
nacos:
username: nacos
password: L4s6f9y3
server-addr: 49.235.96.75:8848
ip: 192.168.1.6
discovery:
server-addr: ${spring.cloud.nacos.server-addr}
username: ${spring.cloud.nacos.username}
password: ${spring.cloud.nacos.password}
ip: ${spring.cloud.nacos.ip}

View File

@ -15,7 +15,7 @@ package org.jcnc.llmhub.core.spi.entities.response
*/
data class ChatResponsePart(
val content: String,
val other: String? = null,
val other: Map<String, Any>? = mapOf(),
val done: Boolean = false,
val usage: Usage? = null
)

View File

@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping
* @since 2025-04-25 16:13:00
* @author gewuyou
*/
@RequestMapping("/provider/chat")
@RequestMapping("/provider")
interface LLMProvider {
/**

View File

@ -15,5 +15,9 @@ dependencies {
implementation(libs.okHttp)
implementation(libs.forgeBoot.core.extension)
implementation(libs.jackson.module.kotlin)
implementation(libs.forgeBoot.core.extension)
}

View File

@ -1,6 +1,7 @@
package org.jcnc.llmhub.impl.baiLian.adapter
import com.fasterxml.jackson.databind.ObjectMapper
import com.gewuyou.forgeboot.core.extension.log
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@ -11,6 +12,7 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.jcnc.llmhub.core.spi.entities.response.ChatResponsePart
import org.springframework.stereotype.Component
import java.io.BufferedReader
/**
* 百炼适配器
@ -45,62 +47,57 @@ class DashScopeAdapter(
url: String,
headers: Map<String, String>,
requestBody: Any,
extractContent: (String) -> String,
extractContent: (String) -> ChatResponsePart,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Flow<ChatResponsePart> = flow {
// 将请求体序列化为JSON格式
val requestJson = objectMapper.writeValueAsString(requestBody)
// 构建HTTP请求
log.info("📤 请求参数: {}", requestJson)
val request = Request.Builder()
.url(url)
.headers(headers.toHeaders())
.post(requestJson.toRequestBody("application/json".toMediaType()))
.build()
// 发送HTTP请求
val call = okHttpClient.newCall(request)
val response = call.execute()
// 使用指定的调度器执行请求
val response = withContext(dispatcher) {
call.execute()
}
// 检查HTTP响应是否成功
if (!response.isSuccessful) {
throw RuntimeException("DashScope request failed: ${response.code}")
throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}")
}
// 获取响应体
val responseBody = response.body ?: throw RuntimeException("Empty response body from DashScope")
// 获取响应体的源
val source = responseBody.source().buffer
val responseBody = response.body ?: throw RuntimeException("❌ DashScope 响应体为空")
val bufferedReader: BufferedReader = responseBody.charStream().buffered()
val allContent = StringBuilder()
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)
while (currentCoroutineContext().isActive) {
val line = withContext(dispatcher) {
bufferedReader.readLine()
} ?: break
log.info("📥 接收到行: {}", line)
// 发布聊天响应的部分内容
emit(ChatResponsePart(content = content, done = false))
if (line.startsWith("data:")) {
val jsonPart = line.removePrefix("data:").trim()
try {
val part = extractContent(jsonPart)
allContent.append(part.content)
log.info("✅ 提取内容: {}", part)
emit(part)
} catch (e: Exception) {
log.warn("⚠️ 无法解析 JSON: {}", jsonPart, e)
}
}
}
// 发布结束信号
emit(ChatResponsePart(content = "[END]", done = true))
log.info("📦 完整内容: {}", allContent)
} catch (e: Exception) {
log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e)
throw e
} finally {
// 关闭资源
source.close()
withContext(dispatcher) {
bufferedReader.close()
}
response.close()
}
}

View File

@ -7,6 +7,7 @@ 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.RequestBody
import org.springframework.web.bind.annotation.RestController
/**
@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController
* @author gewuyou
*/
@RestController
//@RequestMapping("/provider")
class BaiLianProvider(
private val baiLianModelService: BaiLianModelService
): LLMProvider {
@ -28,7 +30,8 @@ class BaiLianProvider(
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
override fun chat(request: ChatRequest): Flow<ChatResponsePart> {
// @PostMapping("/chat")
override fun chat(@RequestBody request: ChatRequest): Flow<ChatResponsePart> {
return baiLianModelService.streamChat(request)
}

View File

@ -2,15 +2,18 @@ package org.jcnc.llmhub.impl.baiLian.service.impl
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.gewuyou.forgeboot.core.extension.log
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.core.spi.entities.response.Usage
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
/**
* 百炼模型服务接口实现
*
@ -32,12 +35,14 @@ class BaiLianModelServiceImpl(
override fun streamChat(request: ChatRequest): Flow<ChatResponsePart> {
// 构造请求URL
val url = "${dashScopeProperties.baseUrl}${dashScopeProperties.appId}/completion"
log.info("请求URL: $url")
// 构造请求头,包括授权信息和内容类型
val headers = mapOf(
"Authorization" to "Bearer ${dashScopeProperties.apiKey}",
"Content-Type" to "application/json",
"X-DashScope-SSE" to "enable"
)
log.info("请求头: $headers")
// 构造输入参数主要包括用户的prompt
val inputMap = mutableMapOf("prompt" to request.prompt)
// 获取会话ID如果存在则添加到输入参数中
@ -82,9 +87,38 @@ class BaiLianModelServiceImpl(
return dashScopeAdapter.sendStreamChat(
url, headers, body,
{ json: String ->
// 解析响应JSON提取输出文本
val node = objectMapper.readTree(json)
node["output"]?.get("text")?.asText() ?: ""
val output = node["output"]
val usage = node["usage"]
val requestId = node["request_id"]?.asText() ?: ""
// 提取输出文本
val text = output?.get("text")?.asText() ?: ""
// 判断是否完成finish_reason 通常为 "stop",但你这里是字符串 "null"
val finishReason = output?.get("finish_reason")?.asText()
val done = finishReason == "stop"
// 提取使用情况
val usageNode = usage?.get("models")?.firstOrNull()
val promptTokens = usageNode?.get("input_tokens")?.asInt() ?: 0
val completionTokens = usageNode?.get("output_tokens")?.asInt() ?: 0
val totalTokens = promptTokens + completionTokens
val responseSessionId = output?.get("session_id")?.asText() ?: ""
ChatResponsePart(
content = text,
done = done,
usage = Usage(
promptTokens = promptTokens,
completionTokens = completionTokens,
totalTokens = totalTokens
),
other = mapOf(
"request_id" to requestId,
"session_id" to responseSessionId,
"model" to request.model,
"response" to json
)
)
}
)
}

View File

@ -1,6 +1,8 @@
server:
port: 8082
spring:
config:
import: classpath:bootstrap-dev.yml
application:
name: llmhub-impl-baiLian
cloud:

View File

@ -0,0 +1,12 @@
spring:
cloud:
nacos:
username: nacos
password: L4s6f9y3
server-addr: 49.235.96.75:8848
ip: 192.168.1.6
discovery:
server-addr: ${spring.cloud.nacos.server-addr}
username: ${spring.cloud.nacos.username}
password: ${spring.cloud.nacos.password}
ip: ${spring.cloud.nacos.ip}

View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,7 @@
// 开启springboot
extra[ProjectFlags.USE_SPRING_BOOT] = true
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
dependencies {
// Nacos 服务发现和配置
implementation(libs.springCloudStarter.alibaba.nacos.discovery)
}

View File

@ -0,0 +1,11 @@
package org.jcnc.llmhub.impl.openAi
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class LlmhubImplOpenAiApplication
fun main(args: Array<String>) {
runApplication<LlmhubImplOpenAiApplication>(*args)
}

View File

@ -0,0 +1,13 @@
package org.jcnc.llmhub.impl.openAi
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class LlmhubImplOpenAiApplicationTests {
@Test
fun contextLoads() {
}
}

View File

@ -14,5 +14,7 @@ project(":llmhub-core:llmhub-core-spi").name = "llmhub-core-spi"
include(
"llmhub-impl",
"llmhub-impl:llmhub-impl-baiLian",
"llmhub-impl:llmhub-impl-openAi",
)
project(":llmhub-impl:llmhub-impl-baiLian").name = "llmhub-impl-baiLian"
project(":llmhub-impl:llmhub-impl-openAi").name = "llmhub-impl-openAi"

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="GENERAL_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>