init(all): 初始化基础模块

- 创建项目基本结构和配置
- 添加 Jib 插件支持镜像构建
- 配置 Spring Boot 和 Spring Cloud 依赖
- 设置 Nacos 服务发现
- 添加阿里云 DashScope 配置
This commit is contained in:
gewuyou 2025-05-06 12:48:23 +08:00
commit 0515b8d5e4
57 changed files with 1924 additions and 0 deletions

3
.gitattributes vendored Normal file
View File

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

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
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
.idea/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
gradle.properties
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

1
ReadMe.md Normal file
View File

@ -0,0 +1 @@
基础模块: 用于锁定项目中所使用的依赖版本

126
build.gradle.kts Normal file
View File

@ -0,0 +1,126 @@
plugins {
alias(libs.plugins.java)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.plugin.spring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
alias(libs.plugins.jibLocalPlugin)
}
group = "org.jcnc"
version = "1.0-SNAPSHOT"
/**
* 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
*/
configurations.implementation {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
allprojects {
// 设置全局属性
ext {
set(ProjectFlags.USE_SPRING_BOOT, false)
set(ProjectFlags.USE_LLM_CORE_SPI, false)
set(ProjectFlags.USE_SPRING_CLOUD_BOM, false)
set(ProjectFlags.IS_ROOT_MODULE, false)
}
repositories {
mavenLocal()
maven {
url = uri("http://49.235.96.75:3001/api/packages/gewuyou/maven")
isAllowInsecureProtocol = true
}
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
}
maven {
url = uri("https://raw.githubusercontent.com/eurotech/kura_addons/mvn-repo/")
}
maven {
url = uri("https://maven.aliyun.com/repository/public/")
}
maven {
url = uri("https://maven.aliyun.com/repository/spring/")
}
maven {
url = uri("https://maven.aliyun.com/repository/spring-plugin/")
}
maven {
url = uri("https://maven.aliyun.com/repository/gradle-plugin/")
}
mavenCentral()
google()
gradlePluginPortal()
}
afterEvaluate {
if (project.getPropertyByBoolean(ProjectFlags.IS_ROOT_MODULE)) {
/**
* 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
*/
configurations.implementation {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}
}
}
}
subprojects {
afterEvaluate {
// springbootWeb
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)) {
apply {
plugin(libs.plugins.spring.dependency.management.get().pluginId)
plugin(libs.plugins.spring.boot.get().pluginId)
plugin(libs.plugins.kotlin.plugin.spring.get().pluginId)
}
dependencies {
implementation(libs.springBootStarter.web)
testImplementation(libs.springBootStarter.test)
testRuntimeOnly(libs.junitPlatform.launcher)
}
}
// llmx-core-spi
if (project.getPropertyByBoolean(ProjectFlags.USE_LLM_CORE_SPI)) {
dependencies {
implementation(project(Modules.Core.SPI))
}
}
// springCloudBom
if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_CLOUD_BOM)) {
dependencies {
implementation(platform(libs.springCloudDependencies.bom))
}
}
}
val libs = rootProject.libs
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))
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
jibConfig {
project {
projectName = "llmx-core-service"
ports = listOf("9002")
environment = mapOf("SPRING_PROFILES_ACTIVE" to "prod")
}
}
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
fun Project.getPropertyByBoolean(key: String): Boolean {
return properties[key]?.toString()?.toBoolean() ?: false
}

51
buildSrc/build.gradle.kts Normal file
View File

@ -0,0 +1,51 @@
// buildSrc/build.gradle.kts
plugins {
// 核心:启用 Kotlin DSL 支持
`kotlin-dsl`
alias(libs.plugins.kotlin.jvm)
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 =
"提供简单的配置构建镜像"
}
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenLocal()
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
}
maven {
url = uri("https://raw.githubusercontent.com/eurotech/kura_addons/mvn-repo/")
}
maven {
url = uri("https://maven.aliyun.com/repository/public/")
}
maven {
url = uri("https://maven.aliyun.com/repository/spring/")
}
maven {
url = uri("https://maven.aliyun.com/repository/spring-plugin/")
}
maven {
url = uri("https://maven.aliyun.com/repository/gradle-plugin/")
}
mavenCentral()
google()
gradlePluginPortal()
}

View File

@ -0,0 +1,21 @@
// buildSrc/settings.gradle.kts
pluginManagement {
repositories {
mavenLocal() // 从本地仓库获取插件
gradlePluginPortal() // 从 Gradle 插件门户获取插件
mavenCentral() // 从 Maven 中央仓库获取依赖
}
plugins {
// 解决 JDK 工具链问题
// https://mvnrepository.com/artifact/org.gradle.toolchains/foojay-resolver
id("org.gradle.toolchains.foojay-resolver") version "0.9.0"
}
}
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "buildSrc"

