Compare commits

...

44 Commits

Author SHA1 Message Date
222208862c ci/cd: 添加 master 分支的持续集成和部署流水线- 新增 bootstrap-master.yml 配置文件,用于 Nacos 配置和发现服务
- 移除 application-master.yml 中的 Nacos 相关配置
- 新增 Docker Compose 文件,用于定义服务部署结构
- 新增 Gitea Actions 工作流,实现从代码提交到部署的自动化流程
- 配置缓存策略,提高构建效率
- 添加远程部署步骤,支持内部和新加坡服务器的自动部署
2025-05-11 11:44:34 +08:00
484ffa4f13 feat(core): 重构模型路由管理并更新 Nacos 配置- 移除 ModelProperties 类,使用数据库存储模型路由映射- 新增 ModelRouteMapping 实体类和 ModelRouteMappingRepository 接口
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m32s
- 更新 ModelRouteManager 类,使用数据库查询模型路由信息- 修改 Nacos 配置,将 IP 地址从 192.168.1.8 改为 127.0.0.1
2025-05-10 21:46:35 +08:00
07c85f6f18 build(llmx-core, llmx-impl-bailian): 更新 Nacos 配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m9s
- 移除 application.yml 中的 spring.application.name配置
- 在 bootstrap.yml 中添加 spring.application.name 配置- 修改 bootstrap-dev.yml 和 bootstrap-test.yml 中的 Nacos IP 地址
- 在 bootstrap-dev.yml 和 bootstrap-test.yml 中添加 group 和 namespace 配置
2025-05-10 19:42:55 +08:00
636b589a9e ci:简化配置文件并统一 Nacos 配置- 移除了 application-dev.yml 和 application-test.yml 中的冗余配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m27s
- 调整了 bootstrap-dev.yml 和 bootstrap-test.yml 中的 Nacos 配置格式
- 在 bootstrap 文件中添加了 common-db.yaml 配置
- 修改了 Docker Compose 文件中的数据库名称
2025-05-10 19:08:05 +08:00
f3882418d9 build:重构项目依赖并优化配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 8m35s
- 移除 llmx-core-service 和 llmx-impl-bailian 模块中的重复配置
- 更新 Nacos 配置,启用配置刷新功能
- 添加 PostgreSQL 数据库依赖
-调整 Docker Compose 配置
- 更新项目构建脚本,支持条件依赖加载
2025-05-10 18:43:37 +08:00
6e2ffaf398 fix(build): 修正数据库端口配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m17s
- 将 llmx-database 的端口从 9052 修改为 5432
- 更新了两个镜像构建配置中的数据库连接参数
2025-05-10 14:54:42 +08:00
6794d1a0ef build: 更新构建配置以包含数据库服务
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m46s
- 在 build.gradle.kts 文件中添加 llmx-database服务到 entrypoint 配置- 修改 docker-compose.test.yml 文件中的容器名称,使用更通用的命名
2025-05-10 14:39:53 +08:00
549e4f3b97 build(deps): 添加 spring-cloud-starter-bootstrap 依赖
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 8m59s
- 在 llmx-core 和 llmx-impl-bailian 模块中添加 spring-cloud-starter-bootstrap 依赖
- 更新 gradle/libs.versions.toml 文件,添加 springCloudStarter-bootstrap 依赖项
- 修改 application.yml 和 bootstrap.yml 文件,设置 spring.profiles.active 为 dev
-移除 nacos 配置导入语句
2025-05-10 13:41:55 +08:00
6225e769e2 test(docker): 移除 NACOS_SERVER_IP 环境变量
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m42s
- 从 docker-compose.test.yml 文件中移除了 NACOS_SERVER_IP 环境变量设置
- 保留了其他环境变量设置,包括模式、鉴权和缓存配置
2025-05-10 13:10:07 +08:00
35bbec3b29 env(docker): 更新 Nacos配置并调整数据卷挂载路径
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m56s
- 在环境变量中添加 NACOS_SERVER_IP 设置为0.0.0.0- 修改 Nacos 数据卷挂载路径,从 /nacos/data改为 /home/nacos
2025-05-10 12:55:48 +08:00
dae4a58589 ci: 更新测试环境配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m55s
- 在 llmx-core-service 和 llmx-impl-bailian 模块的 application-test.yml 文件中添加 Nacos 配置导入
- 新增的配置包括命名空间和分组信息,用于区分不同的环境和应用
2025-05-10 12:44:19 +08:00
db63fd6a27 ci: 更新 Nacos 配置文件中的命名空间
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m2s
- 修改 llmx-core-service 的 Nacos命名空间为 1a32d4f4-469a-4e10-ba3b-ce1bc6ba8cc9
- 修改 llmx-impl-bailian 的 Nacos 命名空间为 da45f3a6-e7d0-4d92-b075-776adea07d6d
2025-05-10 12:17:55 +08:00
472e1a8a5a fix(docker): 修正 Docker Compose 配置中的 JWT 密钥变量
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m41s
- 将 NACOS_AUTH_TOKEN_SECRET_KEY 更名为 NACOS_AUTH_TOKEN
- 此修改解决了环境变量配置错误导致的问题
2025-05-10 12:07:50 +08:00
7258dd2ead feat(security): 为 Nacos 配置添加用户名和密码认证
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 4m27s
- 在 docker-compose.test.yml 中添加 Nacos 认证所需的环境变量
- 更新 nacos-test.yml 文件,增加用户名和密码配置- 修复了没有配置认证信息导致的连接失败问题
2025-05-10 11:50:18 +08:00
44bba93112 refactor(llmx): 重构配置文件并增强 Nacos 配置支持
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 7m17s
- 移除 application.yml 中的阿里云配置
-重命名 application-prod.yml 为 application-master.yml
- 更新 application-test.yml 文件,移除阿里云配置
- 删除 bootstrap-test.yml 文件- 新增 nacos-test.yml 文件,用于 Nacos 配置- 更新 build.gradle.kts 文件,添加 Nacos 配置支持- 更新 docker-compose.test.yml,开启 Nacos 鉴权
2025-05-10 11:37:20 +08:00
f4d8aee52d build: 更新 forgeBoot 版本至 1.3.0-SNAPSHOT
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 4m29s
2025-05-09 22:03:00 +08:00
a6e9c91509 build: 更新 forgeBoot 版本号
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 6m46s
- 将 forgeBoot版本从 "1.2.0-SNAPSHOT" 修改为 "1.2.0"
-此更新统一了 maven 和 git 的版本号,确保版本一致性
2025-05-09 21:22:47 +08:00
a192b98787 chore(llmx-core): 添加模型服务映射日志输出
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 4m16s
- 在 resolveServiceName 函数中添加 modelServiceMap 的日志输出- 优化代码结构,提高可读性
2025-05-09 19:24:08 +08:00
690ede78f0 refactor(gradle): 重构项目构建配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m3s
- 更新依赖版本:  - forgeBoot 版本从 1.1.0-SNAPSHOT 升级到 1.2.0-SNAPSHOT - spring-dependency-management 版本 1.1.7 未变
  - aliyun-bailian 版本 2.0.0 未变 - spring-cloud-starter-alibaba-nacos-discovery 版本 2023.0.1.0 未变
  - okHttp 版本 4.12.0 未变 - jib 版本 3.4.2 未变  - org-reactivestreams-reactiveStreams 版本 1.0.4 未变- 调整模块间依赖:
  - llmx-core-service: 依赖从 Core.SPI 改为 Core.COMMON
  - llmx-impl-bailian 和 llmx-impl-openai: 移除冗余依赖,统一配置  - 根项目:添加 USE_LLM_IMPL_PLATFORM_DEPENDENCE 标志

