aiaiaiaiaiaiaiaiaiaiaiaiai

SpringBoot+Vue+MySQL 高校学科竞赛平台 完整可运行版(源码+文档) 本项目基于SpringBoot+Vue+MySQL构建高校学科竞赛管理平台,覆盖学生、教师、领队教师、管理员四大角色,实现竞赛发布、报名、刷题、评分、公示等全流程闭环管理。系统架构清晰,技术主流,配套完整源码、数据库脚本、万字论文、开题报告、答辩PPT及部署文档,支持一键运行,适用于毕业设计、课程设计及院系二次开发,真正实现“开箱即用”。 阅读详情

AI

一、编程辅助插件

二、阿里云百炼

https://bailian.console.aliyun.com/

为了在原有SpringBoot版本的基础上,添加AI的功能,无法使用SpringAI

同类产品--火山引擎

1、申请Api-Key

当前账号的唯一标识

2、配置环境变量

  • 配置环境变量,包含Api-Key

  • 减少泄露的风险

  • 配置完环境变量之后,记得【重启Idea】

  • 对接API

3、测试官方案例

3.1 引入依赖

 

直接引入之后,有一个jar包冲突

        <!--阿里云百炼-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.22.2</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>
3.2 寻找执行官方案例
package com.javasm.qingqing.aitest;

import java.util.Arrays;
import java.lang.System;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.protocol.Protocol;