View File

@ -0,0 +1,19 @@
/**
* Modules对象用于统一管理项目中的各个模块路径
* 主要作用是提供一个集中定义和访问模块路径的地方以便在项目中保持一致性和可维护性
*
* @since 2025-04-03 09:07:33
* @author gewuyou
*/
object Modules {
/**
* Core对象定义了核心模块的相关路径
* 主要包含核心模块的SPIService Provider Interface路径
*/
object Core{
// llmx-core-spi模块的路径用于定义核心功能的SPI
const val SPI = ":llmx-core:llmx-core-spi"
}
}

View File

@ -0,0 +1,6 @@
object ProjectFlags {
const val USE_SPRING_BOOT = "useSpringBoot"
const val USE_SPRING_CLOUD_BOM = "useSpringCloudBom"
const val USE_LLM_CORE_SPI = "useLLMCoreSPI"
const val IS_ROOT_MODULE = "isRootModule"
}

View File

@ -0,0 +1,85 @@
package org.jcnc.llmx.plugin.jib
import com.google.cloud.tools.jib.gradle.JibExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jcnc.llmx.plugin.jib.entity.JibProject
import kotlin.collections.find
/**
*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.llmx.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
}
}
}

64
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,64 @@
[versions]
kotlin-version = "2.0.0"
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.1.0"
forgeBoot-version = "1.1.0-SNAPSHOT"
okHttp-version = "4.12.0"
jib-version = "3.4.2"
[plugins]
# 应用 Java 插件,提供基本的 Java 代码编译和构建能力
java = { id = "java" }
# 应用 Gradle 基础插件,提供项目的基础配置和插件管理能力
javaGradle-plugin = { id = "java-gradle-plugin" }
# 引入 Kotlin 支持
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" }
# 支持 Spring 的 Kotlin 插件
kotlin-plugin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin-version" }
# Spring 依赖管理插件,简化依赖版本管理
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "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 = { 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" }
# 阿里云百炼
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" }
# SpringBootStarter
springBootStarter-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
springBootStarter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
springBootStarter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" }
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-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]

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Normal file
View File

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,9 @@
dependencies {
}
/**
* 由于 Kotlin 插件被引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖
*/
configurations.implementation {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
}

View File

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

37
llmx-core/llmx-core-service/.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
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/

View File

@ -0,0 +1,26 @@
extra {
// 开启springboot
setProperty(ProjectFlags.USE_SPRING_BOOT, true)
setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true)
}
dependencies {
val libs = rootProject.libs
// Nacos 服务发现和配置
implementation(libs.springCloudStarter.alibaba.nacos.discovery)
// WebClient 和 Spring Cloud LoadBalancer
implementation(libs.springBootStarter.webflux)
implementation(libs.springCloudStarter.loadbalancer)
implementation(project(Modules.Core.SPI))
// Kotlin Coroutines
implementation(libs.kotlinx.coruntes.reactor)
implementation(libs.forgeBoot.webmvc.version.springBootStarter)
implementation(libs.forgeBoot.core.extension)
implementation(libs.jackson.module.kotlin)
}

View File

@ -0,0 +1,22 @@
package org.jcnc.llmx.core.service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
class LlmxCoreServiceApplication
/**
* 程序的入口点
*
* 这个函数使用了Kotlin的常规main函数签名标志着一个Kotlin应用程序的开始
* 它通过调用runApplication函数来启动一个Spring Boot应用程序传递当前应用程序的类和命令行参数
*
* @param args 命令行参数允许从命令行传递参数到应用程序中
*/
fun main(args: Array<String>) {
// 启动应用程序的入口点通过传递命令行参数来启动Spring Boot应用程序。
runApplication<LlmxCoreServiceApplication>(*args)
}

View File

@ -0,0 +1,31 @@
package org.jcnc.llmx.core.service.config
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
/**
*应用程序配置
*
* @since 2025-04-25 20:36:18
* @author gewuyou
*/
@Configuration
@EnableConfigurationProperties(ModelProperties::class)
class AppConfiguration {
/**
* 创建一个配置了负载均衡的WebClient构建器
*
* 该方法通过Spring框架的WebClient.builder()初始化一个WebClient构建器并为其配置负载均衡功能
* 负载均衡功能使得WebClient能够智能地在多个服务实例间分配请求提高系统的可伸缩性和可靠性
*
* @return WebClient.Builder 配置了负载均衡功能的WebClient构建器
*/
@Bean
@LoadBalanced
fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
}
}

