LOADING

加载过慢请开启缓存 浏览器默认开启

文心一言SDK使用

Wenxin

侧边栏的问答助手

  • @\components\myAside.vue(template)
<-template->
    <-div v-if="asideshow.wenxin" 
       style="padding: 15px; border-radius: 10px; animation: hideToShow 1s ease-in-out"
       class="shadow-box background-opacity wow"->
    <-div style="color: var(--lightGreen); font-size: 20px; font-weight: bold; margin-bottom: 10px" 
         @click="showWenxin()"->
      文心智能助手
    <-/div->

    <-!-- 问答功能部分 --->
    <-div class="chat-window" style="max-height: 300px; overflow-y: auto; margin-bottom: 10px;"->
      <-div class="message-list"->
        <-div 
          v-for="(msg, index) in messages" 
          :key="index" 
          class="message" 
          :class="{ 'user-message': msg.isUser }"
        ->  
          <-span v-if="msg.isUser"->{{ msg.text }}<-/span->  
          <-span v-else v-html="msg.text"-><-/span->
        <-/div->
      <-/div->
    <-/div->

    <-div style="display: flex; margin-bottom: 10px;"->
      <-input class="ais-SearchBox-input" type="text"
             v-model="userInput"
             placeholder="输入消息 ......" 
             maxlength="32" 
             @keyup.enter="sendMessage"
             style="flex: 1;"->
      <-div class="ais-SearchBox-submit" @click="sendMessage"->
        <-svg style="margin-top: 3.5px; margin-left: 18px" viewBox="0 0 1024 1024" width="20" height="20"->
          <-path
            d="M51.2 508.8c0 256.8 208 464.8 464.8 464.8s464.8-208 464.8-464.8-208-464.8-464.8-464.8-464.8 208-464.8 464.8z"
            fill="#51C492"-><-/path->
          <-path
            d="M772.8 718.4c48-58.4 76.8-132.8 76.8-213.6 0-186.4-151.2-337.6-337.6-337.6-186.4 0-337.6 151.2-337.6 337.6 0 186.4 151.2 337.6 337.6 337.6 81.6 0 156-28.8 213.6-76.8L856 896l47.2-47.2-130.4-130.4zM512 776c-149.6 0-270.4-121.6-270.4-271.2S363.2 233.6 512 233.6c149.6 0 271.2 121.6 271.2 271.2C782.4 654.4 660.8 776 512 776z"
            fill="#FFFFFF"-><-/path->
        <-/svg->
      <-/div->
    <-/div->

    <-button @click="handleGeneratePDF" class="generate-button"->生成并展示心脏诊疗报告<-/button->
  <-/div->
<-/template->
  • @\components\myAside.vue(script)
<-script setup->
import { ChatService } from '@/api/wenxin';  
import { marked } from 'marked'; 
import useDocStore from '@/stores/document.js';   
const docStore = useDocStore();  
const userInput = ref('');  
const messages = ref([]); 

const showWenxin = () =-> {
  store.commit('changeVisibility', { wenxin: !visible.value.wenxin });
  toggleDropdown(0);
}

const sendMessage = async () =-> {  
  const trimmedMessage = userInput.value.trim();  
  if (trimmedMessage) {  
    messages.value.push({ text: trimmedMessage, isUser: true });  
    userInput.value = ''; 

    try {  
      const response = await ChatService({ question: trimmedMessage });  
      const responseMessage = response.data;  
      const parsedMessage = marked(responseMessage);  

      messages.value.push({ text: parsedMessage, isUser: false });  
    } catch (error) {  
      console.error('API 请求失败:', error);  
      messages.value.push({ text: '发送消息失败,请稍后再试。', isUser: false });  
    }  
  }  
}; 

const handleGeneratePDF = async () =-> {
    console.log("generate pdf");   
    await docStore.generatePDF();  
};  

<-/script->
  • color.css