public class Demo1 {
    public static GenerationResult callWithMessage() throws ApiException, NoApiKeyException, InputRequiredException {
        Generation gen = new Generation(Protocol.HTTP.getValue(), "https://dashscope.aliyuncs.com/api/v1");
        Message systemMsg = Message.builder()
                .role(Role.SYSTEM.getValue())
                .content("You are a helpful assistant.")
                .build();
        Message userMsg = Message.builder()
                .role(Role.USER.getValue())
                .content("你是谁?")
                .build();
        GenerationParam param = GenerationParam.builder()
                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .model("qwen3-max")
                .messages(Arrays.asList(systemMsg, userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();
        return gen.call(param);
    }
    public static void main(String[] args) {
        try {
            GenerationResult result = callWithMessage();
            System.out.println(result.getOutput().getChoices().get(0).getMessage().getContent());
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            System.err.println("错误信息:"+e.getMessage());
        }
    }
}
3.2 测试其他案例

文字生成图片

package com.javasm.qingqing.aitest;

import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult;
import com.alibaba.dashscope.common.MultiModalMessage;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.exception.UploadFileException;
import com.alibaba.dashscope.utils.Constants;
import com.alibaba.dashscope.utils.JsonUtils;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class QwenImage {

    static {
        // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1
        Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1";
    }

    // 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
    // 若没有配置环境变量,请用百炼API Key将下行替换为:static String apiKey ="sk-xxx"
    static String apiKey = System.getenv("DASHSCOPE_API_KEY");

    public static void call() throws ApiException, NoApiKeyException, UploadFileException, IOException {

        MultiModalConversation conv = new MultiModalConversation();

        MultiModalMessage userMessage = MultiModalMessage.builder().role(Role.USER.getValue())
                .content(Arrays.asList(
                        Collections.singletonMap("text",
                                "一副背景是白色的对联黑体字,悬挂在灵堂中央,房间是一个安静古典的中式布局,红木的茶桌茶几,上面摆放着一些青花瓷和茶碗,大厅的正中央,摆放着一副黑木棺材,棺材前面挂着太阳的照片,对联上左书“感恩金将军”,右书“将军光芒万丈”, 横批“开会”,字体飘逸,在中间挂着一幅中国风的画作,内容是富士山。")
                )).build();

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("watermark", false);
        parameters.put("prompt_extend", true);
        parameters.put("negative_prompt", "");
        parameters.put("size", "1472*1140");

        MultiModalConversationParam param = MultiModalConversationParam.builder()
                .apiKey(apiKey)
                .model("qwen-image-plus")
                .messages(Collections.singletonList(userMessage))
                .parameters(parameters)
                .build();

        MultiModalConversationResult result = conv.call(param);
        System.out.println(JsonUtils.toJson(result));
    }

    public static void main(String[] args) {
        try {
            call();
        } catch (ApiException | NoApiKeyException | UploadFileException | IOException e) {
            System.out.println(e.getMessage());
        }
        System.exit(0);
    }
}
  • 文字生成视频

package com.javasm.qingqing.aitest;

// Copyright (c) Alibaba, Inc. and its affiliates.

import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesis;
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisListResult;
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisParam;
import com.alibaba.dashscope.aigc.videosynthesis.VideoSynthesisResult;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.task.AsyncTaskListParam;
import com.alibaba.dashscope.utils.JsonUtils;
import com.alibaba.dashscope.utils.Constants;
import java.util.HashMap;
import java.util.Map;

public class Text2Video {


    static {
        // 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1
        Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1";
    }

    // 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx"
    // 新加坡和北京地域的API Key不同。获取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    public static String apiKey = System.getenv("DASHSCOPE_API_KEY");

    /**
     * Create a video compositing task and wait for the task to complete.
     */
    public static void text2Video() throws ApiException, NoApiKeyException, InputRequiredException {
        VideoSynthesis vs = new VideoSynthesis();
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("prompt_extend", true);
        parameters.put("watermark", false);
        parameters.put("seed", 12345);

        VideoSynthesisParam param =
                VideoSynthesisParam.builder()
                        .apiKey(apiKey)
                        .model("wan2.5-t2v-preview")
                        .prompt("一幅史诗级可爱的场景。一只小巧可爱的卡通小猫将军,身穿细节精致的金色盔甲,头戴一个稍大的头盔,勇敢地站在悬崖上。他骑着一匹虽小但英勇的战马,说:”青海长云暗雪山,孤城遥望玉门关。黄沙百战穿金甲,不破楼兰终不还。“。悬崖下方,一支由老鼠组成的、数量庞大、无穷无尽的军队正带着临时制作的武器向前冲锋。这是一个戏剧性的、大规模的战斗场景,灵感来自中国古代的战争史诗。远处的雪山上空,天空乌云密布。整体氛围是“可爱”与“霸气”的搞笑和史诗般的融合。")
                        .audioUrl("https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250923/hbiayh/%E4%BB%8E%E5%86%9B%E8%A1%8C.mp3")
                        .negativePrompt("")
                        .size("832*480")
                        .duration(10)
                        // .audio(true)
                        .parameters(parameters)
                        .build();

        // 异步调用
        VideoSynthesisResult task = vs.asyncCall(param);
        System.out.println(JsonUtils.toJson(task));
        System.out.println("please wait...");

        //获取结果
        VideoSynthesisResult result = vs.wait(task, apiKey);
        System.out.println(JsonUtils.toJson(result));
    }

    // 获取任务列表
    public static void listTask() throws ApiException, NoApiKeyException {
        VideoSynthesis is = new VideoSynthesis();
        AsyncTaskListParam param = AsyncTaskListParam.builder().build();
        param.setApiKey(apiKey);
        VideoSynthesisListResult result = is.list(param);
        System.out.println(result);
    }

    // 获取单个任务结果
    public static void fetchTask(String taskId) throws ApiException, NoApiKeyException {
        VideoSynthesis is = new VideoSynthesis();
        // 如果已设置 DASHSCOPE_API_KEY 为环境变量,apiKey 可为空
        VideoSynthesisResult result = is.fetch(taskId, apiKey);
        System.out.println(result.getOutput());
        System.out.println(result.getUsage());
    }

    public static void main(String[] args) {
        try {
            text2Video();
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            System.out.println(e.getMessage());
        }
        System.exit(0);
    }
}

三、WebSocket + 云百炼

1、案例:AI陪聊

  • 实现思路

    • 点击首页游戏列表中,推荐的陪玩头像

    • 跳转到个人用户的私聊页面

    • 模拟AI与用户聊天

  • 跳转传值

    • url中携带聊天id,根据聊天id的值,查询到当前聊天用户的信息

    • 把首页中,已经查询到的用户信息,通过pinia传输

  • 页面接收参数

    • 首页跳转传入的参数接收到

    • 从pina中取出

  • 整体思路

    • 使用WebSocket,做前后端分离的数据传输

    • 前端项目和后端项目,可以互相发消息

    • 使用JSON作为互相传输的格式

    • 先写好WebSocket端的代码

    • WebSocket中,接入AI,改造官方的Demo案例

    • 测试AI的官方案例代码

    • 实现AI的调用

    • 发送用户的信息,服务端接收问题,把问题发送给AI

    • 服务端把AI的答案,返回给前端

    • 保存聊天记录

    • 前端处理显示回答

  • 先后端,后前端

1.1 生成代码
  • web_user_ai

    • 一部分用户为ai用户

    • 记录了ai用户的人设,和使用的模型

  • chat_history

    • 记录了A用户和其他用户的聊天记录唯一编码

    • 用户每和1个人聊天,都会产生一条聊天记录

  • chat_history_info

    • 作为聊天记录表的副表

    • chat_history和当前表是一对多的关系

    • 1条聊天记录,里面包含了很多条聊天内容

1.2 记录聊天记录详情
​
@Service("chatHistoryInfoService")
public class ChatHistoryInfoServiceImpl extends ServiceImpl<ChatHistoryInfoDao, ChatHistoryInfo> implements ChatHistoryInfoService {
​
    @Override
    public void saveHistory(Integer chatId, Integer uid, String msg) {
        ChatHistoryInfo chatHistoryInfo = new ChatHistoryInfo();
        chatHistoryInfo.setChatId(chatId);
        chatHistoryInfo.setUid(uid);
        chatHistoryInfo.setMessage(msg);
        chatHistoryInfo.setCtime(new Date());
        save(chatHistoryInfo);
    }
    @Override
    @Transactional
    public void saveHistory(Integer chatId, Integer touid, List<String> urlList) {
        List<ChatHistoryInfo> saveList = new ArrayList<>();
        urlList.forEach(url ->{
            ChatHistoryInfo chatHistoryInfo = new ChatHistoryInfo();
            chatHistoryInfo.setChatId(chatId);
            chatHistoryInfo.setUid(touid);
            chatHistoryInfo.setMessage(url);
            chatHistoryInfo.setCtime(new Date());
            saveList.add(chatHistoryInfo);
        });
        saveBatch(saveList);
    }
    @Override
    public List<ChatHistoryInfo> queryListByChatId(Integer chatId) {
        //根据聊天id 查询聊天记录
        LambdaQueryWrapper<ChatHistoryInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ChatHistoryInfo::getChatId,chatId);
        //查询最后5条聊天记录
        queryWrapper.orderByDesc(ChatHistoryInfo::getCtime);
        queryWrapper.last("limit 5");
        //聊天记录的结果
        List<ChatHistoryInfo> list = list(queryWrapper);
        //按照id 正序排列
        list.sort((Comparator.comparing(ChatHistoryInfo::getId)));
        return list;
    }
​
​
}
​
​
1.3 获取聊天id
@Service("chatHistoryService")
public class ChatHistoryServiceImpl extends ServiceImpl<ChatHistoryDao, ChatHistory> implements ChatHistoryService {
​
    @Override
    public Integer queryIdByMyUidAndToUid(Integer myuid, Integer touid) {
        //已知两个人的uid,查询出这两个人的聊天记录id
        LambdaQueryWrapper<ChatHistory> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ChatHistory::getUid,myuid);
        queryWrapper.eq(ChatHistory::getTouid,touid);
        ChatHistory one = getOne(queryWrapper);
        if (one != null){
            return one.getId();
        }
        //如果之前没有聊过
        ChatHistory chatHistory = new ChatHistory();
        chatHistory.setUid(myuid);
        chatHistory.setTouid(touid);
        save(chatHistory);
        return chatHistory.getId();
    }
}
1.4 WebSocket
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AiMessage {
    //协议
    private Integer agreement;
    //返回给前端的答案
    private String answer;
    //回答的状态 0开始回答,1正在回答,2回答结束
    private Integer status;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebUserAi extends Model<WebUserAi> {
​
    @TableId(type = IdType.INPUT)
    private Integer uid;
    //人物性格
    @TableField("`character`")
    private String character;
    //模型
    private String model;
​
​
}
1.5 页面
  • 新建pina的js文件,用于存储数据

import {ref} from 'vue'
import {defineStore} from "pinia";
​
export default defineStore('chatUserStore',()=>{
    const user = ref({
        uid: -1,
        gid: 1,
        nickname: "",
        headPic: "",
        price: 0
    })
    return {
        user
    }
},{
    persist:{
        storage: sessionStorage,
        paths:['user']
    }
})
1.6 跳转的方法
<template>
  <div class="hot-room">
    <div class="row">
      <h1>人气推荐</h1>
    </div>
    <div class="row">
      <div v-for="game in game_list"
           class="col-md-2 col-lg-1 card a game-card"
           :id="'game-card-'+game.id" @click="viewUser(game.id)">
        <img :src="game.icon">
        <div class="card-body">
          <h6 class="card-title">{{game.name}}</h6>
        </div>
      </div>
    </div>
    <div class="row skill-list">
      <div v-for="user in game_user_list"
           class="col-lg-2 col-md-4 card skill-card"
            @click="jumpAiMessage(user)">
        <img :src="user.headPic">
        <div class="card-body">
          <h5 class="card-title">{{user.nickname}}</h5>
        </div>
        <div class="card-body row">
          <div class="col">
            <span class="bg-success text-white p-1">{{user.gameName}}</span>
          </div>
          <div class="col">
            <span class="text-danger a">{{user.price}}/小时</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
​
<script setup>
import {ref, onMounted, nextTick} from "vue";
import router from '@/router/index'
import api from '@/utils/request.js'
import UserHomeGameStore from "@/stores/UserHomeGameStore.js";
​
let game_list = ref([])
let game_user_list=ref([])
import ChatUserSotre from "@/stores/ChatUserSotre.js";
let jumpAiMessage =(user)=>{
    ChatUserSotre().user = user;
    router.push("/ai/index")
}
let viewUser = (gameId) => {
  //移除game-card的class属性 active
  document.querySelectorAll(".game-card").forEach(item => {
    item.classList.remove("active")
  })
  //添加当前game-card的class属性 active
  document.querySelector("#game-card-" + gameId).classList.add("active")
  game_user_list.value = game_map.value.get(gameId);
}
​
let query=()=>{
  //先去SessionStorage中查询数据
  if (UserHomeGameStore().gameList.length > 0){
    //本地有数据,已经缓存了
    game_list.value = UserHomeGameStore().gameList;
    createMap(UserHomeGameStore().gameList)
    nextTick(()=>{
      viewUser(-1)
    })
  }else {
    api.get("/home/game").then(result=>{
      //本页循环需要使用的数据
      game_list.value = result.data;
      //本地SessionStorage缓存数据
      UserHomeGameStore().gameList = result.data;
      createMap(result.data)
      nextTick(()=>{
        viewUser(-1)
      })
    })
  }
​
}
let game_map = ref({})
function createMap(list){
  let map = new Map();
  list.forEach(game=>{
    map.set(game.id,game.gameSkillList);
  })
  game_map.value = map;
}
​
onMounted(() => {
  query();
});
</script>
​
<style scoped>
​
</style>
1.7 聊天页面,显示对方的信息
<template>
  <Top/>
  <div class="container-fluid main-content-wrapper">
    <div class="content-page-box-area">
      <div class="all-messages-body">
        <div class="all-messages-header d-flex justify-content-between align-items-center">
          <h3>消息</h3>
        </div>
        <div class="messages-profile-box">
          <a href="/ai">
            <img :src="toUser.headPic" style="width: 100px;" class="rounded-circle" alt="image">
          </a>
          <h3><a href="#">{{toUser.nickname}}</a></h3>
        </div>
        <div class="messages-chat-container">
          <div class="chat-content" id="chat-content"></div>
          <div class="chat-list-footer">
            <div class="d-flex align-items-center">
              <div class="btn-box d-flex align-items-center me-3">
                <button class="file-attachment-btn d-inline-block me-2" data-bs-toggle="tooltip"
                        data-bs-placement="top" title="File Attachment" type="button"><i
                    class="ri-attachment-2"></i></button>
                <button class="emoji-btn d-inline-block" data-bs-toggle="tooltip"
                        data-bs-placement="top" title="Emoji" type="button"><i
                    class="ri-user-smile-line"></i></button>
              </div>
              <input type="text" class="form-control" placeholder="输入消息..." v-model="messageStr"
                     @keyup.enter="sendMsg">
              <button type="button" class="send-btn d-inline-block" @click="sendMsg">发送</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <Footer/>
</template>
​
<script setup>
import {ref, onMounted} from "vue";
import router from '@/router/index.js'
import api from '@/utils/request.js';
import Top from "@/components/common/Top.vue";
import Footer from "@/components/common/Footer.vue";
​
import ChatUserSotre from "@/stores/ChatUserSotre.js";
let toUser = ChatUserSotre().user;
​
onMounted(() => {
})
</script>
​
​
<style scoped>
​
</style>
1.8.建立socket连接
<template>
  <Top/>
  <div class="container-fluid main-content-wrapper">
    <div class="content-page-box-area">
      <div class="all-messages-body">
        <div class="all-messages-header d-flex justify-content-between align-items-center">
          <h3>消息</h3>
        </div>
        <div class="messages-profile-box">
          <a href="/ai">
            <img :src="toUser.headPic" style="width: 100px;" class="rounded-circle" alt="image">
          </a>
          <h3><a href="#">{{toUser.nickname}}</a></h3>
        </div>
        <div class="messages-chat-container">
          <div class="chat-content" id="chat-content"></div>
          <div class="chat-list-footer">
            <div class="d-flex align-items-center">
              <div class="btn-box d-flex align-items-center me-3">
                <button class="file-attachment-btn d-inline-block me-2" data-bs-toggle="tooltip"
                        data-bs-placement="top" title="File Attachment" type="button"><i
                    class="ri-attachment-2"></i></button>
                <button class="emoji-btn d-inline-block" data-bs-toggle="tooltip"
                        data-bs-placement="top" title="Emoji" type="button"><i
                    class="ri-user-smile-line"></i></button>
              </div>
              <input type="text" class="form-control" placeholder="输入消息..." v-model="messageStr"
                     @keyup.enter="sendMessage">
              <button type="button" class="send-btn d-inline-block" @click="sendMessage">发送</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <Footer/>
</template>
​
<script setup>
import {ref, onMounted} from "vue";
import router from '@/router/index.js'
import api from '@/utils/request.js';
import socket from "@/utils/socket.js";
import UserStore from "@/stores/UserStore.js";
import Top from "@/components/common/Top.vue";
import Footer from "@/components/common/Footer.vue";
​
import ChatUserSotre from "@/stores/ChatUserSotre.js";
let toUser = ChatUserSotre().user;
let loginUser = UserStore().userModel;
//获取当前用户,和对方的用户信息
let toUid = toUser.uid;
let myUid = loginUser.uid;
​
let messageStr = ref("");
​
let webSocket;
let initWebSocket=()=>{
    //ws://192.168.0.151:8080/ai/textChat/{myuid}/{touid}
    let url = "/ai/textChat/"+myUid+"/"+toUid;
    //调用封装好的方法,连接socket
    socket.initWebSocket(url);
    webSocket = socket.getWebSocket();
    //打印收到的消息
    webSocket.onmessage=e=>printMessage(e.data);
}
let sendMessage = () => {
  let param = {
    "question": messageStr.value
  }
​
  webSocket.send(JSON.stringify(param));
  //清空聊天框
  messageStr.value = "";
}
let chatNum = 0;
let printMessage = (msg)=>{
  let msgObj = JSON.parse(msg);
  //协议号
  let agreement = msgObj.agreement
  let pic;
  let chat_mine = "";
  let html = "";
  switch (agreement) {
    case 3001:
      //3001 是用户自己发出去的消息
      let question = msgObj.answer;
      //用户头像
      pic = loginUser.userInfo.headPic
      //默认不是我的
      chat_mine = "chat-right";
      html = '<div class="chat ' + chat_mine + '">\n' +
          '                <div class="chat-avatar">\n' +
          '                  <a class="d-inline-block">\n' +
          '                    <img src="' + pic + '" width="50" height="50"\n' +
          '                         class="rounded-circle" alt="image">\n' +
          '                  </a>\n' +
          '                </div>\n' +
          '                <div class="chat-body">\n' +
          '                  <div class="chat-message">\n' +
          '                    <p>' + question + '</p>\n' +
          '                  </div>\n' +
          '                </div>\n' +
          '              </div>\n' +
          '            </div>'
      document.getElementById("chat-content").insertAdjacentHTML("beforeend", html);
      break;
    case 3002:
      //是ai发送来的,一定是在对面
      let answer = msgObj.answer;
      //用户头像
      pic = toUser.headPic;
      let status = msgObj.status;
      switch (status){
        case 0:
          //开始回答问题,创建一个空的聊天框,没有聊天内容
          html = '<div class="chat ' + chat_mine + '">\n' +
              '                <div class="chat-avatar">\n' +
              '                  <a class="d-inline-block">\n' +
              '                    <img src="' + pic + '" width="50" height="50"\n' +
              '                         class="rounded-circle" alt="image">\n' +
              '                  </a>\n' +
              '                </div>\n' +
              '                <div class="chat-body">\n' +
              //记录 当前是页面里的第几个聊天框
              '                  <div class="chat-message" id="chat-meesage-'+chatNum+'"></div>\n' +
              '                </div>\n' +
              '              </div>\n' +
              '            </div>';
          document.getElementById("chat-content").insertAdjacentHTML("beforeend", html);
          break;
        case 1:
          //回答问题的过程,是ai返回的内容,要追加到聊天框里
          document.getElementById("chat-meesage-"+chatNum)
              .insertAdjacentHTML("beforeend", answer);
          break;
        case 2:
          //回答结束
          chatNum++;
          break;
      }
​
      break;
  }
}
​
onMounted(() => {
  initWebSocket()
  console.log(toUid)
})
</script>
​
​
<style scoped>
​
</style>
1.9 解决md语法显示的问题

引入插件

pnpm install marked

  • 页面引入插件

import {marked} from "marked";
  • 调用

marked(msgContent)
1.10 添加聊天记录
<template>
  <Top/>
  <div class="container-fluid main-content-wrapper">
    <div class="content-page-box-area">
      <div class="all-messages-body">
        <div class="all-messages-header d-flex justify-content-between align-items-center">
          <h3>消息</h3>
        </div>
        <div class="messages-profile-box">
          <a href="/ai">
            <img :src="toUser.headPic" style="width: 100px;" class="rounded-circle" alt="image">
          </a>
          <h3><a href="#">{{toUser.nickname}}</a></h3>
        </div>
        <div class="messages-chat-container">
          <div class="chat-content" id="chat-content"></div>
          <div class="chat-list-footer">
            <div class="d-flex align-items-center">
              <div class="btn-box d-flex align-items-center me-3">
                <button class="file-attachment-btn d-inline-block me-2" data-bs-toggle="tooltip"
                        data-bs-placement="top" title="File Attachment" type="button"><i
                    class="ri-attachment-2"></i></button>
                <button class="emoji-btn d-inline-block" data-bs-toggle="tooltip"
                        data-bs-placement="top" title="Emoji" type="button"><i
                    class="ri-user-smile-line"></i></button>
              </div>
              <input type="text" class="form-control" placeholder="输入消息..." v-model="messageStr"
                     @keyup.enter="sendMessage">
              <button type="button" class="send-btn d-inline-block" @click="sendMessage">发送</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <Footer/>
</template>

<script setup>
import {ref, onMounted} from "vue";
import router from '@/router/index.js'
import api from '@/utils/request.js';
import socket from "@/utils/socket.js";
import UserStore from "@/stores/UserStore.js";
import Top from "@/components/common/Top.vue";
import Footer from "@/components/common/Footer.vue";
import {marked} from "marked";


import ChatUserSotre from "@/stores/ChatUserSotre.js";
let toUser = ChatUserSotre().user;
let loginUser = UserStore().userModel;
//获取当前用户,和对方的用户信息
let toUid = toUser.uid;
let myUid = loginUser.uid;

let messageStr = ref("");

let webSocket;
let initWebSocket=()=>{
    //ws://192.168.0.151:8080/ai/textChat/{myuid}/{touid}
    let url = "/ai/textChat/"+myUid+"/"+toUid;
    //调用封装好的方法,连接socket
    socket.initWebSocket(url);
    webSocket = socket.getWebSocket();
    //打印收到的消息
    webSocket.onmessage=e=>printMessage(e.data);
}
let sendMessage = () => {
  let param = {
    "question": messageStr.value
  }

  webSocket.send(JSON.stringify(param));
  //清空聊天框
  messageStr.value = "";
}
let chatNum = 0;
let printMessage = (msg)=>{
  let msgObj = JSON.parse(msg);
  //协议号
  let agreement = msgObj.agreement
  let pic;
  let chat_mine = "";
  let html = "";
  switch (agreement) {
    case 3001:
      //3001 是用户自己发出去的消息
      let question = msgObj.answer;
      //用户头像
      pic = loginUser.userInfo.headPic
      //默认不是我的
      chat_mine = "chat-right";
      printChatHtml(chat_mine,pic,question)
      break;
    case 3002:
      //是ai发送来的,一定是在对面
      let answer = msgObj.answer;
      //用户头像
      pic = toUser.headPic;
      let status = msgObj.status;
      switch (status){
        case 0:
          allAnswer = "";
          //开始回答问题,创建一个空的聊天框,没有聊天内容
          html = '<div class="chat ' + chat_mine + '">\n' +
              '                <div class="chat-avatar">\n' +
              '                  <a class="d-inline-block">\n' +
              '                    <img src="' + pic + '" width="50" height="50"\n' +
              '                         class="rounded-circle" alt="image">\n' +
              '                  </a>\n' +
              '                </div>\n' +
              '                <div class="chat-body">\n' +
              //记录 当前是页面里的第几个聊天框
              '                  <div class="chat-message" id="chat-meesage-'+chatNum+'"></div>\n' +
              '                </div>\n' +
              '              </div>\n' +
              '            </div>';
          document.getElementById("chat-content").insertAdjacentHTML("beforeend", html);
          break;
        case 1:
          //回答问题的过程,是ai返回的内容,要追加到聊天框里
          document.getElementById("chat-meesage-"+chatNum)
              .insertAdjacentHTML("beforeend", answer);
          //每次的答案内容,都存储的同一个变量
          allAnswer += answer;
          break;
        case 2:
          //回答结束
            html = marked(allAnswer);
          document.getElementById("chat-meesage-"+chatNum).innerHTML = marked(allAnswer);
          allAnswer = "";
          chatNum++;
          break;
      }

      break;
    case 2001:
      //聊天记录
        let chatList = msgObj.list;
        chatList.forEach(chat=>{
          let msgUid = chat.uid;
          let msgContent = chat.message;
          let chat_mine = "";
          let chat_Pic = toUser.headPic;
          if (msgUid === myUid){
            chat_mine = "chat-right"
            chat_Pic = loginUser.userInfo.headPic;
          }
          printChatHtml(chat_mine,chat_Pic,marked(msgContent))
        })
      break;
  }

}

let allAnswer = "";

let printChatHtml = (chat_mine,pic,question)=>{
  let html = '<div class="chat ' + chat_mine + '">\n' +
      '                <div class="chat-avatar">\n' +
      '                  <a class="d-inline-block">\n' +
      '                    <img src="' + pic + '" width="50" height="50"\n' +
      '                         class="rounded-circle" alt="image">\n' +
      '                  </a>\n' +
      '                </div>\n' +
      '                <div class="chat-body">\n' +
      '                  <div class="chat-message">\n' +
      '                    <p>' + question + '</p>\n' +
      '                  </div>\n' +
      '                </div>\n' +
      '              </div>\n' +
      '            </div>'
  document.getElementById("chat-content").insertAdjacentHTML("beforeend", html);
}
onMounted(() => {
  initWebSocket()
  console.log(toUid)
})
</script>


<style scoped>

</style>

2、案例:图片生成图片

效果:用户在聊天的时候,如果上传了图片,会按照用户描述的内容加上图片的内容,给用户生成新的图片

生成的图片,返回到聊天对话框中

2.1 上传按钮 和 预览图片
<div class="row">
              <div class="col-md-12">
                <input type="file" accept="image/*" @change="onFileSelected">
              </div>

            </div>
            <div class="row">
              <div class="col-md-12">
                <img :src="previewImage" style="width: 100%"/>
              </div>
            </div>
//预览图的路径
let previewImage = ref("")
//当前正在选择的图片对象,点击发送按钮的时候使用
let consultFile = null;
//上传的方法
let onFileSelected=(event)=>{
    //获取文件
  const file = event.target.files[0];
  //如果传了文件
  if (file){
    //新建一个读取图片对象
    const reader = new FileReader();
    //当图片被加载的时候,执行的方法
    reader.onload=(e)=>{
      previewImage.value = e.target.result;
    }
    //读取图片
    reader.readAsDataURL(file)
    consultFile = file;
  }
}
2.2 发送图片
//发送图片到服务器
function sendImage(){
  //通过WebSocket发送,发送二进制数据,到服务器
  const reader = new FileReader();
  reader.onload=(e)=>{
    //获取图片的二进制数据
    let imgbytes = new Uint8Array(e.target.result);
    //构建图片的头信息--告诉服务器,来的二进制数据,类型是图片
    //发送二进制数据到服务端的时候,固定前6位,表示文件类型
    const header = new TextEncoder().encode("IMAGE:")
    //构建完整的数据
    const conbined = new Uint8Array(header.length +imgbytes.length);
    //从第0位开始,是header的信息
    conbined.set(header,0);
    //header结尾的位置开始,是图片的数据
    conbined.set(imgbytes,header.length);
    //数据准备完成,判断session是否存在
    if (webSocket.readyState === WebSocket.OPEN){
      //连接状态
      //发送二进制数据
      webSocket.send(conbined.buffer);
      console.log("图片上传完成")
    }else {
      console.error("WebSocket未连接,无法发送图片")
    }

  }

  //读取图片
  reader.readAsArrayBuffer(consultFile);
}
2.3 在js中的WebSocket,添加配置二进制数据
let webSocket;

export default {
    //初始化socket连接
    initWebSocket(url){
        let baseUrl = import.meta.env.VITE_APP_WebSocket_BASE_API;
        ///room/mws://127.0.0.1:8080ultiuser/{roomId}/{uid}

        if ('WebSocket' in  window){
            //判断当前浏览器是否支持WebSocket
            webSocket = new WebSocket(baseUrl + url);
            //设置二进制类型是arraybuffer
            webSocket.binaryType='arraybuffer';
        }
        //连接WebSocket,在客户端发生的事情
        webSocket.onopen = function (){
            console.log("连接成功!")
        }
        //当连接关闭的时候,执行的方法
        webSocket.onclose = function (){
            console.log("断开 连接")
        }
        window.onbeforeunload = function (){
            //当页面刷新的时候,主动关闭连接
            webSocket.close(3000,"关闭");
        }


    },
    getWebSocket(){
        return webSocket;
    }
}
2.4 发送消息

计划让文字消息,和图片消息,一起发送,通过参数来区分是图片消息还是文字消息

文本已经分为了两个类型 文字消息和文字+图片的消息

let sendMessage = () => {
  //type 1文字消息 2图片消息
  let type = 1;
  if (consultFile){
      //如果有图片,先把图片上传到服务器,等待文字消息上传
    sendImage();
    type = 2;
  }
  let param = {
    "question": messageStr.value,
    "type":type
  }


  webSocket.send(JSON.stringify(param));
  //清空聊天框
  messageStr.value = "";
}
2.5 修改WebSocket,允许上传二进制数据

现在需要接收二进制数据

必须在OnOpen的位置,设置好,二进制消息的缓冲区

如果这里忘记了设置,无法接收二进制消息

只要发送了二进制消息,就会自动断开连接

  • 配置服务器端二进制

    @OnOpen
    public void startAi(@PathParam("myuid") Integer myuid,
                        @PathParam("touid") Integer touid,
                        Session session) throws IOException {
        //设置二进制缓冲区 100MB-- 缓冲区大小,不能超过jvm的内存空间
        session.setMaxBinaryMessageBufferSize(100 * 1024 * 1024);
    }
2.6 修改OnMessage

新增对图片的处理

    //处理二进制文件
    @OnMessage
    public void onMessage(@PathParam("myuid") Integer myuid,
                          @PathParam("touid") Integer touid,
                          Session session,byte[] bytes){
        //预先预定好,所有的文件上传,都要配置一个头信息,以此区分不同的文件类型
        //进入的二进制数据,必须长度 >6
        if (bytes == null || bytes.length < 6){
            return;
        }
        //解析头信息
        String header = new String(bytes,0,6, StandardCharsets.UTF_8);
        if ("IMAGE:".equals(header)){
            //是图片,根据图片的方案,继续处理
            byte[] imageData = Arrays.copyOfRange(bytes,6,bytes.length);

            image2image(myuid,touid,session,imageData);
        }
    }

    private void image2image(Integer myuid, Integer touid, Session session, byte[] imageData) {
        
    }
2.7 编写图片生成图片的方法

找到AI能够图片生成图片的模型

计划使用url地址,访问ai的接口,这样更具有通用性。

curl --location 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $DASHSCOPE_API_KEY" \
--data '{
    "model": "qwen-image-edit-plus",
    "input": {
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "image": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250925/fpakfo/image36.webp"
                    },
                    {
                        "text": "生成一张符合深度图的图像,遵循以下描述:一辆红色的破旧的自行车停在一条泥泞的小路上,背景是茂密的原始森林"
                    }
                ]
            }
        ]
    },
    "parameters": {
        "n": 2,
        "negative_prompt": " ",
        "prompt_extend": true,
        "watermark": false
    }
}'
2.8 参数Vo
/**
 * 图片到图片的请求VO类
 * 用于封装调用AI图像编辑接口所需的数据
 */