View File

@ -0,0 +1,21 @@
package org.jcnc.llmx.core.service.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.cloud.context.config.annotation.RefreshScope
/**
*模型属性
*
* @since 2025-04-26 18:01:48
* @author gewuyou
*/
@ConfigurationProperties(prefix = "llmhub.model-route")
@RefreshScope
open class ModelProperties {
/**
* 模型名前缀 -> 服务名映射
* 该映射表存储了模型名前缀与服务名的对应关系用于快速查找模型对应的服务
* openai -> llmhub-impl-openai
*/
var modelServiceMap: Map<String, String> = emptyMap()
}

View File

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

View File

@ -0,0 +1,38 @@
package org.jcnc.llmx.core.service.manager
import org.jcnc.llmx.core.service.config.ModelProperties
import org.springframework.stereotype.Component
/**
* 模型路由表管理器
*
* 该类负责管理模型名与服务名之间的映射关系根据模型名前缀来确定对应的后端服务
* 主要作用是动态解析和路由模型请求到正确的服务实例
*
* @since 2025-04-26 17:53:06
* @author gewuyou
*/
@Component
class ModelRouteManager(
private val modelProperties: ModelProperties
) {
/**
* 根据模型名查找对应服务
*
* 该方法通过遍历模型服务映射表找到与模型名前缀匹配的服务名并返回
* 如果没有找到匹配项则抛出异常表示不支持该模型类型
*
* @param model 模型名用于查找对应服务
* @return 匹配的后端服务名
* @throws IllegalArgumentException 如果模型名不匹配任何已知前缀抛出此异常
*/
fun resolveServiceName(model: String): String {
for ((prefix, serviceName) in modelProperties.modelServiceMap) {
if (model.startsWith(prefix)) {
return serviceName
}
}
throw IllegalArgumentException("Unsupported model type: $model")
}
}

View File

@ -0,0 +1,27 @@
package org.jcnc.llmx.core.service.service
import kotlinx.coroutines.flow.Flow
import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.springframework.web.bind.annotation.PostMapping
/**
*LLM服务
*
* @since 2025-04-26 17:38:18
* @author gewuyou
*/
fun interface LLMService {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
@PostMapping("/chat")
fun chat(request: ChatRequest): Flow<ChatResponsePart>
}

View File

@ -0,0 +1,43 @@
package org.jcnc.llmx.core.service.service.impl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import org.jcnc.llmx.core.service.manager.ModelRouteManager
import org.jcnc.llmx.core.service.service.LLMService
import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
/**
*LLM服务实现
*
* @since 2025-04-25 20:38:13
* @author gewuyou
*/
@Service
class LLMServiceImpl(
private val modelRouteManager: ModelRouteManager,
private val webClientBuilder: WebClient.Builder
) : LLMService {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
override fun chat(request: ChatRequest): Flow<ChatResponsePart> {
val serviceName = modelRouteManager.resolveServiceName(request.model)
val webClient = webClientBuilder.build()
return webClient.post()
.uri("http://$serviceName/provider/chat")
.bodyValue(request)
.retrieve()
.bodyToFlux(ChatResponsePart::class.java)
.asFlow()
}
}

View File

@ -0,0 +1,13 @@
server:
port: 8081
spring:
config:
import: classpath:bootstrap-dev.yml
llmhub:
model-route:
modelServiceMap:
qwen-turbo: llmhub-impl-baiLian
qwen-max: llmhub-impl-baiLian
qwen-plus: llmhub-impl-baiLian

View File

@ -0,0 +1,7 @@
server:
port: 9002
spring:
cloud:
nacos:
discovery:
server-addr: 49.235.96.75:9001

View File

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

@ -0,0 +1,14 @@
package org.jcnc.llmx.core.service;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class LlmhubCoreServiceApplicationTests {
@Test
void contextLoads() {
// null
}
}

View File

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

40
llmx-core/llmx-core-spi/.gitignore vendored Normal file
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,9 @@
apply {
plugin(libs.plugins.spring.dependency.management.get().pluginId)
plugin(libs.plugins.spring.boot.get().pluginId)
plugin(libs.plugins.kotlin.plugin.spring.get().pluginId)
}
dependencies {
compileOnly(libs.kotlinx.coruntes.reactor)
compileOnly(libs.springBootStarter.web)
}

View File

