diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml new file mode 100644 index 0000000..e832654 --- /dev/null +++ b/.gitea/workflows/deploy.master.yml @@ -0,0 +1,219 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - master # 触发构建的分支 + +env: + # ========== 环境变量配置 ========== + DOCKER_REGISTRY_URL: ${{vars.DOCKER_REGISTRY_URL}} # 私有Docker镜像仓库地址 + INTERNAL_DOCKER_REGISTRY_URL: ${{vars.INTERNAL_DOCKER_REGISTRY_URL}} + PROJECT_NAME: llmx # 项目名称 + MAIN_COMPOSE_FILE: docker/docker-compose.master.main.yml + AGENT_COMPOSE_FILE: docker/docker-compose.master.agent.yml + SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }} # 仓库密码 + JCNC_GITEA_URL: ${{vars.SERVER_GITEA_URL}} # Gitea地址 + RUNNER_TOOL_CACHE: /opt/tools-cache # 工具缓存目录 + GRADLE_CACHE_KEY: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + SPRING_PROFILES_ACTIVE: master + INTERNAL_SERVER_HOST: ${{ vars.INTERNAL_SERVER_HOST }} + INTERNAL_SERVER_PROT: ${{ vars.INTERNAL_SERVER_PROT }} + SINGAPORE_SERVER_HOST: ${{ vars.SINGAPORE_SERVER_HOST }} + SSH_PROT: ${{ vars.SSH_PROT }} +jobs: + build-and-deploy: + runs-on: ubuntu-latest + container: + image: jcnc/act-runner:latest # 使用自定义Runner镜像 + options: --user root # 以root用户运行(需要docker权限) + + steps: + # ========== 1. 代码检出 ========== + - name: 🛒 Checkout source code + uses: ${{env.JCNC_GITEA_URL}}/actions/checkout@v4 + with: + fetch-depth: 0 # 获取完整git历史(某些插件需要) + + # ========== 2. Docker环境准备 ========== + - name: 🐳 Install Docker Environment + run: | + echo "=== 检查Docker安装状态 ===" + if ! command -v docker >/dev/null; then + echo "❌ Docker未安装,开始安装..." + curl -fsSL https://get.docker.com | sh | tee docker-install.log + echo "✅ Docker安装完成" + echo "✅ Docker Compose安装完成" + else + echo "ℹ️ Docker已安装,版本: $(docker -v)" + echo "ℹ️ Docker Compose已安装,版本: $(docker compose version)" + fi + + # ========== 3. Gradle环境准备 ========== + - name: 🔧 Prepare Gradle Environment + run: | + echo "赋予gradlew执行权限..." + chmod +x gradlew + echo "当前目录结构:" + ls -al + # ========== 4. 恢复缓存 ========== + - name: 📦 Use Cache + id: cache + uses: ${{env.JCNC_GITEA_URL}}/actions/cache/restore@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.cache + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + env: + ACTIONS_RUNNER_DEBUG: true # 启用缓存调试输出 + + - name: ⚙️ Setup Gradle + uses: ${{env.JCNC_GITEA_URL}}/gradle/actions/setup-gradle@v4 + with: + gradle-version: wrapper # 使用项目自带的gradle-wrapper + - name: 📦 Copy Compose File to Internal Server + uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 + with: + host: ${{ env.INTERNAL_SERVER_HOST }} + port: $INTERNAL_SERVER_PROT + username: root + password: ${{ secrets.INTERNAL_SERVER_PASSWORD }} + source: $MAIN_COMPOSE_FILE + target: "/home/luke/deploy/llmx/docker-compose.master.yml" + - name: 📦 Copy Compose File to Singapore Server + uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 + with: + host: $SINGAPORE_SERVER_HOST + port: $SSH_PROT + username: root + password: ${{ secrets.SINGAPORE_SERVER_PASSWORD }} + source: $AGENT_COMPOSE_FILE + target: "/home/deploy/llmx/docker-compose.master.yml" + # ========== 5. 构建阶段 ========== + - name: 🏗️ Build with Jib + run: | + echo "开始构建Docker镜像..." + ./gradlew jib --stacktrace --build-cache --info -Dorg.gradle.caching=true -Dorg.gradle.jvmargs="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=1g" | tee build.log + + echo "=== 镜像构建结果 ===" + docker images | grep ${{ env.PROJECT_NAME }} || true + - name: 🛑 Stop Gradle Daemon + run: | + echo "停止Gradle守护进程..." + ./gradlew --stop + echo "剩余Java进程:" + ps aux | grep java || true + - name: 🛰️ Tag & Push to Internal Registry + run: | + echo "标记并推送镜像到内部服务器..." + docker tag ${{env.DOCKER_REGISTRY_URL}}/llmx-core-service:latest ${{env.INTERNAL_DOCKER_REGISTRY_URL}}/llmx-core-service:latest + docker tag ${{env.DOCKER_REGISTRY_URL}}/llmx-impl-bailian:latest ${{env.INTERNAL_DOCKER_REGISTRY_URL}}/llmx-impl-bailian:latest + + echo "${{ secrets.INTERNAL_DOCKER_REGISTRY_PASSWORD }}" | docker login ${{env.INTERNAL_DOCKER_REGISTRY_URL}} -u root --password-stdin + docker push ${{env.INTERNAL_DOCKER_REGISTRY_URL}}/llmx-core-service:latest + docker push ${{env.INTERNAL_DOCKER_REGISTRY_URL}}/llmx-impl-bailian:latest + docker logout ${{env.INTERNAL_DOCKER_REGISTRY_URL}} + - name: 🛰️ Tag & Push to Singapore Registry + run: | + echo "标记并推送镜像到内部服务器..." + docker tag ${{env.DOCKER_REGISTRY_URL}}/llmx-core-service:latest ${{env.SINGAPORE_DOCKER_REGISTRY_URL}}/llmx-core-service:latest + docker tag ${{env.DOCKER_REGISTRY_URL}}/llmx-impl-bailian:latest ${{env.SINGAPORE_DOCKER_REGISTRY_URL}}/llmx-impl-bailian:latest + + echo "${{ secrets.INTERNAL_DOCKER_REGISTRY_PASSWORD }}" | docker login ${{env.SINGAPORE_DOCKER_REGISTRY_URL}} -u root --password-stdin + docker push ${{env.SINGAPORE_DOCKER_REGISTRY_URL}}/llmx-core-service:latest + docker push ${{env.SINGAPORE_DOCKER_REGISTRY_URL}}/llmx-impl-bailian:latest + docker logout ${{env.SINGAPORE_DOCKER_REGISTRY_URL}} + # ========== 6. 保存缓存 ========== + - name: 📦 Save Cache + id: cache + uses: ${{env.JCNC_GITEA_URL}}/actions/cache/save@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.cache + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + env: + ACTIONS_RUNNER_DEBUG: true # 启用缓存调试输出 + - name: 🧼 Cleanup Dangling Images + run: | + echo "开始清理无标签镜像..." + docker image prune -f + remote-internal-deploy: + needs: build-and-deploy + runs-on: ubuntu-latest + container: + image: jcnc/act-runner:latest # 使用自定义Runner镜像 + options: --user root # 以root用户运行(需要docker权限) + steps: + - name: ✈️ Deploy on Internal Server + uses: ${{env.JCNC_GITEA_URL}}/appleboy/ssh-action@v1 + with: + host: $INTERNAL_SERVER_HOST + port: $INTERNAL_SERVER_PROT + username: root + password: ${{ secrets.INTERNAL_SERVER_PASSWORD }} + script: | + cd /home/luke/deploy/llmx + echo "准备部署环境..." + chmod +x docker-compose.master.yml + echo "当前Docker状态:" + docker ps -a + echo "清理旧容器..." + docker compose -f docker-compose.master.yml down --remove-orphans + echo "清理后Docker状态:" + docker ps -a + echo "拉取最新镜像..." + docker compose -f docker-compose.master.yml pull + echo "启动新服务..." + docker compose -f docker-compose.master.yml up -d + docker compose ps + echo "=== 服务状态检查 ===" + docker compose -f docker-compose.master.yml ps + echo "开始清理无标签镜像..." + docker image prune -f + echo "清理docker-compose.master.yml" + rm -rf docker-compose.master.yml + remote-singapore-deploy: + needs: build-and-deploy + runs-on: ubuntu-latest + container: + image: jcnc/act-runner:latest # 使用自定义Runner镜像 + options: --user root # 以root用户运行(需要docker权限) + steps: + - name: ✈️ Deploy on Internal Server + uses: ${{env.JCNC_GITEA_URL}}/appleboy/ssh-action@v1 + with: + host: $SINGAPORE_SERVER_HOST + port: $SSH_PROT + username: root + password: ${{ secrets.SINGAPORE_SERVER_PASSWORD }} + script: | + cd /home/deploy/llmx + echo "准备部署环境..." + chmod +x docker-compose.master.yml + echo "当前Docker状态:" + docker ps -a + echo "清理旧容器..." + docker compose -f docker-compose.master.yml down --remove-orphans + echo "清理后Docker状态:" + docker ps -a + echo "拉取最新镜像..." + docker compose -f docker-compose.master.yml pull + echo "启动新服务..." + docker compose -f docker-compose.master.yml up -d + docker compose ps + echo "=== 服务状态检查 ===" + docker compose -f docker-compose.master.yml ps + echo "开始清理无标签镜像..." + docker image prune -f + echo "清理docker-compose.master.yml" + rm -rf docker-compose.master.yml diff --git a/.gitea/workflows/deploy.test.yml b/.gitea/workflows/deploy.test.yml new file mode 100644 index 0000000..fcd3933 --- /dev/null +++ b/.gitea/workflows/deploy.test.yml @@ -0,0 +1,138 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - test # 触发构建的分支 + +env: + # ========== 环境变量配置 ========== + DOCKER_REGISTRY_URL: ${{vars.DOCKER_REGISTRY_URL}} # 私有Docker镜像仓库地址 + PROJECT_NAME: llmx # 项目名称 + COMPOSE_FILE: docker/docker-compose.test.yml # Docker compose文件路径 + SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }} # 仓库密码 + JCNC_GITEA_URL: ${{vars.SERVER_GITEA_URL}} # Gitea地址 + RUNNER_TOOL_CACHE: /opt/tools-cache # 工具缓存目录 + GRADLE_CACHE_KEY: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + SPRING_PROFILES_ACTIVE: test + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + container: + image: jcnc/act-runner:latest # 使用自定义Runner镜像 + options: --user root # 以root用户运行(需要docker权限) + + steps: + # ========== 1. 代码检出 ========== + - name: 🛒 Checkout source code + uses: ${{env.JCNC_GITEA_URL}}/actions/checkout@v4 + with: + fetch-depth: 0 # 获取完整git历史(某些插件需要) + + # ========== 2. Docker环境准备 ========== + - name: 🐳 Install Docker Environment + run: | + echo "=== 检查Docker安装状态 ===" + if ! command -v docker >/dev/null; then + echo "❌ Docker未安装,开始安装..." + curl -fsSL https://get.docker.com | sh | tee docker-install.log + echo "✅ Docker安装完成" + echo "✅ Docker Compose安装完成" + else + echo "ℹ️ Docker已安装,版本: $(docker -v)" + echo "ℹ️ Docker Compose已安装,版本: $(docker compose version)" + fi + + # ========== 3. Gradle环境准备 ========== + - name: 🔧 Prepare Gradle Environment + run: | + echo "赋予gradlew执行权限..." + chmod +x gradlew + echo "当前目录结构:" + ls -al + # ========== 4. 恢复缓存 ========== + - name: 📦 Use Cache + id: cache + uses: ${{env.JCNC_GITEA_URL}}/actions/cache/restore@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.cache + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + env: + ACTIONS_RUNNER_DEBUG: true # 启用缓存调试输出 + + - name: ⚙️ Setup Gradle + uses: ${{env.JCNC_GITEA_URL}}/gradle/actions/setup-gradle@v4 + with: + gradle-version: wrapper # 使用项目自带的gradle-wrapper + + # ========== 5. 构建阶段 ========== + - name: 🏗️ Build with Jib + run: | + echo "开始构建Docker镜像..." + ./gradlew jib --stacktrace --build-cache --info -Dorg.gradle.caching=true -Dorg.gradle.jvmargs="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=1g" | tee build.log + + echo "=== 镜像构建结果 ===" + docker images | grep ${{ env.PROJECT_NAME }} || true + - name: 🛑 Stop Gradle Daemon + run: | + echo "停止Gradle守护进程..." + ./gradlew --stop + echo "剩余Java进程:" + ps aux | grep java || true + # ========== 6. 保存缓存 ========== + - name: 📦 Save Cache + id: cache + uses: ${{env.JCNC_GITEA_URL}}/actions/cache/save@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.cache + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + env: + ACTIONS_RUNNER_DEBUG: true # 启用缓存调试输出 + # ========== 7. 部署阶段 ========== + - name: 🔧 Prepare Deployment + run: | + echo "当前路径..." + pwd && ls -al + echo "准备部署环境..." + chmod +x ${COMPOSE_FILE} + echo "当前Docker状态:" + docker ps -a + + - name: 🧹 Clean Old Containers + run: | + echo "当前路径..." + pwd && ls -al + echo "清理旧容器..." + docker compose -f ${COMPOSE_FILE} down --remove-orphans + echo "清理后Docker状态:" + docker ps -a + + - name: 🚀 Deploy New Version + run: | + echo "当前路径..." + pwd && ls -al + echo "拉取最新镜像..." + docker compose -f ${COMPOSE_FILE} pull + + echo "启动新服务..." + docker compose -f ${COMPOSE_FILE} up -d + + echo "=== 服务状态检查 ===" + docker compose -f ${COMPOSE_FILE} ps + - name: 🧼 Cleanup Dangling Images + run: | + echo "开始清理无标签镜像..." + docker image prune -f \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d7de339..fa0737c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,6 @@ plugins { 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) } @@ -19,10 +18,14 @@ configurations.implementation { allprojects { // 设置全局属性 ext { - set(ProjectFlags.USE_SPRING_BOOT, false) - set(ProjectFlags.USE_LLM_CORE_SPI, false) + set(ProjectFlags.USE_SPRING_BOOT_WEB, false) + set(ProjectFlags.USE_LLM_KT_IMPL_DEPENDENCE, false) set(ProjectFlags.USE_SPRING_CLOUD_BOM, false) set(ProjectFlags.IS_ROOT_MODULE, false) + set(ProjectFlags.USE_SPRING_BOOT_BOM, false) + set(ProjectFlags.USE_LLM_IMPL_PLATFORM_DEPENDENCE, false) + set(ProjectFlags.USE_NACOS_DEPENDENCE, false) + set(ProjectFlags.USE_DAO_DEPENDENCE, false) } repositories { mavenLocal() @@ -33,12 +36,10 @@ allprojects { 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/central") } maven { url = uri("https://maven.aliyun.com/repository/spring/") } @@ -66,24 +67,38 @@ allprojects { subprojects { afterEvaluate { - // springbootWeb - if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)) { + if (project.getPropertyByBoolean(ProjectFlags.USE_LLM_IMPL_PLATFORM_DEPENDENCE)) { apply { - plugin(libs.plugins.spring.dependency.management.get().pluginId) - plugin(libs.plugins.spring.boot.get().pluginId) plugin(libs.plugins.kotlin.plugin.spring.get().pluginId) } + ext { + setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true) + setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM, true) + setProperty(ProjectFlags.USE_LLM_KT_IMPL_DEPENDENCE, true) + setProperty(ProjectFlags.USE_NACOS_DEPENDENCE, true) + } dependencies { - implementation(libs.springBootStarter.web) - - testImplementation(libs.springBootStarter.test) - testRuntimeOnly(libs.junitPlatform.launcher) + // 核心spi依赖 + implementation(project(Modules.Core.SPI)) + // okHttp依赖 + implementation(libs.okHttp) + // forgeBoot依赖 + implementation(libs.forgeBoot.core.extension) + implementation(libs.forgeBoot.core.extension) } } - // llmx-core-spi - if (project.getPropertyByBoolean(ProjectFlags.USE_LLM_CORE_SPI)) { + if(project.getPropertyByBoolean(ProjectFlags.USE_DAO_DEPENDENCE)){ + dependencies{ + runtimeOnly(libs.postgresql) + implementation(libs.springBootStarter.data.jpa) + } + } + // nacos dependence + if(project.getPropertyByBoolean(ProjectFlags.USE_NACOS_DEPENDENCE)){ dependencies { - implementation(project(Modules.Core.SPI)) + implementation(libs.springCloudStarter.alibaba.nacos.discovery) + implementation(libs.springCloudStarter.alibaba.nacos.config) + implementation(libs.springCloudStarter.bootstrap) } } // springCloudBom @@ -92,6 +107,34 @@ subprojects { implementation(platform(libs.springCloudDependencies.bom)) } } + // springBootBom + if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT_BOM)) { + dependencies { + implementation(platform(libs.springBootDependencies.bom)) + } + } + // springbootWeb + if (project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT_WEB)) { + apply { + plugin(libs.plugins.spring.boot.get().pluginId) + } + dependencies { + implementation(libs.springBootStarter.web) + implementation(platform(libs.springBootDependencies.bom)) + testImplementation(libs.springBootStarter.test) + testRuntimeOnly(libs.junitPlatform.launcher) + } + } + // 使用kt实现impl服务依赖 + if (project.getPropertyByBoolean(ProjectFlags.USE_LLM_KT_IMPL_DEPENDENCE)) { + dependencies { + // jackson kt模块依赖 + implementation(libs.jackson.module.kotlin) + // kt协程依赖 + implementation(libs.kotlinx.coruntes.reactor) + } + } + } val libs = rootProject.libs apply { @@ -99,7 +142,6 @@ subprojects { plugin(libs.plugins.kotlin.jvm.get().pluginId) plugin(libs.plugins.jibLocalPlugin.get().pluginId) } - println(project.name + ":" + project.getPropertyByBoolean(ProjectFlags.USE_SPRING_BOOT)) kotlin { compilerOptions { @@ -107,10 +149,30 @@ subprojects { } } jibConfig { + val env = System.getenv("SPRING_PROFILES_ACTIVE") project { projectName = "llmx-core-service" ports = listOf("9002") - environment = mapOf("SPRING_PROFILES_ACTIVE" to "prod") + environment = mapOf("SPRING_PROFILES_ACTIVE" to env) + imageName = "llmx-core-service" + paths = listOf(File(rootProject.projectDir, "scripts").absolutePath) + entrypoint = listOf( + "/bin/sh", "-c", + "/entrypoint.sh -d llmx-nacos:8848,llmx-database:5432 -c " + + "'java -cp $( cat /app/jib-classpath-file ) $( cat /app/jib-main-class-file )'" + ) + } + project { + projectName = "llmx-impl-bailian" + ports = listOf("9003") + environment = mapOf("SPRING_PROFILES_ACTIVE" to env) + imageName = "llmx-impl-bailian" + paths = listOf(File(rootProject.projectDir, "scripts").absolutePath) + entrypoint = listOf( + "/bin/sh", "-c", + "/entrypoint.sh -d llmx-nacos:8848,llmx-database:5432 -c " + + "'java -cp $( cat /app/jib-classpath-file ) $( cat /app/jib-main-class-file )'" + ) } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index aec6e88..ff9ec97 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -12,8 +12,8 @@ dependencies { gradlePlugin { plugins { register("jib-plugin") { - id = "org.jcnc.llmhub.plugin.jib" - implementationClass = "org.jcnc.llmhub.plugin.jib.JibPlugin" + id = "org.jcnc.llmx.plugin.jib" + implementationClass = "org.jcnc.llmx.plugin.jib.JibPlugin" description = "提供简单的配置构建镜像" } diff --git a/buildSrc/src/main/kotlin/Modules.kt b/buildSrc/src/main/kotlin/Modules.kt index dacb471..6b0d211 100644 --- a/buildSrc/src/main/kotlin/Modules.kt +++ b/buildSrc/src/main/kotlin/Modules.kt @@ -14,6 +14,8 @@ object Modules { object Core{ // llmx-core-spi模块的路径,用于定义核心功能的SPI const val SPI = ":llmx-core:llmx-core-spi" + const val API = ":llmx-core:llmx-core-api" + const val COMMON = ":llmx-core:llmx-core-common" } } diff --git a/buildSrc/src/main/kotlin/ProjectFlags.kt b/buildSrc/src/main/kotlin/ProjectFlags.kt index eb7999b..ba8c4cd 100644 --- a/buildSrc/src/main/kotlin/ProjectFlags.kt +++ b/buildSrc/src/main/kotlin/ProjectFlags.kt @@ -1,6 +1,14 @@ object ProjectFlags { - const val USE_SPRING_BOOT = "useSpringBoot" + const val USE_SPRING_BOOT_WEB = "useSpringBootWeb" + const val USE_SPRING_BOOT_BOM = "useSpringBootBom" const val USE_SPRING_CLOUD_BOM = "useSpringCloudBom" - const val USE_LLM_CORE_SPI = "useLLMCoreSPI" + const val USE_LLM_KT_IMPL_DEPENDENCE = "useLLMKtImplDependence" const val IS_ROOT_MODULE = "isRootModule" + const val USE_NACOS_DEPENDENCE = "useNacosDependence" + const val USE_DAO_DEPENDENCE = "useDaoDependence" + + /** + * 使用实现服务第三方平台依赖 + */ + const val USE_LLM_IMPL_PLATFORM_DEPENDENCE = "useLLMImplPlatformDependence" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/JibPlugin.kt b/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/JibPlugin.kt index f4a886c..5bc92c1 100644 --- a/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/JibPlugin.kt +++ b/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/JibPlugin.kt @@ -52,12 +52,13 @@ class JibPlugin : Plugin { } to { image = - "${System.getenv("LUKE_SERVER_DOCKER_REGISTRY_URL")}/${jibProject.imageName}:${jibProject.version}" + "${System.getenv("DOCKER_REGISTRY_URL")}/${jibProject.imageName}:${jibProject.version}" auth { username = "root" - password = System.getenv("LUKE_SERVER_DOCKER_REGISTRY_PASSWORD") + password = System.getenv("SERVER_PASSWORD") } } + setAllowInsecureRegistries(true) // 动态配置容器参数 container { ports = jibProject.ports @@ -69,7 +70,9 @@ class JibPlugin : Plugin { // 动态配置额外目录 extraDirectories { - setPaths(jibProject.paths) + if(jibProject.paths.isNotEmpty()){ + setPaths(jibProject.paths) + } permissions.putAll(jibProject.permissions) } // 将动态部分移到任务配置中 diff --git a/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/entity/JibProject.kt b/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/entity/JibProject.kt index b980719..33480b9 100644 --- a/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/entity/JibProject.kt +++ b/buildSrc/src/main/kotlin/org/jcnc/llmx/plugin/jib/entity/JibProject.kt @@ -19,14 +19,14 @@ data class JibProject( var ports: List = listOf("8080"), var environment: Map = mapOf("SPRING_PROFILES_ACTIVE" to "prod"), var entrypoint: List = emptyList(), - var paths: List = listOf("llmhub-base/scripts/entrypoint.sh"), + var paths: List = listOf(), var imageName: String = "", var version: String = "latest", - var permissions: Map = mapOf("/scripts/entrypoint.sh" to "755"), - var baseImage: String = "docker://bellsoft/liberica-openjdk-debian:21" + var permissions: Map = mapOf("/entrypoint.sh" to "755"), + var baseImage: String = "docker://gewuyou/liberica-openjdk-debian-nc:21" ) { init { - if (imageName.isEmpty()) { + if (imageName.isBlank()) { imageName = projectName } } diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..9576670 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,42 @@ +services: + llmx-core-database: + image: postgres:16-alpine # 长期支持版本推荐用 16 + container_name: llmx-core-database + restart: always + ports: + - "5432:9052" + networks: + - llmx-net + environment: + POSTGRES_DB: llmx_db + POSTGRES_USER: llmx + POSTGRES_PASSWORD: L4s6f9y3, + volumes: + - llmx-core-db-volume:/var/lib/postgresql/data + llmx-core-service: + image: ${Docker_REGISTRY_URL}/llmx-core-service + container_name: llmx-core-service + ports: + - "9002:9002" + networks: + - llmx-net + volumes: + - llmx-core-service-volume:/app/volume + restart: always + llmx-impl-baiLian: + image: ${Docker_REGISTRY_URL}/llmx-impl-bailian + container_name: llmx-impl-bailian + ports: + - "9003:9003" + networks: + - llmx-net + volumes: + - llmx-impl-baiLian-volume:/app/volume + restart: always +networks: + llmx-net: + driver: bridge +volumes: + llmx-core-service-volume: + llmx-impl-baiLian-volume: + llmx-core-db-volume: \ No newline at end of file diff --git a/docker/docker-compose.master.agent.yml b/docker/docker-compose.master.agent.yml new file mode 100644 index 0000000..212ec44 --- /dev/null +++ b/docker/docker-compose.master.agent.yml @@ -0,0 +1,32 @@ +services: + llmx-core-service-banana: + image: ${DOCKER_REGISTRY_URL}/llmx-core-service + container_name: llmx-core-service + ports: + - "9002:9002" + networks: + - llmx-net-master + environment: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + volumes: + - llmx-core-service-volume:/app/volume + restart: always + llmx-impl-bailian-banana: + image: ${DOCKER_REGISTRY_URL}/llmx-impl-bailian + container_name: llmx-impl-bailian + ports: + - "9003:9003" + networks: + - llmx-net-master + environment: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + volumes: + - llmx-impl-bailian-volume:/app/volume + restart: always + +networks: + llmx-net-master: + driver: bridge +volumes: + llmx-core-service-volume: + llmx-impl-bailian-volume: diff --git a/docker/docker-compose.master.main.yml b/docker/docker-compose.master.main.yml new file mode 100644 index 0000000..0d61505 --- /dev/null +++ b/docker/docker-compose.master.main.yml @@ -0,0 +1,48 @@ +services: + llmx-core-service-apple: + image: ${DOCKER_REGISTRY_URL}/llmx-core-service + container_name: llmx-core-service + ports: + - "9002:9002" + networks: + - llmx-net-master + environment: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + volumes: + - llmx-core-service-volume:/app/volume + restart: always + llmx-database: + image: postgres:16-alpine # 长期支持版本推荐用 16 + container_name: llmx-database + restart: always + ports: + - "9052:5432" + networks: + - llmx-net-master + environment: + POSTGRES_DB: llmx_db + POSTGRES_USER: llmx + POSTGRES_PASSWORD: L4s6f9y3, + volumes: + - llmx-db-volume + llmx-impl-bailian-apple: + image: ${DOCKER_REGISTRY_URL}/llmx-impl-bailian + container_name: llmx-impl-bailian + ports: + - "9003:9003" + networks: + - llmx-net-master + environment: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + volumes: + - llmx-impl-bailian-volume:/app/volume + restart: always + +networks: + llmx-net-master: + driver: bridge + +volumes: + llmx-core-service-volume: + llmx-impl-bailian-volume: + llmx-db-volume: diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml new file mode 100644 index 0000000..737ff70 --- /dev/null +++ b/docker/docker-compose.test.yml @@ -0,0 +1,67 @@ +services: + llmx-nacos: + image: nacos/nacos-server:v2.3.2 + container_name: llmx-nacos + restart: always + ports: + - "8848:8848" + - "9848:9848" + - "9849:9849" + networks: + - llmx-net-test + environment: + MODE: standalone # 显式指定为单体模式 + NACOS_AUTH_ENABLE: "true" # ✅ 开启鉴权 + NACOS_AUTH_CACHE_ENABLE: "false" + NACOS_AUTH_IDENTITY_KEY: "nacos" # 可选,默认是 nacos + NACOS_AUTH_IDENTITY_VALUE: "L4s6f9y3," # 可选 + NACOS_AUTH_TOKEN: "h61bUSqvp0npCNHIZ0VzqBFz2U59UKrECE6TvBt58DQ=" # ✅ JWT 密钥 + volumes: + - llmx-nacos-volume:/home/nacos + llmx-core-service: + image: ${DOCKER_REGISTRY_URL}/llmx-core-service + container_name: llmx-core-service + ports: + - "9002:9002" + networks: + - llmx-net-test + environment: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + volumes: + - llmx-core-service-volume:/app/volume + restart: always + llmx-impl-bailian: + image: ${DOCKER_REGISTRY_URL}/llmx-impl-bailian + container_name: llmx-impl-bailian + ports: + - "9003:9003" + networks: + - llmx-net-test + environment: + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + volumes: + - llmx-impl-bailian-volume:/app/volume + restart: always + llmx-core-database: + image: postgres:16-alpine # 长期支持版本推荐用 16 + container_name: llmx-database + restart: always + ports: + - "9052:5432" + networks: + - llmx-net-test + environment: + POSTGRES_DB: llmx_db + POSTGRES_USER: llmx + POSTGRES_PASSWORD: L4s6f9y3, + volumes: + - llmx-core-db-volume:/var/lib/postgresql/data + +networks: + llmx-net-test: + driver: bridge +volumes: + llmx-core-service-volume: + llmx-impl-bailian-volume: + llmx-core-db-volume: + llmx-nacos-volume: \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f54e0f..61f44af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,9 +5,11 @@ 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" +forgeBoot-version = "1.3.0-SNAPSHOT" okHttp-version = "4.12.0" jib-version = "3.4.2" +org-reactivestreams-reactiveStreams-version = "1.0.4" +postgresql-version = "42.7.4" [plugins] # 应用 Java 插件,提供基本的 Java 代码编译和构建能力 java = { id = "java" } @@ -26,11 +28,12 @@ spring-dependency-management = { id = "io.spring.dependency-management", 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" } +jibLocalPlugin = { id = "org.jcnc.llmx.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" } +springBootDependencies-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot-version" } # kotlinx # 响应式协程库 kotlinx-coruntes-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" } @@ -40,15 +43,20 @@ aliyun-bailian = { group = "com.aliyun", name = "bailian20231229", version.ref = # 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-config = { group = "com.alibaba.cloud", name = "spring-cloud-starter-alibaba-nacos-config", version.ref = "spring-cloud-starter-alibaba-nacos-discovery-version" } springCloudStarter-loadbalancer = { group = "org.springframework.cloud", name = "spring-cloud-starter-loadbalancer" } - +springCloudStarter-openfeign = { group = "org.springframework.cloud", name = "spring-cloud-starter-openfeign" } +springCloudStarter-bootstrap = { group = "org.springframework.cloud", name = "spring-cloud-starter-bootstrap" } # 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" } +springBootStarter-data-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" } junitPlatform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } +# org-reactivestreams +org-reactivestreams-reactiveStreams = { group = "org.reactivestreams", name = "reactive-streams" ,version.ref="org-reactivestreams-reactiveStreams-version"} # OkHttp okHttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp-version" } # forgeBoot @@ -61,4 +69,6 @@ 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"} + +postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql-version" } [bundles] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a79..3b023b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..e93a03e 100644 --- a/gradlew +++ b/gradlew @@ -207,7 +207,7 @@ 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 +# * For gewuyou: 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 -- \ diff --git a/llmx-impl/llmx-impl-baiLian/.gitattributes b/llmx-app/.gitattributes similarity index 100% rename from llmx-impl/llmx-impl-baiLian/.gitattributes rename to llmx-app/.gitattributes diff --git a/llmx-impl/llmx-impl-baiLian/.gitignore b/llmx-app/.gitignore similarity index 100% rename from llmx-impl/llmx-impl-baiLian/.gitignore rename to llmx-app/.gitignore diff --git a/llmx-app/build.gradle.kts b/llmx-app/build.gradle.kts new file mode 100644 index 0000000..d1a1567 --- /dev/null +++ b/llmx-app/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + +} +/** + * 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖 + */ +configurations.implementation { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") +} diff --git a/llmx-app/llmx-app-multimodality/.gitattributes b/llmx-app/llmx-app-multimodality/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/llmx-app/llmx-app-multimodality/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/llmx-app/llmx-app-multimodality/.gitignore b/llmx-app/llmx-app-multimodality/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/llmx-app/llmx-app-multimodality/.gitignore @@ -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 diff --git a/llmx-app/llmx-app-multimodality/build.gradle.kts b/llmx-app/llmx-app-multimodality/build.gradle.kts new file mode 100644 index 0000000..af933bf --- /dev/null +++ b/llmx-app/llmx-app-multimodality/build.gradle.kts @@ -0,0 +1,13 @@ +extra { + // 开启springboot + setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true) + setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM, true) +} +dependencies { + // Nacos 服务发现和配置 + implementation(libs.springCloudStarter.alibaba.nacos.discovery) + implementation(libs.forgeBoot.core.extension) + implementation(libs.forgeBoot.webmvc.version.springBootStarter) + implementation(libs.jackson.module.kotlin) + implementation(project(Modules.Core.API)) +} diff --git a/llmx-app/llmx-app-multimodality/src/main/kotlin/org/jcnc/llmx/app/multimodality/LlmxAppMultimodalityApplication.kt b/llmx-app/llmx-app-multimodality/src/main/kotlin/org/jcnc/llmx/app/multimodality/LlmxAppMultimodalityApplication.kt new file mode 100644 index 0000000..8133bab --- /dev/null +++ b/llmx-app/llmx-app-multimodality/src/main/kotlin/org/jcnc/llmx/app/multimodality/LlmxAppMultimodalityApplication.kt @@ -0,0 +1,11 @@ +package org.jcnc.llmx.app.multimodality + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class LlmxAppMultimodalityApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/llmx-app/llmx-app-multimodality/src/main/resources/application-dev.yml b/llmx-app/llmx-app-multimodality/src/main/resources/application-dev.yml new file mode 100644 index 0000000..3360232 --- /dev/null +++ b/llmx-app/llmx-app-multimodality/src/main/resources/application-dev.yml @@ -0,0 +1,8 @@ +server: + port: 8081 +spring: + config: + import: classpath:bootstrap-dev.yml + + + diff --git a/llmx-core/llmx-core-service/src/main/resources/application-prod.yml b/llmx-app/llmx-app-multimodality/src/main/resources/application-master.yml similarity index 100% rename from llmx-core/llmx-core-service/src/main/resources/application-prod.yml rename to llmx-app/llmx-app-multimodality/src/main/resources/application-master.yml diff --git a/llmx-app/llmx-app-multimodality/src/main/resources/application-test.yml b/llmx-app/llmx-app-multimodality/src/main/resources/application-test.yml new file mode 100644 index 0000000..d75e277 --- /dev/null +++ b/llmx-app/llmx-app-multimodality/src/main/resources/application-test.yml @@ -0,0 +1,7 @@ +server: + port: 9003 +spring: + config: + import: classpath:bootstrap-test.yml + + diff --git a/llmx-app/llmx-app-multimodality/src/main/resources/application.yml b/llmx-app/llmx-app-multimodality/src/main/resources/application.yml new file mode 100644 index 0000000..9c9db5f --- /dev/null +++ b/llmx-app/llmx-app-multimodality/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + application: + name: llmx-app-multimodality + profiles: + active: dev diff --git a/llmx-impl/llmx-impl-baiLian/src/main/resources/bootstrap-dev.yml b/llmx-app/llmx-app-multimodality/src/main/resources/bootstrap-dev.yml similarity index 92% rename from llmx-impl/llmx-impl-baiLian/src/main/resources/bootstrap-dev.yml rename to llmx-app/llmx-app-multimodality/src/main/resources/bootstrap-dev.yml index d1e2592..37654f9 100644 --- a/llmx-impl/llmx-impl-baiLian/src/main/resources/bootstrap-dev.yml +++ b/llmx-app/llmx-app-multimodality/src/main/resources/bootstrap-dev.yml @@ -4,7 +4,7 @@ spring: username: nacos password: L4s6f9y3 server-addr: 49.235.96.75:8848 - ip: 192.168.1.6 + ip: 192.168.1.100 discovery: server-addr: ${spring.cloud.nacos.server-addr} username: ${spring.cloud.nacos.username} diff --git a/llmx-app/llmx-app-multimodality/src/main/resources/bootstrap-test.yml b/llmx-app/llmx-app-multimodality/src/main/resources/bootstrap-test.yml new file mode 100644 index 0000000..c9dc5c7 --- /dev/null +++ b/llmx-app/llmx-app-multimodality/src/main/resources/bootstrap-test.yml @@ -0,0 +1,10 @@ +spring: + cloud: + nacos: + username: nacos + password: L4s6f9y3 + server-addr: 49.235.96.75:9001 + discovery: + server-addr: ${spring.cloud.nacos.server-addr} + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} \ No newline at end of file diff --git a/llmx-app/llmx-app-multimodality/src/test/kotlin/org/jcnc/llmx/app/multimodality/LlmxAppMultimodalityApplicationTests.kt b/llmx-app/llmx-app-multimodality/src/test/kotlin/org/jcnc/llmx/app/multimodality/LlmxAppMultimodalityApplicationTests.kt new file mode 100644 index 0000000..98e9d18 --- /dev/null +++ b/llmx-app/llmx-app-multimodality/src/test/kotlin/org/jcnc/llmx/app/multimodality/LlmxAppMultimodalityApplicationTests.kt @@ -0,0 +1,13 @@ +package org.jcnc.llmx.app.multimodality + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class LlmxAppMultimodalityApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/llmx-core/build.gradle.kts b/llmx-core/build.gradle.kts index e972465..d1a1567 100644 --- a/llmx-core/build.gradle.kts +++ b/llmx-core/build.gradle.kts @@ -2,7 +2,7 @@ dependencies { } /** - * 由于 Kotlin 插件被引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖 + * 由于 Kotlin 插件引入时会自动添加依赖,但根项目不需要 Kotlin 依赖,因此需要排除 Kotlin 依赖 */ configurations.implementation { exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") diff --git a/llmx-core/llmx-core-api/.gitattributes b/llmx-core/llmx-core-api/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/llmx-core/llmx-core-api/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/llmx-core/llmx-core-api/.gitignore b/llmx-core/llmx-core-api/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/llmx-core/llmx-core-api/.gitignore @@ -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 diff --git a/llmx-core/llmx-core-api/build.gradle.kts b/llmx-core/llmx-core-api/build.gradle.kts new file mode 100644 index 0000000..ccff2b8 --- /dev/null +++ b/llmx-core/llmx-core-api/build.gradle.kts @@ -0,0 +1,10 @@ +ext{ + set(ProjectFlags.USE_SPRING_CLOUD_BOM,true) +} +dependencies { + api(project(Modules.Core.COMMON)) + api(libs.org.reactivestreams.reactiveStreams) + // Spring Cloud OpenFeign + implementation(libs.springCloudStarter.openfeign) +} + diff --git a/llmx-core/llmx-core-api/src/main/kotlin/org/jcnc/llmx/core/api/LLMCoreFeignAutoConfiguration.kt b/llmx-core/llmx-core-api/src/main/kotlin/org/jcnc/llmx/core/api/LLMCoreFeignAutoConfiguration.kt new file mode 100644 index 0000000..54052b2 --- /dev/null +++ b/llmx-core/llmx-core-api/src/main/kotlin/org/jcnc/llmx/core/api/LLMCoreFeignAutoConfiguration.kt @@ -0,0 +1,14 @@ +package org.jcnc.llmx.core.api + +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Configuration + +/** + *LLM 核心Feign自动配置 + * + * @since 2025-05-08 16:48:23 + * @author gewuyou + */ +@Configuration +@EnableFeignClients(basePackages = ["org.jcnc.llmx.core.api"]) +open class LLMCoreFeignAutoConfiguration \ No newline at end of file diff --git a/llmx-core/llmx-core-api/src/main/kotlin/org/jcnc/llmx/core/api/LLMCoreFeignClient.kt b/llmx-core/llmx-core-api/src/main/kotlin/org/jcnc/llmx/core/api/LLMCoreFeignClient.kt new file mode 100644 index 0000000..f1257b3 --- /dev/null +++ b/llmx-core/llmx-core-api/src/main/kotlin/org/jcnc/llmx/core/api/LLMCoreFeignClient.kt @@ -0,0 +1,21 @@ +package org.jcnc.llmx.core.api + +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.reactivestreams.Publisher +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody + +/** + *LLM 核心客户端 + * + * @since 2025-05-08 16:22:22 + * @author gewuyou + */ +@FeignClient(name = "llmx-core-service") // name 对应 Nacos 中注册的服务名 +fun interface LLMCoreFeignClient { + @PostMapping("/api/v1/chat/stream", consumes = [MediaType.APPLICATION_JSON_VALUE]) + fun chat(@RequestBody request: ChatRequest): Publisher +} \ No newline at end of file diff --git a/llmx-core/llmx-core-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/llmx-core/llmx-core-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4b731f2 --- /dev/null +++ b/llmx-core/llmx-core-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.jcnc.llmx.core.api.LLMCoreFeignAutoConfiguration \ No newline at end of file diff --git a/llmx-core/llmx-core-common/.gitattributes b/llmx-core/llmx-core-common/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/llmx-core/llmx-core-common/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/llmx-core/llmx-core-common/.gitignore b/llmx-core/llmx-core-common/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/llmx-core/llmx-core-common/.gitignore @@ -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 diff --git a/llmx-core/llmx-core-common/build.gradle.kts b/llmx-core/llmx-core-common/build.gradle.kts new file mode 100644 index 0000000..f9b855f --- /dev/null +++ b/llmx-core/llmx-core-common/build.gradle.kts @@ -0,0 +1,7 @@ +ext{ + set(ProjectFlags.USE_SPRING_BOOT_BOM,true) +} +dependencies { + implementation(libs.jackson.core) + implementation(libs.jackson.module.kotlin) +} diff --git a/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/deserializer/MultiModalContentDeserializer.kt b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/deserializer/MultiModalContentDeserializer.kt new file mode 100644 index 0000000..d12674f --- /dev/null +++ b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/deserializer/MultiModalContentDeserializer.kt @@ -0,0 +1,42 @@ +package org.jcnc.llmx.core.common.deserializer + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.node.ObjectNode +import org.jcnc.llmx.core.common.entities.request.MultiModalContent + +/** + * 多模态内容反序列化器 + * + * 该类用于将JSON表示的多模态内容反序列化为MultiModalContent数据结构 + * 主要处理两种情况:文本内容和图像内容,并相应地创建Text或Image实例 + * 如果JSON中不包含已知的内容类型,则抛出IllegalArgumentException + * + * @since 2025-05-08 20:41:15 + * @author gewuyou + */ +class MultiModalContentDeserializer : JsonDeserializer() { + /** + * 反序列化方法 + * + * 根据JSON节点中的内容类型,创建相应的MultiModalContent实例 + * 如果内容类型未知,则抛出异常 + * + * @param p JsonParser对象,用于解析JSON输入 + * @param ctxt DeserializationContext对象,提供反序列化上下文 + * @return MultiModalContent实例,表示反序列化的多模态内容 + * @throws IllegalArgumentException 如果JSON节点中的内容类型未知 + */ + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): MultiModalContent { + // 解析JSON输入并将其读取为ObjectNode + val node: ObjectNode = p.codec.readTree(p) + + // 根据JSON节点中的内容类型,返回相应的MultiModalContent实例 + return when { + node.has("text") -> MultiModalContent.Text(node["text"].asText()) + node.has("image") -> MultiModalContent.Image(node["image"].asText()) + else -> throw IllegalArgumentException("Unknown content type in: $node") + } + } +} diff --git a/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/request/ChatRequest.kt b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/request/ChatRequest.kt new file mode 100644 index 0000000..28be69d --- /dev/null +++ b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/request/ChatRequest.kt @@ -0,0 +1,52 @@ +package org.jcnc.llmx.core.common.entities.request + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.jcnc.llmx.core.common.deserializer.MultiModalContentDeserializer + +/** + * MultiModalContent密封类,用于定义消息内容的多种模态 + * 这是一个密封类,意味着所有的子类都需要在这个文件中定义 + */ +@JsonDeserialize(using = MultiModalContentDeserializer::class) +sealed class MultiModalContent { + /** + * Text数据类,表示文本内容的模态 + * @param text 具体的文本内容 + */ + data class Text(val text: String) : MultiModalContent() + + /** + * Image数据类,表示图像内容的模态 + * @param image 图像的base64 + */ + data class Image(val image: String) : MultiModalContent() +} + +/** + * ChatRequest数据类,用于封装聊天请求的参数 + * @since 2025-04-25 17:07:02 + * @author gewuyou + * @param prompt 用户的聊天提示或消息,是聊天请求的主要输入 + * @param model 使用的聊天模型名称,决定了解析和响应的方式 + * @param messages 包含多个模态消息的列表,每个消息有特定的角色和内容 + * @param options 可选的额外参数集合,用于定制聊天请求的行为和输出 + * 可以包括如最大回复长度、温度(随机性)等 + */ +data class ChatRequest( + val prompt: String? = "", + val model: String, + val messages: List =listOf(), + val options: Map = mapOf() +) + +/** + * MultiModalMessage数据类,用于定义聊天请求中的单个消息 + * @param role 消息的角色,可以是"system", "user", "assistant"等 + * @param content 消息的内容,可以是文本、图像等多种模态的组合 + */ +data class MultiModalMessage( + val role: String, // "system", "user", "assistant" + val content: List +) + + diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/request/EmbeddingRequest.kt b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/request/EmbeddingRequest.kt similarity index 90% rename from llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/request/EmbeddingRequest.kt rename to llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/request/EmbeddingRequest.kt index d30ea3d..6a9603f 100644 --- a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/request/EmbeddingRequest.kt +++ b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/request/EmbeddingRequest.kt @@ -1,4 +1,4 @@ -package org.jcnc.llmx.core.spi.entities.request +package org.jcnc.llmx.core.common.entities.request /** * 嵌入请求类 diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/response/ChatResponse.kt b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/response/ChatResponse.kt similarity index 95% rename from llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/response/ChatResponse.kt rename to llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/response/ChatResponse.kt index f0ca30c..4fb9730 100644 --- a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/response/ChatResponse.kt +++ b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/response/ChatResponse.kt @@ -1,4 +1,4 @@ -package org.jcnc.llmx.core.spi.entities.response +package org.jcnc.llmx.core.common.entities.response /** * 聊天响应类 diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/response/EmbeddingResponse.kt b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/response/EmbeddingResponse.kt similarity index 91% rename from llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/response/EmbeddingResponse.kt rename to llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/response/EmbeddingResponse.kt index fa60996..09ee7c8 100644 --- a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/response/EmbeddingResponse.kt +++ b/llmx-core/llmx-core-common/src/main/kotlin/org/jcnc/llmx/core/common/entities/response/EmbeddingResponse.kt @@ -1,4 +1,4 @@ -package org.jcnc.llmx.core.spi.entities.response +package org.jcnc.llmx.core.common.entities.response /** * 嵌入响应类 diff --git a/llmx-core/llmx-core-service/build.gradle.kts b/llmx-core/llmx-core-service/build.gradle.kts index c35e025..24c0ba8 100644 --- a/llmx-core/llmx-core-service/build.gradle.kts +++ b/llmx-core/llmx-core-service/build.gradle.kts @@ -1,19 +1,17 @@ extra { // 开启springboot - setProperty(ProjectFlags.USE_SPRING_BOOT, true) + setProperty(ProjectFlags.USE_SPRING_BOOT_WEB, true) setProperty(ProjectFlags.USE_SPRING_CLOUD_BOM,true) + setProperty(ProjectFlags.USE_NACOS_DEPENDENCE, true) + setProperty(ProjectFlags.USE_DAO_DEPENDENCE, 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)) + implementation(project(Modules.Core.COMMON)) // Kotlin Coroutines implementation(libs.kotlinx.coruntes.reactor) diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/LlmxCoreServiceApplication.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/LlmxCoreServiceApplication.kt index c9b9fec..0a06e34 100644 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/LlmxCoreServiceApplication.kt +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/LlmxCoreServiceApplication.kt @@ -6,7 +6,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient @SpringBootApplication @EnableDiscoveryClient -class LlmxCoreServiceApplication +open class LlmxCoreServiceApplication /** * 程序的入口点。 diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/AppConfiguration.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/AppConfiguration.kt index fda061a..bb50d8c 100644 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/AppConfiguration.kt +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/AppConfiguration.kt @@ -1,6 +1,5 @@ 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 @@ -13,8 +12,7 @@ import org.springframework.web.reactive.function.client.WebClient * @author gewuyou */ @Configuration -@EnableConfigurationProperties(ModelProperties::class) -class AppConfiguration { +open class AppConfiguration { /** * 创建一个配置了负载均衡的WebClient构建器 * @@ -25,7 +23,7 @@ class AppConfiguration { */ @Bean @LoadBalanced - fun webClientBuilder(): WebClient.Builder { + open fun webClientBuilder(): WebClient.Builder { return WebClient.builder() } } \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/CorsConfig.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/CorsConfig.kt new file mode 100644 index 0000000..cfda34b --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/CorsConfig.kt @@ -0,0 +1,36 @@ +package org.jcnc.llmx.core.service.config + +import com.gewuyou.forgeboot.core.extension.log +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +/** + * CORS配置类 + * + * 该类用于全局配置跨域请求设置,以确保前端应用可以与后端服务进行通信 + * 主要通过重写addCorsMappings方法来配置跨域请求映射,以及通过corsWebFilter方法来提供更细粒度的跨域支持 + * + * @since 2025-04-02 17:03:41 + * @author gewuyou + */ +@Configuration +open class CorsConfig : WebMvcConfigurer { + /** + * 添加跨域请求映射 + * + * 该方法重写了父接口中的addCorsMappings方法,用于配置全局的跨域请求规则 + * 主要配置了允许所有路径、所有来源、常见HTTP方法、所有请求头的跨域请求,并设置了不携带Cookie以及预检请求缓存时间 + * + * @param registry 跨域请求注册表,用于添加跨域请求映射 + */ + override fun addCorsMappings(registry: CorsRegistry) { + log.info("Web CORS配置生效") + registry.addMapping("/**") // 匹配所有路径 + .allowedOrigins("*") // 允许所有来源(生产环境建议指定具体域名) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的HTTP方法 + .allowedHeaders("*") // 允许所有请求头 + .allowCredentials(false) // 是否允许携带Cookie(true时需要明确指定allowedOrigins,不能为*) + .maxAge(3600) // 预检请求缓存时间(秒) + } +} diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/ModelProperties.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/ModelProperties.kt deleted file mode 100644 index a6d8091..0000000 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/config/ModelProperties.kt +++ /dev/null @@ -1,21 +0,0 @@ -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 = emptyMap() -} \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/controller/ChatController.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/controller/ChatController.kt index ce5ea4c..a851e34 100644 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/controller/ChatController.kt +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/controller/ChatController.kt @@ -1,11 +1,12 @@ package org.jcnc.llmx.core.service.controller +import com.gewuyou.forgeboot.core.extension.log import com.gewuyou.forgeboot.webmvc.version.annotation.ApiVersion -import kotlinx.coroutines.flow.Flow +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart 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.reactivestreams.Publisher +import org.springframework.http.MediaType import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -33,8 +34,23 @@ class ChatController( * @param request 聊天请求对象,包含了发起聊天所需的各种参数和用户信息 * @return 返回一个Flow流,流中依次提供了聊天响应的部分数据,允许异步处理和逐步消费响应内容 */ - @PostMapping("/stream") - fun chat(@RequestBody request: ChatRequest): Flow { + @PostMapping("/stream", produces = [MediaType.APPLICATION_NDJSON_VALUE]) + fun chat(@RequestBody request: ChatRequest): Publisher { return llmServiceImpl.chat(request) } + /** + * 处理多模态聊天请求的函数 + * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 + * + * @param request 包含聊天请求信息的数据类 + * @return 返回一个发布者,用于异步地发送聊天响应的部分 + * + * 注意:该函数被设计为异步处理,以提高性能和响应性 + * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 + */ + @PostMapping("/streamMultimodality", produces = [MediaType.APPLICATION_NDJSON_VALUE]) + fun multimodalityChat(@RequestBody request: ChatRequest): Publisher{ + log.info("request: {}", request) + return llmServiceImpl.multimodalityChat(request) + } } \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/domain/model/ModelRouteMapping.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/domain/model/ModelRouteMapping.kt new file mode 100644 index 0000000..a4dea5c --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/domain/model/ModelRouteMapping.kt @@ -0,0 +1,74 @@ +package org.jcnc.llmx.core.service.domain.model + +import jakarta.persistence.* +import org.hibernate.annotations.ColumnDefault +import java.time.OffsetDateTime + +/** + * 表示模型与其路由信息之间的映射关系。 + * 该类用于定义模型与对应服务之间的关联, + * 包括相关的描述信息和状态信息。 + * + * 使用注解来定义表名、主键生成策略以及字段与数据库列的映射关系。 + */ +@Entity +@Table(name = "model_route_mapping", schema = "core") +open class ModelRouteMapping { + /** + * 映射关系的唯一标识符。 + * 使用序列生成器来自动生成ID。 + */ + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "model_route_mapping_id_gen") + @SequenceGenerator( + name = "model_route_mapping_id_gen", + sequenceName = "model_route_mapping_id_seq", + allocationSize = 1 + ) + @Column(name = "id", nullable = false) + open var id: Long? = null + + /** + * 模型名称。 + * 用于标识与路由信息相关联的模型。 + */ + @Column(name = "model", nullable = false, length = 50) + open lateinit var model: String + + /** + * 对应的服务名称。 + * 表示该模型所关联的具体服务实例。 + */ + @Column(name = "service_name", nullable = false, length = 50) + open lateinit var serviceName: String + + /** + * 映射描述。 + * 提供有关映射的额外信息,便于理解和维护。 + */ + @Column(name = "description", length = 150) + open var description: String? = null + + /** + * 映射状态。 + * 表示当前映射是否启用。默认情况下,新映射是启用的。 + */ + @ColumnDefault("true") + @Column(name = "enabled") + open var enabled: Boolean=true + + /** + * 创建时间。 + * 默认为创建新映射时的当前时间戳。 + */ + @ColumnDefault("CURRENT_TIMESTAMP") + @Column(name = "create_at", nullable = false) + open var createAt: OffsetDateTime? = null + + /** + * 最后更新时间。 + * 记录映射信息的最后修改时间。 + */ + @Column(name = "update_at") + open var updateAt: OffsetDateTime? = null +} diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/manager/ModelRouteManager.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/manager/ModelRouteManager.kt index 72ef815..3c59b71 100644 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/manager/ModelRouteManager.kt +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/manager/ModelRouteManager.kt @@ -1,7 +1,8 @@ package org.jcnc.llmx.core.service.manager -import org.jcnc.llmx.core.service.config.ModelProperties +import com.gewuyou.forgeboot.core.extension.log +import org.jcnc.llmx.core.service.repositories.ModelRouteMappingRepository import org.springframework.stereotype.Component /** @@ -15,7 +16,7 @@ import org.springframework.stereotype.Component */ @Component class ModelRouteManager( - private val modelProperties: ModelProperties + private val modelRouteMappingRepository: ModelRouteMappingRepository, ) { /** * 根据模型名查找对应服务 @@ -28,8 +29,11 @@ class ModelRouteManager( * @throws IllegalArgumentException 如果模型名不匹配任何已知前缀,抛出此异常 */ fun resolveServiceName(model: String): String { - for ((prefix, serviceName) in modelProperties.modelServiceMap) { - if (model.startsWith(prefix)) { + val modelServiceMap = modelRouteMappingRepository.findAllByEnabled(true) + .associate { it.model to it.serviceName } + log.info("modelServiceMap: $modelServiceMap") + for ((model, serviceName) in modelServiceMap) { + if (model.startsWith(model)) { return serviceName } } diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/repositories/ModelRouteMappingRepository.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/repositories/ModelRouteMappingRepository.kt new file mode 100644 index 0000000..cf4b7ec --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/repositories/ModelRouteMappingRepository.kt @@ -0,0 +1,10 @@ +package org.jcnc.llmx.core.service.repositories + +import org.jcnc.llmx.core.service.domain.model.ModelRouteMapping +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.JpaSpecificationExecutor + +interface ModelRouteMappingRepository : JpaRepository, + JpaSpecificationExecutor { + fun findAllByEnabled(isEnabled: Boolean): List +} \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/LLMService.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/LLMService.kt index badf32f..bd0ae6f 100644 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/LLMService.kt +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/LLMService.kt @@ -1,8 +1,9 @@ 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.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.reactivestreams.Publisher import org.springframework.web.bind.annotation.PostMapping @@ -12,7 +13,7 @@ import org.springframework.web.bind.annotation.PostMapping * @since 2025-04-26 17:38:18 * @author gewuyou */ -fun interface LLMService { +interface LLMService { /** * 初始化与聊天服务的连接,以处理聊天请求 * @@ -23,5 +24,6 @@ fun interface LLMService { * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 */ @PostMapping("/chat") - fun chat(request: ChatRequest): Flow + fun chat(request: ChatRequest): Publisher + fun multimodalityChat(request: ChatRequest): Publisher } \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/impl/LLMServiceImpl.kt b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/impl/LLMServiceImpl.kt index cdf45d8..2222156 100644 --- a/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/impl/LLMServiceImpl.kt +++ b/llmx-core/llmx-core-service/src/main/kotlin/org/jcnc/llmx/core/service/service/impl/LLMServiceImpl.kt @@ -1,11 +1,11 @@ package org.jcnc.llmx.core.service.service.impl -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.reactive.asFlow + +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart 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.reactivestreams.Publisher import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient @@ -30,7 +30,7 @@ class LLMServiceImpl( * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 */ - override fun chat(request: ChatRequest): Flow { + override fun chat(request: ChatRequest): Publisher { val serviceName = modelRouteManager.resolveServiceName(request.model) val webClient = webClientBuilder.build() return webClient.post() @@ -38,6 +38,15 @@ class LLMServiceImpl( .bodyValue(request) .retrieve() .bodyToFlux(ChatResponsePart::class.java) - .asFlow() + } + + override fun multimodalityChat(request: ChatRequest): Publisher { + val serviceName = modelRouteManager.resolveServiceName(request.model) + val webClient = webClientBuilder.build() + return webClient.post() + .uri("http://$serviceName/provider/multimodalityChat") + .bodyValue(request) + .retrieve() + .bodyToFlux(ChatResponsePart::class.java) } } \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/resources/application-dev.yml b/llmx-core/llmx-core-service/src/main/resources/application-dev.yml index f6ea2d1..54b155f 100644 --- a/llmx-core/llmx-core-service/src/main/resources/application-dev.yml +++ b/llmx-core/llmx-core-service/src/main/resources/application-dev.yml @@ -1,13 +1,2 @@ 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 - - + port: 8081 \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/resources/application-master.yml b/llmx-core/llmx-core-service/src/main/resources/application-master.yml new file mode 100644 index 0000000..5af7893 --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/resources/application-master.yml @@ -0,0 +1,2 @@ +server: + port: 9002 \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/resources/application-test.yml b/llmx-core/llmx-core-service/src/main/resources/application-test.yml new file mode 100644 index 0000000..8091132 --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/resources/application-test.yml @@ -0,0 +1,4 @@ +server: + port: 9002 + + diff --git a/llmx-core/llmx-core-service/src/main/resources/application.yml b/llmx-core/llmx-core-service/src/main/resources/application.yml index 4e5448d..3d7808a 100644 --- a/llmx-core/llmx-core-service/src/main/resources/application.yml +++ b/llmx-core/llmx-core-service/src/main/resources/application.yml @@ -1,5 +1,3 @@ spring: - application: - name: llmhub-core-service profiles: active: dev diff --git a/llmx-core/llmx-core-service/src/main/resources/bootstrap-dev.yml b/llmx-core/llmx-core-service/src/main/resources/bootstrap-dev.yml index d1e2592..62646f9 100644 --- a/llmx-core/llmx-core-service/src/main/resources/bootstrap-dev.yml +++ b/llmx-core/llmx-core-service/src/main/resources/bootstrap-dev.yml @@ -1,12 +1,25 @@ spring: cloud: nacos: + ip: 127.0.0.1 username: nacos - password: L4s6f9y3 + password: L4s6f9y3, server-addr: 49.235.96.75:8848 - ip: 192.168.1.6 discovery: - server-addr: ${spring.cloud.nacos.server-addr} + ip: ${spring.cloud.nacos.ip} username: ${spring.cloud.nacos.username} password: ${spring.cloud.nacos.password} - ip: ${spring.cloud.nacos.ip} \ No newline at end of file + server-addr: ${spring.cloud.nacos.server-addr} + group: llmx-${spring.profiles.active} + namespace: a17d57ec-4fd9-44c7-a617-7f6003a0b332 + config: + file-extension: yaml + namespace: a17d57ec-4fd9-44c7-a617-7f6003a0b332 + refresh-enabled: true + extension-configs: + - data-id: ${spring.application.name}-${spring.profiles.active}.yaml + refresh: true + group: ${spring.application.name} + - data-id: common-db.yaml + refresh: true + group: infra \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/resources/bootstrap-master.yml b/llmx-core/llmx-core-service/src/main/resources/bootstrap-master.yml new file mode 100644 index 0000000..473128d --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/resources/bootstrap-master.yml @@ -0,0 +1,23 @@ +spring: + cloud: + nacos: + username: nacos + password: L4s6f9y3, + server-addr: 49.235.96.75:8848 + discovery: + server-addr: ${spring.cloud.nacos.server-addr} + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} + group: llmx-${spring.profiles.active} + namespace: ab34d859-6f1a-4f28-ac6b-27a7410ab27b + config: + file-extension: yaml + namespace: ab34d859-6f1a-4f28-ac6b-27a7410ab27b + refresh-enabled: true + extension-configs: + - data-id: ${spring.application.name}-${spring.profiles.active}.yaml + refresh: true + group: ${spring.application.name} + - data-id: common-db.yaml + refresh: true + group: infra \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/resources/bootstrap-test.yml b/llmx-core/llmx-core-service/src/main/resources/bootstrap-test.yml new file mode 100644 index 0000000..2870fad --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/resources/bootstrap-test.yml @@ -0,0 +1,23 @@ +spring: + cloud: + nacos: + server-addr: llmx-nacos:8848 + username: nacos + password: L4s6f9y3, + discovery: + server-addr: ${spring.cloud.nacos.server-addr} + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} + group: llmx-${spring.profiles.active} + namespace: 54a289f7-5f4a-4c83-8a0a-199defa35458 + config: + file-extension: yaml + namespace: 54a289f7-5f4a-4c83-8a0a-199defa35458 + refresh-enabled: true + extension-configs: + - data-id: ${spring.application.name}-${spring.profiles.active}.yaml + refresh: true + group: ${spring.application.name} + - data-id: common-db.yaml + refresh: true + group: infra \ No newline at end of file diff --git a/llmx-core/llmx-core-service/src/main/resources/bootstrap.yml b/llmx-core/llmx-core-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..dd04bca --- /dev/null +++ b/llmx-core/llmx-core-service/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + application: + name: llmx-core-service + profiles: + active: dev diff --git a/llmx-core/llmx-core-spi/build.gradle.kts b/llmx-core/llmx-core-spi/build.gradle.kts index 923d5d7..8821f83 100644 --- a/llmx-core/llmx-core-spi/build.gradle.kts +++ b/llmx-core/llmx-core-spi/build.gradle.kts @@ -4,6 +4,7 @@ apply { plugin(libs.plugins.kotlin.plugin.spring.get().pluginId) } dependencies { - compileOnly(libs.kotlinx.coruntes.reactor) compileOnly(libs.springBootStarter.web) + api(libs.org.reactivestreams.reactiveStreams) + api(project(Modules.Core.COMMON)) } \ No newline at end of file diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/request/ChatRequest.kt b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/request/ChatRequest.kt deleted file mode 100644 index ada4e27..0000000 --- a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/entities/request/ChatRequest.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 = mapOf() -) diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/provider/LLMProvider.kt b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/provider/LLMProvider.kt index f390c4b..69c41fb 100644 --- a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/provider/LLMProvider.kt +++ b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/provider/LLMProvider.kt @@ -1,11 +1,12 @@ 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.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.request.EmbeddingRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.jcnc.llmx.core.common.entities.response.EmbeddingResponse +import org.reactivestreams.Publisher +import org.springframework.http.MediaType import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -29,9 +30,20 @@ interface LLMProvider { * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 */ - @PostMapping("/chat") - fun chat(request: ChatRequest): Flow - + @PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) + fun chat(request: ChatRequest): Publisher + /** + * 处理多模态聊天请求的函数 + * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 + * + * @param request 包含聊天请求信息的数据类 + * @return 返回一个发布者,用于异步地发送聊天响应的部分 + * + * 注意:该函数被设计为异步处理,以提高性能和响应性 + * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 + */ + @PostMapping("/multimodalityChat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) + fun multimodalityChat(request: ChatRequest): Publisher /** * 嵌入功能方法 * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 diff --git a/llmx-impl/llmx-impl-baiLian/build.gradle.kts b/llmx-impl/llmx-impl-baiLian/build.gradle.kts deleted file mode 100644 index 12471f2..0000000 --- a/llmx-impl/llmx-impl-baiLian/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ - -// 开启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) -} - diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/adapter/DashScopeAdapter.kt b/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/adapter/DashScopeAdapter.kt deleted file mode 100644 index d1b31c0..0000000 --- a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/adapter/DashScopeAdapter.kt +++ /dev/null @@ -1,105 +0,0 @@ -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, - requestBody: Any, - extractContent: (String) -> ChatResponsePart, - dispatcher: CoroutineDispatcher = Dispatchers.IO - ): Flow = 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() - } - } -} diff --git a/llmx-impl/llmx-impl-baiLian/src/main/resources/application-dev.yml b/llmx-impl/llmx-impl-baiLian/src/main/resources/application-dev.yml deleted file mode 100644 index 7bb8cd6..0000000 --- a/llmx-impl/llmx-impl-baiLian/src/main/resources/application-dev.yml +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/llmx-impl/llmx-impl-baiLian/src/main/resources/application-prod.yml b/llmx-impl/llmx-impl-baiLian/src/main/resources/application-prod.yml deleted file mode 100644 index b39b48c..0000000 --- a/llmx-impl/llmx-impl-baiLian/src/main/resources/application-prod.yml +++ /dev/null @@ -1,22 +0,0 @@ -server: - port: 9003 -spring: - application: - name: llmhub-impl-baiLian - cloud: - nacos: - discovery: - server-addr: 49.235.96.75:9001 # Nacos 服务地址 -# 阿里云配置 -aliyun: - # DashScope服务配置 - dash: - # 访问凭证配置 - scope: - access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID - access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥 - endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点 - workspace-id: llm-axfkuqft05uzbjpi # 工作区ID - api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥 - app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID - base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL diff --git a/llmx-impl/llmx-impl-baiLian/src/main/resources/application.yml b/llmx-impl/llmx-impl-baiLian/src/main/resources/application.yml deleted file mode 100644 index b9fc61b..0000000 --- a/llmx-impl/llmx-impl-baiLian/src/main/resources/application.yml +++ /dev/null @@ -1,18 +0,0 @@ -spring: - application: - name: llmhub-impl-baiLian - profiles: - active: dev -# 阿里云配置 -aliyun: - # DashScope服务配置 - dash: - # 访问凭证配置 - scope: - access-key-id: LTAI5tHiA2Ry3XTAfoSEJW6z # 阿里云访问密钥ID - access-key-secret: K5sf4FxZZuUgLEFnyfepBfMqFGmDcD # 阿里云访问密钥密钥 - endpoint: bailian.cn-beijing.aliyuncs.com # 阿里云服务端点 - workspace-id: llm-axfkuqft05uzbjpi # 工作区ID - api-key: sk-78af4dd964a94f4cb373851064dbdc12 # API密钥 - app-id: 3fae0bbab2e54a90a37aa02cd12dd62c # 应用ID - base-url: https://dashscope.aliyuncs.com/api/v1/apps/ # 基础API URL \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/.gitattributes b/llmx-impl/llmx-impl-bailian/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/llmx-impl/llmx-impl-bailian/.gitignore b/llmx-impl/llmx-impl-bailian/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/.gitignore @@ -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 diff --git a/llmx-impl/llmx-impl-bailian/build.gradle.kts b/llmx-impl/llmx-impl-bailian/build.gradle.kts new file mode 100644 index 0000000..cd168b9 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/build.gradle.kts @@ -0,0 +1,6 @@ +// 开启LLM实现平台依赖 +setProperty(ProjectFlags.USE_LLM_IMPL_PLATFORM_DEPENDENCE, true) +dependencies { + implementation(libs.aliyun.bailian) +} + diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/LlmxImplBailianApplication.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/LlmxImplBailianApplication.kt similarity index 100% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/LlmxImplBailianApplication.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/LlmxImplBailianApplication.kt diff --git a/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/adapter/DashScopeAdapter.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/adapter/DashScopeAdapter.kt new file mode 100644 index 0000000..1d83300 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/adapter/DashScopeAdapter.kt @@ -0,0 +1,137 @@ +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.FlowCollector +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.common.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, + requestBody: Any, + extractContent: (String) -> ChatResponsePart, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): Flow = flow { + val requestJson = objectMapper.writeValueAsString(requestBody) + log.info("📤 请求参数: {}", requestJson) + + val request = buildRequest(url, headers, requestJson) + + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + log.error("🚨 DashScope 请求失败: code ${response.code} message: ${response.message} body: ${response.body}") + throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}") + } + + val responseBody = response.body ?: throw RuntimeException("❌ DashScope 响应体为空") + + responseBody.charStream().buffered().use { reader -> + val allContent = StringBuilder() + try { + processResponse(dispatcher, reader, extractContent, allContent) + log.info("📦 完整内容: {}", allContent) + } catch (e: Exception) { + log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e) + throw e + } + } + } + } + + /** + * 处理响应流 + * + * 本函数读取HTTP响应中的数据行,解析并提取内容。 + * 它在循环中读取每一行,并使用提供的函数提取内容。 + * + * @param dispatcher 协程调度器 + * @param reader 响应体的BufferedReader + * @param extractContent 一个函数,用于从JSON响应中提取内容 + * @param allContent 保存所有提取内容的StringBuilder + */ + private suspend fun FlowCollector.processResponse( + dispatcher: CoroutineDispatcher, + reader: BufferedReader, + extractContent: (String) -> ChatResponsePart, + allContent: StringBuilder, + ) { + while (currentCoroutineContext().isActive) { + val line = withContext(dispatcher) { + reader.readLine() + } ?: break + + log.debug("📥 接收到行: {}", line) + + if (line.startsWith("data:")) { + val jsonPart = line.removePrefix("data:").trim() + try { + val part = extractContent(jsonPart) + allContent.append(part.content) + log.debug("✅ 提取内容: {}", part) + emit(part) + } catch (e: Exception) { + log.warn("⚠️ 无法解析 JSON: {}", jsonPart, e) + } + } + } + } + + /** + * 构建请求 + * + * 本函数构建一个OkHttp请求对象,用于发送聊天请求。 + * + * @param url 请求的URL + * @param headers 请求的头部信息 + * @param json 请求的主体内容的JSON字符串 + * @return 返回构建好的Request对象 + */ + private fun buildRequest(url: String, headers: Map, json: String): Request { + return Request.Builder() + .url(url) + .headers(headers.toHeaders()) + .post(json.toRequestBody("application/json".toMediaType())) + .build() + } +} diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/ClientConfig.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/ClientConfig.kt similarity index 100% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/ClientConfig.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/ClientConfig.kt diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/DashScopeConfig.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/DashScopeConfig.kt similarity index 100% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/DashScopeConfig.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/DashScopeConfig.kt diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/entities/DashScopeProperties.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/entities/DashScopeProperties.kt similarity index 92% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/entities/DashScopeProperties.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/entities/DashScopeProperties.kt index 608d3b5..7b79431 100644 --- a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/entities/DashScopeProperties.kt +++ b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/config/entities/DashScopeProperties.kt @@ -44,5 +44,9 @@ class DashScopeProperties { * 应用调用基路径,例如 /api/v1。 */ var baseUrl: String = "" + /** + * 多模态对话调用路径 + */ + var multimodalityUrl: String = "" } diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt similarity index 66% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt index 028c275..039502e 100644 --- a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt +++ b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt @@ -1,16 +1,20 @@ 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 kotlinx.coroutines.reactive.asPublisher +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.request.EmbeddingRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.jcnc.llmx.core.common.entities.response.EmbeddingResponse + import org.jcnc.llmx.core.spi.provider.LLMProvider import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService +import org.reactivestreams.Publisher import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController + /** *百炼提供商 * @@ -18,10 +22,9 @@ import org.springframework.web.bind.annotation.RestController * @author gewuyou */ @RestController -//@RequestMapping("/provider") class BaiLianProvider( private val baiLianModelService: BaiLianModelService -): LLMProvider { +) : LLMProvider { /** * 初始化与聊天服务的连接,以处理聊天请求 * @@ -31,9 +34,12 @@ class BaiLianProvider( * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 */ -// @PostMapping("/chat") - override fun chat(@RequestBody request: ChatRequest): Flow { - return baiLianModelService.streamChat(request) + override fun chat(@RequestBody request: ChatRequest): Publisher { + return baiLianModelService.streamChat(request).asPublisher() + } + + override fun multimodalityChat(@RequestBody request: ChatRequest): Publisher { + return baiLianModelService.streamMultimodalityChat(request).asPublisher() } /** diff --git a/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/entities/response/DashScopeResponse.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/entities/response/DashScopeResponse.kt new file mode 100644 index 0000000..8c7c907 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/entities/response/DashScopeResponse.kt @@ -0,0 +1,117 @@ +package org.jcnc.llmx.impl.baiLian.entities.response + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Dash范围响应类 + * + * 该类用于表示从Dash服务接收到的响应数据结构它包含了响应的输出信息、使用情况和请求ID + * 主要用于JSON序列化和反序列化 + * + * @param output 输出信息,包括生成的内容和完成原因 + * @param usage 使用情况,包括输入和输出的令牌详情 + * @param requestId 请求ID,用于跟踪和调试请求 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class DashScopeResponse( + val output: Output?, + val usage: Usage?, + @JsonProperty("request_id") + val requestId: String? +) + +/** + * 输出数据类 + * + * 该类包含Dash服务生成的内容,主要是通过choices列表来提供可能的回复选项 + * + * @param choices 生成内容的选项列表 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class Output( + val choices: List? +) + +/** + * 选择项数据类 + * + * 该类表示生成内容的一个选项,包括完成原因和具体的消息内容 + * + * @param finishReason 完成原因,解释生成内容结束的原因 + * @param message 消息内容,包括角色和具体内容 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class Choice( + @JsonProperty("finish_reason") + val finishReason: String?, + val message: Message? +) + +/** + * 消息数据类 + * + * 该类表示一条消息,包括消息的角色(如用户、助手)和具体内容 + * 具体内容通过一个内容列表来表示 + * + * @param role 消息的角色,如用户、助手等 + * @param content 消息的具体内容列表 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class Message( + val role: String?, + val content: List? +) + +/** + * 内容数据类 + * + * 该类表示消息中的具体内容,目前只支持文本内容 + * + * @param text 文本内容 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class Content( + val text: String? +) + +/** + * 使用情况数据类 + * + * 该类详细记录了Dash服务的使用情况,包括输入和输出的令牌数量和详情 + * + * @param inputTokensDetails 输入令牌的详情 + * @param outputTokensDetails 输出令牌的详情 + * @param inputTokens 输入令牌的数量 + * @param outputTokens 输出令牌的数量 + * @param totalTokens 总令牌的数量 + * @param imageTokens 图像令牌的数量,如果适用 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class Usage( + @JsonProperty("input_tokens_details") + val inputTokensDetails: TokenDetail?, + @JsonProperty("output_tokens_details") + val outputTokensDetails: TokenDetail?, + @JsonProperty("input_tokens") + val inputTokens: Int?, + @JsonProperty("output_tokens") + val outputTokens: Int?, + @JsonProperty("total_tokens") + val totalTokens: Int?, + @JsonProperty("image_tokens") + val imageTokens: Int? +) + +/** + * 令牌详情数据类 + * + * 该类提供了令牌的详细信息,目前仅支持文本令牌的数量 + * + * @param textTokens 文本令牌的数量 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +data class TokenDetail( + @JsonProperty("text_tokens") + val textTokens: Int? +) diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/BaiLianModelService.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/BaiLianModelService.kt similarity index 54% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/BaiLianModelService.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/BaiLianModelService.kt index 3f6a488..d6c6a6b 100644 --- a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/BaiLianModelService.kt +++ b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/BaiLianModelService.kt @@ -1,8 +1,8 @@ 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 +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart /** @@ -20,4 +20,11 @@ interface BaiLianModelService { * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 */ fun streamChat(request: ChatRequest): Flow + /** + * 使用流式多模态聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 + */ + fun streamMultimodalityChat(request: ChatRequest) : Flow } diff --git a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt similarity index 61% rename from llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt index a0bb423..f5340e6 100644 --- a/llmx-impl/llmx-impl-baiLian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt +++ b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/service/impl/BaiLianModelServiceImpl.kt @@ -4,11 +4,13 @@ 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.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.jcnc.llmx.core.common.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.entities.response.DashScopeResponse import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService import org.springframework.stereotype.Service @@ -47,7 +49,7 @@ class BaiLianModelServiceImpl( // 构造输入参数,主要包括用户的prompt val inputMap = mutableMapOf("prompt" to request.prompt) // 获取会话ID,如果存在,则添加到输入参数中 - val sessionId = (request.options["session_id"] ?: "").toString() + val sessionId = (request.options["session_id"] ?: "") if (StringUtils.hasText(sessionId)) { inputMap["session_id"] = sessionId } @@ -123,4 +125,75 @@ class BaiLianModelServiceImpl( } ) } + + /** + * 使用流式多模态聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 + */ + override fun streamMultimodalityChat(request: ChatRequest): Flow { + // 构造请求URL + val url = dashScopeProperties.multimodalityUrl + 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") + // 构造输入参数,主要包括用户的message + val inputMap = mutableMapOf("messages" to request.messages) + // 构造参数地图,包括是否包含思考、模型ID、增量输出等 + val parametersMap = mutableMapOf( + "incremental_output" to true, + ) + // 构造请求体,包括输入参数、构造的参数地图和调试信息 + val body = mapOf( + "model" to request.model, + "input" to inputMap, + "parameters" to parametersMap, + ) + // 发送流式聊天请求,并处理响应 + return dashScopeAdapter.sendStreamChat( + url, headers, body, + { json: String -> + val response = objectMapper.readValue(json, DashScopeResponse::class.java) + val choices = response + .output + ?.choices + ?.getOrNull(0) + // 提取输出文本 + val text = choices + ?.message + ?.content + ?.getOrNull(0) + ?.text + ?: "" + // 判断是否完成(finish_reason 通常为 "stop",但你这里是字符串 "null") + val finishReason = choices?.finishReason + val done = finishReason == "stop" + // 提取使用情况 + val usage = response.usage + val promptTokens = usage?.inputTokens ?: 0 + val completionTokens = usage?.outputTokens ?: 0 + val totalTokens = usage?.totalTokens ?: 0 + ChatResponsePart( + content = text, + done = done, + usage = Usage( + promptTokens = promptTokens, + completionTokens = completionTokens, + totalTokens = totalTokens + ), + other = mapOf( + "request_id" to (response.requestId ?: ""), + "model" to request.model, + "response" to json + ) + ) + } + ) + } } diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/application-dev.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/application-dev.yml new file mode 100644 index 0000000..0884131 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/application-dev.yml @@ -0,0 +1,2 @@ +server: + port: 8082 \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/application-master.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/application-master.yml new file mode 100644 index 0000000..9beecae --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/application-master.yml @@ -0,0 +1,2 @@ +server: + port: 9003 \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/application-test.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/application-test.yml new file mode 100644 index 0000000..f9c0b55 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/application-test.yml @@ -0,0 +1,2 @@ +server: + port: 9003 diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/application.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/application.yml new file mode 100644 index 0000000..caf4dfc --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: dev \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-dev.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-dev.yml new file mode 100644 index 0000000..2b145bd --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-dev.yml @@ -0,0 +1,22 @@ +spring: + cloud: + nacos: + ip: 127.0.0.1 + username: nacos + password: L4s6f9y3, + server-addr: 49.235.96.75:8848 + discovery: + ip: ${spring.cloud.nacos.ip} + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} + server-addr: ${spring.cloud.nacos.server-addr} + group: llmx-${spring.profiles.active} + namespace: a17d57ec-4fd9-44c7-a617-7f6003a0b332 + config: + file-extension: yaml + namespace: a17d57ec-4fd9-44c7-a617-7f6003a0b332 + refresh-enabled: true + extension-configs: + - data-id: ${spring.application.name}-${spring.profiles.active}.yaml + refresh: true + group: ${spring.application.name} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-master.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-master.yml new file mode 100644 index 0000000..399cdd9 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-master.yml @@ -0,0 +1,20 @@ +spring: + cloud: + nacos: + username: nacos + password: L4s6f9y3, + server-addr: 49.235.96.75:8848 + discovery: + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} + server-addr: ${spring.cloud.nacos.server-addr} + group: llmx-${spring.profiles.active} + namespace: ab34d859-6f1a-4f28-ac6b-27a7410ab27b + config: + file-extension: yaml + namespace: ab34d859-6f1a-4f28-ac6b-27a7410ab27b + refresh-enabled: true + extension-configs: + - data-id: ${spring.application.name}-${spring.profiles.active}.yaml + refresh: true + group: ${spring.application.name} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-test.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-test.yml new file mode 100644 index 0000000..3998b26 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-test.yml @@ -0,0 +1,20 @@ +spring: + cloud: + nacos: + username: nacos + password: L4s6f9y3, + server-addr: llmx-nacos:8848 + discovery: + username: ${spring.cloud.nacos.username} + password: ${spring.cloud.nacos.password} + server-addr: ${spring.cloud.nacos.server-addr} + group: llmx-${spring.profiles.active} + namespace: 54a289f7-5f4a-4c83-8a0a-199defa35458 + config: + file-extension: yaml + namespace: 54a289f7-5f4a-4c83-8a0a-199defa35458 + refresh-enabled: true + extension-configs: + - data-id: ${spring.application.name}-${spring.profiles.active}.yaml + refresh: true + group: ${spring.application.name} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..4e63fe8 --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + application: + name: llmx-impl-bailian + profiles: + active: dev diff --git a/llmx-impl/llmx-impl-bailian/src/test/java/com/gewuyou/bailian/util/Base64Image.java b/llmx-impl/llmx-impl-bailian/src/test/java/com/gewuyou/bailian/util/Base64Image.java new file mode 100644 index 0000000..78420fe --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/test/java/com/gewuyou/bailian/util/Base64Image.java @@ -0,0 +1,21 @@ +package com.gewuyou.bailian.util; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; + +/** + * Base 64 图像 + * + * @author gewuyou + * @since 2025-05-08 18:56:02 + */ +public class Base64Image { + public static void main(String[] args) throws Exception { + byte[] imageBytes = Files.readAllBytes(Paths.get("F:/gewuyou/Project/Idea/demo-llm/src/main/resources/2025俏春装/10147009251.jpg")); + String base64Image = Base64.getEncoder().encodeToString(imageBytes); + String imagePayload = "data:image/jpeg;base64," + base64Image; + + System.out.println(imagePayload); // 你可以把它放到 JSON 中使用 + } +} diff --git a/llmx-impl/llmx-impl-bailian/src/test/java/com/gewuyou/bailian/util/DashScopeLocalImageClient.java b/llmx-impl/llmx-impl-bailian/src/test/java/com/gewuyou/bailian/util/DashScopeLocalImageClient.java new file mode 100644 index 0000000..6d3536f --- /dev/null +++ b/llmx-impl/llmx-impl-bailian/src/test/java/com/gewuyou/bailian/util/DashScopeLocalImageClient.java @@ -0,0 +1,85 @@ +package com.gewuyou.bailian.util; + + + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.util.Base64; +/** + * DashScopeLocalImageClient + * + * @author gewuyou + * @since 2025-05-08 18:57:58 + */ +public class DashScopeLocalImageClient { + + public static void main(String[] args) throws Exception { + // Step 1: 读取本地图片并转成 base64 + String imagePath = "F:/gewuyou/Project/Idea/demo-llm/src/main/resources/2025俏春装/10147009251.jpg"; + byte[] imageBytes = Files.readAllBytes(new File(imagePath).toPath()); + String base64Image = Base64.getEncoder().encodeToString(imageBytes); + String imageDataUrl = "data:image/jpeg;base64," + base64Image; + System.out.println(imageDataUrl); + // Step 2: 构造请求 JSON + String jsonBody = """ + { + "model": "qwen-vl-max-latest", + "input": { + "messages": [ + { + "role": "system", + "content": [ + { "text": "You are a helpful assistant." } + ] + }, + { + "role": "user", + "content": [ + { "image": "%s" }, + { "text": "请给这张图打标签" } + ] + } + ] + }, + "parameters": { + "incremental_output": true + } + } + """.formatted(imageDataUrl.replace("\n", "")); + + // Step 3: 发送 POST 请求 + URL url = new URL("https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer sk-78af4dd964a94f4cb373851064dbdc12"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + try (OutputStream os = connection.getOutputStream()) { + os.write(jsonBody.getBytes()); + os.flush(); + } + + // Step 4: 读取响应 + int responseCode = connection.getResponseCode(); + System.out.println("Response Code: " + responseCode); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(responseCode == 200 ? + connection.getInputStream() : connection.getErrorStream(), "GBK"))) { + + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } + + connection.disconnect(); + } +} diff --git a/llmx-impl/llmx-impl-baiLian/src/test/kotlin/org/jcnc/llmx/impl/baiLian/LlmhubImplBailianApplicationTests.kt b/llmx-impl/llmx-impl-bailian/src/test/kotlin/org/jcnc/llmx/impl/baiLian/LlmhubImplBailianApplicationTests.kt similarity index 100% rename from llmx-impl/llmx-impl-baiLian/src/test/kotlin/org/jcnc/llmx/impl/baiLian/LlmhubImplBailianApplicationTests.kt rename to llmx-impl/llmx-impl-bailian/src/test/kotlin/org/jcnc/llmx/impl/baiLian/LlmhubImplBailianApplicationTests.kt diff --git a/llmx-impl/llmx-impl-openai/.gitattributes b/llmx-impl/llmx-impl-openai/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/llmx-impl/llmx-impl-openai/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/llmx-impl/llmx-impl-openai/.gitignore b/llmx-impl/llmx-impl-openai/.gitignore new file mode 100644 index 0000000..5a979af --- /dev/null +++ b/llmx-impl/llmx-impl-openai/.gitignore @@ -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 diff --git a/llmx-impl/llmx-impl-openai/build.gradle.kts b/llmx-impl/llmx-impl-openai/build.gradle.kts new file mode 100644 index 0000000..1ccb355 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/build.gradle.kts @@ -0,0 +1,5 @@ +// 开启LLM实现平台依赖 +setProperty(ProjectFlags.USE_LLM_IMPL_PLATFORM_DEPENDENCE, true) +dependencies { + +} diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/LlmxImplOpenaiApplication.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/LlmxImplOpenaiApplication.kt new file mode 100644 index 0000000..2379a0d --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/LlmxImplOpenaiApplication.kt @@ -0,0 +1,11 @@ +package org.jcnc.llmx.impl.openai + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class LlmxImplOpenaiApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/application.properties b/llmx-impl/llmx-impl-openai/src/main/resources/application.properties new file mode 100644 index 0000000..ab85386 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=llmx-impl-openai diff --git a/llmx-impl/llmx-impl-openai/src/test/kotlin/org/jcnc/llmx/impl/openai/LlmxImplOpenaiApplicationTests.kt b/llmx-impl/llmx-impl-openai/src/test/kotlin/org/jcnc/llmx/impl/openai/LlmxImplOpenaiApplicationTests.kt new file mode 100644 index 0000000..80e1231 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/test/kotlin/org/jcnc/llmx/impl/openai/LlmxImplOpenaiApplicationTests.kt @@ -0,0 +1,13 @@ +package org.jcnc.llmx.impl.openai + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class LlmxImplOpenaiApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..0378a7e --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,99 @@ +#!/bin/bash +#set -x # 调试模式,可以启用以打印每行脚本的执行情况 + +# 设置默认的等待时间间隔,默认值为2秒 +: "${SLEEP_SECOND:=2}" +# 设置默认的超时时间,默认值为60秒 +: "${TIMEOUT:=60}" + +# 带时间戳+emoji的小型日志函数 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +# 等待服务函数 +wait_for() { + local host="$1" + local port="$2" + local timeout="$TIMEOUT" + local start_time + local current_time + local elapsed_time + local attempt=0 + + start_time=$(date +%s) + log "🔄 开始等待依赖服务 $host:$port 可用 (超时时间: ${timeout}s, 间隔: ${SLEEP_SECOND}s)" + + while ! nc -z "$host" "$port" 2>/dev/null; do + current_time=$(date +%s) + elapsed_time=$((current_time - start_time)) + attempt=$((attempt + 1)) + + if [ "$elapsed_time" -ge "$timeout" ]; then + log "🛑 [ERROR] 等待超时!依赖服务 $host:$port 在 ${timeout}s 内未启动" + return 1 + fi + + log "🔄 第 ${attempt} 次检测:$host:$port 未就绪,已等待 ${elapsed_time}s..." + sleep "$SLEEP_SECOND" + done + + total_time=$(( $(date +%s) - start_time )) + log "✅ [SUCCESS] 依赖服务 $host:$port 已启动,耗时 ${total_time}s" + return 0 +} + +# 声明变量 +declare DEPENDS +declare CMD + +# 解析参数 +while getopts "d:c:" arg; do + case "$arg" in + d) + DEPENDS="$OPTARG" + ;; + c) + CMD="$OPTARG" + ;; + ?) + log "🛑 [ERROR] 未知参数" + exit 1 + ;; + esac +done + +# 检查依赖 +if [ -n "$DEPENDS" ]; then + log "📦 检测到依赖服务列表: $DEPENDS" + for var in ${DEPENDS//,/ }; do + host=${var%:*} + port=${var#*:} + if [ -z "$host" ] || [ -z "$port" ]; then + log "🛑 [ERROR] 依赖项格式错误: $var,应为 host:port" + exit 1 + fi + + if ! wait_for "$host" "$port"; then + log "❌ [ERROR] 依赖服务 $host:$port 启动失败,终止执行" + exit 1 + fi + done +else + log "⚡️ 未配置依赖服务,跳过依赖检测" +fi + +# 执行命令 +if [ -n "$CMD" ]; then + log "🚀 准备执行命令: $CMD" + eval "$CMD" + cmd_exit_code=$? + if [ $cmd_exit_code -eq 0 ]; then + log "✅ [SUCCESS] 命令执行完成,退出码: $cmd_exit_code" + else + log "❌ [ERROR] 命令执行失败,退出码: $cmd_exit_code" + exit $cmd_exit_code + fi +else + log "⚠️ [WARNING] 未指定要执行的命令,脚本结束" +fi diff --git a/settings.gradle.kts b/settings.gradle.kts index 282051c..68e41c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,15 +6,25 @@ include( "llmx-core", ":llmx-core:llmx-core-service", ":llmx-core:llmx-core-spi", + ":llmx-core:llmx-core-api", + ":llmx-core:llmx-core-common", ) project(":llmx-core:llmx-core-service").name = "llmx-core-service" project(":llmx-core:llmx-core-spi").name = "llmx-core-spi" +project(":llmx-core:llmx-core-api").name = "llmx-core-api" +project(":llmx-core:llmx-core-common").name = "llmx-core-common" include( "llmx-impl", - "llmx-impl:llmx-impl-baiLian", -// "llmx-impl:llmx-impl-openAi", + ":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" \ No newline at end of file +project(":llmx-impl:llmx-impl-bailian").name = "llmx-impl-bailian" +project(":llmx-impl:llmx-impl-openai").name = "llmx-impl-openai" + +include( + "llmx-app", + ":llmx-app:llmx-app-multimodality" +) +project(":llmx-app:llmx-app-multimodality").name = "llmx-app-multimodality" \ No newline at end of file