:root {
    /* 背景 */
    --background: white;
    --gradualBackground: linear-gradient(to right bottom, #ee7752, #e73c7e, #23a6d5, #23d5ab);
    --favoriteBg: #f7f9fe;

    /* 字体 */
    --fontColor: black;
    /* 边框 */
    --borderColor: rgba(0, 0, 0, 0.5);
    /* 边框 */
    --borderHoverColor: rgba(110, 110, 110, 0.4);
    /* 文章字体 */
    --articleFontColor: #1f1f1f;
    /* 文章灰色字体 */
    --articleGreyFontColor: #616161;
    /* 评论背景颜色 */
    --commentContent: #F7F9FE;


    /* 主题背景 */
    --themeBackground: orange;
    /* 主题悬停背景 */
    --gradualRed: linear-gradient(to right, #ff4b2b, #ff416c);

    /* 导航栏字体 */
    --toolbarFont: #333333;
    /* 导航栏背景 */
    --toolbarBackground: rgba(255, 255, 255, 1);
    /* 灰色字体 */
    --greyFont: #797979;
    --maxGreyFont: #595A5A;
    /* footer背景 */
    --gradientBG: linear-gradient(-90deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
    /* 白色遮罩 */
    --whiteMask: rgba(255, 255, 255, 0.3);
    /* max白色遮罩 */
    --maxWhiteMask: rgba(255, 255, 255, 0.5);
    --maxMaxWhiteMask: rgba(255, 255, 255, 0.7);
    /* mini白色遮罩 */
    --miniWhiteMask: rgba(255, 255, 255, 0.15);
    /* 透明 */
    --transparent: rgba(0, 0, 0, 0);
    /* mini黑色遮罩 */
    --miniMask: rgba(249, 245, 245, 0.15);
    /* 黑色遮罩 */
    --mask: rgba(0, 0, 0, 0.3);
    /* 半透明 */
    --translucent: rgba(0, 0, 0, 0.5);
    /* 深黑遮罩 */
    --maxMask: rgba(0, 0, 0, 0.7);


    --white: white;

    --red: red;
    --lightRed: #ff4b2b;
    --maxLightRed: #ff416c;
    --orangeRed: #EF794F;

    --azure: #ECF7FE;
    --blue: rgb(3, 169, 244);

    --lightGray: #DDDDDD;
    --maxLightGray: #EEEEEE;
    --maxMaxLightGray: rgba(242, 242, 242, 0.5);

    --lightGreen: #39c5bb;

    --green: #67C23A;
    --black: black;
    --lightYellow: #F4E1C0;

    --globalFont: poetize-font;
}
  • @\components\myAside.vue(style)
 <-style scoped->
    .shadow-box {
        box-shadow: 0 1px 20px -6px var(--borderColor);
        transition: all 0.3s ease;
    }

    .shadow-box:hover {
        box-shadow: 0 5px 10px 5px var(--borderHoverColor);
    }

    .background-opacity {
        /*background: var(--background);*/
        /* opacity: 0.88; */
    }
      .chat-window {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 8px;
      margin-bottom: 10px;
    }

    .message-list {
      display: flex;
      flex-direction: column;
    }

    .message {
      padding: 8px;
      margin: 5px 0;
      border-radius: 4px;
      max-width: 100%;
      word-wrap: break-word;
    }

    .user-message {
      background-color: #d1e7dd;
      align-self: flex-end;
    }

      .ais-SearchBox-input {
      padding: 0 14px;
      height: 30px;
      width: calc(100% - 50px);
      outline: 0;
      border: 2px solid var(--lightGreen);
      border-right: 0;
      border-radius: 40px 0 0 40px;
      color: var(--maxGreyFont);
      background: var(--white);
    }

   .ais-SearchBox-submit {
      height: 30px;
      width: 50px;
      border: 2px solid var(--lightGreen);
      border-left: 0;
      border-radius: 0 40px 40px 0;
      background: var(--white);
      cursor: pointer;
   }

    .generate-button {  
        position: relative;      
        margin-top: 12px;      
        background: var(--lightGreen); 
        cursor: pointer;        
        width: 100%;             
        height: 35px;          
        border-radius: 1rem;   
        text-align: center;    
        line-height: 35px;      
        color: var(--white);   
        overflow: hidden;        
        z-index: 1;           
        margin-bottom: 25px;    
        border: none;            
        padding: 0;             
    }  

    .generate-button:hover {  
        background: #87CEEB;       
    }  


<-/style->

问答助手组件

  • @\components\common\chat.vue
<-template->  
  <-div class="chat-container"->  
    <-div class="chat-window"->  
      <-div class="message-list"->  
        <-div   
          v-for="(msg, index) in messages"   
          :key="index"   
          class="message"   
          :class="{ 'user-message': msg.isUser }"  
        ->  
          <-span v-if="msg.isUser"->{{ msg.text }}<-/span->  
          <-span v-else v-html="msg.text"-><-/span-> <-!-- 修正为使用 span 标签关闭 v-html --->
        <-/div->  
      <-/div->  
    <-/div->  
    <-div class="input-container"->  
      <-input   
        v-model="userInput"   
        @keyup.enter="sendMessage"   
        placeholder="输入你的消息..."  
      /->  
      <-button @click="sendMessage"->发送<-/button->  
    <-/div->  
  <-/div->  
<-/template->  

<-script setup->  
import { ref } from 'vue';  
import { ChatService } from '@/api/wenxin';  
import { marked } from 'marked'; // 假设需要对 Markdown 格式进行处理

const userInput = ref('');  
const messages = ref([]);  

const sendMessage = async () =-> {  
  const trimmedMessage = userInput.value.trim();  
  if (trimmedMessage) {  
    // 添加用户消息到消息列表  
    messages.value.push({ text: trimmedMessage, isUser: true });  
    userInput.value = ''; // 清空输入框  

    try {  
      // 调用 API 获取响应  
      const response = await ChatService({ question: trimmedMessage });  
      const responseMessage = response.data; // 假设响应数据在 response.data 中  

      // 使用 marked 解析 Markdown,或直接使用 HTML
      const parsedMessage = marked(responseMessage);  

      messages.value.push({ text: parsedMessage, isUser: false });  
    } catch (error) {  
      console.error('API 请求失败:', error);  
      messages.value.push({ text: '发送消息失败,请稍后再试。', isUser: false });  
    }  
  }  
};  
<-/script->  

<-style scoped->  
.chat-container {  
  width: 800px;  
  margin: auto;
  padding: 20px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  height: 500px;  
  border: 1px solid #ddd;  
  border-radius: 8px;  
  display: flex;  
  flex-direction: column;  
}  

.chat-window {  
  flex: 1;  
  overflow-y: auto;  
  padding: 10px;  
}  

.message-list {  
  display: flex;  
  flex-direction: column;  
}  

.message {  
  padding: 8px;  
  margin: 5px 0;  
  border-radius: 4px;  
  max-width: 70%; /* 限制消息的最大宽度 */
  word-wrap: break-word; /* 自动换行 */
}  

.user-message {  
  background-color: #d1e7dd;  
  align-self: flex-end;  
}  

.input-container {  
  display: flex;  
  padding: 10px;  
}  

input {  
  flex: 1;  
  padding: 8px;  
  border: 1px solid #ddd;  
  border-radius: 4px;  
}  

button {  
  margin-left: 10px;  
  padding: 8px 12px;  
  background-color: #007bff;  
  color: #fff;  
  border: none;  
  border-radius: 4px;  
  cursor: pointer;  
}  
<-/style->

后端代码

  • pom.xml
    <-!--文心大模型依赖--->
    <-dependency->
      <-groupId->com.squareup.okhttp3<-/groupId->
      <-artifactId->okhttp<-/artifactId->
      <-version->4.9.3<-/version->
    <-/dependency->

    <-dependency->
      <-groupId->com.alibaba<-/groupId->
      <-artifactId->fastjson<-/artifactId->
      <-version->1.2.79<-/version->
    <-/dependency->

    <-!--javax--->
    <-dependency->
      <-groupId->javax.annotation<-/groupId->
      <-artifactId->javax.annotation-api<-/artifactId->
      <-version->1.3.2<-/version->
    <-/dependency->
  • application.yml
okhttp:
  connect-timeout: 30
  read-timeout: 30
  write-timeout: 30
  max-idle-connections: 200
  keep-alive-duration: 300

wenxin:
  apiKey: ...
  secretKey: ...
  accessTokenUrl: https://aip.baidubce.com/oauth/2.0/token
  ERNIE-Bot4:
    0URL: https://aip.baid
  • src/main/java/com/itheima/config/OkHttpConfiguration.java
package com.itheima.config;

import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.net.ssl.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;


@Configuration
public class OkHttpConfiguration {

    @Value("${okhttp.connect-timeout}")
    private Integer connectTimeout;

    @Value("${okhttp.read-timeout}")
    private Integer readTimeout;

    @Value("${okhttp.write-timeout}")
    private Integer writeTimeout;

    @Value("${okhttp.max-idle-connections}")
    private Integer maxIdleConnections;

    @Value("${okhttp.keep-alive-duration}")
    private Long keepAliveDuration;

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory(), x509TrustManager())
                // 是否开启缓存
                .retryOnConnectionFailure(false)
                .connectionPool(pool())
                .connectTimeout(connectTimeout, TimeUnit.SECONDS)
                .readTimeout(readTimeout, TimeUnit.SECONDS)
                .writeTimeout(writeTimeout,TimeUnit.SECONDS)
                .hostnameVerifier((hostname, session) --> true)
                // 设置代理
//                .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8888)))
                // 拦截器
//                .addInterceptor()
                .build();
    }

    @Bean
    public X509TrustManager x509TrustManager() {
        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType)
                    throws CertificateException {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType)
                    throws CertificateException {
            }
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    }

    @Bean
    public SSLSocketFactory sslSocketFactory() {
        try {
            // 信任任何链接
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Bean
    public ConnectionPool pool() {
        return new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS);
    }
}
  • src/main/java/com/itheima/config/WenXinConfig.java
package com.itheima.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okhttp3.MediaType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.util.Date;

@Configuration
@Slf4j
public class WenXinConfig {

    @Value("${wenxin.apiKey}")
    public String API_KEY;
    @Value("${wenxin.secretKey}")
    public String SECRET_KEY;
    @Value("${wenxin.accessTokenUrl}")
    public String ACCESS_TOKEN_URL;
    @Value("${wenxin.ERNIE-Bot4.0URL}")
    public String ERNIE_Bot_4_0_URL;

    //过期时间为30天
    public String ACCESS_TOKEN = null;
    public String REFRESH_TOKEN = null;

    public Date CREATE_TIME = null;//accessToken创建时间

    public Date EXPIRATION_TIME = null;//accessToken到期时间

    /**
     * 获取accessToken
     * @return true表示成功 false表示失败
     */
    public synchronized String flushAccessToken(){
        //判断当前AccessToken是否为空且判断是否过期
        if(ACCESS_TOKEN != null && EXPIRATION_TIME.getTime() -> CREATE_TIME.getTime()) return ACCESS_TOKEN;

        //构造请求体 包含请求参数和请求头等信息
        RequestBody body = RequestBody.create(MediaType.parse("application/json"),"");
        Request request = new Request.Builder()
                .url(ACCESS_TOKEN_URL+"?client_id="+API_KEY+"&client_secret="+SECRET_KEY+"&grant_type=client_credentials")
                .method("POST", body)
                .addHeader("Content-Type", "application/json")
                .addHeader("Accept", "application/json")
                .build();
        OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder().build();
        String response = null;
        try {
            //请求
            response = HTTP_CLIENT.newCall(request).execute().body().string();
        } catch (IOException e) {
            log.error("ACCESS_TOKEN获取失败");
            return null;
        }

        //刷新令牌以及更新令牌创建时间和过期时间
        JSONObject jsonObject = JSON.parseObject(response);
        ACCESS_TOKEN = jsonObject.getString("access_token");
        REFRESH_TOKEN = jsonObject.getString("refresh_token");
        CREATE_TIME = new Date();
        EXPIRATION_TIME = new Date(Long.parseLong(jsonObject.getString("expires_in")) + CREATE_TIME.getTime());

        return ACCESS_TOKEN;
    }
}
  • src/main/java/com/itheima/controller/ChatController.java
package com.itheima.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.itheima.config.WenXinConfig;
import com.itheima.pojo.Result;
import com.itheima.pojo.User;
import com.itheima.service.UserService;
import com.itheima.utils.ThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@Slf4j
@RequestMapping("/wenxin")
public class ChatController {

    @Resource
    private WenXinConfig wenXinConfig;

    // 历史对话,需要按照 user, assistant
    List<-Map<-String, String->-> messages = new ArrayList<-->();

    @Autowired
    private UserService userService;   // 假设已经通过依赖注入的用户服务

    @PostMapping("/info")
    public Result<-String-> info() throws IOException {
        Map<-String, Object-> map = ThreadLocalUtil.get();
        String username = (String) map.get("username");

        // 获取用户信息,如果没有找到用户则返回错误
        User user = userService.findByUserName(username);
        if (user == null) {
            return Result.error("用户未找到");
        }

        String question = "我的名字是 " + user.getUsername() + "之后的回答需要添加这个姓名"; // 获取用户名并构造问题
        log.info("用户询问信息: {}", question); // 日志记录

        // 调用处理用户问题的方法并返回结果
        return handleUserQuestion(question);
    }


    @PostMapping("/ask")
    public Result<-String-> ask(@RequestParam String question) throws IOException {
        if (question == null || question.isEmpty()) {
            return Result.error("请输入问题");
        }

        log.info("用户提问: {}", question);

        // 调用封装好的处理函数
        return handleUserQuestion(question);
    }

    private Result<-String-> handleUserQuestion(String question) throws IOException {
        String responseJson;
        String accessToken = wenXinConfig.flushAccessToken();
        if (accessToken != null) {
            HashMap<-String, String-> userMessage = new HashMap<-->();
            userMessage.put("role", "user");
            userMessage.put("content", question);
            messages.add(userMessage);

            String requestJson = constructRequestJson(1, 0.95, 0.8, 1.0, false, messages);

            RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestJson);
            Request request = new Request.Builder()
                    .url(wenXinConfig.ERNIE_Bot_4_0_URL + "?access_token=" + accessToken)
                    .method("POST", body)
                    .addHeader("Content-Type", "application/json")
                    .build();

            OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder().build();

            printRequest(request);

            try {
                responseJson = HTTP_CLIENT.newCall(request).execute().body().string();
                log.info("API 响应: {}", responseJson);

                // 处理API响应并构建返回结果
                return processApiResponse(responseJson);
            } catch (IOException e) {
                log.error("网络有问题", e);
                HashMap<-String, String-> assistantMessage = new HashMap<-->();
                assistantMessage.put("role", "assistant");
                assistantMessage.put("content", "...");
                messages.add(assistantMessage);
                return Result.error("网络有问题,请稍后重试");
            }
        }
        return Result.error("获取令牌失败"); // 处理获取令牌的错误情况
    }

    private Result<-String-> processApiResponse(String responseJson) {
        // 将回复的内容转为一个 JSONObject
        JSONObject responseObject = JSON.parseObject(responseJson);
        // 将回复的内容添加到消息中
        HashMap<-String, String-> assistantMessage = new HashMap<-->();
        assistantMessage.put("role", "assistant");
        assistantMessage.put("content", responseObject.getString("result"));
        messages.add(assistantMessage);

        return Result.success(responseObject.getString("result")); // 返回成功结果
    }

    /**
     * 构造请求的请求参数
     */
    public String constructRequestJson(Integer userId,
                                       Double temperature,
                                       Double topP,
                                       Double penaltyScore,
                                       boolean stream,
                                       List<-Map<-String, String->-> messages) {
        Map<-String, Object-> request = new HashMap<-->();
        request.put("user_id", userId.toString());
        request.put("temperature", temperature);
        request.put("top_p", topP);
        request.put("penalty_score", penaltyScore);
        request.put("stream", stream);
        request.put("messages", messages);
        log.info("用户提问: {}", messages);
        return JSON.toJSONString(request);
    }

    /**
     * 打印 Request
     */
    private void printRequest(Request request) {
        System.out.println("Request Details:");
        System.out.println("Method: " + request.method());
        System.out.println("URL: " + request.url());
        System.out.println("Headers: " + request.headers());

        // 如果请求体存在且不是空的,打印请求体
        if (request.body() != null) {
            // 注意:需要手动读取请求体内容,通常需要在发送请求前就读取
            // 这里示范性地打印出内容
            System.out.println("Request body exists.");
        }
    }
}