@Data
@NoArgsConstructor
public class Image2ImageVo {
    
    /**
     * 构造方法
     * 
     * @param message 文本消息内容,将被设置为content.text
     * @param imgUrl 图片URL,将被设置为content.image
     * @param modelName 模型名称,将被设置为model属性
     */
    public Image2ImageVo(String message, String imgUrl, String modelName) {
        // 设置模型名称
        this.model = modelName;
        
        // 创建输入参数
        this.input = new Input();
        
        // 创建消息列表
        List<Message> messages = new ArrayList<>();
        Message msg = new Message();
        msg.role = "user"; // 默认角色设置为"user"
        
        // 创建内容列表
        List<Content> contents = new ArrayList<>();
        
        // 添加图片内容(如果提供了图片URL)
        if (imgUrl != null && !imgUrl.isEmpty()) {
            ImageContent imageContent = new ImageContent();
            imageContent.setImage(imgUrl);
            contents.add(imageContent);
        }
        
        // 添加文本内容(如果提供了文本消息)
        if (message != null && !message.isEmpty()) {
            TextContent textContent = new TextContent();
            textContent.setText(message);
            contents.add(textContent);
        }
        
        // 设置内容列表到消息中
        msg.content = contents;
        // 将消息添加到消息列表中
        messages.add(msg);
        // 设置消息列表到输入参数中
        this.input.messages = messages;
        
        // 创建默认参数配置
        this.parameters = new Parameters();
        this.parameters.n = 2; // 默认生成2张图片
        this.parameters.prompt_extend = true; // 默认启用提示词扩展
        this.parameters.watermark = false; // 默认不添加水印
        this.parameters.negative_prompt = ""; // 默认空的负面提示词
    }