- 移除跨域配置类 CorsConfig 中的注释
- 修改 LlmxCoreServiceApplication 类为 open
- 删除 ChatController 中的 @CrossOrigin 注解
2025-05-09 18:15:10 +08:00
7400c4d24f refactor(llmx-core):调整跨域配置并优化部分代码
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 4m13s
- 移除全局 CorsConfig 类,注释保留备用- 在 ChatController 中添加跨域配置
- 将 AppConfiguration 类和 webClientBuilder 方法改为开放
2025-05-09 15:12:31 +08:00
e6149ecb02 feat(llmx-core-service): 添加CORS配置类
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m22s
- 新增CorsConfig类,实现全局CORS配置
- 允许所有来源、所有HTTP方法和所有请求头- 禁用Cookie携带,预检请求缓存时间为3600秒
2025-05-09 14:16:56 +08:00
67a6c32c6c feat(llmx-impl): 添加 OpenAI 实现模块
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 5m38s
- 新增 llmx-impl-openai 模块
- 添加基本的项目结构和配置
- 创建 Spring Boot应用程序入口和测试类- 设置 Git 属性和忽略文件
2025-05-09 12:02:16 +08:00
7205123043 ci(deploy): 添加清理无标签镜像步骤
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m43s
- 在部署流程结束后增加清理无标签镜像的步骤
- 使用 docker image prune -f 命令快速清理无标签镜像
2025-05-09 11:52:34 +08:00
8a19372b43 build(jib): 更新基础镜像版本
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m29s
- 将 baseImage属性从 "docker://gewuyou/liberica-openjdk-debian-nc" 修改为 "docker://gewuyou/liberica-openjdk-debian-nc:21"
- 此更新统一了基础镜像版本,确保构建的一致性和稳定性
2025-05-09 11:39:39 +08:00
8ee99bb398 build(jib): 更新基础镜像并调整构建配置
- 移除 build.gradle.kts 中的 netcat-openbsd 安装指令- 更新 JibProject 中的 baseImage 为 gewuyou/liberica-openjdk-debian-nc- 重新启用并修正 llmx-impl-openai 模块的名称
2025-05-09 11:32:39 +08:00
3bdbf42adf build: 修改入口点权限设置- 将 "/scripts/entrypoint.sh" 的路径修改为 "/entrypoint.sh"
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 3m28s
- 保持权限设置为 "755"
2025-05-09 11:10:46 +08:00
89c19bbcfe build: 更新 Docker 镜像构建配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m25s
- 为 llmx-core-service 和 llmx-impl-bailian 项目添加自定义 entrypoint
- 安装 netcat-openbsd 并使用 /entrypoint.sh 启动应用
-指定 Nacos 服务器地址和端口
- 优化镜像构建过程,提高启动速度和可靠性
2025-05-09 10:45:08 +08:00
c2d40091c2 refactor(llmx-core): 统一模型服务映射配置
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m44s
- 将所有 qwen 模型的服务映射统一为 llmx-impl-bailian
- 修正了服务映射名称的拼写错误(将 BaiLian 改为 bailian)
2025-05-09 10:30:02 +08:00
224c612525 refactor(llmx-core, llmx-impl): 移除 Nacos 配置中的冗余参数- 从 llmx-core 和 llmx-impl 模块的 bootstrap-test.yml 文件中移除了 Nacos 配置的 username 和 password 参数
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m0s
- 简化了 Nacos discovery 配置,仅保留 server-addr 参数
2025-05-09 10:18:15 +08:00
0265d7cfe0 ci: 更新 Nacos 服务地址
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m28s
- 将 Nacos 服务地址从具体的 IP 地址修改为 llmx-nacos:8848
- 此修改适用于 llmx-core 和 llmx-impl 两个模块的配置文件
2025-05-09 10:04:55 +08:00
a257504949 test(infrastructure): 调整测试环境端口映射并关闭 Nacos 鉴权- 修改 llmx-impl-bailian 应用的测试端口为 9003
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 2m34s
- 更新 Docker Compose 测试配置,调整 Nacos 和数据库端口映射
- 在 Nacos 容器中添加环境变量禁用鉴权
2025-05-09 08:55:45 +08:00
f873d7bb10 ci:修正 Docker 构像路径变量引用
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 3m55s
- 在 llmx-core-service 和 llmx-impl-bailian 服务中
-将 ${${DOCKER_REGISTRY_URL}}/llmx-core-service 和 ${${DOCKER_REGISTRY_URL}}/llmx-impl-bailian
- 修改为 ${DOCKER_REGISTRY_URL}/llmx-core-service 和 ${DOCKER_REGISTRY_URL}/llmx-impl-bailian
-以解决变量引用嵌套导致的路径错误问题
2025-05-09 08:29:00 +08:00
0f6dc2449d fix(docker): 修正 Docker Compose 配置
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 1m29s
- 更新环境变量引用,使用正确的变量名 ${${DOCKER_REGISTRY_URL}}
- 修正服务名称和卷名称中的大小写错误
- 统一使用小写字母以提高可读性和一致性
2025-05-09 08:25:24 +08:00
bd38cf8536 ci(deploy): 更新部署工作流并添加调试信息
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 1m37s
- 修改 COMPOSE_FILE 路径为 docker/docker-compose.test.yml
- 在部署阶段的关键步骤添加当前路径和文件列表的打印,以便于调试- 在每个主要步骤前添加调试信息,提高日志可读性
2025-05-09 08:07:57 +08:00
7d71f4d32d feat(core): 新增多模态聊天功能
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 2h4m17s
- 在 llmx-core 中添加了多模态聊天相关的数据结构和接口
- 在 llmx-impl-bailian 中实现了多模态聊天的适配器和服务
- 新增了多模态聊天的控制器和相关配置- 重构了原有的聊天请求和响应结构,支持多模态内容
2025-05-08 21:47:00 +08:00
383533eb35 refactor(llmx): 将 LLMProvider 接口中的 chat 方法返回类型改为 Publisher
修改了 LLMProvider 接口和 BaiLianProvider 实现类中的 chat 方法,将返回类型从 Flow 改为 Publisher,以适应 Spring WebFlux 的数据处理方式。这一变更使得系统能够更好地集成和使用 Spring WebFlux 的特性。
2025-05-08 14:39:02 +08:00
1ace055e37 ci: 更新环境变量名
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 1h0m4s
- 将 LUKE_SERVER_DOCKER_REGISTRY_PASSWORD 改为 SERVER_PASSWORD
2025-05-08 13:15:09 +08:00
181070b5bc ci: 更新环境变量名
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 36m36s
- 将 REGISTRY_URL 环境变量名修改为 DOCKER_REGISTRY_URL
- 此修改是为了适应 .env 文件中的变量名变更
2025-05-08 11:35:09 +08:00
cc03088927 ci: 更新部署流程
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 49m19s
- 在部署阶段添加了进入 docker 目录的步骤
- 这个改动确保了后续的部署操作在正确的目录下执行,避免了潜在的路径问题
2025-05-08 10:36:32 +08:00
05bc48cb55 build: 更新 Maven 仓库并调整项目配置
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 18m28s
- 注释掉 eurotech/kura_addons 的 Maven仓库
- 添加阿里云 Central Maven 仓库
- 移除注释中的 springbootWeb 相关配置
- 优化 jib配置,调整项目名称和端口设置
2025-05-08 10:11:42 +08:00
5209f78513 refactor(cicd): 修复文件夹名称错误
Some checks failed
CI/CD Pipeline / build-and-deploy (push) Failing after 9m43s
2025-05-07 23:03:38 +08:00
6bf046d6e0 refactor(llmx): 重构项目配置和网络请求处理
- 更新 Nacos 配置中的 IP地址
- 修改 Spring Boot项目配置,使用更具体的 USE_SPRING_BOOT_WEB 标志
- 重构 DashScopeAdapter 中的网络请求和响应处理逻辑,提高可读性和维护性
- 在 ChatController 和 LLMProvider 中添加 NDJSON 媒体类型支持
2025-05-07 22:59:39 +08:00
73eeaa19c1 ci/cd: 新增测试环境部署工作流
- 移除旧的 .env 文件
- 添加新的应用配置文件
- 更新 Docker Compose 文件
- 新增 Gitea CI/CD 工作流
- 修改 Nacos 配置
- 修正文件名大小写
2025-05-07 22:14:06 +08:00
8b17f6cb84 feat(core): 重构项目并添加 Docker 支持
- 重命名应用名称和包名,统一使用 llmx 前缀- 添加生产环境和测试环境的 Docker 配置文件
- 新增环境变量配置,用于 Docker部署
- 更新构建脚本,支持多模块构建
- 优化应用配置,适配 Docker 环境
2025-05-07 20:43:05 +08:00
102 changed files with 2076 additions and 349 deletions

