From 222208862cbacf8dc06bd05024d820a872973894 Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 11:44:34 +0800 Subject: [PATCH 1/8] =?UTF-8?q?ci/cd:=20=E6=B7=BB=E5=8A=A0=20master=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=E7=9A=84=E6=8C=81=E7=BB=AD=E9=9B=86=E6=88=90?= =?UTF-8?q?=E5=92=8C=E9=83=A8=E7=BD=B2=E6=B5=81=E6=B0=B4=E7=BA=BF-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20bootstrap-master.yml=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E7=94=A8=E4=BA=8E=20Nacos=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=92=8C=E5=8F=91=E7=8E=B0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=20-=20=E7=A7=BB=E9=99=A4=20application-master.yml=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20Nacos=20=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Docker=20Compose=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=AE=9A=E4=B9=89=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E7=BB=93=E6=9E=84=20-=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20Gitea=20Actions=20=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BB=8E=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=88=B0=E9=83=A8=E7=BD=B2=E7=9A=84=E8=87=AA=E5=8A=A8=E5=8C=96?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=20-=20=E9=85=8D=E7=BD=AE=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E7=AD=96=E7=95=A5=EF=BC=8C=E6=8F=90=E9=AB=98=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=95=88=E7=8E=87=20-=20=E6=B7=BB=E5=8A=A0=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E6=AD=A5=E9=AA=A4=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=86=85=E9=83=A8=E5=92=8C=E6=96=B0=E5=8A=A0=E5=9D=A1=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E7=9A=84=E8=87=AA=E5=8A=A8=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy.master.yml | 219 ++++++++++++++++++ docker/docker-compose.master.agent.yml | 32 +++ docker/docker-compose.master.main.yml | 48 ++++ .../src/main/resources/application-master.yml | 7 +- .../src/main/resources/bootstrap-master.yml | 23 ++ .../src/main/resources/application-master.yml | 22 +- .../src/main/resources/bootstrap-master.yml | 20 ++ 7 files changed, 344 insertions(+), 27 deletions(-) create mode 100644 .gitea/workflows/deploy.master.yml create mode 100644 docker/docker-compose.master.agent.yml create mode 100644 docker/docker-compose.master.main.yml create mode 100644 llmx-core/llmx-core-service/src/main/resources/bootstrap-master.yml create mode 100644 llmx-impl/llmx-impl-bailian/src/main/resources/bootstrap-master.yml 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/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/llmx-core/llmx-core-service/src/main/resources/application-master.yml b/llmx-core/llmx-core-service/src/main/resources/application-master.yml index e78b1c6..5af7893 100644 --- a/llmx-core/llmx-core-service/src/main/resources/application-master.yml +++ b/llmx-core/llmx-core-service/src/main/resources/application-master.yml @@ -1,7 +1,2 @@ server: - port: 9002 -spring: - cloud: - nacos: - discovery: - server-addr: 49.235.96.75:9001 \ No newline at end of file + port: 9002 \ 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-impl/llmx-impl-bailian/src/main/resources/application-master.yml b/llmx-impl/llmx-impl-bailian/src/main/resources/application-master.yml index 292217d..9beecae 100644 --- a/llmx-impl/llmx-impl-bailian/src/main/resources/application-master.yml +++ b/llmx-impl/llmx-impl-bailian/src/main/resources/application-master.yml @@ -1,22 +1,2 @@ server: - port: 9003 -spring: - application: - name: llmx-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 + port: 9003 \ 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 -- 2.47.2 From a12c8b61f7fa8fa23018601647e95b88dbb66610 Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 11:49:02 +0800 Subject: [PATCH 2/8] =?UTF-8?q?style:=E8=B0=83=E6=95=B4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BC=A9=E8=BF=9B=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 remote-singapore-deploy 部分的代码缩进格式从混用 tab 和空格改为统一使用空格 - 保持了代码的一致性和可读性 --- .gitea/workflows/deploy.master.yml | 68 +++++++++++++++--------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml index e832654..7d6ac54 100644 --- a/.gitea/workflows/deploy.master.yml +++ b/.gitea/workflows/deploy.master.yml @@ -183,37 +183,37 @@ jobs: 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 + 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 -- 2.47.2 From fb224b3bcedf3a2121e0290cc8c8adccbfa4055f Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 12:05:33 +0800 Subject: [PATCH 3/8] =?UTF-8?q?ci(deploy):=20=E4=BC=98=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=B5=81=E7=A8=8B=E5=B9=B6=E6=8F=90=E5=8D=87=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加下载并使用 drone-scp 二进制文件的步骤,以提高 SCP传输的安全性 - 修改目标路径,将 docker-compose 文件直接复制到目标目录 - 为 SCP传输步骤设置 DRONE_SCP_BINARY 环境变量,指定自定义 SCP 二进制文件 --- .gitea/workflows/deploy.master.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml index 7d6ac54..fc21365 100644 --- a/.gitea/workflows/deploy.master.yml +++ b/.gitea/workflows/deploy.master.yml @@ -76,24 +76,33 @@ jobs: uses: ${{env.JCNC_GITEA_URL}}/gradle/actions/setup-gradle@v4 with: gradle-version: wrapper # 使用项目自带的gradle-wrapper + - name: 🔽 Download drone-scp + run: | + curl -L "https://github.moeyy.xyz/https://github.com/appleboy/drone-scp/releases/download/v1.8.0/drone-scp-1.8.0-linux-amd64" \ + -o drone-scp + chmod +x drone-scp - name: 📦 Copy Compose File to Internal Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 + env: + DRONE_SCP_BINARY: ./drone-scp 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" + target: "/home/luke/deploy/llmx/" - name: 📦 Copy Compose File to Singapore Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 + env: + DRONE_SCP_BINARY: ./drone-scp 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" + target: "/home/deploy/llmx/" # ========== 5. 构建阶段 ========== - name: 🏗️ Build with Jib run: | -- 2.47.2 From 8e3ce4d6ee382201600bd352d94f3c2d6c38b02f Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 12:28:08 +0800 Subject: [PATCH 4/8] =?UTF-8?q?ci(deploy):=20=E6=9B=B4=E6=96=B0=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=B9=B6=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E7=AB=AF=E5=8F=A3=E6=98=A0=E5=B0=84?= =?UTF-8?q?-=20=E7=A7=BB=E9=99=A4=20deploy.master.yml=20=E4=B8=AD=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=92=8C=E4=BD=BF=E7=94=A8=20drone-scp=20=E7=9A=84?= =?UTF-8?q?=E6=AD=A5=E9=AA=A4=20-=20=E6=9B=B4=E6=96=B0=20docker-compose.ma?= =?UTF-8?q?ster.main.yml=20=E4=B8=AD=E6=95=B0=E6=8D=AE=E5=BA=93=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E7=AB=AF=E5=8F=A3=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy.master.yml | 9 --------- docker/docker-compose.master.main.yml | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml index fc21365..0a84d08 100644 --- a/.gitea/workflows/deploy.master.yml +++ b/.gitea/workflows/deploy.master.yml @@ -76,15 +76,8 @@ jobs: uses: ${{env.JCNC_GITEA_URL}}/gradle/actions/setup-gradle@v4 with: gradle-version: wrapper # 使用项目自带的gradle-wrapper - - name: 🔽 Download drone-scp - run: | - curl -L "https://github.moeyy.xyz/https://github.com/appleboy/drone-scp/releases/download/v1.8.0/drone-scp-1.8.0-linux-amd64" \ - -o drone-scp - chmod +x drone-scp - name: 📦 Copy Compose File to Internal Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 - env: - DRONE_SCP_BINARY: ./drone-scp with: host: ${{ env.INTERNAL_SERVER_HOST }} port: $INTERNAL_SERVER_PROT @@ -94,8 +87,6 @@ jobs: target: "/home/luke/deploy/llmx/" - name: 📦 Copy Compose File to Singapore Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 - env: - DRONE_SCP_BINARY: ./drone-scp with: host: $SINGAPORE_SERVER_HOST port: $SSH_PROT diff --git a/docker/docker-compose.master.main.yml b/docker/docker-compose.master.main.yml index 0d61505..5bce838 100644 --- a/docker/docker-compose.master.main.yml +++ b/docker/docker-compose.master.main.yml @@ -16,7 +16,7 @@ services: container_name: llmx-database restart: always ports: - - "9052:5432" + - "5432:5432" networks: - llmx-net-master environment: -- 2.47.2 From 9f6c246acbf0a702303f5e7d3f0c49627245641b Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 15:23:11 +0800 Subject: [PATCH 5/8] =?UTF-8?q?ci:=E4=BC=98=E5=8C=96=20GitHub=20Actions=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=B8=AD=E7=9A=84=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -将环境变量引用从 $VAR_NAME 形式更改为 ${{ env.VAR_NAME }} 形式- 修正了几个变量引用,使其与新的语法一致 -这些更改提高了工作流的可读性和可维护性 --- .gitea/workflows/deploy.master.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml index 0a84d08..3550a9e 100644 --- a/.gitea/workflows/deploy.master.yml +++ b/.gitea/workflows/deploy.master.yml @@ -80,19 +80,19 @@ jobs: uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 with: host: ${{ env.INTERNAL_SERVER_HOST }} - port: $INTERNAL_SERVER_PROT + port: ${{ env.INTERNAL_SERVER_PROT }} username: root password: ${{ secrets.INTERNAL_SERVER_PASSWORD }} - source: $MAIN_COMPOSE_FILE + source: ${{ env.MAIN_COMPOSE_FILE }} target: "/home/luke/deploy/llmx/" - name: 📦 Copy Compose File to Singapore Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 with: - host: $SINGAPORE_SERVER_HOST - port: $SSH_PROT + host: ${{ env.SINGAPORE_SERVER_HOST }} + port: ${{ env.SSH_PROT }} username: root password: ${{ secrets.SINGAPORE_SERVER_PASSWORD }} - source: $AGENT_COMPOSE_FILE + source: ${{ env.AGENT_COMPOSE_FILE }} target: "/home/deploy/llmx/" # ========== 5. 构建阶段 ========== - name: 🏗️ Build with Jib @@ -157,8 +157,8 @@ jobs: - name: ✈️ Deploy on Internal Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/ssh-action@v1 with: - host: $INTERNAL_SERVER_HOST - port: $INTERNAL_SERVER_PROT + host: ${{ env.INTERNAL_SERVER_HOST }} + port: ${{ env.INTERNAL_SERVER_PROT }} username: root password: ${{ secrets.INTERNAL_SERVER_PASSWORD }} script: | @@ -192,8 +192,8 @@ jobs: - name: ✈️ Deploy on Internal Server uses: ${{env.JCNC_GITEA_URL}}/appleboy/ssh-action@v1 with: - host: $SINGAPORE_SERVER_HOST - port: $SSH_PROT + host: ${{ env.SINGAPORE_SERVER_HOST }} + port: ${{ env.SSH_PROT }} username: root password: ${{ secrets.SINGAPORE_SERVER_PASSWORD }} script: | -- 2.47.2 From ab92fb591185fe2550557336248db83a0b8c6ca2 Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 15:58:40 +0800 Subject: [PATCH 6/8] =?UTF-8?q?ci(deploy):=20=E7=94=A8=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=20scp=20=E6=9B=BF=E4=BB=A3=20appleboy/scp-action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了对 appleboy/scp-action 的依赖,改用原生 scp 命令 - 更新了部署流程,使用原生 scp 命令将 Compose 文件复制到内部服务器和新加坡服务器 - 添加了 StrictHostKeyChecking=no 选项以避免 SCP 连接时的主机密钥检查提示 --- .gitea/workflows/deploy.master.yml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml index 3550a9e..4e51c8d 100644 --- a/.gitea/workflows/deploy.master.yml +++ b/.gitea/workflows/deploy.master.yml @@ -76,24 +76,16 @@ jobs: 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: ${{ env.INTERNAL_SERVER_PROT }} - username: root - password: ${{ secrets.INTERNAL_SERVER_PASSWORD }} - source: ${{ env.MAIN_COMPOSE_FILE }} - target: "/home/luke/deploy/llmx/" - - name: 📦 Copy Compose File to Singapore Server - uses: ${{env.JCNC_GITEA_URL}}/appleboy/scp-action@v1 - with: - host: ${{ env.SINGAPORE_SERVER_HOST }} - port: ${{ env.SSH_PROT }} - username: root - password: ${{ secrets.SINGAPORE_SERVER_PASSWORD }} - source: ${{ env.AGENT_COMPOSE_FILE }} - target: "/home/deploy/llmx/" + - name: 📦 Copy Compose File to Internal Server (via native scp) + run: | + scp -P ${{ env.INTERNAL_SERVER_PROT }} -o StrictHostKeyChecking=no \ + ${{ env.MAIN_COMPOSE_FILE }} \ + root@${{ env.INTERNAL_SERVER_HOST }}:/home/luke/deploy/llmx/ + - name: 📦 Copy Compose File to Singapore Server (via native scp) + run: | + scp -P ${{ env.SSH_PROT }} -o StrictHostKeyChecking=no \ + ${{ env.AGENT_COMPOSE_FILE }} \ + root@${{ env.SINGAPORE_SERVER_HOST }}:/home/deploy/llmx/ # ========== 5. 构建阶段 ========== - name: 🏗️ Build with Jib run: | -- 2.47.2 From a33a800e130f5f2a50432d9849c3aa8491bc38ba Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 16:12:46 +0800 Subject: [PATCH 7/8] =?UTF-8?q?ci(deploy):=20=E4=BD=BF=E7=94=A8=20sshpass?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20native=20scp=20=E8=BF=9B=E8=A1=8C=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=BC=A0=E8=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 安装 sshpass 以支持密码认证 - 更新内部服务器和新加坡服务器的文件传输方式 - 使用 sshpass命令行参数传递服务器密码 --- .gitea/workflows/deploy.master.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/deploy.master.yml b/.gitea/workflows/deploy.master.yml index 4e51c8d..857eba6 100644 --- a/.gitea/workflows/deploy.master.yml +++ b/.gitea/workflows/deploy.master.yml @@ -76,16 +76,21 @@ jobs: uses: ${{env.JCNC_GITEA_URL}}/gradle/actions/setup-gradle@v4 with: gradle-version: wrapper # 使用项目自带的gradle-wrapper - - name: 📦 Copy Compose File to Internal Server (via native scp) + - name: 🔧 Install sshpass run: | - scp -P ${{ env.INTERNAL_SERVER_PROT }} -o StrictHostKeyChecking=no \ - ${{ env.MAIN_COMPOSE_FILE }} \ - root@${{ env.INTERNAL_SERVER_HOST }}:/home/luke/deploy/llmx/ - - name: 📦 Copy Compose File to Singapore Server (via native scp) + apt-get update && apt-get install -y sshpass + - name: 📦 Copy Compose File to Internal Server (with password) run: | - scp -P ${{ env.SSH_PROT }} -o StrictHostKeyChecking=no \ - ${{ env.AGENT_COMPOSE_FILE }} \ - root@${{ env.SINGAPORE_SERVER_HOST }}:/home/deploy/llmx/ + sshpass -p "${{ secrets.INTERNAL_SERVER_PASSWORD }}" \ + scp -P ${{ env.INTERNAL_SERVER_PROT }} -o StrictHostKeyChecking=no \ + ${{ env.MAIN_COMPOSE_FILE }} \ + root@${{ env.INTERNAL_SERVER_HOST }}:/home/luke/deploy/llmx/ + - name: 📦 Copy Compose File to Singapore Server (with password) + run: | + sshpass -p "${{ secrets.SINGAPORE_SERVER_PASSWORD }}" \ + scp -P ${{ env.SSH_PROT }} -o StrictHostKeyChecking=no \ + ${{ env.AGENT_COMPOSE_FILE }} \ + root@${{ env.SINGAPORE_SERVER_HOST }}:/home/deploy/llmx/ # ========== 5. 构建阶段 ========== - name: 🏗️ Build with Jib run: | -- 2.47.2 From f32ee8830044f98609a50dabf7b2623daff89dcd Mon Sep 17 00:00:00 2001 From: gewuyou Date: Sun, 11 May 2025 21:48:41 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat(llmx):=20=E6=B7=BB=E5=8A=A0=20OpenAI?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 OpenAI 适配器和相关配置 - 实现 OpenAI 模型服务和控制器 - 添加 ChatRequest 扩展函数以支持 OpenAI 请求格式 - 更新 ModelRouteManager 中的模型服务映射逻辑 --- build.gradle.kts | 1 - .../common/entities/request/ChatRequest.kt | 10 +- .../core/service/manager/ModelRouteManager.kt | 4 +- llmx-core/llmx-core-spi/build.gradle.kts | 6 + .../spi/adapter/AbstractChatStreamAdapter.kt | 100 +++++++++++++ .../core/spi/adapter/ChatStreamAdapter.kt | 38 +++++ .../llmx/core/spi/provider/LLMProvider.kt | 5 +- .../impl/baiLian/adapter/DashScopeAdapter.kt | 103 +++---------- .../baiLian/controller/BaiLianProvider.kt | 8 +- ... => DashScopeMultimodalityChatResponse.kt} | 2 +- .../baiLian/service/BaiLianModelService.kt | 12 +- .../service/impl/BaiLianModelServiceImpl.kt | 9 +- .../llmx/impl/openai/adapter/OpenAiAdapter.kt | 60 ++++++++ .../llmx/impl/openai/config/ClientConfig.kt | 60 ++++++++ .../llmx/impl/openai/config/OpenAiConfig.kt | 15 ++ .../config/entities/OpenAiProperties.kt | 15 ++ .../config/service/OpenAiModelService.kt | 29 ++++ .../service/impl/OpenAiModelServiceImpl.kt | 89 +++++++++++ .../impl/openai/controller/OpenAiProvider.kt | 33 +++++ .../OpenAiMultimodalityChatReposponse.kt | 140 ++++++++++++++++++ .../openai/extension/ChatRequestExtension.kt | 36 +++++ .../src/main/resources/application-dev.yml | 2 + .../src/main/resources/application-master.yml | 2 + .../src/main/resources/application-test.yml | 2 + .../src/main/resources/application.properties | 1 - .../src/main/resources/application.yml | 3 + .../src/main/resources/bootstrap-dev.yml | 22 +++ .../src/main/resources/bootstrap-master.yml | 20 +++ .../src/main/resources/bootstrap-test.yml | 20 +++ .../src/main/resources/bootstrap.yml | 5 + 30 files changed, 740 insertions(+), 112 deletions(-) create mode 100644 llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/AbstractChatStreamAdapter.kt create mode 100644 llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/ChatStreamAdapter.kt rename llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/entities/response/{DashScopeResponse.kt => DashScopeMultimodalityChatResponse.kt} (98%) create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/adapter/OpenAiAdapter.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/ClientConfig.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/OpenAiConfig.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/entities/OpenAiProperties.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/OpenAiModelService.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/impl/OpenAiModelServiceImpl.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/controller/OpenAiProvider.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/entities/response/OpenAiMultimodalityChatReposponse.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/extension/ChatRequestExtension.kt create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/application-dev.yml create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/application-master.yml create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/application-test.yml delete mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/application.properties create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/application.yml create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-dev.yml create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-master.yml create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-test.yml create mode 100644 llmx-impl/llmx-impl-openai/src/main/resources/bootstrap.yml diff --git a/build.gradle.kts b/build.gradle.kts index fa0737c..b9cf05f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,7 +84,6 @@ subprojects { implementation(libs.okHttp) // forgeBoot依赖 implementation(libs.forgeBoot.core.extension) - implementation(libs.forgeBoot.core.extension) } } if(project.getPropertyByBoolean(ProjectFlags.USE_DAO_DEPENDENCE)){ 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 index 28be69d..4832c59 100644 --- 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 @@ -17,7 +17,7 @@ sealed class MultiModalContent { /** * Image数据类,表示图像内容的模态 - * @param image 图像的base64 + * @param image 图像的base64或者url */ data class Image(val image: String) : MultiModalContent() } @@ -35,8 +35,8 @@ sealed class MultiModalContent { data class ChatRequest( val prompt: String? = "", val model: String, - val messages: List =listOf(), - val options: Map = mapOf() + val messages: List = listOf(), + val options: Map = mapOf(), ) /** @@ -46,7 +46,5 @@ data class ChatRequest( */ data class MultiModalMessage( val role: String, // "system", "user", "assistant" - val content: List + val content: List, ) - - 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 3c59b71..bc05807 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 @@ -32,8 +32,8 @@ class ModelRouteManager( val modelServiceMap = modelRouteMappingRepository.findAllByEnabled(true) .associate { it.model to it.serviceName } log.info("modelServiceMap: $modelServiceMap") - for ((model, serviceName) in modelServiceMap) { - if (model.startsWith(model)) { + for ((modelId, serviceName) in modelServiceMap) { + if (modelId == model) { return serviceName } } diff --git a/llmx-core/llmx-core-spi/build.gradle.kts b/llmx-core/llmx-core-spi/build.gradle.kts index 8821f83..9497820 100644 --- a/llmx-core/llmx-core-spi/build.gradle.kts +++ b/llmx-core/llmx-core-spi/build.gradle.kts @@ -5,6 +5,12 @@ apply { } dependencies { compileOnly(libs.springBootStarter.web) + // kt协程依赖 + compileOnly(libs.kotlinx.coruntes.reactor) + // okHttp依赖 + compileOnly(libs.okHttp) api(libs.org.reactivestreams.reactiveStreams) api(project(Modules.Core.COMMON)) + // forgeBoot依赖 + implementation(libs.forgeBoot.core.extension) } \ No newline at end of file diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/AbstractChatStreamAdapter.kt b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/AbstractChatStreamAdapter.kt new file mode 100644 index 0000000..01b532d --- /dev/null +++ b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/AbstractChatStreamAdapter.kt @@ -0,0 +1,100 @@ +package org.jcnc.llmx.core.spi.adapter + +import com.fasterxml.jackson.databind.ObjectMapper +import com.gewuyou.forgeboot.core.extension.log +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.reactive.asPublisher +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.reactivestreams.Publisher +import org.springframework.http.MediaType +import java.io.BufferedReader + +/** + * 抽象聊天流适配器 + * + * @since 2025-05-11 20:43:16 + * @author gewuyou + */ +abstract class AbstractChatStreamAdapter( + private val okHttpClient: OkHttpClient, + private val objectMapper: ObjectMapper, +) : ChatStreamAdapter { + /** + * 发送流式聊天请求 + * + * 该函数负责向指定URL发送聊天请求,并根据响应流提取内容 + * 使用协程调度器来控制并发和异步处理逻辑 + * + * @param url 请求的URL地址 + * @param headers 请求头,包含认证等信息 + * @param requestBody 请求体,可以是任意类型,根据API需求而定 + * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 + * @param dispatcher 协程调度器,用于执行异步或并发操作 + * @return 返回一个发布者,用于订阅聊天响应流 + */ + override fun sendStreamChat( + url: String, + headers: Map, + requestBody: Any, + extractContent: (String) -> ChatResponsePart, + dispatcher: CoroutineDispatcher, + ): Publisher = 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("🚨 请求失败: ${response.code} ${response.message}") + throw RuntimeException("❌ 请求失败: HTTP ${response.code}") + } + val responseBody = response.body ?: throw RuntimeException("❌ 响应体为空") + responseBody.charStream().buffered().use { reader -> + val allContent = StringBuilder() + processResponse(reader, extractContent, allContent, dispatcher) + log.info("📦 完整内容: $allContent") + } + } + }.asPublisher() + + /** + * 构建HTTP请求 + * + * @param url 请求的URL地址 + * @param headers 请求头,包含认证等信息 + * @param json 请求体的JSON字符串 + * @return 返回构建好的HTTP请求对象 + */ + open fun buildRequest(url: String, headers: Map, json: String): Request { + return Request.Builder() + .url(url) + .headers(headers.toHeaders()) + .post(json.toRequestBody(MediaType.APPLICATION_JSON_VALUE.toMediaType())) + .build() + } + + /** + * 处理响应流的抽象方法 + * + * 此方法需由子类实现,用以解析响应流并发射聊天响应部分 + * + * @param reader 响应流的BufferedReader对象 + * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 + * @param allContent 用于存储响应流的完整内容 + * @param dispatcher 协程调度器,用于执行异步或并发操作 + */ + abstract suspend fun FlowCollector.processResponse( + reader: BufferedReader, + extractContent: (String) -> ChatResponsePart, + allContent: StringBuilder, + dispatcher: CoroutineDispatcher + ) +} diff --git a/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/ChatStreamAdapter.kt b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/ChatStreamAdapter.kt new file mode 100644 index 0000000..f1a0ec9 --- /dev/null +++ b/llmx-core/llmx-core-spi/src/main/kotlin/org/jcnc/llmx/core/spi/adapter/ChatStreamAdapter.kt @@ -0,0 +1,38 @@ +package org.jcnc.llmx.core.spi.adapter + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.reactivestreams.Publisher + +/** + * 聊天流适配器 + * + * 该接口定义了一个函数,用于发送流式聊天请求,并处理响应流 + * 主要用途是与聊天API进行交互,以流的方式处理聊天消息,从而实现高效的实时通信 + * + * @since 2025-05-11 20:19:29 + * @author gewuyou + */ +interface ChatStreamAdapter { + /** + * 发送流式聊天请求 + * + * 该函数负责向指定URL发送聊天请求,并根据响应流提取内容 + * 使用协程调度器来控制并发和异步处理逻辑 + * + * @param url 请求的URL地址 + * @param headers 请求头,包含认证等信息 + * @param requestBody 请求体,可以是任意类型,根据API需求而定 + * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 + * @param dispatcher 协程调度器,用于执行异步或并发操作 + * @return 返回一个发布者,用于订阅聊天响应流 + */ + fun sendStreamChat( + url: String, + headers: Map, + requestBody: Any, + extractContent: (String) -> ChatResponsePart, + dispatcher: CoroutineDispatcher=Dispatchers.IO + ): Publisher +} 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 69c41fb..5573f8c 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 @@ -8,6 +8,7 @@ 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.RequestBody import org.springframework.web.bind.annotation.RequestMapping /** @@ -31,7 +32,7 @@ interface LLMProvider { * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 */ @PostMapping("/chat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) - fun chat(request: ChatRequest): Publisher + fun chat(@RequestBody request: ChatRequest): Publisher /** * 处理多模态聊天请求的函数 * 该函数通过 POST 方法接收一个聊天请求,并以 NDJSON 的形式返回聊天响应的部分 @@ -43,7 +44,7 @@ interface LLMProvider { * 它使用了响应式编程模型,适合处理高并发和大数据量的响应 */ @PostMapping("/multimodalityChat", produces = [MediaType.APPLICATION_NDJSON_VALUE]) - fun multimodalityChat(request: ChatRequest): Publisher + fun multimodalityChat(@RequestBody request: ChatRequest): Publisher /** * 嵌入功能方法 * 该方法允许用户发送嵌入请求,以获取LLM生成的嵌入向量 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 index 1d83300..a187b13 100644 --- 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 @@ -2,18 +2,14 @@ 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.CoroutineDispatcher +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.flow -import okhttp3.Headers.Companion.toHeaders -import okhttp3.MediaType.Companion.toMediaType +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import org.jcnc.llmx.core.common.entities.response.ChatResponsePart - - +import org.jcnc.llmx.core.spi.adapter.AbstractChatStreamAdapter import org.springframework.stereotype.Component import java.io.BufferedReader @@ -30,71 +26,26 @@ import java.io.BufferedReader */ @Component class DashScopeAdapter( - private val okHttpClient: OkHttpClient, - private val objectMapper: ObjectMapper, -) { + okHttpClient: OkHttpClient, + objectMapper: ObjectMapper, +) : AbstractChatStreamAdapter(okHttpClient, objectMapper) { /** - * 发送流式聊天请求 + * 处理响应流的抽象方法 * - * 本函数构建并发送一个聊天请求,然后以流的形式接收和处理响应。 - * 它主要用于与DashScope API进行交互,提取并发布聊天响应的部分内容。 + * 该方法用于处理从API接收到的响应流。它读取响应流的每一行, + * 如果行以"data:"开头,则提取该行的内容,解析为ChatResponsePart对象, + * 并将解析后的对象发射到流中。 * - * @param url 请求的URL - * @param headers 请求的头部信息 - * @param requestBody 请求的主体内容 - * @param extractContent 一个函数,用于从JSON响应中提取内容 - * @param dispatcher 协程调度器,默认为IO调度器 - * @return 返回一个Flow,发布聊天响应的部分内容 + * @param reader 响应流的BufferedReader对象 + * @param extractContent 函数用于从响应流中提取聊天响应的部分内容 + * @param allContent 用于存储响应流的完整内容 + * @param dispatcher 协程调度器,用于执行异步或并发操作 */ - 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, + override suspend fun FlowCollector.processResponse( reader: BufferedReader, extractContent: (String) -> ChatResponsePart, allContent: StringBuilder, + dispatcher: CoroutineDispatcher, ) { while (currentCoroutineContext().isActive) { val line = withContext(dispatcher) { @@ -116,22 +67,4 @@ class DashScopeAdapter( } } } - - /** - * 构建请求 - * - * 本函数构建一个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/controller/BaiLianProvider.kt b/llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/controller/BaiLianProvider.kt index 039502e..ae3ec9e 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 @@ -34,12 +34,12 @@ class BaiLianProvider( * @param request 聊天请求对象,包含建立聊天所需的信息,如用户标识、会话标识等 * @return 返回一个Flow流,通过该流可以接收到聊天响应的部分数据,如消息、状态更新等 */ - override fun chat(@RequestBody request: ChatRequest): Publisher { - return baiLianModelService.streamChat(request).asPublisher() + override fun chat(request: ChatRequest): Publisher { + return baiLianModelService.streamChat(request) } - override fun multimodalityChat(@RequestBody request: ChatRequest): Publisher { - return baiLianModelService.streamMultimodalityChat(request).asPublisher() + override fun multimodalityChat(request: ChatRequest): Publisher { + return baiLianModelService.streamMultimodalityChat(request) } /** 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/DashScopeMultimodalityChatResponse.kt similarity index 98% rename from llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/entities/response/DashScopeResponse.kt rename to llmx-impl/llmx-impl-bailian/src/main/kotlin/org/jcnc/llmx/impl/baiLian/entities/response/DashScopeMultimodalityChatResponse.kt index 8c7c907..0aaa1fa 100644 --- 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/DashScopeMultimodalityChatResponse.kt @@ -14,7 +14,7 @@ import com.fasterxml.jackson.annotation.JsonProperty * @param requestId 请求ID,用于跟踪和调试请求 */ @JsonIgnoreProperties(ignoreUnknown = true) -data class DashScopeResponse( +data class DashScopeMultimodalityChatResponse( val output: Output?, val usage: Usage?, @JsonProperty("request_id") 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 index d6c6a6b..48389d4 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,9 +1,8 @@ package org.jcnc.llmx.impl.baiLian.service -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.reactivestreams.Publisher /** * 百炼模型服务接口 @@ -17,14 +16,15 @@ interface BaiLianModelService { * 使用流式聊天交互 * * @param request 聊天请求对象,包含用户输入、上下文等信息 - * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 + * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 */ - fun streamChat(request: ChatRequest): Flow + fun streamChat(request: ChatRequest): Publisher + /** * 使用流式多模态聊天交互 * * @param request 聊天请求对象,包含用户输入、上下文等信息 - * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 + * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 */ - fun streamMultimodalityChat(request: ChatRequest) : Flow + fun streamMultimodalityChat(request: ChatRequest): Publisher } 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 index f5340e6..52ae5ca 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 @@ -10,8 +10,9 @@ 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.entities.response.DashScopeMultimodalityChatResponse import org.jcnc.llmx.impl.baiLian.service.BaiLianModelService +import org.reactivestreams.Publisher import org.springframework.stereotype.Service import org.springframework.util.CollectionUtils @@ -35,7 +36,7 @@ class BaiLianModelServiceImpl( * @param request 聊天请求对象,包含用户输入、上下文等信息 * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 */ - override fun streamChat(request: ChatRequest): Flow { + override fun streamChat(request: ChatRequest): Publisher { // 构造请求URL val url = "${dashScopeProperties.baseUrl}${dashScopeProperties.appId}/completion" log.info("请求URL: $url") @@ -132,7 +133,7 @@ class BaiLianModelServiceImpl( * @param request 聊天请求对象,包含用户输入、上下文等信息 * @return 返回一个Flow流,包含部分聊天响应,允许逐步处理和消费响应 */ - override fun streamMultimodalityChat(request: ChatRequest): Flow { + override fun streamMultimodalityChat(request: ChatRequest): Publisher { // 构造请求URL val url = dashScopeProperties.multimodalityUrl log.info("请求URL: $url") @@ -159,7 +160,7 @@ class BaiLianModelServiceImpl( return dashScopeAdapter.sendStreamChat( url, headers, body, { json: String -> - val response = objectMapper.readValue(json, DashScopeResponse::class.java) + val response = objectMapper.readValue(json, DashScopeMultimodalityChatResponse::class.java) val choices = response .output ?.choices diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/adapter/OpenAiAdapter.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/adapter/OpenAiAdapter.kt new file mode 100644 index 0000000..bfe692e --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/adapter/OpenAiAdapter.kt @@ -0,0 +1,60 @@ +package org.jcnc.llmx.impl.openai.adapter + +import com.fasterxml.jackson.databind.ObjectMapper +import com.gewuyou.forgeboot.core.extension.log +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.jcnc.llmx.core.spi.adapter.AbstractChatStreamAdapter +import org.springframework.stereotype.Component +import java.io.BufferedReader + +/** + *Open AI 适配器 + * + * @since 2025-05-11 18:01:15 + * @author gewuyou + */ +@Component +class OpenAiAdapter( + okHttpClient: OkHttpClient, + objectMapper: ObjectMapper, +) : AbstractChatStreamAdapter(okHttpClient, objectMapper) { + + override suspend fun FlowCollector.processResponse( + reader: BufferedReader, + extractContent: (String) -> ChatResponsePart, + allContent: StringBuilder, + dispatcher: CoroutineDispatcher, + ) { + while (currentCoroutineContext().isActive) { + val line = withContext(dispatcher) { + reader.readLine() + } ?: break + + if (line.startsWith("event:")) { + // 可选:可读取事件类型 + continue + } + + if (line.startsWith("data:")) { + val jsonPart = line.removePrefix("data:").trim() + try { + val part = extractContent(jsonPart) + allContent.append(part.content) + log.debug("✅ 提取内容: {}", part) + if (part.content.isNotBlank()) { + emit(part) + } + } catch (e: Exception) { + log.warn("⚠️ 无法解析 JSON: {}", jsonPart, e) + } + } + } + } + +} diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/ClientConfig.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/ClientConfig.kt new file mode 100644 index 0000000..2290390 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/ClientConfig.kt @@ -0,0 +1,60 @@ +package org.jcnc.llmx.impl.openai.config + +import com.gewuyou.forgeboot.core.extension.log +import okhttp3.OkHttpClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + + +/** + *客户端配置 + * + * @since 2025-03-28 17:03:48 + * @author gewuyou + */ +@Configuration +class ClientConfig { + /** + * OkHttpClient + */ + @Bean + fun createOkHttpClient(environment: Environment): OkHttpClient { + val activeProfiles = environment.activeProfiles.getOrNull(0) + return when (activeProfiles) { + "dev", "test" -> { + val trustAllCerts = arrayOf( + object : X509TrustManager { + override fun checkClientTrusted(chain: Array?, authType: String?) {} + override fun checkServerTrusted(chain: Array?, authType: String?) {} + override fun getAcceptedIssuers(): Array = arrayOf() + } + ) + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustAllCerts, SecureRandom()) + val sslSocketFactory = sslContext.socketFactory + log.warn("警告: 开启了不安全的https请求,请不要在正式环境使用!") + OkHttpClient.Builder() + .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier { _, _ -> true } + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } + else -> { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } + } + } +} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/OpenAiConfig.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/OpenAiConfig.kt new file mode 100644 index 0000000..fc7c0ee --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/OpenAiConfig.kt @@ -0,0 +1,15 @@ +package org.jcnc.llmx.impl.openai.config + +import org.jcnc.llmx.impl.openai.config.entities.OpenAiProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Configuration + +/** + *Open AI 配置 + * + * @since 2025-05-11 16:57:14 + * @author gewuyou + */ +@Configuration +@EnableConfigurationProperties(OpenAiProperties::class) +class OpenAiConfig \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/entities/OpenAiProperties.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/entities/OpenAiProperties.kt new file mode 100644 index 0000000..2fedc75 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/entities/OpenAiProperties.kt @@ -0,0 +1,15 @@ +package org.jcnc.llmx.impl.openai.config.entities + +import org.springframework.boot.context.properties.ConfigurationProperties + +/** + *Open AI 属性 + * + * @since 2025-05-11 16:56:15 + * @author gewuyou + */ +@ConfigurationProperties(prefix = "openai") +class OpenAiProperties { + var apiKey: String = "" + var baseUrl: String = "" +} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/OpenAiModelService.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/OpenAiModelService.kt new file mode 100644 index 0000000..7d8d05d --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/OpenAiModelService.kt @@ -0,0 +1,29 @@ +package org.jcnc.llmx.impl.openai.config.service + +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.response.ChatResponsePart +import org.reactivestreams.Publisher + +/** + *Open AI 模型服务 + * + * @since 2025-05-11 17:25:15 + * @author gewuyou + */ +interface OpenAiModelService{ + /** + * 使用流式聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 + */ + fun streamChat(request: ChatRequest): Publisher + + /** + * 使用流式多模态聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 + */ + fun streamMultimodalityChat(request: ChatRequest): Publisher +} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/impl/OpenAiModelServiceImpl.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/impl/OpenAiModelServiceImpl.kt new file mode 100644 index 0000000..33c1cdb --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/config/service/impl/OpenAiModelServiceImpl.kt @@ -0,0 +1,89 @@ +package org.jcnc.llmx.impl.openai.config.service.impl + +import com.fasterxml.jackson.databind.ObjectMapper +import com.gewuyou.forgeboot.core.extension.log +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.openai.adapter.OpenAiAdapter +import org.jcnc.llmx.impl.openai.config.entities.OpenAiProperties +import org.jcnc.llmx.impl.openai.config.service.OpenAiModelService +import org.jcnc.llmx.impl.openai.entities.response.FullResponse +import org.jcnc.llmx.impl.openai.entities.response.OutputTextDelta +import org.jcnc.llmx.impl.openai.entities.response.OutputTextDone +import org.jcnc.llmx.impl.openai.extension.toOpenAIMultimodalityChatRequest +import org.reactivestreams.Publisher +import org.springframework.stereotype.Service + +/** + *Open AI 模型服务实现 + * + * @since 2025-05-11 17:26:23 + * @author gewuyou + */ +@Service +class OpenAiModelServiceImpl( + private val openAiProperties: OpenAiProperties, + private val openAiAdapter: OpenAiAdapter, + private val objectMapper: ObjectMapper, +) : OpenAiModelService { + /** + * 使用流式聊天交互 + * + * @param request 聊天请求对象,包含用户输入、上下文等信息 + * @return 返回一个发布者对象,用于订阅聊天响应的部分数据 + */ + override fun streamChat(request: ChatRequest): Publisher { + TODO("Not yet implemented") + } + + override fun streamMultimodalityChat(request: ChatRequest): Publisher { + val url = "${openAiProperties.baseUrl}/v1/responses" + val headers = mapOf( + "Authorization" to "Bearer ${openAiProperties.apiKey}", + "Content-Type" to "application/json" + ) + val openAIRequest = request.toOpenAIMultimodalityChatRequest() + val body = mutableMapOf( + "stream" to true + ).also { + it.putAll(openAIRequest) + } + return openAiAdapter.sendStreamChat( + url, headers, body, + extractContent = { json -> + val node = objectMapper.readTree(json) + val type = node["type"]?.asText() + + when (type) { + "response.output_text.delta" -> { + val outputTextDelta = objectMapper.treeToValue(node, OutputTextDelta::class.java) + ChatResponsePart(content = outputTextDelta.delta) + } + + "response.output_text.done" -> { + val outputTextDone = objectMapper.treeToValue(node, OutputTextDone::class.java) + ChatResponsePart(content = outputTextDone.text, done = true) + } + "response.completed" -> { + val completed = objectMapper.treeToValue(node, FullResponse::class.java) + val usage = completed.usage?.let { + Usage( + promptTokens = it.inputTokens ?: 0, + completionTokens = it.outputTokens ?: 0, + totalTokens = it.totalTokens ?: 0 + ) + } + ChatResponsePart(content = "", done = true, usage = usage) + } + + else -> { + log.debug("忽略事件类型: {}", type) + ChatResponsePart(content = "") + } + } + } + + ) + } +} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/controller/OpenAiProvider.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/controller/OpenAiProvider.kt new file mode 100644 index 0000000..eb33100 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/controller/OpenAiProvider.kt @@ -0,0 +1,33 @@ +package org.jcnc.llmx.impl.openai.controller + +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.openai.config.service.OpenAiModelService +import org.reactivestreams.Publisher +import org.springframework.web.bind.annotation.RestController + +/** + *Open AI 提供商 + * + * @since 2025-05-11 16:32:51 + * @author gewuyou + */ +@RestController +class OpenAiProvider( + private val openAiModelService: OpenAiModelService +): LLMProvider { + override fun chat(request: ChatRequest): Publisher { + TODO("Not yet implemented") + } + + override fun multimodalityChat(request: ChatRequest): Publisher { + return openAiModelService.streamMultimodalityChat(request) + } + + override fun embedding(request: EmbeddingRequest): EmbeddingResponse { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/entities/response/OpenAiMultimodalityChatReposponse.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/entities/response/OpenAiMultimodalityChatReposponse.kt new file mode 100644 index 0000000..c36371a --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/entities/response/OpenAiMultimodalityChatReposponse.kt @@ -0,0 +1,140 @@ +package org.jcnc.llmx.impl.openai.entities.response + +import com.fasterxml.jackson.annotation.JsonProperty + +/** 通用 SSE 响应封装类型 */ +data class SseEvent( + @JsonProperty("type") + val type: String, + @JsonProperty("data") + val data: T +) + +/** response.created, response.in_progress, response.completed 共用结构 */ +data class FullResponse( + @JsonProperty("id") + val id: String, + @JsonProperty("object") + val objectType: String, + @JsonProperty("created_at") + val createdAt: Long, + @JsonProperty("status") + val status: String, + @JsonProperty("model") + val model: String, + @JsonProperty("output") + val output: List?, + @JsonProperty("service_tier") + val serviceTier: String?, + @JsonProperty("store") + val store: Boolean?, + @JsonProperty("temperature") + val temperature: Double?, + @JsonProperty("top_p") + val topP: Double?, + @JsonProperty("tool_choice") + val toolChoice: String?, + @JsonProperty("tools") + val tools: List?, + @JsonProperty("text") + val text: TextFormat?, + @JsonProperty("usage") + val usage: UsageInfo? +) + +data class TextFormat( + @JsonProperty("format") + val format: FormatType +) + +data class FormatType( + @JsonProperty("type") + val type: String +) + +data class UsageInfo( + @JsonProperty("input_tokens") + val inputTokens: Int?, + @JsonProperty("output_tokens") + val outputTokens: Int?, + @JsonProperty("total_tokens") + val totalTokens: Int? +) + +/** response.output_item.added, response.output_item.done 共用结构 */ +data class OutputItemEvent( + @JsonProperty("output_index") + val outputIndex: Int, + @JsonProperty("item") + val item: MessageItem +) + +data class MessageItem( + @JsonProperty("id") + val id: String, + @JsonProperty("type") + val type: String, + @JsonProperty("status") + val status: String, + @JsonProperty("content") + val content: List, + @JsonProperty("role") + val role: String +) + +data class MessageContent( + @JsonProperty("type") + val type: String, + @JsonProperty("text") + val text: String, + @JsonProperty("annotations") + val annotations: List? +) + +/** response.output_text.delta */ +data class OutputTextDelta( + @JsonProperty("item_id") + val itemId: String, + @JsonProperty("output_index") + val outputIndex: Int, + @JsonProperty("content_index") + val contentIndex: Int, + @JsonProperty("delta") + val delta: String +) + +/** response.output_text.done */ +data class OutputTextDone( + @JsonProperty("item_id") + val itemId: String, + @JsonProperty("output_index") + val outputIndex: Int, + @JsonProperty("content_index") + val contentIndex: Int, + @JsonProperty("text") + val text: String +) + +/** response.content_part.done */ +data class ContentPartDone( + @JsonProperty("item_id") + val itemId: String, + @JsonProperty("output_index") + val outputIndex: Int, + @JsonProperty("content_index") + val contentIndex: Int, + @JsonProperty("part") + val part: MessageContent +) + +/** response.content_part.added */ +data class ContentPartAdded( + @JsonProperty("item_id") + val itemId: String, + @JsonProperty("output_index") + val outputIndex: Int, + @JsonProperty("content_index") + val contentIndex: Int, + @JsonProperty("part") + val part: MessageContent +) diff --git a/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/extension/ChatRequestExtension.kt b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/extension/ChatRequestExtension.kt new file mode 100644 index 0000000..73e811b --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/kotlin/org/jcnc/llmx/impl/openai/extension/ChatRequestExtension.kt @@ -0,0 +1,36 @@ +package org.jcnc.llmx.impl.openai.extension + +import org.jcnc.llmx.core.common.entities.request.ChatRequest +import org.jcnc.llmx.core.common.entities.request.MultiModalContent + +fun ChatRequest.toOpenAIMultimodalityChatRequest(): Map { + val input = this.messages.map { msg -> + mapOf( + "role" to msg.role, + "content" to msg.content.map { content -> + when (content) { + is MultiModalContent.Text -> mapOf( + "type" to "input_text", + "text" to content.text + ) + + is MultiModalContent.Image -> mapOf( + "type" to "input_image", + "image_url" to content.image + ) + } + } + ) + } + + val base = mutableMapOf( + "model" to model, + "input" to input, + ) + + // 附加参数(比如 temperature, max_tokens 等) + base.putAll(options) + + return base +} + diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/application-dev.yml b/llmx-impl/llmx-impl-openai/src/main/resources/application-dev.yml new file mode 100644 index 0000000..9899989 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/resources/application-dev.yml @@ -0,0 +1,2 @@ +server: + port: 8083 \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/application-master.yml b/llmx-impl/llmx-impl-openai/src/main/resources/application-master.yml new file mode 100644 index 0000000..f0253ad --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/resources/application-master.yml @@ -0,0 +1,2 @@ +server: + port: 9004 \ No newline at end of file diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/application-test.yml b/llmx-impl/llmx-impl-openai/src/main/resources/application-test.yml new file mode 100644 index 0000000..2a26b84 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/resources/application-test.yml @@ -0,0 +1,2 @@ +server: + port: 9004 diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/application.properties b/llmx-impl/llmx-impl-openai/src/main/resources/application.properties deleted file mode 100644 index ab85386..0000000 --- a/llmx-impl/llmx-impl-openai/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=llmx-impl-openai diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/application.yml b/llmx-impl/llmx-impl-openai/src/main/resources/application.yml new file mode 100644 index 0000000..3d7808a --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: dev diff --git a/llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-dev.yml b/llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-dev.yml new file mode 100644 index 0000000..2b145bd --- /dev/null +++ b/llmx-impl/llmx-impl-openai/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-openai/src/main/resources/bootstrap-master.yml b/llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-master.yml new file mode 100644 index 0000000..399cdd9 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/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-openai/src/main/resources/bootstrap-test.yml b/llmx-impl/llmx-impl-openai/src/main/resources/bootstrap-test.yml new file mode 100644 index 0000000..3998b26 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/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-openai/src/main/resources/bootstrap.yml b/llmx-impl/llmx-impl-openai/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..29d9890 --- /dev/null +++ b/llmx-impl/llmx-impl-openai/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + application: + name: llmx-impl-openai + profiles: + active: dev -- 2.47.2