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->
: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->
后端代码
<-!--文心大模型依赖--->
<-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->
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.");
}
}
}