@ -0,0 +1,17 @@
package org.jcnc.llmx.core.spi.entities.request
/**
* ChatRequest数据类用于封装聊天请求的参数
* @since 2025-04-25 17:07:02
* @author gewuyou
* @param prompt 用户的聊天提示或消息是聊天请求的主要输入
* @param model 使用的聊天模型名称决定了解析和响应的方式
* @param options 可选的额外参数集合用于定制聊天请求的行为和输出
* 可以包括如最大回复长度温度随机性
*/
data class ChatRequest(
val prompt: String,
val model: String,
val options: Map<String, String> = mapOf()
)

View File

@ -0,0 +1,18 @@
package org.jcnc.llmx.core.spi.entities.request
/**
* 嵌入请求类
*
* 该类用于定义文本嵌入的请求数据结构包含需要进行嵌入处理的输入文本列表
* 和用于处理嵌入的模型标识符
*
* @param input 输入文本列表每个文本作为一个列表元素
* @param model 指定的嵌入模型标识符用于确定使用哪种模型进行嵌入处理
*
* @since 2025-04-25 17:08:13
* @author gewuyou
*/
data class EmbeddingRequest(
val input: List<String>,
val model: String
)

View File

@ -0,0 +1,38 @@
package org.jcnc.llmx.core.spi.entities.response
/**
* 聊天响应类
*
* 该类用于封装聊天机器人的响应内容包括聊天内容和使用情况
* 主要用途是提供一个结构化的方式来处理和展示聊天机器人的回复信息
*
* @param content 聊天内容字符串表示机器人的回复
* @param other 可选参数表示其他信息例如提示词完成词等
* @param done 可选参数表示聊天是否完成默认为false
* @param usage 可选参数表示聊天的使用情况包括使用的token数量等信息
* @since 2025-04-25 17:07:40
* @author gewuyou
*/
data class ChatResponsePart(
val content: String,
val other: Map<String, Any>? = mapOf(),
val done: Boolean = false,
val usage: Usage? = null
)
/**
* 使用情况类
*
* 该类用于详细记录聊天过程中token的使用情况包括提示词完成词和总词数
* 主要用途是提供详细的统计信息以便用户了解token的消耗情况
*
* @param promptTokens 提示词使用的token数量
* @param completionTokens 完成词使用的token数量
* @param totalTokens 总共使用的token数量
*/
data class Usage(
val promptTokens: Int,
val completionTokens: Int,
val totalTokens: Int
)

View File

@ -0,0 +1,18 @@
package org.jcnc.llmx.core.spi.entities.response
/**
* 嵌入响应类
*
* 该类用于表示嵌入向量的响应结果包括向量数据及其维度信息
* 主要用于自然语言处理图像识别等领域where向量表示法被广泛应用
*
* @param vectors 一个包含多个浮点数列表的列表每个浮点数列表代表一个嵌入向量
* @param dimensions 一个整数表示嵌入向量的维度
*
* @since 2025-04-25 17:10:50
* @author gewuyou
*/
data class EmbeddingResponse(
val vectors: List<List<Float>>,
val dimensions: Int
)

View File

@ -0,0 +1,44 @@
package org.jcnc.llmx.core.spi.provider
import kotlinx.coroutines.flow.Flow
import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.request.EmbeddingRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.jcnc.llmx.core.spi.entities.response.EmbeddingResponse
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
/**
* LLM 提供商接口
* 该接口定义了与LLMLarge Language Model提供商交互的标准方法
* 主要提供了聊天和嵌入两种功能
*
* @since 2025-04-25 16:13:00
* @author gewuyou
*/
@RequestMapping("/provider")
interface LLMProvider {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
@PostMapping("/chat")
fun chat(request: ChatRequest): Flow<ChatResponsePart>
/**
* 嵌入功能方法
* 该方法允许用户发送嵌入请求以获取LLM生成的嵌入向量
*
* @param request 嵌入请求对象包含需要进行嵌入处理的数据
* @return EmbeddingResponse 嵌入响应对象包含生成的嵌入向量信息
*/
@PostMapping("/embedding")
fun embedding(request: EmbeddingRequest): EmbeddingResponse
}

View File

@ -0,0 +1,5 @@
// 开启springboot
extra[ProjectFlags.IS_ROOT_MODULE] = true
dependencies {
}

View File

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

40
llmx-impl/llmx-impl-baiLian/.gitignore vendored Normal file
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,23 @@
// 开启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)
implementation(libs.jackson.module.kotlin)
implementation(libs.forgeBoot.core.extension)
}

View File

