AI聊天更新历史记录
This commit is contained in:
7
.trae/TODO.md
Normal file
7
.trae/TODO.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# TODO:
|
||||
|
||||
- [x] 1: 分析控制台输出,确定API返回数据的正确结构 (priority: High)
|
||||
- [x] 2: 修复initChat方法中AI消息内容的数据映射 (priority: High)
|
||||
- [x] 3: 修复loadMoreHistory方法中的数据映射 (priority: High)
|
||||
- [x] 4: 移除调试代码,恢复正常UI显示 (priority: Medium)
|
||||
- [x] 5: 测试修复后的AI消息显示功能 (priority: Medium)
|
||||
@@ -1,38 +1,78 @@
|
||||
// src/api/index.js
|
||||
// src/api/ai_index.js
|
||||
// import request from '@/utils/ai_request.js'
|
||||
import request from "../../utils/ai_request";
|
||||
import request from "@/utils/ai_request.js";
|
||||
|
||||
// 获取历史
|
||||
export const getHistory = ({
|
||||
conversationId,
|
||||
user,
|
||||
limit = 20
|
||||
limit = 20,
|
||||
beforeId
|
||||
}) => {
|
||||
return request({
|
||||
url: '/aitutor/aichat/getMessagesToUser',
|
||||
method: 'get',
|
||||
params: {
|
||||
const params = {
|
||||
conversationId,
|
||||
user,
|
||||
limit
|
||||
};
|
||||
|
||||
// 如果有beforeId参数,添加到请求中
|
||||
if (beforeId) {
|
||||
params.beforeId = beforeId;
|
||||
}
|
||||
// headers: {
|
||||
// Authorization: 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjBmMTY3NmY2LTgwOGMtNGUwMC04NDJjLWIwNmY1ZTM5NzJlNCJ9.VVc6OwQ-Xn9pxzYbPhlCpvDp6TwESS00gJi9IXUEIbFw4RFACZDmYCYjQ7voTM4fppy9SAMJCWT-L7Uy-K1eqw'
|
||||
// }
|
||||
|
||||
return request({
|
||||
url: '/aitutor/aichat/getMessagesToUser',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// export const getHistory = ({
|
||||
// conversationId,
|
||||
// user,
|
||||
// limit = 20
|
||||
// }) => {
|
||||
// return request({
|
||||
// url: '/aitutor/aichat/getMessagesToUser',
|
||||
// method: 'get',
|
||||
// params: {
|
||||
// conversationId,
|
||||
// user,
|
||||
// limit
|
||||
// }
|
||||
// // headers: {
|
||||
// // Authorization: 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjBmMTY3NmY2LTgwOGMtNGUwMC04NDJjLWIwNmY1ZTM5NzJlNCJ9.VVc6OwQ-Xn9pxzYbPhlCpvDp6TwESS00gJi9IXUEIbFw4RFACZDmYCYjQ7voTM4fppy9SAMJCWT-L7Uy-K1eqw'
|
||||
// // }
|
||||
// });
|
||||
// };
|
||||
|
||||
// 点赞/点踩 action: 1 点赞 0 点踩
|
||||
export const sendFeedback = ({
|
||||
messageId,
|
||||
action
|
||||
action,
|
||||
user
|
||||
}) => {
|
||||
return request({
|
||||
url: '/api/chat/feedback',
|
||||
url: '/aitutor/aichat/feedback',
|
||||
method: 'post',
|
||||
data: {
|
||||
messageId,
|
||||
action
|
||||
message_id: messageId,
|
||||
rating: action === 1 ? 'like' : 'dislike', // 添加rating参数
|
||||
user
|
||||
}
|
||||
});
|
||||
};
|
||||
// export const sendFeedback = ({
|
||||
// messageId,
|
||||
// action
|
||||
// }) => {
|
||||
// return request({
|
||||
// url: '/api/chat/feedback',
|
||||
// method: 'post',
|
||||
// data: {
|
||||
// messageId,
|
||||
// action
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
@@ -52,106 +52,94 @@
|
||||
<script>
|
||||
import {
|
||||
getHistory
|
||||
} from '../../api/aiChat/ai_index.js'; // 历史记录API
|
||||
} from '../../api/aiChat/ai_index.js';
|
||||
|
||||
export default {
|
||||
name: 'HistoryDrawer',
|
||||
props: {
|
||||
visible: Boolean // 控制抽屉显示
|
||||
visible: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
historyRecords: [], // 原始历史记录
|
||||
filteredRecords: [], // 过滤后的历史记录
|
||||
searchKeyword: '' // 搜索关键词
|
||||
historyRecords: [],
|
||||
filteredRecords: [],
|
||||
searchKeyword: '', // 移除了重复定义
|
||||
loading: false // 新增loading状态
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// 监听visible变化,显示时加载记录
|
||||
visible(newVal) {
|
||||
if (newVal) this.loadHistoryRecords();
|
||||
else this.clearSearch();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 关闭抽屉
|
||||
closeDrawer() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
/* ========== 1. 读本地缓存兜底 ========== */
|
||||
renderLocal(list) {
|
||||
// 将本地缓存的消息按日期分组
|
||||
const groupMap = {};
|
||||
list.forEach((m, idx) => {
|
||||
const date = new Date();
|
||||
const formatted = {
|
||||
date: this.formatDate(date),
|
||||
time: this.formatTime(date),
|
||||
content: m.sender === 'user' ? m.content : '',
|
||||
reply: m.sender === 'ai' ? m.content : '',
|
||||
id: idx
|
||||
};
|
||||
const title = this.getGroupTitle(date);
|
||||
(groupMap[title] ||= []).push(formatted);
|
||||
});
|
||||
|
||||
// 转换为数组形式
|
||||
this.historyRecords = Object.entries(groupMap).map(([t, arr]) => ({
|
||||
title: t,
|
||||
list: arr
|
||||
}));
|
||||
this.filteredRecords = [...this.historyRecords];
|
||||
},
|
||||
|
||||
/* ========== 2. 加载接口或本地缓存 ========== */
|
||||
async loadHistoryRecords() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 1. 先读本地缓存,立即显示(避免空白)
|
||||
const local = uni.getStorageSync('chatHistory') || [];
|
||||
if (local.length) this.renderLocal(local);
|
||||
// 1. 获取当前用户学号
|
||||
const userNo = uni.getStorageSync('stuNo');
|
||||
if (!userNo) throw new Error('未获取到用户学号');
|
||||
|
||||
// 2. 再调接口,成功后覆盖本地
|
||||
// 2. 调用接口获取数据
|
||||
const res = await getHistory({
|
||||
conversationId: '5665af64-22b4-4a59-b15f-2126eb056302',
|
||||
user: '2023429112',
|
||||
limit: 50
|
||||
user: userNo,
|
||||
conversationId: '',
|
||||
limit: 20
|
||||
});
|
||||
console.log('原始返回结构', res);
|
||||
console.log('接口响应:', res);
|
||||
|
||||
const list = res?.data?.data || [];
|
||||
if (!list.length) return; // 接口返回空数组也保留本地
|
||||
// 3. 处理响应数据 - 修正数据结构解析
|
||||
const list = Array.isArray(res.data?.data) ? res.data.data : [];
|
||||
console.log('解析后的数据列表:', list);
|
||||
|
||||
// 处理API返回的数据
|
||||
// 4. 分组处理
|
||||
const groupMap = {};
|
||||
list.forEach(item => {
|
||||
const ts = item.created_at ?? item.create_time ?? item.timestamp ?? 0;
|
||||
const date = String(ts).length === 13 ? new Date(ts) : new Date(ts * 1000);
|
||||
// 修正字段映射
|
||||
const timestamp = item.created_at || item.create_time || item.timestamp || Date.now() /
|
||||
1000;
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
const formatted = {
|
||||
const record = {
|
||||
id: item.id || Math.random().toString(36).slice(2),
|
||||
date: this.formatDate(date),
|
||||
time: this.formatTime(date),
|
||||
content: item.query || item.question || item.content || '未知内容',
|
||||
content: item.query || item.content || '未知内容',
|
||||
reply: item.answer || item.reply || '暂无回复',
|
||||
id: item.id || Math.random().toString(36).slice(2)
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
||||
const title = this.getGroupTitle(date);
|
||||
(groupMap[title] ||= []).push(formatted);
|
||||
(groupMap[title] ||= []).push(record);
|
||||
});
|
||||
|
||||
// 按时间排序并更新数据
|
||||
// 5. 排序并更新数据
|
||||
this.historyRecords = Object.entries(groupMap).map(([title, arr]) => ({
|
||||
title,
|
||||
list: arr.sort((a, b) => b.timestamp - a.timestamp)
|
||||
}));
|
||||
|
||||
this.filteredRecords = [...this.historyRecords];
|
||||
console.log('最终处理的历史记录:', this.historyRecords);
|
||||
} catch (e) {
|
||||
console.error('历史接口 401,已使用本地缓存兜底', e);
|
||||
// 接口失败时,保留本地缓存(不覆盖)
|
||||
console.error('加载失败:', e);
|
||||
uni.showToast({
|
||||
title: `加载失败: ${e.message}`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
this.historyRecords = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 处理搜索输入
|
||||
handleSearch() {
|
||||
const kw = this.searchKeyword.trim().toLowerCase();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<!-- pages/aiChat/ai_index -->
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<!-- 状态保持:当AI聊天可见时才显示聊天内容 -->
|
||||
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
@@ -14,16 +11,23 @@
|
||||
</view>
|
||||
<!-- 中间标题 -->
|
||||
<view class="nav-title">智水AI辅导员</view>
|
||||
|
||||
<!-- 右侧:新建聊天图标 -->
|
||||
<view class="nav-right" @click="newChat">
|
||||
<image src="/static/newChat.svg" mode="aspectFit" class="nav-icon"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view scroll-y class="message-list" :scroll-top="scrollTop" scroll-with-animation>
|
||||
<block v-for="(item, index) in messages" :key="index">
|
||||
<scroll-view scroll-y class="message-list" :scroll-top="scrollTop" scroll-with-animation enable-passive="true"
|
||||
@scroll="onScroll" @scrolltoupper="loadMoreHistory" upper-threshold="50">
|
||||
<!-- 加载提示 -->
|
||||
<view v-if="isLoadingHistory" class="loading-history">
|
||||
<text>正在加载历史记录...</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多历史记录提示 -->
|
||||
<view v-if="!hasMoreHistory && messages.length > 0" class="no-more-history">
|
||||
<text>没有更多历史记录了</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<block v-for="(item, index) in messages" :key="item.messageId || index">
|
||||
<view :class="['message-item', item.sender === 'user' ? 'user-message' : 'ai-message']">
|
||||
<!-- 用户/AI头像 -->
|
||||
<image class="avatar" :src="item.avatar"></image>
|
||||
@@ -34,12 +38,12 @@
|
||||
<text v-else-if="item.content">{{ item.content }}</text>
|
||||
<!-- 图片内容 -->
|
||||
<image v-if="item.image" :src="item.image" class="sent-image"></image>
|
||||
|
||||
<!-- AI 特有内容 -->
|
||||
<view v-if="item.sender === 'ai'" class="ai-hint">
|
||||
<!-- 引用来源部分 -->
|
||||
<view v-if="item.retrieverResources && item.retrieverResources.length"
|
||||
class="reference-section">
|
||||
<view class="reference-section">
|
||||
<text class="reference-title">引用来源:</text>
|
||||
<!-- 遍历每个引用资源 -->
|
||||
<view v-for="(ref, idx) in item.retrieverResources" :key="idx"
|
||||
@@ -57,7 +61,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI操作区域(点赞/点踩) -->
|
||||
<view class="ai-actions">
|
||||
<text class="ai-text">回答由AI生成</text>
|
||||
@@ -98,60 +102,58 @@
|
||||
|
||||
<script>
|
||||
/* ========== 依赖 ========== */
|
||||
import HistoryDrawer from '@/components/aiChat/HistoryDrawer.vue'; // 历史记录抽屉组件
|
||||
import HistoryDrawer from '@/components/aiChat/HistoryDrawer.vue';
|
||||
import {
|
||||
createChatStream
|
||||
} from '@/utils/ai_stream.js'; // 流式聊天API
|
||||
import MarkdownIt from 'markdown-it'; // Markdown解析器
|
||||
import DOMPurify from 'dompurify'; // HTML净化器
|
||||
} from '@/utils/ai_stream.js';
|
||||
import {
|
||||
sendFeedback,
|
||||
getHistory
|
||||
} from '@/api/aiChat/ai_index.js';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
/* DOMPurify 白名单加固 */
|
||||
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
||||
if (node.tagName === 'A') node.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
|
||||
// 初始化Markdown解析器
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
});
|
||||
|
||||
/* ========== 配置 ========== */
|
||||
const BASE_URL = (function() {
|
||||
// #ifdef H5
|
||||
return 'http://localhost:8080/dev-api/aitutor/aichat'; // H5环境API地址
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// return 'http://192.168.31.123:8080/aitutor/aichat'; // 真机调试改成你的电脑 IP
|
||||
// #endif
|
||||
})();
|
||||
|
||||
/* ========== 组件 ========== */
|
||||
export default {
|
||||
components: {
|
||||
HistoryDrawer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showHistoryDrawer: false, // 是否显示历史记录抽屉
|
||||
inputMessage: '', // 用户输入的消息
|
||||
messages: [], // 聊天消息列表
|
||||
scrollTop: 0, // 滚动位置
|
||||
conversation_id: null, // 当前会话ID
|
||||
showSingleReference: {}, // 控制引用来源的显示状态
|
||||
currentReader: null, // 当前流式读取器
|
||||
/* 新增锁 & 基础信息 */
|
||||
sending: false, // 是否正在发送消息
|
||||
user: uni.getStorageSync('stuNo') || '', // 用户学号
|
||||
userId: uni.getStorageSync('stuId') || '', // 用户ID
|
||||
userName: uni.getStorageSync('stuName') || '' // 用户名
|
||||
showHistoryDrawer: false,
|
||||
inputMessage: '',
|
||||
messages: [],
|
||||
scrollTop: 0,
|
||||
conversation_id: null,
|
||||
showSingleReference: {},
|
||||
currentCancel: null,
|
||||
sending: false,
|
||||
user: uni.getStorageSync('stuNo') || '',
|
||||
userId: uni.getStorageSync('stuId') || '',
|
||||
userName: uni.getStorageSync('stuName') || '',
|
||||
// 分页加载相关字段
|
||||
isLoadingHistory: false,
|
||||
hasMoreHistory: true,
|
||||
earliestMessageId: null,
|
||||
scrollHeight: 0,
|
||||
lastScrollTime: 0,
|
||||
scrollDebounce: 100,
|
||||
lastUserScrollTime: 0, // 添加这个字段用于智能滚动
|
||||
};
|
||||
},
|
||||
|
||||
/* ---------- 生命周期 ---------- */
|
||||
onLoad() {
|
||||
// 页面加载时检查登录状态
|
||||
if (!this.user) {
|
||||
this.$toast('请先登录');
|
||||
setTimeout(() => uni.navigateTo({
|
||||
@@ -159,40 +161,112 @@
|
||||
}), 1500);
|
||||
return;
|
||||
}
|
||||
this.initChat(); // 初始化聊天
|
||||
|
||||
// 从本地存储获取conversation_id
|
||||
this.conversation_id = uni.getStorageSync('conversation_id') || null;
|
||||
|
||||
// 初始化聊天并确保滚动到底部
|
||||
this.initChat().then(() => {
|
||||
// 多重延迟确保DOM完全渲染
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.forceScrollToBottom();
|
||||
// 再次确保滚动(处理某些设备的渲染延迟)
|
||||
setTimeout(() => {
|
||||
this.forceScrollToBottom();
|
||||
}, 500);
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
// 页面卸载时取消流式读取
|
||||
this.currentReader?.cancel?.('页面卸载');
|
||||
this.currentReader = null;
|
||||
},
|
||||
async onPullDownRefresh() {
|
||||
// 下拉刷新
|
||||
await this.initChat();
|
||||
uni.stopPullDownRefresh();
|
||||
this.$toast('刷新成功', 'success');
|
||||
|
||||
/**
|
||||
* 页面显示时触发 - 确保每次进入页面都滚动到底部
|
||||
*/
|
||||
onShow() {
|
||||
// 确保页面显示时滚动到底部
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.forceScrollToBottom();
|
||||
// 额外延迟处理某些设备的渲染问题
|
||||
setTimeout(() => {
|
||||
this.forceScrollToBottom();
|
||||
}, 200);
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
||||
/* ---------- 方法 ---------- */
|
||||
methods: {
|
||||
// 返回首页
|
||||
/**
|
||||
* 返回首页
|
||||
*/
|
||||
goHome() {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
},
|
||||
|
||||
// 切换历史记录抽屉显示状态
|
||||
/**
|
||||
* 强制滚动到底部 - 优化版本
|
||||
*/
|
||||
forceScrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('.message-list').boundingClientRect(rect => {
|
||||
if (rect && rect.scrollHeight > rect.height) {
|
||||
// 计算需要滚动的距离
|
||||
const targetScrollTop = rect.scrollHeight - rect.height + 50; // 额外50px确保完全显示
|
||||
this.scrollTop = targetScrollTop;
|
||||
|
||||
// 双重确保滚动生效
|
||||
setTimeout(() => {
|
||||
this.scrollTop = targetScrollTop + 1;
|
||||
setTimeout(() => {
|
||||
this.scrollTop = targetScrollTop;
|
||||
}, 50);
|
||||
}, 100);
|
||||
}
|
||||
}).exec();
|
||||
});
|
||||
},
|
||||
|
||||
forceScrollToTop() {
|
||||
this.$nextTick(() => {
|
||||
// 直接滚动到顶部
|
||||
this.scrollTop = 999;
|
||||
|
||||
// 双重确保滚动生效
|
||||
setTimeout(() => {
|
||||
this.scrollTop = 1;
|
||||
setTimeout(() => {
|
||||
this.scrollTop = 0;
|
||||
}, 50);
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换历史记录抽屉显示状态
|
||||
*/
|
||||
toggleHistoryDrawer() {
|
||||
this.showHistoryDrawer = !this.showHistoryDrawer;
|
||||
},
|
||||
|
||||
// 渲染Markdown内容
|
||||
/**
|
||||
* 渲染Markdown内容
|
||||
* @param {string} text - 需要渲染的Markdown文本
|
||||
* @returns {string} 安全的HTML内容
|
||||
*/
|
||||
renderMarkdown(text) {
|
||||
return DOMPurify.sanitize(md.render(text || ''));
|
||||
},
|
||||
|
||||
/* 轻量 toast 封装 */
|
||||
/**
|
||||
* 显示提示消息
|
||||
* @param {string} title - 提示内容
|
||||
* @param {string} icon - 图标类型
|
||||
*/
|
||||
$toast(title, icon = 'none') {
|
||||
uni.showToast({
|
||||
title,
|
||||
@@ -200,61 +274,326 @@
|
||||
});
|
||||
},
|
||||
|
||||
/* 获取历史会话 or 欢迎语 */
|
||||
async initChat() {
|
||||
if (!this.user) return this.initConversation();
|
||||
try {
|
||||
// 请求获取用户历史消息
|
||||
const res = await uni.request({
|
||||
url: BASE_URL + '/getMessagesToUser',
|
||||
method: 'POST',
|
||||
data: {
|
||||
user: this.user
|
||||
}
|
||||
});
|
||||
const data = res.data || {};
|
||||
if (data.code === 200 && data.data?.conversationId) {
|
||||
// 设置会话ID和消息列表
|
||||
this.conversation_id = data.data.conversationId;
|
||||
/**
|
||||
* 历史记录点击处理
|
||||
* @param {Object} item - 历史记录项
|
||||
*/
|
||||
onHistoryItemClick(item) {
|
||||
// 清理当前消息和分页状态
|
||||
this.messages = [];
|
||||
this.isLoadingHistory = false;
|
||||
this.hasMoreHistory = true;
|
||||
this.earliestMessageId = null;
|
||||
|
||||
// 添加历史对话到消息列表
|
||||
const userMessage = {
|
||||
sender: 'user',
|
||||
avatar: '/static/yonghu.png',
|
||||
content: item.content,
|
||||
image: '',
|
||||
messageId: 'history-user-' + Date.now().toString()
|
||||
};
|
||||
|
||||
const aiMessage = {
|
||||
sender: 'ai',
|
||||
avatar: '/static/AI.png',
|
||||
content: item.reply,
|
||||
retrieverResources: [],
|
||||
image: '',
|
||||
messageId: 'history-ai-' + Date.now().toString()
|
||||
};
|
||||
|
||||
this.messages = [userMessage, aiMessage];
|
||||
this.earliestMessageId = userMessage.messageId;
|
||||
|
||||
// 设置对话ID(如果有的话)
|
||||
if (item.conversationId) {
|
||||
this.conversation_id = item.conversationId;
|
||||
uni.setStorageSync('conversation_id', this.conversation_id);
|
||||
this.messages = Array.isArray(data.data.messages) && data.data.messages.length ?
|
||||
data.data.messages :
|
||||
this.welcomeMessage();
|
||||
} else {
|
||||
this.initConversation();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取历史失败', e);
|
||||
this.$toast('连接失败,使用新会话');
|
||||
this.initConversation();
|
||||
}
|
||||
|
||||
// 确保滚动到底部
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
this.saveMessagesToLocal(); // 保存到本地
|
||||
this.$toast('已加载历史对话', 'success');
|
||||
},
|
||||
|
||||
/* 欢迎语兜底 */
|
||||
welcomeMessage() {
|
||||
/**
|
||||
* 初始化聊天 - 从接口获取历史记录
|
||||
*/
|
||||
async initChat() {
|
||||
if (!this.user) return this.initConversation();
|
||||
|
||||
// 重置状态
|
||||
this.isLoadingHistory = false;
|
||||
this.hasMoreHistory = true;
|
||||
this.earliestMessageId = null;
|
||||
this.messages = [];
|
||||
|
||||
try {
|
||||
const res = await getHistory({
|
||||
user: this.user,
|
||||
conversationId: this.conversation_id || '',
|
||||
limit: 20
|
||||
});
|
||||
|
||||
console.log('API响应数据:', res); // 调试日志
|
||||
|
||||
if (res.code === 200 && res.data && Array.isArray(res.data.data)) {
|
||||
// 新消息数组
|
||||
const newMessages = [];
|
||||
|
||||
// 处理每条消息
|
||||
res.data.data.forEach(msg => {
|
||||
console.log('处理单条消息:', msg); // 调试日志
|
||||
|
||||
// 用户消息
|
||||
if (msg.query) {
|
||||
newMessages.push({
|
||||
sender: 'user',
|
||||
avatar: '/static/yonghu.png',
|
||||
content: msg.query,
|
||||
image: '',
|
||||
messageId: msg.id,
|
||||
conversationId: msg.conversation_id
|
||||
});
|
||||
}
|
||||
|
||||
// AI消息
|
||||
if (msg.answer) {
|
||||
newMessages.push({
|
||||
sender: 'ai',
|
||||
avatar: '/static/AI.png',
|
||||
content: msg.answer,
|
||||
retrieverResources: msg.retriever_resources || [],
|
||||
image: '',
|
||||
messageId: 'ai-' + msg.id,
|
||||
conversationId: msg.conversation_id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 按创建时间排序
|
||||
newMessages.sort((a, b) => {
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
});
|
||||
|
||||
this.messages = newMessages;
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
this.conversation_id = newMessages[0].conversationId;
|
||||
uni.setStorageSync('conversation_id', this.conversation_id);
|
||||
this.earliestMessageId = newMessages[0].messageId;
|
||||
}
|
||||
|
||||
this.hasMoreHistory = res.data.has_more || false;
|
||||
this.scrollToBottom();
|
||||
} else {
|
||||
this.messages = this.getWelcomeMessage();
|
||||
this.hasMoreHistory = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('初始化聊天失败:', e);
|
||||
this.$toast('加载历史记录失败');
|
||||
this.initConversation();
|
||||
}
|
||||
|
||||
// 确保初始化完成后滚动到底部
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.forceScrollToBottom();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载更多历史记录 - 基于ID分页
|
||||
*/
|
||||
async loadMoreHistory() {
|
||||
if (this.isLoadingHistory || !this.hasMoreHistory || !this.conversation_id || !this
|
||||
.earliestMessageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoadingHistory = true;
|
||||
|
||||
try {
|
||||
// 记录当前滚动位置和高度
|
||||
const currentScrollTop = this.scrollTop;
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
const currentScrollHeight = await new Promise(resolve => {
|
||||
query.select('.message-list').boundingClientRect(rect => {
|
||||
resolve(rect.height);
|
||||
}).exec();
|
||||
});
|
||||
|
||||
// 使用当前最早的消息ID作为分页参数
|
||||
const beforeId = this.earliestMessageId;
|
||||
|
||||
const res = await getHistory({
|
||||
user: this.user,
|
||||
conversationId: this.conversation_id,
|
||||
limit: 10,
|
||||
beforeId: beforeId // 获取指定ID之前的记录
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 修正数据结构解析:使用res.data.data而不是res.data
|
||||
if (res.code === 200 && res.data && Array.isArray(res.data.data) && res.data.data.length > 0) {
|
||||
|
||||
// 处理新获取的消息
|
||||
// 在数据解析后添加调试日志
|
||||
const newMessages = res.data.data.map(msg => {
|
||||
// 根据控制台数据结构分析,修正字段映射
|
||||
let sender, content;
|
||||
|
||||
if (msg.inputs?.user_name) {
|
||||
// 用户消息
|
||||
sender = 'user';
|
||||
content = msg.query || msg.content || '';
|
||||
} else {
|
||||
// AI消息 - 根据实际数据结构调整字段映射
|
||||
sender = 'ai';
|
||||
content = msg.answer || msg.content || msg.reply || '';
|
||||
}
|
||||
|
||||
const message = {
|
||||
sender: sender,
|
||||
avatar: sender === 'user' ? '/static/yonghu.png' : '/static/AI.png',
|
||||
content: content,
|
||||
retrieverResources: msg.retriever_resources || [],
|
||||
image: '',
|
||||
messageId: msg.id,
|
||||
conversationId: msg.conversation_id
|
||||
};
|
||||
|
||||
return message;
|
||||
});
|
||||
|
||||
// 按ID排序
|
||||
newMessages.sort((a, b) => {
|
||||
const aId = parseInt(a.messageId) || 0;
|
||||
const bId = parseInt(b.messageId) || 0;
|
||||
return aId - bId;
|
||||
});
|
||||
|
||||
// 插入到现有消息的前面
|
||||
this.insertMessagesAtFront(newMessages);
|
||||
|
||||
// 使用has_more字段判断是否还有更多数据
|
||||
this.hasMoreHistory = res.data.has_more || false;
|
||||
|
||||
// 保持滚动位置,避免跳变
|
||||
this.$nextTick(() => {
|
||||
query.select('.message-list').boundingClientRect(rect => {
|
||||
if (rect) {
|
||||
const heightDiff = rect.height - currentScrollHeight;
|
||||
this.scrollTop = currentScrollTop + heightDiff;
|
||||
}
|
||||
}).exec();
|
||||
});
|
||||
} else {
|
||||
this.hasMoreHistory = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史记录失败:', error);
|
||||
this.$toast('加载历史记录失败');
|
||||
} finally {
|
||||
this.isLoadingHistory = false;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* 按ID顺序插入新消息到前面
|
||||
* @param {Array} newMessages - 新消息数组
|
||||
*/
|
||||
insertMessagesAtFront(newMessages) {
|
||||
if (!newMessages || newMessages.length === 0) return;
|
||||
|
||||
// 确保新消息有正确的messageId
|
||||
const processedMessages = newMessages.map(msg => ({
|
||||
...msg,
|
||||
messageId: msg.messageId || msg.id || 'history-' + Date.now() + '-' + Math.random()
|
||||
}));
|
||||
|
||||
// 按ID排序,确保正确的时间顺序
|
||||
processedMessages.sort((a, b) => {
|
||||
const aId = parseInt(a.messageId.replace(/\D/g, '')) || 0;
|
||||
const bId = parseInt(b.messageId.replace(/\D/g, '')) || 0;
|
||||
return aId - bId;
|
||||
});
|
||||
|
||||
// 插入到消息列表前面
|
||||
this.messages = [...processedMessages, ...this.messages];
|
||||
|
||||
// 更新最早消息ID
|
||||
if (processedMessages.length > 0) {
|
||||
this.earliestMessageId = processedMessages[0].messageId;
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
this.saveMessagesToLocal();
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化新对话
|
||||
*/
|
||||
initConversation() {
|
||||
this.conversation_id = null;
|
||||
this.messages = this.getWelcomeMessage();
|
||||
this.hasMoreHistory = false; // 新对话没有历史记录
|
||||
this.saveMessagesToLocal();
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存消息到本地存储
|
||||
*/
|
||||
saveMessagesToLocal() {
|
||||
if (!this.user) return;
|
||||
|
||||
const storageKey = `chatMessages_${this.user}_${this.conversation_id || 'default'}`;
|
||||
|
||||
// 限制本地存储的消息数量,避免占用过多空间
|
||||
const maxMessages = 100;
|
||||
const messagesToSave = this.messages.length > maxMessages ?
|
||||
this.messages.slice(0, maxMessages) :
|
||||
this.messages;
|
||||
|
||||
uni.setStorageSync(storageKey, messagesToSave);
|
||||
},
|
||||
|
||||
/**
|
||||
* 欢迎消息
|
||||
* @returns {Array} 欢迎消息数组
|
||||
*/
|
||||
getWelcomeMessage() {
|
||||
return [{
|
||||
sender: 'ai',
|
||||
avatar: '/static/AI.png',
|
||||
content: '你好!我是您的 AI 小助手,有什么可以帮您?😊',
|
||||
retrieverResources: [],
|
||||
image: ''
|
||||
image: '',
|
||||
messageId: 'welcome-' + Date.now().toString()
|
||||
}];
|
||||
},
|
||||
|
||||
// 初始化新会话
|
||||
initConversation() {
|
||||
uni.removeStorageSync('chatHistory');
|
||||
this.conversation_id = null;
|
||||
this.messages = this.welcomeMessage();
|
||||
},
|
||||
|
||||
/* 发送消息(带并发锁) */
|
||||
/**
|
||||
* 基于SSE的发送消息实现
|
||||
*/
|
||||
async sendMessage() {
|
||||
const msg = this.inputMessage.trim();
|
||||
if (!msg || this.sending) return;
|
||||
this.sending = true;
|
||||
|
||||
// 添加用户消息到列表
|
||||
// 添加用户消息
|
||||
this.messages.push({
|
||||
sender: 'user',
|
||||
avatar: '/static/yonghu.png',
|
||||
@@ -263,23 +602,29 @@
|
||||
messageId: Date.now().toString()
|
||||
});
|
||||
this.inputMessage = '';
|
||||
this.saveMessagesToLocal(); // 保存用户消息
|
||||
|
||||
// 添加AI占位消息
|
||||
// 添加AI消息占位
|
||||
const aiIdx = this.messages.push({
|
||||
sender: 'ai',
|
||||
avatar: '/static/AI.png',
|
||||
content: '',
|
||||
content: '<span class="loading-text">正在思考...</span>',
|
||||
retrieverResources: [],
|
||||
image: '',
|
||||
messageId: null
|
||||
messageId: 'pending-' + Date.now().toString()
|
||||
}) - 1;
|
||||
this.$set(this.showSingleReference, aiIdx, {});
|
||||
this.scrollToBottom();
|
||||
|
||||
// 取消之前的请求
|
||||
if (this.currentCancel) {
|
||||
this.currentCancel('新消息发送,终止旧连接');
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建流式聊天连接
|
||||
const {
|
||||
stream
|
||||
stream,
|
||||
cancel
|
||||
} = createChatStream({
|
||||
conversationId: this.conversation_id,
|
||||
prompt: msg,
|
||||
@@ -287,13 +632,15 @@
|
||||
userId: this.userId,
|
||||
userName: this.userName
|
||||
});
|
||||
|
||||
this.currentCancel = cancel;
|
||||
const {
|
||||
reader,
|
||||
decoder
|
||||
} = await stream;
|
||||
|
||||
let buffer = '';
|
||||
// 流式读取数据
|
||||
// 流式处理响应
|
||||
while (true) {
|
||||
const {
|
||||
done,
|
||||
@@ -306,113 +653,183 @@
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
// 处理每行数据
|
||||
for (const l of lines) {
|
||||
if (!l.startsWith('data:')) continue;
|
||||
const text = l.slice(5).trim();
|
||||
if (text === '[DONE]') continue;
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
// 处理消息内容
|
||||
if (json.event === 'message' && json.answer) {
|
||||
this.$set(this.messages[aiIdx], 'content', this.messages[aiIdx].content + json
|
||||
.answer);
|
||||
this.scrollToBottom();
|
||||
let jsonStr = line.replace(/^data:/, '').trim();
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
// 更新消息内容
|
||||
if (data.event === 'message' && data.answer) {
|
||||
const currentContent = this.messages[aiIdx].content;
|
||||
const newContent = (currentContent.includes('loading-text') ? '' :
|
||||
currentContent) + data.answer;
|
||||
this.$set(this.messages[aiIdx], 'content', newContent);
|
||||
|
||||
// 每次更新内容后立即滚动到底部
|
||||
this.scrollToBottom(true); // 传入true表示是流式响应中的滚动
|
||||
this.saveMessagesToLocal();
|
||||
}
|
||||
// 处理消息结束事件
|
||||
if (json.event === 'message_end') {
|
||||
if (json['conversation id']) this.conversation_id = json['conversation id'];
|
||||
if (json.metadata?.retriever_resources) {
|
||||
this.$set(this.messages[aiIdx], 'retrieverResources', json.metadata
|
||||
|
||||
// 处理结束消息
|
||||
if (data.event === 'message_end') {
|
||||
if (data.conversation_id) {
|
||||
this.conversation_id = data.conversation_id;
|
||||
uni.setStorageSync('conversation_id', this.conversation_id);
|
||||
}
|
||||
if (data.metadata?.retriever_resources) {
|
||||
this.$set(this.messages[aiIdx], 'retrieverResources', data.metadata
|
||||
.retriever_resources);
|
||||
this.$set(this.messages[aiIdx], 'messageId', json.message_id || Date.now()
|
||||
.toString());
|
||||
}
|
||||
if (data.message_id) {
|
||||
this.$set(this.messages[aiIdx], 'messageId', data.message_id);
|
||||
}
|
||||
|
||||
// 最终完成后再次确保滚动到底部
|
||||
this.forceScrollToTop();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('JSON 解析失败', text, e);
|
||||
console.warn('JSON解析失败:', line, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('流式请求失败', e);
|
||||
this.$set(this.messages[aiIdx], 'content', '抱歉,AI 回复失败,请重试。');
|
||||
console.error('流式请求失败:', e);
|
||||
this.$set(this.messages[aiIdx], 'content', 'AI回复失败: ' + (e.message || '网络错误'));
|
||||
} finally {
|
||||
this.sending = false;
|
||||
this.scrollToBottom();
|
||||
uni.setStorageSync('chatHistory', this.messages);
|
||||
this.currentCancel = null;
|
||||
|
||||
// 确保AI消息有有效的messageId
|
||||
if (this.messages[aiIdx].messageId.startsWith('pending-')) {
|
||||
this.$set(this.messages[aiIdx], 'messageId', 'ai-' + Date.now().toString());
|
||||
}
|
||||
|
||||
// 保存消息到本地存储
|
||||
this.saveMessagesToLocal();
|
||||
}
|
||||
},
|
||||
|
||||
/* 滚动到底部 */
|
||||
scrollToBottom() {
|
||||
/**
|
||||
* 滚动到底部
|
||||
* @param {boolean} isStreaming - 是否是流式响应中的滚动
|
||||
*/
|
||||
// 修改原有scrollToBottom方法
|
||||
scrollToBottom(isStreaming = false) {
|
||||
const now = Date.now();
|
||||
if (now - this.lastScrollTime < this.scrollDebounce && isStreaming) {
|
||||
return;
|
||||
}
|
||||
this.lastScrollTime = now;
|
||||
|
||||
this.$nextTick(() => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('.message-list')
|
||||
.boundingClientRect(rect => {
|
||||
this.scrollTop = (rect?.height || 0) + 9999;
|
||||
})
|
||||
.exec();
|
||||
setTimeout(() => {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('.message-list').boundingClientRect(rect => {
|
||||
if (rect) {
|
||||
// 确保滚动到最底部
|
||||
this.scrollTop = rect.scrollHeight;
|
||||
}
|
||||
}).exec();
|
||||
}, isStreaming ? 50 : 100);
|
||||
});
|
||||
},
|
||||
|
||||
// 切换单个引用来源的显示状态
|
||||
/**
|
||||
* 切换单个引用的显示状态
|
||||
* @param {number} msgIdx - 消息索引
|
||||
* @param {number} refIdx - 引用索引
|
||||
*/
|
||||
toggleSingleReference(msgIdx, refIdx) {
|
||||
if (!this.showSingleReference[msgIdx]) this.$set(this.showSingleReference, msgIdx, {});
|
||||
const cur = this.showSingleReference[msgIdx][refIdx];
|
||||
this.$set(this.showSingleReference[msgIdx], refIdx, !cur);
|
||||
},
|
||||
|
||||
// 点赞消息处理
|
||||
/**
|
||||
* 点赞处理
|
||||
* @param {string} id - 消息ID
|
||||
*/
|
||||
handleThumbUpClick(id) {
|
||||
if (!id) return;
|
||||
uni.request({
|
||||
url: BASE_URL + '/feedback',
|
||||
method: 'POST',
|
||||
data: {
|
||||
messageId: id,
|
||||
action: 1
|
||||
if (!id) {
|
||||
this.$toast('消息ID不存在', 'error');
|
||||
return;
|
||||
}
|
||||
}).then(() => this.$toast('点赞成功', 'success'))
|
||||
.catch(() => this.$toast('点赞失败'));
|
||||
|
||||
sendFeedback({
|
||||
messageId: id,
|
||||
action: 1,
|
||||
user: this.user
|
||||
}).then(res => {
|
||||
if (res.code === 200) {
|
||||
this.$toast('点赞成功', 'success');
|
||||
} else {
|
||||
this.$toast(res.msg || '点赞失败', 'error');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('点赞失败:', err);
|
||||
this.$toast('点赞失败', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// 点踩消息处理
|
||||
/**
|
||||
* 点踩处理
|
||||
* @param {string} id - 消息ID
|
||||
*/
|
||||
handleThumbDownClick(id) {
|
||||
if (!id) return;
|
||||
uni.request({
|
||||
url: BASE_URL + '/feedback',
|
||||
method: 'POST',
|
||||
data: {
|
||||
messageId: id,
|
||||
action: 0
|
||||
if (!id) {
|
||||
this.$toast('消息ID不存在', 'error');
|
||||
return;
|
||||
}
|
||||
}).then(() => this.$toast('已反馈'))
|
||||
.catch(() => this.$toast('反馈失败'));
|
||||
|
||||
sendFeedback({
|
||||
messageId: id,
|
||||
action: 0,
|
||||
user: this.user
|
||||
}).then(res => {
|
||||
if (res.code === 200) {
|
||||
this.$toast('已反馈', 'success');
|
||||
} else {
|
||||
this.$toast(res.msg || '反馈失败', 'error');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('反馈失败:', err);
|
||||
this.$toast('反馈失败', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// 新建聊天会话
|
||||
newChat() {
|
||||
this.messages = [];
|
||||
this.conversation_id = null;
|
||||
uni.removeStorageSync('conversation_id');
|
||||
uni.removeStorageSync('chatHistory');
|
||||
this.initConversation();
|
||||
this.$toast('新聊天已开始');
|
||||
/**
|
||||
* 滚动事件监听
|
||||
* @param {Object} e - 事件对象
|
||||
*/
|
||||
onScroll(e) {
|
||||
this.scrollHeight = e.detail.scrollHeight;
|
||||
this.lastUserScrollTime = Date.now();
|
||||
},
|
||||
|
||||
/* 选择图片并上传 */
|
||||
/**
|
||||
* 智能滚动到顶部
|
||||
* 如果用户在3秒内没有滚动,则自动滚动到顶部
|
||||
*/
|
||||
smartScrollToTop() {
|
||||
// 如果用户在3秒内没有滚动,则自动滚动到顶部
|
||||
if (Date.now() - this.lastUserScrollTime > 3000) {
|
||||
this.forceScrollToTop();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择图片并上传
|
||||
*/
|
||||
selectImage() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'], // 压缩
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: res => {
|
||||
const temp = res.tempFilePaths[0];
|
||||
// 上传图片
|
||||
uni.uploadFile({
|
||||
url: BASE_URL + '/files/upload',
|
||||
url: '/aitutor/aichat/files/upload',
|
||||
filePath: temp,
|
||||
name: 'file',
|
||||
formData: {
|
||||
@@ -428,7 +845,6 @@
|
||||
url
|
||||
} = JSON.parse(data);
|
||||
if (url) {
|
||||
// 添加图片消息到列表
|
||||
this.messages.push({
|
||||
sender: 'user',
|
||||
avatar: '/static/yonghu.png',
|
||||
@@ -437,6 +853,7 @@
|
||||
messageId: Date.now().toString()
|
||||
});
|
||||
this.scrollToBottom();
|
||||
this.saveMessagesToLocal(); // 保存图片消息
|
||||
}
|
||||
} catch (e) {
|
||||
this.$toast('上传解析失败');
|
||||
@@ -444,7 +861,8 @@
|
||||
},
|
||||
fail: () => this.$toast('上传失败')
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => this.$toast('选择图片失败')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #F5F5F5;
|
||||
background-color: #f5f5f5;
|
||||
padding-top: 10px;
|
||||
/* 为固定导航栏预留空间 */
|
||||
box-sizing: border-box;
|
||||
@@ -45,6 +45,7 @@
|
||||
color: #333;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
margin-right: 45px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
@@ -166,7 +167,7 @@
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
@@ -260,10 +261,9 @@
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* 可点击文档名 */
|
||||
.doc-name-link {
|
||||
color: #007AFF;
|
||||
color: #007aff;
|
||||
text-decoration: underline;
|
||||
margin-right: 16rpx;
|
||||
font-size: 10rpx;
|
||||
@@ -301,7 +301,7 @@
|
||||
}
|
||||
|
||||
.doc-name-link {
|
||||
color: #007AFF;
|
||||
color: #007aff;
|
||||
text-decoration: underline;
|
||||
margin-right: 16rpx;
|
||||
font-size: clamp(13px, 3vw, 15px);
|
||||
@@ -344,6 +344,28 @@
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.loading-history {
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.no-more-history {
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
border-top: 1px dashed #eee;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
/* ============= 小屏设备适配 ============= */
|
||||
@media (max-width: 600px) {
|
||||
.message-content {
|
||||
|
||||
@@ -6,9 +6,13 @@ import {
|
||||
|
||||
const service = axios.create({
|
||||
// baseURL: 'http://localhost:9090/dev-api/aitutor/aichat',
|
||||
baseURL: 'http://localhost:8088/aitutor/aichat',
|
||||
// baseURL: 'http://localhost:8088/aitutor/aichat',
|
||||
// baseURL: 'http://localhost:8080/aitutor/aichat',
|
||||
baseURL: 'http://localhost:8088',
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器:统一加 token
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// src/utils/ai_stream.js (H5 优化版)
|
||||
// src/utils/ai_stream.js
|
||||
import {
|
||||
getToken
|
||||
} from '@/utils/auth';
|
||||
@@ -23,7 +23,6 @@ export function createChatStream(params) {
|
||||
const fetchPromise = fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'text/event-stream',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'X-Request-ID': requestId
|
||||
@@ -38,20 +37,21 @@ export function createChatStream(params) {
|
||||
}),
|
||||
signal: controller.signal
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
if (!resp.body) throw new Error('Response body is null');
|
||||
return resp.body;
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.body) throw new Error('Response body is null');
|
||||
return {
|
||||
reader: response.body.getReader(),
|
||||
decoder: new TextDecoder('utf-8')
|
||||
};
|
||||
});
|
||||
|
||||
return fetchPromise.then(body => ({
|
||||
stream: Promise.resolve({
|
||||
reader: body.getReader(),
|
||||
decoder: new TextDecoder('utf-8')
|
||||
}),
|
||||
cancel: reason => !controller.signal.aborted && controller.abort(reason)
|
||||
})).catch(err => ({
|
||||
stream: Promise.reject(err),
|
||||
cancel: () => {}
|
||||
}));
|
||||
return {
|
||||
stream: fetchPromise,
|
||||
cancel: (reason) => {
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort(reason);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user