View File

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

View File

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

View File

@ -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 )'"
)
}
}
}

View File

@ -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 =
"提供简单的配置构建镜像"
}

View File

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

View File

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

View File

@ -52,12 +52,13 @@ class JibPlugin : Plugin<Project> {
}
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<Project> {
// 动态配置额外目录
extraDirectories {
setPaths(jibProject.paths)
if(jibProject.paths.isNotEmpty()){
setPaths(jibProject.paths)
}
permissions.putAll(jibProject.permissions)
}
// 将动态部分移到任务配置中

View File

@ -19,14 +19,14 @@ data class JibProject(
var ports: List<String> = listOf("8080"),
var environment: Map<String, String> = mapOf("SPRING_PROFILES_ACTIVE" to "prod"),
var entrypoint: List<String> = emptyList(),
var paths: List<String> = listOf("llmhub-base/scripts/entrypoint.sh"),
var paths: List<String> = listOf(),
var imageName: String = "",
var version: String = "latest",
var permissions: Map<String, String> = mapOf("/scripts/entrypoint.sh" to "755"),
var baseImage: String = "docker://bellsoft/liberica-openjdk-debian:21"
var permissions: Map<String, String> = mapOf("/entrypoint.sh" to "755"),
var baseImage: String = "docker://gewuyou/liberica-openjdk-debian-nc:21"
) {
init {
if (imageName.isEmpty()) {
if (imageName.isBlank()) {
imageName = projectName
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
gradlew vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>) {
runApplication<LlmxAppMultimodalityApplication>(*args)
}

View File

@ -0,0 +1,8 @@
server:
port: 8081
spring:
config:
import: classpath:bootstrap-dev.yml

View File

@ -0,0 +1,7 @@
server:
port: 9003
spring:
config:
import: classpath:bootstrap-test.yml

View File

@ -0,0 +1,5 @@
spring:
application:
name: llmx-app-multimodality
profiles:
active: dev

View File

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

View File

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

View File

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

View File

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

View File

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

40
llmx-core/llmx-core-api/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
org.jcnc.llmx.core.api.LLMCoreFeignAutoConfiguration

View File

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

40
llmx-core/llmx-core-common/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,7 @@
ext{
set(ProjectFlags.USE_SPRING_BOOT_BOM,true)
}
dependencies {
implementation(libs.jackson.core)
implementation(libs.jackson.module.kotlin)
}

View File

@ -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<MultiModalContent>() {
/**
* 反序列化方法
*
* 根据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")
}
}
}

View File

@ -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<MultiModalMessage> =listOf(),
val options: Map<String, String> = mapOf()
)
/**
* MultiModalMessage数据类用于定义聊天请求中的单个消息
* @param role 消息的角色可以是"system", "user", "assistant"
* @param content 消息的内容可以是文本图像等多种模态的组合
*/
data class MultiModalMessage(
val role: String, // "system", "user", "assistant"
val content: List<MultiModalContent>
)

View File

@ -1,4 +1,4 @@
package org.jcnc.llmx.core.spi.entities.request
package org.jcnc.llmx.core.common.entities.request
/**
* 嵌入请求类

View File

@ -1,4 +1,4 @@
package org.jcnc.llmx.core.spi.entities.response
package org.jcnc.llmx.core.common.entities.response
/**
* 聊天响应类

View File

@ -1,4 +1,4 @@
package org.jcnc.llmx.core.spi.entities.response
package org.jcnc.llmx.core.common.entities.response
/**
* 嵌入响应类

View File

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

View File

@ -6,7 +6,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
class LlmxCoreServiceApplication
open class LlmxCoreServiceApplication
/**
* 程序的入口点

View File

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

View File

@ -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) // 是否允许携带Cookietrue时需要明确指定allowedOrigins不能为*
.maxAge(3600) // 预检请求缓存时间(秒)
}
}

View File

@ -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<String, String> = emptyMap()
}

View File

@ -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<ChatResponsePart> {
@PostMapping("/stream", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun chat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> {
return llmServiceImpl.chat(request)
}
/**
* 处理多模态聊天请求的函数
* 该函数通过 POST 方法接收一个聊天请求并以 NDJSON 的形式返回聊天响应的部分
*
* @param request 包含聊天请求信息的数据类
* @return 返回一个发布者用于异步地发送聊天响应的部分
*
* 注意该函数被设计为异步处理以提高性能和响应性
* 它使用了响应式编程模型适合处理高并发和大数据量的响应
*/
@PostMapping("/streamMultimodality", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun multimodalityChat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart>{
log.info("request: {}", request)
return llmServiceImpl.multimodalityChat(request)
}
}

View File

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

View File

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

View File

@ -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<ModelRouteMapping, Long>,
JpaSpecificationExecutor<ModelRouteMapping> {
fun findAllByEnabled(isEnabled: Boolean): List<ModelRouteMapping>
}

View File

@ -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<ChatResponsePart>
fun chat(request: ChatRequest): Publisher<ChatResponsePart>
fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart>
}

View File

@ -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<ChatResponsePart> {
override fun chat(request: ChatRequest): Publisher<ChatResponsePart> {
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<ChatResponsePart> {
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)
}
}

View File

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

View File

@ -0,0 +1,2 @@
server:
port: 9002

View File

@ -0,0 +1,4 @@
server:
port: 9002

View File

@ -1,5 +1,3 @@
spring:
application:
name: llmhub-core-service
profiles:
active: dev

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
spring:
application:
name: llmx-core-service
profiles:
active: dev

View File

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

View File

@ -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<String, String> = mapOf()
)

View File

@ -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<ChatResponsePart>
@PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun chat(request: ChatRequest): Publisher<ChatResponsePart>
/**
* 处理多模态聊天请求的函数
* 该函数通过 POST 方法接收一个聊天请求并以 NDJSON 的形式返回聊天响应的部分
*
* @param request 包含聊天请求信息的数据类
* @return 返回一个发布者用于异步地发送聊天响应的部分
*
* 注意该函数被设计为异步处理以提高性能和响应性
* 它使用了响应式编程模型适合处理高并发和大数据量的响应
*/
@PostMapping("/multimodalityChat", produces = [MediaType.APPLICATION_NDJSON_VALUE])
fun multimodalityChat(request: ChatRequest): Publisher<ChatResponsePart>
/**
* 嵌入功能方法
* 该方法允许用户发送嵌入请求以获取LLM生成的嵌入向量

View File

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

View File

@ -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<String, String>,
requestBody: Any,
extractContent: (String) -> ChatResponsePart,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Flow<ChatResponsePart> = flow {
val requestJson = objectMapper.writeValueAsString(requestBody)
log.info("📤 请求参数: {}", requestJson)
val request = Request.Builder()
.url(url)
.headers(headers.toHeaders())
.post(requestJson.toRequestBody("application/json".toMediaType()))
.build()
val call = okHttpClient.newCall(request)
val response = call.execute()
if (!response.isSuccessful) {
throw RuntimeException("❌ DashScope 请求失败: HTTP ${response.code}")
}
val responseBody = response.body ?: throw RuntimeException("❌ DashScope 响应体为空")
val bufferedReader: BufferedReader = responseBody.charStream().buffered()
val allContent = StringBuilder()
try {
while (currentCoroutineContext().isActive) {
val line = withContext(dispatcher) {
bufferedReader.readLine()
} ?: break
log.info("📥 接收到行: {}", line)
if (line.startsWith("data:")) {
val jsonPart = line.removePrefix("data:").trim()
try {
val part = extractContent(jsonPart)
allContent.append(part.content)
log.info("✅ 提取内容: {}", part)
emit(part)
} catch (e: Exception) {
log.warn("⚠️ 无法解析 JSON: {}", jsonPart, e)
}
}
}
log.info("📦 完整内容: {}", allContent)
} catch (e: Exception) {
log.error("🚨 读取 DashScope 响应流失败: {}", e.message, e)
throw e
} finally {
withContext(dispatcher) {
bufferedReader.close()
}
response.close()
}
}
}

View File

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

View File

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

View File

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

View File

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

40
llmx-impl/llmx-impl-bailian/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,6 @@
// 开启LLM实现平台依赖
setProperty(ProjectFlags.USE_LLM_IMPL_PLATFORM_DEPENDENCE, true)
dependencies {
implementation(libs.aliyun.bailian)
}

View File

@ -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<String, String>,
requestBody: Any,
extractContent: (String) -> ChatResponsePart,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
): Flow<ChatResponsePart> = 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<ChatResponsePart>.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<String, String>, json: String): Request {
return Request.Builder()
.url(url)
.headers(headers.toHeaders())
.post(json.toRequestBody("application/json".toMediaType()))
.build()
}
}

View File

@ -44,5 +44,9 @@ class DashScopeProperties {
* 应用调用基路径例如 /api/v1
*/
var baseUrl: String = ""
/**
* 多模态对话调用路径
*/
var multimodalityUrl: String = ""
}

View File

@ -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<ChatResponsePart> {
return baiLianModelService.streamChat(request)
override fun chat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> {
return baiLianModelService.streamChat(request).asPublisher()
}
override fun multimodalityChat(@RequestBody request: ChatRequest): Publisher<ChatResponsePart> {
return baiLianModelService.streamMultimodalityChat(request).asPublisher()
}
/**

View File

@ -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<Choice>?
)
/**
* 选择项数据类
*
* 该类表示生成内容的一个选项包括完成原因和具体的消息内容
*
* @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<Content>?
)
/**
* 内容数据类
*
* 该类表示消息中的具体内容目前只支持文本内容
*
* @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?
)

View File

@ -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<ChatResponsePart>
/**
* 使用流式多模态聊天交互
*
* @param request 聊天请求对象包含用户输入上下文等信息
* @return 返回一个Flow流包含部分聊天响应允许逐步处理和消费响应
*/
fun streamMultimodalityChat(request: ChatRequest) : Flow<ChatResponsePart>
}

View File

@ -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<ChatResponsePart> {
// 构造请求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
)
)
}
)
}
}

View File

@ -0,0 +1,2 @@
server:
port: 8082

View File

@ -0,0 +1,2 @@
server:
port: 9003

View File

@ -0,0 +1,2 @@
server:
port: 9003

View File

@ -0,0 +1,3 @@
spring:
profiles:
active: dev

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
spring:
application:
name: llmx-impl-bailian
profiles:
active: dev

View File

@ -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 中使用
}
}

View File

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

View File

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

40
llmx-impl/llmx-impl-openai/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,5 @@
// 开启LLM实现平台依赖
setProperty(ProjectFlags.USE_LLM_IMPL_PLATFORM_DEPENDENCE, true)
dependencies {
}

View File

@ -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<String>) {
runApplication<LlmxImplOpenaiApplication>(*args)
}

View File

@ -0,0 +1 @@
spring.application.name=llmx-impl-openai

View File

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

Some files were not shown because too many files have changed in this diff Show More