    /**
     * 模型名称
     */
    private String model;

    /**
     * 输入参数
     */
    private Input input;

    /**
     * 其他参数配置
     */
    private Parameters parameters;

    /**
     * 输入参数内部类
     */
    @Data
    public static class Input {
        /**
         * 消息列表
         */
        private List<Message> messages;
    }

    /**
     * 参数配置内部类
     */
    @Data
    public static class Parameters {
        /**
         * 生成图片数量
         */
        private Integer n;
        
        /**
         * 负面提示词
         */
        private String negative_prompt;
        
        /**
         * 是否扩展提示词
         */
        private boolean prompt_extend;
        
        /**
         * 是否添加水印
         */
        private boolean watermark;
    }

    /**
     * 消息内部类
     */
    @Data
    public static class Message {
        /**
         * 角色
         */
        private String role;
        
        /**
         * 内容列表
         */
        private List<Content> content;
    }

    /**
     * 内容项基类
     * 根据JSON结构,实际使用时会创建其子类ImageContent或TextContent
     */
    @Data
    public static class Content {
        // 基类,不包含具体字段
    }
    
    /**
     * 图片内容项
     */
    @Data
    public static class ImageContent extends Content {
        /**
         * 图片URL
         */
        private String image;
    }
    
    /**
     * 文本内容项
     */
    @Data
    public static class TextContent extends Content {
        /**
         * 文本内容
         */
        private String text;
    }
}
2.9 结果VO
{
    "output": {
        "choices": [
            {
                "finish_reason": "stop",
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "image": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxx"
                        },
                        {
                            "image": "https://dashscope-result-sz.oss-cn-shenzhen.aliyuncs.com/xxx.png?Expires=xxx"
                        }
                    ]
                }
            }
        ]
    },
    "usage": {
        "width": 1248,
        "image_count": 2,
        "height": 832
    },
    "request_id": "bf37ca26-0abe-98e4-8065-xxxxxx"
}
package com.javasm.qingqing.chat.client.vo;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.util.List;

/**
 * 图片到图片的响应结果类
 * 用于接收和解析AI图像编辑接口返回的数据
 */
@Data
@NoArgsConstructor
public class Image2ImageResult {
    
    /**
     * 输出结果
     */
    private Output output;
    
    /**
     * 使用情况统计
     */
    private Usage usage;
    
    /**
     * 请求ID
     */
    private String request_id;
    
    /**
     * 输出结果内部类
     */
    @Data
    public static class Output {
        /**
         * 选择结果列表
         */
        private List<Choice> choices;
    }
    
    /**
     * 选择结果内部类
     */
    @Data
    public static class Choice {
        /**
         * 结束原因
         */
        private String finish_reason;
        
        /**
         * 消息内容
         */
        private Message message;
    }
    
    /**
     * 消息内部类
     */
    @Data
    public static class Message {
        /**
         * 角色
         */
        private String role;
        
        /**
         * 内容列表
         */
        @JsonDeserialize(using = ContentDeserializer.class)
        private List<Content> content;
    }
    
    /**
     * 内容项基类
     */
    @Data
    public static class Content {
        // 基类,不包含具体字段
    }
    
    /**
     * 图片内容项
     */
    @Data
    public static class ImageContent extends Content {
        /**
         * 图片URL
         */
        private String image;
    }
    
    /**
     * 内容反序列化器
     */
    public static class ContentDeserializer extends JsonDeserializer<List<Content>> {
        @Override
        public List<Content> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            ObjectCodec codec = jsonParser.getCodec();
            JsonNode node = codec.readTree(jsonParser);
            
            if (node.isArray()) {
                List<Content> contentList = new java.util.ArrayList<>();
                for (JsonNode contentNode : node) {
                    if (contentNode.has("image")) {
                        ImageContent imageContent = new ImageContent();
                        imageContent.setImage(contentNode.get("image").asText());
                        contentList.add(imageContent);
                    } else {
                        contentList.add(new Content());
                    }
                }
                return contentList;
            }
            
            return new java.util.ArrayList<>();
        }
    }
    
    /**
     * 使用情况统计内部类
     */
    @Data
    public static class Usage {
        /**
         * 图片宽度
         */
        private Integer width;
        
        /**
         * 图片数量
         */
        private Integer image_count;
        
        /**
         * 图片高度
         */
        private Integer height;
    }
}
2.10 HttpClient
@Component
public class TongYiHttpClient {

    @Resource
    RestTemplate restTemplate;

    private static final String Image2ImageUrl =
            "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation";

    private static final String TongYiKey = System.getenv("DASHSCOPE_API_KEY");

    //message生成图片的文字描述
    //imageUrl在线图片的地址
    //modelName 模型的名称
    public List<String> sendImage2Image(String message, String imgUrl, String modelName) {
        //Header
        HttpHeaders headers = new HttpHeaders();
        //--header 'Content-Type: application/json' \
        headers.setContentType(MediaType.APPLICATION_JSON);
        //--header "Authorization: Bearer $DASHSCOPE_API_KEY" \
        headers.set("Authorization", "Bearer " + TongYiKey);
        //配置参数
        Image2ImageVo vo = new Image2ImageVo(message, imgUrl, modelName);
        //创建request对象,发送数据
        HttpEntity<Image2ImageVo> request = new HttpEntity<>(vo, headers);
        //发送请求,生成图片,同步生成,代码到这里会等待
        Image2ImageResult image2ImageResult =
                restTemplate.postForObject(Image2ImageUrl, request, Image2ImageResult.class);
        //根据响应的地址,获取图片集合
        List<Image2ImageResult.Content> contentList = image2ImageResult.getOutput().getChoices().get(0).getMessage().getContent();
        //筛选出图片结果的url集合
        List<String> urlList = contentList.stream().map(content -> {
            Image2ImageResult.ImageContent img = (Image2ImageResult.ImageContent) content;
            return img.getImage();
        }).toList();
        return urlList;
    }
}
2.12 上传二进制数据,保存为在线图片
  • 引入七牛云的依赖

        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>[7.7.0, 7.10.99]</version>
        </dependency>
  • 新建配置类

package com.javasm.qingqing.common.utils;

import com.alibaba.fastjson2.JSON;

import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.Getter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

public class QiniuKodoUtil {

    private static Logger logger = LogManager.getLogger(QiniuKodoUtil.class);

    private static final String accessKey = "t0oUPjPwjqQiS2EM3vm5FIIGI8InWKOOXeNnFpZg";
    private static final String secretKey = "66Hd-TKVVbwRrHSTtiQqE8i8L-VOsUaJ1iWTO8rv";
    //空间名称
    private static final String bucketName = "javasmcd";
    //域名地址
    @Getter
    private static final String domain = "http://cd.ray-live.cn/";