@ -0,0 +1,12 @@
package org.jcnc.llmx.impl.baiLian
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
class LlmxImplBaiLianApplication
fun main(args: Array<String>) {
runApplication<LlmxImplBaiLianApplication>(*args)
}

View File

@ -0,0 +1,105 @@
package org.jcnc.llmx.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
import okhttp3.Headers.Companion.toHeaders
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.springframework.stereotype.Component
import java.io.BufferedReader
/**
* 百炼适配器
*
* 该类负责与百炼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) -> ChatResponsePart,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Flow<ChatResponsePart> = flow {
val requestJson = objectMapper.writeValueAsString(requestBody)
log.info("📤 请求参数: {}", requestJson)
val request = Request.Builder()
.url(url)
.headers(headers.toHeaders())
.post(requestJson.toRequestBody("application/json".toMediaType()))
.build()
val call = okHttpClient.newCall(request)
val response = call.execute()
if (!response.isSuccessful) {
throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}")
}
val responseBody = response.body ?: throw RuntimeException("❌ DashScope 响应体为空")
val bufferedReader: BufferedReader = responseBody.charStream().buffered()
val allContent = StringBuilder()
try {
while (currentCoroutineContext().isActive) {
val line = withContext(dispatcher) {
bufferedReader.readLine()
} ?: break
log.info("📥 接收到行: {}", line)
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)
}
}
}
log.info("📦 完整内容: {}", allContent)
} catch (e: Exception) {
log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e)
throw e
} finally {
withContext(dispatcher) {
bufferedReader.close()
}
response.close()
}
}
}

View File

@ -0,0 +1,28 @@
package org.jcnc.llmx.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,16 @@
package org.jcnc.llmx.impl.baiLian.config
import org.jcnc.llmx.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.llmx.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,49 @@
package org.jcnc.llmx.impl.baiLian.controller
import kotlinx.coroutines.flow.Flow
import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.request.EmbeddingRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.jcnc.llmx.core.spi.entities.response.EmbeddingResponse
import org.jcnc.llmx.core.spi.provider.LLMProvider
import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
/**
*百炼提供商
*
* @since 2025-04-27 10:12:12
* @author gewuyou
*/
@RestController
//@RequestMapping("/provider")
class BaiLianProvider(
private val baiLianModelService: BaiLianModelService
): LLMProvider {
/**
* 初始化与聊天服务的连接以处理聊天请求
*
* 此函数接收一个聊天请求对象并返回一个Flow流用于接收聊天响应的部分数据
* 它主要用于建立聊天通信的通道而不是发送具体的消息
*
* @param request 聊天请求对象包含建立聊天所需的信息如用户标识会话标识等
* @return 返回一个Flow流通过该流可以接收到聊天响应的部分数据如消息状态更新等
*/
// @PostMapping("/chat")
override fun chat(@RequestBody 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,23 @@
package org.jcnc.llmx.impl.baiLian.service
import kotlinx.coroutines.flow.Flow
import org.jcnc.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.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,126 @@
package org.jcnc.llmx.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.llmx.core.spi.entities.request.ChatRequest
import org.jcnc.llmx.core.spi.entities.response.ChatResponsePart
import org.jcnc.llmx.core.spi.entities.response.Usage
import org.jcnc.llmx.impl.baiLian.adapter.DashScopeAdapter
import org.jcnc.llmx.impl.baiLian.config.entities.DashScopeProperties
import org.jcnc.llmx.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"
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如果存在则添加到输入参数中
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 ->
val node = objectMapper.readTree(json)
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

@ -0,0 +1,24 @@
server:
port: 8082
spring:
config:
import: classpath:bootstrap-dev.yml
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

@ -0,0 +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

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,13 @@
package org.jcnc.llmhub.impl.baiLian
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class LlmhubImplBailianApplicationTests {
@Test
fun contextLoads() {
}
}

20
settings.gradle.kts Normal file
View File

@ -0,0 +1,20 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "llmx"
include(
"llmx-core",
":llmx-core:llmx-core-service",
":llmx-core:llmx-core-spi",
)
project(":llmx-core:llmx-core-service").name = "llmx-core-service"
project(":llmx-core:llmx-core-spi").name = "llmx-core-spi"
include(
"llmx-impl",
"llmx-impl:llmx-impl-baiLian",
// "llmx-impl:llmx-impl-openAi",
)
project(":llmx-impl:llmx-impl-baiLian").name = "llmx-impl-baiLian"
//project(":llmx-impl:llmx-impl-openAi").name = "llmx-impl-openAi"