    /**
     * 获取七牛token,返回给客户端
     *
     * @return
     */
    public static String getToken() {
        Auth auth = Auth.create(accessKey, secretKey);
        return auth.uploadToken(bucketName);
    }




    // 新增方法:下载图片字节流
    private static byte[] downloadImage(String imageUrl) throws IOException {
        URL url = new URL(imageUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setConnectTimeout(5000);
        connection.setReadTimeout(10000);

        try (InputStream inputStream = connection.getInputStream();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            return outputStream.toByteArray();

        } finally {
            connection.disconnect();
        }
    }

    // 新增方法:获取文件扩展名
    private static String getFileExtension(String url) {
        try {
            String path = new URL(url).getPath();
            int dotIndex = path.lastIndexOf('.');
            return (dotIndex == -1) ? "" : path.substring(dotIndex);
        } catch (MalformedURLException e) {
            return "";
        }
    }

    public static String uploadBytes(byte[] data, String key) {
        Configuration cfg = new Configuration(Region.region2());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;
        UploadManager uploadManager = new UploadManager(cfg);

        String upToken = getToken();
        try {
            Response response = uploadManager.put(data, key, upToken);
            DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
            logger.info("上传成功:{}", putRet.key);
            return domain + putRet.key; // 注意这里使用key而不是hash
        } catch (QiniuException ex) {
            logger.error("上传失败:{}", ex.response.toString());
            return null;
        }
    }

    public static String uploadAudio(byte[] audio) {
        if (audio == null) {
            return null;
        }
        String key = "audio/" + RandomUtil.getFileName("test.wav");
        return uploadBytes(audio, key);
    }


    public static String upload(String filePath) {
        return upload(new File(filePath));
    }

    public static String upload(File file) {
        //构造一个带指定 Region 对象的配置类
        /**
         * 华东	Region.region0(), Region.huadong()
         * 华北	Region.region1(), Region.huabei()
         * 华南	Region.region2(), Region.huanan()
         * 北美	Region.regionNa0(), Region.beimei()
         * 东南亚	Region.regionAs0(), Region.xinjiapo()
         */
        Configuration cfg = new Configuration(Region.region2());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;// 指定分片上传版本
        //上传对象
        UploadManager uploadManager = new UploadManager(cfg);
        //默认不指定key的情况下,以文件内容的hash值作为文件名
        String key = null;//file.getName();//null;
        String upToken = getToken();
        try {
            Response response = uploadManager.put(file, key, upToken);
            //解析上传成功的结果
            DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
            logger.info(putRet.key);
            logger.info(domain + putRet.hash);
            return domain + putRet.hash;
        } catch (QiniuException ex) {
            Response r = ex.response;
            logger.error(r.toString());
            try {
                logger.error(r.bodyString());
            } catch (QiniuException ex2) {
                //ignore
            }
        }
        return null;
    }

    public static void main(String[] args) {

    }

    public static String uploadAdminHeader(MultipartFile file) {
        try {
            byte[] bytes = file.getBytes();
            String key = "admin_header/" + RandomUtil.getFileName(Objects.requireNonNull(file.getOriginalFilename()));
            return uploadBytes(bytes, key);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static String uploadAiQuestionChatImage(byte[] data){
        String key = "ai_question/"+RandomUtil.getFileName(".png");
        return uploadBytes(data,key);
    }
    public static String uploadAiAnswerChatImage(byte[] data){
        String key = "ai_answer/"+RandomUtil.getFileName(".png");
        return uploadBytes(data,key);
    }
    public static String uploadAiAnswerChatImage(String url){
        try {
            byte[] bytes = downloadImage(url);
            return uploadAiAnswerChatImage(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static String getRandomHeader() {
        int num = ThreadLocalRandom.current().nextInt(1,220);
        return String.format("http://cd.ray-live.cn/imgs/headpic/pic_%s.jpg", num);
    }

}
2.13 修改WebSocket流程,添加图片生成
package com.javasm.qingqing.chat.websocket;

/**
 * AI聊天WebSocket处理器
 * 处理用户与AI之间的实时聊天功能
 */

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.javasm.qingqing.chat.client.TongYiHttpClient;
import com.javasm.qingqing.chat.entity.ChatHistoryInfo;
import com.javasm.qingqing.chat.service.ChatHistoryInfoService;
import com.javasm.qingqing.chat.service.ChatHistoryService;
import com.javasm.qingqing.chat.vo.AiMessage;
import com.javasm.qingqing.chat.vo.ChatHistoryMessage;
import com.javasm.qingqing.common.constans.WebSocketAgreement;
import com.javasm.qingqing.common.utils.QiniuKodoUtil;
import com.javasm.qingqing.common.utils.SpringContextUtil;
import com.javasm.qingqing.webuser.entity.WebUserAi;
import com.javasm.qingqing.webuser.entity.WebUserInfo;
import com.javasm.qingqing.webuser.service.WebUserAiService;
import com.javasm.qingqing.webuser.service.WebUserInfoService;
import io.reactivex.Flowable;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Component
@ServerEndpoint("/ai/textChat/{myuid}/{touid}")
public class AiChatSocket {
    //查询出,和我聊天的人的信息
    WebUserAi userAi = new WebUserAi();
    //聊天这个ai的具体详情
    WebUserInfo userInfo = new WebUserInfo();
    //两个人聊天记录的id
    private Integer chatId = 0;
    List<Message> msgList = new ArrayList<>();

    /**
     * 获取聊天历史服务实例
     *
     * @return ChatHistoryService 聊天历史服务实例
     */
    public ChatHistoryService getChatHistoryService() {
        return SpringContextUtil.getBean(ChatHistoryService.class);
    }

    /**
     * 获取聊天历史信息服务实例
     *
     * @return ChatHistoryInfoService 聊天历史信息服务实例
     */
    public ChatHistoryInfoService getChatHistoryInfoService() {
        return SpringContextUtil.getBean(ChatHistoryInfoService.class);
    }

    /**
     * WebSocket连接打开时的处理方法
     * 初始化AI聊天环境,加载用户信息和聊天历史
     *
     * @param myuid   当前用户的ID
     * @param touid   对方用户(AI)的ID
     * @param session WebSocket会话对象
     * @throws IOException IO异常
     */
    @OnOpen
    public void startAi(@PathParam("myuid") Integer myuid,
                        @PathParam("touid") Integer touid,
                        Session session) throws IOException {
        //设置合理的二进制消息最大大小限制 (100MB)
        session.setMaxBinaryMessageBufferSize(100 * 1024 * 1024);
        //设置文本消息最大大小限制 (1MB)
        session.setMaxTextMessageBufferSize(1024 * 1024);
        //进入聊天界面的时候,第一个触发的方法
        //根据uid,查询出用户的信息,touid 和我聊天的这个人的用户信息
        WebUserAiService aiUserService = SpringContextUtil.getBean(WebUserAiService.class);
        userAi = aiUserService.getById(touid);
        //查询个人详情信息
        WebUserInfoService infoUserService = SpringContextUtil.getBean(WebUserInfoService.class);
        userInfo = infoUserService.getById(touid);
        //开启对话,配置聊天对象,ai的人设
        String aiProm = "You are a helpful assistant.";
        if (userAi != null && userAi.getCharacter() != null) {
            aiProm = userAi.getCharacter();
        }
        //构建ai的角色对象
        Message systemMsg = Message.builder()
                .role(Role.SYSTEM.getValue())
                .content(aiProm)
                .build();
        msgList.add(systemMsg);
        //查询两个人是否有聊天记录
        chatId = getChatHistoryService().queryIdByMyUidAndToUid(myuid, touid);
        if (chatId != null) {
            //根据聊天id 查询两人的具体聊天记录
            List<ChatHistoryInfo> historyInfoList =
                    getChatHistoryInfoService().queryListByChatId(chatId);
            if (!historyInfoList.isEmpty()) {
                //把聊天记录 发送给用户
                ChatHistoryMessage chatHistoryMessage =
                        new ChatHistoryMessage(WebSocketAgreement.Chat_History, historyInfoList);
                //json对象转换
                sendText(session, chatHistoryMessage);
            }
        }


    }

    /**
     * WebSocket连接关闭时的处理方法
     *
     * @param myuid   当前用户的ID
     * @param touid   对方用户(AI)的ID
     * @param session WebSocket会话对象
     */
    @OnClose
    public void endAi(@PathParam("myuid") Integer myuid,
                      @PathParam("touid") Integer touid,
                      Session session) {

    }

    /**
     * WebSocket发生错误时的处理方法
     *
     * @param throwable 异常对象
     */
    @OnError
    public void error(Throwable throwable) {
        throwable.printStackTrace();
    }

    //文本消息
    private static String question = "";
    /**
     * 接收并处理WebSocket文本消息
     * 处理用户提问,调用AI生成回答,并保存聊天记录
     *
     * @param myuid   当前用户的ID
     * @param touid   对方用户(AI)的ID
     * @param session WebSocket会话对象
     * @param json    前端发送的JSON格式消息
     * @throws NoApiKeyException      缺少API密钥异常
     * @throws InputRequiredException 输入必需异常
     */
    @OnMessage
    public void replyMessage(@PathParam("myuid") Integer myuid,
                             @PathParam("touid") Integer touid,
                             Session session, String json) throws NoApiKeyException, InputRequiredException {
        //把前端传入的json进行处理
        JSONObject jsonObject = JSONObject.parse(json);
        //问题
        Object questionObj = jsonObject.get("question");
        Object typeObj = jsonObject.get("type");
        if (questionObj != null && typeObj != null) {
            //转成问题字符串
            question = questionObj.toString();
            //当前的提问类型 1文字,2 图片
            if (typeObj.equals(1)){
                //文本聊天
                textChat(myuid,touid,session);
            }else if (typeObj.equals(2)){
                //图片生成,暂时不做处理
            }


        }
    }

    private void textChat(Integer myuid,Integer touid,Session session) throws NoApiKeyException, InputRequiredException {

        //服务端收到了客户端传入的问题
        //保存聊天记录
        getChatHistoryInfoService().saveHistory(chatId, myuid, question);
        //用户发送来的内容,只有在聊天记录中显示,才能让用户意识到已经发送成功了
        //把这个消息再返回给用户
        AiMessage myQuestion = new AiMessage(WebSocketAgreement.AI_Question, question, 2);
        sendText(session, myQuestion);
        //开始向AI提问  ↓
        Message userMsg = Message.builder()
                .role(Role.USER.getValue())
                .content(question)
                .build();
        msgList.add(userMsg);
        String modelName = "qwen3-max";
        if (userAi.getModel() != null) {
            modelName = userAi.getModel();
        }
        //组装完 用户的提问
        Generation gen = new Generation();
        GenerationParam param = GenerationParam.builder()
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .model(modelName)
                .messages(msgList)
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .incrementalOutput(true)
                .build();
        Flowable<GenerationResult> result = gen.streamCall(param);
        //代码运行到这里,ai思考结束,回答开始
        //通知客户端,ai开始回答问题
        AiMessage aiMessageAnswerStart = new AiMessage(WebSocketAgreement.AI_Answer, "回答开始", 0);
        sendText(session, aiMessageAnswerStart);
        //循环显示答案内容
        StringBuilder saveAnswer = new StringBuilder();
        result.blockingForEach(data -> {
            //data中有完整的答案,json字符串
            String content = data.getOutput().getChoices().get(0).getMessage().getContent();
            //发送答案给客户端
            AiMessage aiMessageAnswer = new AiMessage(WebSocketAgreement.AI_Answer, content, 1);
            sendText(session, aiMessageAnswer);
            saveAnswer.append(content);
        });
        //循环发送结束
        //告诉客户端,回答终止了
        AiMessage aiMessageAnswerEnd = new AiMessage(WebSocketAgreement.AI_Answer, "回答结束", 2);
        sendText(session, aiMessageAnswerEnd);
        //保存聊天记录
        getChatHistoryInfoService().saveHistory(chatId, touid, saveAnswer.toString());
    }
    /**
     * 向客户端发送文本消息
     * 将对象序列化为JSON字符串并通过WebSocket会话发送
     *
     * @param session WebSocket会话对象
     * @param object  需要发送的对象
     */
    private void sendText(Session session, Object object) {
        try {
            String msg = JSON.toJSONString(object);
            session.getBasicRemote().sendText(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //处理二进制文件
    @OnMessage
    public void onMessage(@PathParam("myuid") Integer myuid,
                          @PathParam("touid") Integer touid,
                          Session session,byte[] bytes){
        //预先预定好,所有的文件上传,都要配置一个头信息,以此区分不同的文件类型
        //进入的二进制数据,必须长度 >6
        if (bytes == null || bytes.length < 6){
            return;
        }
        
        //检查消息大小,防止内存溢出
        if (bytes.length > 10 * 1024 * 1024) { // 限制为10MB
            try {
                session.getBasicRemote().sendText("{\"error\":\"File size too large, maximum 10MB allowed\"}");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }
        
        //解析头信息
        String header = new String(bytes,0,6, StandardCharsets.UTF_8);
        if ("IMAGE:".equals(header)){
            //是图片,根据图片的方案,继续处理
            byte[] imageData = Arrays.copyOfRange(bytes,6,bytes.length);
            image2image(myuid,touid,session,imageData);
        }
    }

    public TongYiHttpClient getAiHttpClient(){
        return SpringContextUtil.getBean(TongYiHttpClient.class);
    }
    //图片生成图片的方法
    private void image2image(Integer myuid, Integer touid, Session session, byte[] imageData) {
        //获取客户端对象,RestTemplate
        //获取message,用户生成图片的文字描述
        //图片的在线地址,模型名称
        String modelName = "qwen-image-edit-plus";
        String questionUrl = QiniuKodoUtil.uploadAiQuestionChatImage(imageData);
        //保存聊天记录---我说的话
        getChatHistoryInfoService().saveHistory(chatId,myuid,question);
        AiMessage aiMessage = new AiMessage(WebSocketAgreement.AI_Question,question,2);
        sendText(session,aiMessage);
        //保存我发的图片
        getChatHistoryInfoService().saveHistory(chatId,myuid,questionUrl);
        //答案保存之后,把信息发送给用户
        AiMessage questionImg = new AiMessage(WebSocketAgreement.AI_Question_Image,questionUrl,2);
        sendText(session,questionImg);
        //告诉前端,开始生成生成图片
        AiMessage startAiImage = new AiMessage(WebSocketAgreement.AI_Answer_Image,"开始生成图片",0);
        sendText(session,startAiImage);
        //ai生成图片
        List<String> urlList = getAiHttpClient().sendImage2Image(question, questionUrl, modelName);
        //七牛云可以直接保存在线图片
        List<String> saveList = new ArrayList<>();
        urlList.forEach(url ->{
            String saveUrl = QiniuKodoUtil.uploadAiAnswerChatImage(url);
            saveList.add(saveUrl);
            //输出图片
            AiMessage imageUrlMessage = new AiMessage(WebSocketAgreement.AI_Answer_Image,url,1);
            sendText(session,imageUrlMessage);
        });
        //图片循环结束
        AiMessage endAiImage = new AiMessage(WebSocketAgreement.AI_Answer_Image,"图片生成结束",2);
        sendText(session,endAiImage);

        //把ai生成的图片答案,保存聊天记录
        getChatHistoryInfoService().saveHistory(chatId,touid,saveList);

    }
}
2.14 前端页面聊天记录显示
<template>
  <Top/>
  <div class="container-fluid main-content-wrapper">
    <div class="content-page-box-area">
      <div class="all-messages-body">
        <div class="all-messages-header d-flex justify-content-between align-items-center">
          <h3>消息</h3>
        </div>
        <div class="messages-profile-box">
          <a href="/ai">
            <img :src="toUser.headPic" style="width: 100px;" class="rounded-circle" alt="image">
          </a>
          <h3><a href="#">{{ toUser.nickname }}</a></h3>
        </div>
        <div class="messages-chat-container">
          <div class="chat-content" >
            <div id="chat-content"></div>
            <div v-if="isLoading" class="img-loading">
              <el-progress type="dashboard" :percentage="percentage2" :color="colors"/>
            </div>
          </div>
          <div class="chat-list-footer">
            <div class="d-flex align-items-center row">
              <div class="col-md-10">
                <input type="text" class="form-control" placeholder="输入消息..." v-model="messageStr"
                       @keyup.enter="sendMessage">
              </div>
              <div class="col-md-2">
                <button type="button" class="btn btn-success" @click="sendMessage">
                  <i class="bi bi-send"></i>&nbsp;发送
                </button>
              </div>
            </div>
            <div class="row">
              <div class="col-md-12">
                <input type="file" accept="image/*" @change="onFileSelected">
              </div>

            </div>
            <div class="row">
              <div class="col-md-12">
                <img :src="previewImage" style="width: 100%"/>
              </div>
            </div>

          </div>
        </div>
      </div>
    </div>
  </div>
  <Footer/>
</template>

<script setup>
import {onMounted, ref} from "vue";
import socket from "@/utils/socket.js";
import UserStore from "@/stores/UserStore.js";
import Top from "@/components/common/Top.vue";
import Footer from "@/components/common/Footer.vue";
import {marked} from "marked";


import ChatUserSotre from "@/stores/ChatUserSotre.js";

let toUser = ChatUserSotre().user;
let loginUser = UserStore().userModel;
//获取当前用户,和对方的用户信息
let toUid = toUser.uid;
let myUid = loginUser.uid;

let messageStr = ref("");

let webSocket;
let initWebSocket = () => {
  //ws://192.168.0.151:8080/ai/textChat/{myuid}/{touid}
  let url = "/ai/textChat/" + myUid + "/" + toUid;
  //调用封装好的方法,连接socket
  socket.initWebSocket(url);
  webSocket = socket.getWebSocket();
  //打印收到的消息
  webSocket.onmessage = e => printMessage(e.data);
}
let sendMessage = () => {
  //type 1文字消息 2图片消息
  let type = 1;
  if (consultFile) {
    sendImage();
    type = 2;
  }
  let param = {
    "question": messageStr.value,
    "type": type
  }


  webSocket.send(JSON.stringify(param));
  //清空聊天框
  messageStr.value = "";
}
let chatNum = 0;
let printMessage = (msg) => {
  let msgObj = JSON.parse(msg);
  //协议号
  let agreement = msgObj.agreement
  let pic;
  let chat_mine = "";
  let html = "";
  let status = 2;
  switch (agreement) {
    case 3001:
      //3001 是用户自己发出去的消息
      let question = msgObj.answer;
      //用户头像
      pic = loginUser.userInfo.headPic
      //默认不是我的
      chat_mine = "chat-right";
      printChatHtml(chat_mine, pic, question)
      break;
    case 3002:
      //是ai发送来的,一定是在对面
      let answer = msgObj.answer;
      //用户头像
      pic = toUser.headPic;
      status = msgObj.status;
      switch (status) {
        case 0:
          allAnswer = "";
          //开始回答问题,创建一个空的聊天框,没有聊天内容
          html = '<div class="chat ' + chat_mine + '">\n' +
              '                <div class="chat-avatar">\n' +
              '                  <a class="d-inline-block">\n' +
              '                    <img src="' + pic + '" width="50" height="50"\n' +
              '                         class="rounded-circle" alt="image">\n' +
              '                  </a>\n' +
              '                </div>\n' +
              '                <div class="chat-body">\n' +
              //记录 当前是页面里的第几个聊天框
              '                  <div class="chat-message" id="chat-meesage-' + chatNum + '"></div>\n' +
              '                </div>\n' +
              '              </div>\n' +
              '            </div>';
          document.getElementById("chat-content").insertAdjacentHTML("beforeend", html);
          break;
        case 1:
          //回答问题的过程,是ai返回的内容,要追加到聊天框里
          document.getElementById("chat-meesage-" + chatNum)
              .insertAdjacentHTML("beforeend", answer);
          //每次的答案内容,都存储的同一个变量
          allAnswer += answer;
          break;
        case 2:
          //回答结束
          html = marked(allAnswer);
          document.getElementById("chat-meesage-" + chatNum).innerHTML = marked(allAnswer);
          allAnswer = "";
          chatNum++;
          break;
      }

      break;
    case 2001:
      //聊天记录
      let chatList = msgObj.list;
      chatList.forEach(chat => {
        let msgUid = chat.uid;
        let msgContent = chat.message;
        let chat_mine = "";
        let chat_Pic = toUser.headPic;
        if (msgUid === myUid) {
          chat_mine = "chat-right"
          chat_Pic = loginUser.userInfo.headPic;
        }
        //应该在聊天记录的实体类中,增加一个是否为图片的字段,是否为音频的字段等标识信息
        //做了简化处理,只是判断聊天内容的结尾是否以.png结束,可以优化
        if (msgContent.lastIndexOf(".png") !== -1) {
          printChatHtml(chat_mine, chat_Pic, "<img src='" + msgContent + "' style='width: 200px;height: 200px;'>")
        } else {
          printChatHtml(chat_mine, chat_Pic, marked(msgContent))
        }

      })
      break;
    case 3004:
      console.log(msgObj)
      //自己发送出去的图片
      let questionUrl = msgObj.answer;
      //用户头像
      pic = loginUser.userInfo.headPic
      //默认不是我的
      chat_mine = "chat-right";
      printChatHtml(chat_mine, pic, "<img src='" + questionUrl + "' style='width: 200px'>")
      break;
    case 3003:
      console.log(msgObj)
      //AI生成的图片
      let answerUrl = msgObj.answer;
      //用户头像
      pic = toUser.headPic;
      status = msgObj.status;
      //默认不是我的
      chat_mine = "";
      //增加动画,获取图片之后,结束动画等
      if (status === 0) {
        //显示进度条
        isLoading.value = true;
        percentage2.value = 0;
        setInterval(() => {
          percentage2.value = (percentage2.value % 100) + 1
        }, 1000)
      } else if (status === 1) {
        isLoading.value = false;
        printChatHtml(chat_mine, pic, "<img src='" + answerUrl + "' style='width: 200px'>")
      }

      break;
  }

}

let allAnswer = "";

let test1=()=>{
  //显示进度条
  isLoading.value = true;
  percentage2.value = 0;
  setInterval(() => {
    percentage2.value = (percentage2.value % 100) + 1
  }, 1000)
}
let printChatHtml = (chat_mine, pic, question) => {
  let html = '<div class="chat ' + chat_mine + '">\n' +
      '                <div class="chat-avatar">\n' +
      '                  <a class="d-inline-block">\n' +
      '                    <img src="' + pic + '" width="50" height="50"\n' +
      '                         class="rounded-circle" alt="image">\n' +
      '                  </a>\n' +
      '                </div>\n' +
      '                <div class="chat-body">\n' +
      '                  <div class="chat-message">\n' +
      question
  '                  </div>\n' +
  '                </div>\n' +
  '              </div>\n' +
  '            </div>'
  document.getElementById("chat-content").insertAdjacentHTML("beforeend", html);
}
//预览图的路径
let previewImage = ref("")
//当前正在选择的图片对象
let consultFile = null;
//上传的方法
let onFileSelected = (event) => {
  //获取文件
  const file = event.target.files[0];
  //如果传了文件
  if (file) {
    //新建一个读取图片对象
    const reader = new FileReader();
    //当图片被加载的时候,执行的方法
    reader.onload = (e) => {
      previewImage.value = e.target.result;
    }
    //读取图片
    reader.readAsDataURL(file)
    consultFile = file;
  }
}

//发送图片到服务器
function sendImage() {
  //通过WebSocket发送,发送二进制数据,到服务器
  const reader = new FileReader();
  reader.onload = (e) => {
    //获取图片的二进制数据
    let imgbytes = new Uint8Array(e.target.result);
    //构建图片的头信息--告诉服务器,来的二进制数据,类型是图片
    //发送二进制数据到服务端的时候,固定前6位,表示文件类型
    const header = new TextEncoder().encode("IMAGE:")
    //构建完整的数据
    const conbined = new Uint8Array(header.length + imgbytes.length);
    //从第0位开始,是header的信息
    conbined.set(header, 0);
    //header结尾的位置开始,是图片的数据
    conbined.set(imgbytes, header.length);
    //数据准备完成,判断session是否存在
    if (webSocket.readyState === WebSocket.OPEN) {
      //连接状态
      //发送二进制数据
      webSocket.send(conbined.buffer);
      console.log("图片上传完成")
    } else {
      console.error("WebSocket未连接,无法发送图片")
    }

  }

  //读取图片
  reader.readAsArrayBuffer(consultFile);
}

//是否正在生成图片
let isLoading = ref(false);
const percentage2 = ref(0)
const colors = [
  {color: '#f56c6c', percentage: 20},
  {color: '#e6a23c', percentage: 40},
  {color: '#5cb87a', percentage: 60},
  {color: '#1989fa', percentage: 80},
  {color: '#6f7ad3', percentage: 100},
]
onMounted(() => {
  initWebSocket()
})
</script>


<style scoped>
.demo-progress .el-progress--line {
  margin-bottom: 15px;
  max-width: 600px;
}

.demo-progress .el-progress--circle {
  margin-right: 15px;
}
.img-loading{
  float: left;
  margin-left: 72px;
}
</style>

四、WebSocket + 火山方舟

1、申请豆包API-Key

https://www.volcengine.com/product/ark

下载保存

2、官方案例

https://www.volcengine.com/docs/82379/1399008?lang=zh
  • 配置环境变量

setx ARK_API_KEY=<ARK_API_KEY>

  • 重启Idea

3、运行官方案例

  • 引入依赖

<!--火山方舟-->
<dependency>
    <groupId>com.volcengine</groupId>
    <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
    <version>LATEST</version>
</dependency>
3.2 运行
package com.javasm.qingqing.aitest;

import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest;
import com.volcengine.ark.runtime.model.completion.chat.ChatMessage;
import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole;
import com.volcengine.ark.runtime.service.ArkService;
import java.util.ArrayList;
import java.util.List;

/**
 * 这是一个示例类,展示了如何使用ArkService来完成聊天功能。
 */
public class ChatCompletionsExample {
    public static void main(String[] args) {
        // 从环境变量中获取API密钥
        String apiKey = System.getenv("ARK_API_KEY");
        // The base URL for model invocation
        ArkService arkService = ArkService
                .builder()
                .apiKey(apiKey)
                .baseUrl("https://ark.cn-beijing.volces.com/api/v3")
                .build();

        // 初始化消息列表
        List<ChatMessage> chatMessages = new ArrayList<>();

        // 创建用户消息
        ChatMessage userMessage = ChatMessage.builder()
                .role(ChatMessageRole.USER) // 设置消息角色为用户
                .content("简历中提到,你之前是在抖音平台做舞蹈类主播,请做个自我介绍,说一下上一工会的离职原因,和你对直播行业前景的分析。") // 设置消息内容
                .build();

        // 将用户消息添加到消息列表
        chatMessages.add(userMessage);

        // 创建聊天完成请求
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model("doubao-seed-1-6-251015") // Replace with Model ID
                .messages(chatMessages) // 设置消息列表
                .build();

        // 发送聊天完成请求并打印响应
        try {
            // 获取响应并打印每个选择的消息内容
            arkService.createChatCompletion(chatCompletionRequest)
                    .getChoices()
                    .forEach(choice ->
                            System.out.println(choice.getMessage().getContent()));
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        } finally {
            // 关闭服务执行器
            arkService.shutdownExecutor();
        }
    }
}
public class ImageGenerationsExample {
    public static void main(String[] args) {
        // Get API Key:https://console.volcengine.com/ark/region:ark+cn-beijing/apikey
        String apiKey = System.getenv("ARK_API_KEY");
        ConnectionPool connectionPool = new ConnectionPool(5, 1, TimeUnit.SECONDS);
        Dispatcher dispatcher = new Dispatcher();
        ArkService service = ArkService.builder()
                .baseUrl("https://ark.cn-beijing.volces.com/api/v3") // The base URL for model invocation
                .dispatcher(dispatcher)
                .connectionPool(connectionPool)
                .apiKey(apiKey)
                .build();

        GenerateImagesRequest generateRequest = GenerateImagesRequest.builder()
                .model("doubao-seedream-4-5-251128") // Replace with Model ID
                .prompt("一个正在送美团外卖的年轻人,身后背着一把屠龙宝刀,胯下一匹冒着火焰的骷髅马,骑着骷髅马,穿梭在城市的大楼之间,夜晚的路灯格外显眼")
                .size("2K")
                .sequentialImageGeneration("disabled")
                .responseFormat(ResponseFormat.Url)
                .stream(false)
                .watermark(false)
                .build();
        ImagesResponse imagesResponse = service.generateImages(generateRequest);
        System.out.println(imagesResponse.getData().get(0).getUrl());

        service.shutdownExecutor();
    }
}
SAP PS 收入确认方法配置清单(成本推导收入 / 项目进度推导收入) 摘要:本文详细介绍了SAPPS模块中两种收入确认方法的配置流程:成本推导收入(成本比例法)和项目进度推导收入(里程碑法)。通过事务码OKG1-OKG5等关键配置步骤,分别实现基于累计成本比例和里程碑完成权重的收入确认逻辑。文章对比了两种方法的适用场景和管控要点:成本法适用于成本与进度强相关的项目,需严控预计总成本;进度法则依赖明确的里程碑验收。同时提供了落地建议,包括项目类型绑定、数据校验和变更管控等最佳实践,为SAPPS系统收入确认功能实施提供完整指导。 阅读详情

相关推荐

Qwen-Image-2.1本地图像生成整合包:8G显存一键部署指南

文生图模型是AI视觉创作的核心技术,其本地化部署需兼顾模型能力、硬件适配与工程易用性。Qwen-Image-2.1作为支持文本生成、图像编辑与多图参考的统一架构模型,突破了传统Diffusion模型在消费级显卡上的运行瓶颈。通过AWQ量化、vLLM推理优化与ComfyUI节点重构,实现8G显存稳定运行,显著降低CUDA环境配置复杂度。该方案面向设计师、教师与普通用户,提供开箱即用的本地AI图像工作流,适用于电商主图迭代、教学配图生成与IP角色延展等真实场景,让高质量图像生成真正走出云端,落地桌面。

weixin_29082143的博客 398

豆包图像创作模型 Seedream 4.5发布:聚焦商业生产力场景

12月3日,火山引擎正式发布(以下简称 Seedream 4.5)。新一代模型在等方面实现迭代,进一步提升了图像生成的整体质量与稳定性。此次升级重点强化了,确保多源素材融合时的自然感与一致性;同时优化了,支持高精度图文混排,广告物料生成更简单高效。目前,Seedream 4.5已全面支持等核心场景应用。

volcenginetod的博客 1991

Copula函数实战:如何用MATLAB计算气象干旱与农业干旱的联合概率?

本文提供了一份基于MATLAB的Copula函数实战指南,详细阐述了如何利用该多变量联合分布函数计算气象干旱与农业干旱的联合概率及条件概率。通过从数据预处理、边缘分布拟合到Copula模型选择与检验的完整流程,帮助环境科学与农业气象研究者量化干旱事件的关联风险,为风险评估提供定量依据。

weixin_29213655的博客 431

仅仅三步完成豆包图像生成模型使用

A: 返回的是JSON格式,包含图像的base64编码或URL。A: 检查API密钥是否正确、是否过期、环境变量是否设置正确。A: 通过详细的prompt描述,目前不支持直接的质量参数。A: 通常10-30秒,取决于图像复杂度和服务器负载。A: 登录控制台,在"用量统计"页面查看。

interpromotion的博客 1557

Hi3559AV100制作ext4文件系统和烧录emmc

1、首先生成所需文件系统,见本博客&lt;Hi3559AV100文件系统制作cramfs&gt;。 2、使用工具生成EXT4文件系统 ./make_ext4fs -s -l 96M rootfs_hi3559av100.ext4 ./rootfs 3、烧录文件系统 mw.b 0x82000000 ff 0x12C00000;tftp 0x82000000 STM_...

阿Sir问道 7033

ROS智能车定位导航仿真+功能包自带的地图实现+自己建立的地图实现

ROS智能车定位导航仿真一、前言二、效果图三、准备工作[1.在Ubuntu上安装ROS Kinetic](https://blog.youkuaiyun.com/qq_42585108/article/details/104327661)2.创建ROS工作区间①1创建racecar_ws和src文件夹②初始化工作空间③下载racecar源代码包,并编译工程四、启动仿真1.设置环境变量2.启动地图五、建图1.启动slam和rviz2 控制车辆(左侧有个控制窗口),WASD控制前左后右的运动就可以建图了:3.新建终端保存地

qq_42585108的博客 1万+

基于 Doubao-Seedream-4.5 的单张图片生成后端接口实战

本文详细介绍了如何接入火山引擎Doubao-Seedream-4.5模型的API实现单张图片生成功能。文章从模型优势分析入手,详细说明了开发前的环境配置、权限获取等准备工作,并提供了完整的Java后端接口开发流程,包括核心代码实现、参数优化、异常处理等关键环节。通过封装工具类ArkServiceUtil和服务实现类ChatImageServiceImpl,作者展示了生产环境中API调用的最佳实践,包含参数校验、重试机制、日志记录等功能。最后通过效果演示验证了接口的可用性,为开发者提供了完整的图片生成解决方案

weixin_66243333的博客 2584

开源libmodbus编译为32位和64位动态库

libmodbus 是支持Linux,Mac OS X,window 的一个开源库,支持modbus TCP和RTU 两种模式。

lemontree1945的博客 1923

InnoDB 内存结构总览:Change Buffer、AHI 与 Log Buffer

本文系统梳理InnoDB内存结构中的ChangeBuffer、Adaptive Hash Index(AHI)与LogBuffer。ChangeBuffer缓存二级索引页不在内存时的变更,减少随机IO,仅在页加载时合并;聚簇索引修改立即执行,因数据本身不可延后。AHI基于高频等值查询自动构建哈希索引,加速定位,仅支持等值操作。LogBuffer为redo log内存缓冲,配合磁盘日志实现崩溃恢复。三者均服务于性能优化,其中ChangeBuffer与AHI共享BufferPool空间,而LogBuffer独立

bksczm的博客 286

Qt软件开发: 开发MQTT客户端软件,模拟硬件登录各大主流MQTT平台完成通信

此软件是使用QT编写MQTT客户端(根据mqtt官方文档3.1.1,自己实现过程代码,没有使用其他库,稳定,方便移植),模拟硬件登录各大主流的MQTT物联网服务器,完成主题订阅、发布等操作,实现数据通信交互。 已经测试OneNET、阿里云IOT、华为云IOT、百度IOT、电信IOT、原子云IOT等等常用的物联网云平台,都可以正常实现数据通信,只要云平台支持标准MQTT协议都可以使用此软件进行登录测试。 特别是大家没有硬件,想测试与MQTT物联网云服务器通信的,此软件是非常的方便。使用简单,比官方的软件操作

1万+

如何在 SAPGUI 里显示上传到 ABAP 服务器的 PDF 文件

在 SAPGUI 里显示 PDF 文件是实际项目中一个很有用的需求。因为在 SAP 实际业务场景中,有大量的流程会生成 PDF 格式的数据。把这种类型的数据输出显示在 SAPGUI 里,方便最终用户查看。本教程后续步骤,还会向大家介绍如何使用 ABAP 代码生成 PDF 格式的数据。本步骤只是简单的将本地 PDF 文件,上传到 ABAP 系统并显示在 SAPGUI 里。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2687

V8 机制、手写核心函数与 TypeScript 类型体操

或固定初始化(如数组先定长、对象先给全字段)保持类型与形状稳定;这些都比"换框架"更基础。只能处理纯数据(丢函数、Date、RegExp、循环引用报错)。生产级深拷贝要处理:原始值短路、循环引用、V8(Chrome / Node 的 JS 引擎)不是"解释一行跑一行"那么简单,它用。图:V8 执行流程——解释执行 + 热点优化,优化失败会 Deopt 回退。JS 内存自动管理,但"自动"不等于"不会漏"。是"固定间隔最多执行一次"。手写 Promise 是"懂异步"的试金石。是"停下后才执行最后一次",

qq_38468358的博客 346

PDBfixer: PDB修补蛋白残基

PDBFixer是一款专门用于修复蛋白质数据库(PDB)文件常见问题的工具。它能自动处理X射线晶体学结构中常见的缺陷,包括缺失氢原子、不完整侧链、非标准残基等问题。该工具提供三种使用方式:图形界面、命令行和Python API,支持添加缺失原子(全部/重原子/氢原子)、替换非标准残基、补全缺失残基等功能。特别适用于分子动力学模拟前的预处理,可自动添加水盒子并调整离子浓度。用户可通过简单命令或自定义脚本实现复杂修复流程,内置非标准残基替换表确保结构标准化。

Protein Designer的博客 933

Java EE 进阶:8.MyBatis 操作数据库(入门)(第一弹)

Java EE 进阶:8.MyBatis 操作数据库(入门)(第一弹)

要努力去发光,而不是被照亮~ 128

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Spring Boot 数据源与 MyBatis(-Plus) 自定义配置详解

本文详解 Spring Boot 中单数据源环境下 HikariCP 与 MyBatis-Plus 的自定义配置。通过手动声明 SqlSessionFactory,实现下划线转驼峰、日志输出、一级缓存策略等精细化控制。结合 HikariCP 核心参数调优(如最大连接数、连接生命周期),确保高性能与稳定性。适用于仅需访问单一数据库的场景,避免多数据源复杂性,兼顾性能与可维护性。

BADAO_LIUMANG_QIZHI的博客 198

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

LeetCode Hot 100 二叉树专题(二)3 题(Java)

本篇文章共 3 题:543. 二叉树的直径、102. 二叉树的层序遍历、108. 将有序数组转换为二叉搜索树。 每道题都统一按「① 题目分析 & 解题思路 → ② 常见解法(附代码)及讲解 → ③ 知识点总结」的顺序组织;代码使用 Java 编写,节点定义沿用 LeetCode 默认的 TreeNode。

2401_88612756的博客 365

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

【从零理解 Spring AI · 12】MCP 的两种角色:做客户端还是做服务端

上一篇讲了 MCP 的概念:标准协议、三大角色、M+N 替代 M×N。

nice_lcj520的博客 499

JDK 下载安装:装完 Java 提示「不是内部或外部命令」?环境变量没配

JDK 是写 Java 程序必须装的东西。它包含运行 Java 的虚拟机(JRE)和编译 Java 的编译器(javac)——装了它,能跑、.java文件能编译成可执行程序。免费,学习和小规模生产都不花钱。

科小怪的博客 634

java框架大全

java框架大全

qq_39326137的博客 183

Java 多态 学习笔记

多态是面向对象编程的重要特性,指同一操作作用于不同对象时产生不同行为。其核心在于“父类引用指向子类对象”,结合方法重写实现运行时动态绑定。多态使代码更灵活、可扩展,如用Person p = new Student();统一处理各类人员注册,无需修改调用逻辑。但多态无法直接调用子类特有方法,需通过instanceof判断并强制转换。编译时看左边(类型),运行时看右边(实际对象),实现解耦与维护便利。

AA_AA114514的博客 360

SecureRandom做随机源,这个Java随机字符串工具类有3个方法

本文分享一款JDK17环境下的Java随机字符串工具类,内置3个开箱即用的方法,全部采用SecureRandom加密安全随机源,不存在弱随机、可预测、取模偏差等问题。工具类支持默认62字符集随机串、纯数字随机串、自定义字符集随机串三种生成模式,支持Unicode码点级别的字符集处理,兼容Emoji等增补字符,边界处理严格,异常信息明确,适合验证码、邀请码、盐值、随机口令、短链码等场景,源码可直接复制使用。

Java后端 | AI智能体应用实战派 | 电脑技能行家 298

IDEA 简介:从零认识 IntelliJ IDEA

IDEA 全称,是由 JetBrains 公司开发的一款主流 Java 集成开发环境(IDE)。它集代码编辑、编译、调试、测试、版本控制、构建管理等功能于一体,被大量 Java 开发者视为日常开发的首选工具。所谓 IDE(Integrated Development Environment,集成开发环境),就是把写代码所需的各类工具整合到同一个界面中。相比只用文本编辑器加命令行编译,IDE 能显著减少重复操作,让开发者把精力集中在业务逻辑上。

ztI10的博客 151

Java实现批量线段相交检测(跨积判定+扫描线优化与踩坑复盘)

【代码】Java实现批量线段相交检测(跨积判定+扫描线优化与踩坑复盘)

m0_64476639的博客 328

ubuntu安装hadoop系统

本文详细介绍了基于VMware克隆虚拟机搭建三节点Hadoop分布式集群的全过程:使用同一Ubuntu镜像克隆出master、worker1、worker2三台虚拟机,依次配置主机名、固定IP(如192.168.82.10/11/12)、SSH服务及免密登录,确保节点间网络互通。随后安装Java,下载配置Hadoop并分发至各节点,最终初始化启动集群,验证其存储与计算功能,实现高效的大数据环境部署。

qq_43592674的博客 334

TRingBuffer两端都能增删(快递站版)

TRingBuffer 是一个支持两端增删、下标访问且容量动态增长的环形双端队列。它通过无符号索引的永不归零设计,利用减法自动模运算实现空满判断与元素计数,避免了传统环形缓冲浪费格子的问题。内部以票号(索引)标记首尾,扩容时重排元素为连续存储,保证 O(1) 操作与高缓存效率。虽不线程安全,但其高效性使其成为游戏开发中事件队列、命令缓冲等场景的理想选择。

u013768914的博客 227

Mac上装了最新版本的Android Studio,但是没找到jdk的设置选项也没找到jbr的目录

新版Android Studio(2025.3.1+)因三项变更导致常见问题:一、JDK设置移至“DaemonJVM criteria”,新项目默认自动探测或下载JDK;二、JBR目录改名为带版本号的jbr-21等,需进入.app/Contents/查看,或通过About窗口确认运行时;三、命令行缺adb因platform-tools未加入PATH,需在SDK Manager中勾选并配置~/.zshrc中的ANDROID_HOME及PATH。重装或清理残留可解决缺失问题,切勿手动设JAVA_HOME或安装冲

Programming is a highly artistic endeavor 353
上一篇: Token
下一篇: 异步任务la
change探索者
博客等级 码龄4年 133粉丝 87原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值