feat: 完成对话手动停止的功能

This commit is contained in:
zoujing
2025-08-06 11:47:07 +08:00
parent 6e658c9967
commit 65653525a0
6 changed files with 182 additions and 114 deletions

17
.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
insert_final_newline = false

View File

@@ -26,7 +26,8 @@
maxlength="300" maxlength="300"
/> />
<view class="input-container-send" @click="sendMessage"> <view class="input-container-send" @click="sendMessage">
<image src='/static/input_send_icon.png'></image> <image v-if="props.isSessionActive" src='/static/input_stop_icon.png'></image>
<image v-else src='/static/input_send_icon.png'></image>
</view> </view>
</view> </view>
</template> </template>
@@ -36,7 +37,9 @@ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
const props = defineProps({ const props = defineProps({
inputMessage: String, inputMessage: String,
holdKeyboard: Boolean holdKeyboard: Boolean,
isSessionActive: Boolean,
stopRequest: Function
}) })
const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide']) const emit = defineEmits(['update:inputMessage', 'send', 'noHideKeyboard', 'keyboardShow', 'keyboardHide'])
@@ -46,6 +49,7 @@ const inputMessage = ref(props.inputMessage || '')
const isFocused = ref(false) const isFocused = ref(false)
const keyboardHeight = ref(0) const keyboardHeight = ref(0)
// 保持和父组件同步 // 保持和父组件同步
watch(() => props.inputMessage, (val) => { watch(() => props.inputMessage, (val) => {
inputMessage.value = val inputMessage.value = val
@@ -65,6 +69,13 @@ onMounted(() => {
}) })
const sendMessage = () => { const sendMessage = () => {
if (props.isSessionActive) {
// 如果会话进行中,调用停止请求函数
if (props.stopRequest) {
props.stopRequest();
}
} else {
// 否则发送新消息
if (!inputMessage.value.trim()) return; if (!inputMessage.value.trim()) return;
emit('send', inputMessage.value) emit('send', inputMessage.value)
inputMessage.value = '' inputMessage.value = ''
@@ -77,6 +88,7 @@ const sendMessage = () => {
}) })
} }
} }
}
const handleFocus = () => { const handleFocus = () => {
isFocused.value = true isFocused.value = true

View File

@@ -64,6 +64,8 @@
ref="inputAreaRef" ref="inputAreaRef"
v-model="inputMessage" v-model="inputMessage"
:holdKeyboard="holdKeyboard" :holdKeyboard="holdKeyboard"
:is-session-active="isSessionActive"
:stop-request="stopRequest"
@send="sendMessageAction" @send="sendMessageAction"
@noHideKeyboard="handleNoHideKeyboard" @noHideKeyboard="handleNoHideKeyboard"
@keyboardShow="handleKeyboardShow" @keyboardShow="handleKeyboardShow"
@@ -129,6 +131,8 @@
const chatMsgList = ref([]) const chatMsgList = ref([])
/// 输入口的输入消息 /// 输入口的输入消息
const inputMessage = ref('') const inputMessage = ref('')
/// 加载中
let currentAIMsgIndex = 0
/// 从个渠道获取如二维,没有的时候就返回首页的数据 /// 从个渠道获取如二维,没有的时候就返回首页的数据
const sceneId = ref('') const sceneId = ref('')
@@ -140,7 +144,9 @@
const mainPageDataModel = ref({}) const mainPageDataModel = ref({})
// 会话进行中标志 // 会话进行中标志
let isSessionActive = false; const isSessionActive = ref(false);
// 请求任务引用
const requestTaskRef = ref(null);
/// 指令 /// 指令
let commonType = '' let commonType = ''
@@ -229,7 +235,7 @@
if (!inputText.trim()) return; if (!inputText.trim()) return;
handleNoHideKeyboard() handleNoHideKeyboard()
sendMessage(inputText) sendMessage(inputText)
if(!isSessionActive) { if(!isSessionActive.value) {
inputMessage.value = '' inputMessage.value = ''
} }
// 发送消息后保持键盘状态 // 发送消息后保持键盘状态
@@ -319,14 +325,14 @@
/// 发送消息的参数拼接 /// 发送消息的参数拼接
const sendMessage = (message, isInstruct = false) => { const sendMessage = (message, isInstruct = false) => {
if (isSessionActive) { if (isSessionActive.value) {
uni.showToast({ uni.showToast({
title: '请等待当前回复完成', title: '请等待当前回复完成',
icon: 'none' icon: 'none'
}); });
return; return;
} }
isSessionActive = true; isSessionActive.value = true;
const newMsg = { const newMsg = {
msgId: `msg_${chatMsgList.value.length}`, msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.ME, msgType: MessageRole.ME,
@@ -368,6 +374,7 @@
} }
chatMsgList.value.push(aiMsg) chatMsgList.value.push(aiMsg)
const aiMsgIndex = chatMsgList.value.length - 1 const aiMsgIndex = chatMsgList.value.length - 1
currentAIMsgIndex = aiMsgIndex
// 动态加载中动画 // 动态加载中动画
let dotCount = 1; let dotCount = 1;
@@ -385,7 +392,7 @@
} }
// 流式接收内容 // 流式接收内容
agentChatStream(args, (chunk) => { const { promise, requestTask } = agentChatStream(args, (chunk) => {
console.log('分段内容:', chunk) console.log('分段内容:', chunk)
if (chunk && chunk.error) { if (chunk && chunk.error) {
chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试'; chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试';
@@ -393,7 +400,7 @@
loadingTimer = null; loadingTimer = null;
isTyping = false; isTyping = false;
typeWriterTimer = null; typeWriterTimer = null;
isSessionActive = false; // 出错也允许再次发送 isSessionActive.value = false; // 出错也允许再次发送
console.error('流式错误:', chunk.message, chunk.detail); console.error('流式错误:', chunk.message, chunk.detail);
return; return;
} }
@@ -444,14 +451,22 @@
chatMsgList.value[aiMsgIndex].question = chunk.question chatMsgList.value[aiMsgIndex].question = chunk.question
} }
isSessionActive = false; isSessionActive.value = false;
scrollToBottom(); scrollToBottom();
} }
}, 50); }, 50);
} }
}).catch(e => { })
isSessionActive = false; // 出错也允许再次发送
console.log('error:', e) // 存储请求任务
requestTaskRef.value = requestTask;
// 可选处理Promise完成/失败, 已经在回调中处理数据,此处无需再处理
promise.then(data => {
console.log('请求完成');
}).catch(err => {
isSessionActive.value = false; // 出错也允许再次发送
console.log('error:', err)
}); });
// 打字机函数 // 打字机函数
@@ -472,6 +487,29 @@
} }
// 停止请求函数
const stopRequest = () => {
if (requestTaskRef.value && requestTaskRef.value.abort) {
requestTaskRef.value.abort();
// 重置状态
isSessionActive.value = false;
const msg = chatMsgList.value[currentAIMsgIndex].msg;
if (!msg || msg === '加载中.' || msg.startsWith('加载中')) {
chatMsgList.value[currentAIMsgIndex].msg = '已终止请求,请重试';
}
// 清除计时器
if (loadingTimer) {
clearInterval(loadingTimer);
loadingTimer = null;
}
if (typeWriterTimer) {
clearTimeout(typeWriterTimer);
typeWriterTimer = null;
}
setTimeoutScrollToBottom()
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -7,16 +7,17 @@ const API = '/agent/assistant/chat';
* 获取AI聊天流式信息仅微信小程序支持 * 获取AI聊天流式信息仅微信小程序支持
* @param {Object} params 请求参数 * @param {Object} params 请求参数
* @param {Function} onChunk 回调,每收到一段数据触发 * @param {Function} onChunk 回调,每收到一段数据触发
* @returns {Promise} * @returns {Object} 包含Promise和requestTask的对象
*/ */
function agentChatStream(params, onChunk) { function agentChatStream(params, onChunk) {
return new Promise((resolve, reject) => { let requestTask;
const promise = new Promise((resolve, reject) => {
const token = uni.getStorageSync('token'); const token = uni.getStorageSync('token');
let hasError = false; let hasError = false;
console.log("发送请求内容: ", params) console.log("发送请求内容: ", params)
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
const requestTask = uni.request({ requestTask = uni.request({
url: BASE_URL + API, // 替换为你的接口地址 url: BASE_URL + API, // 替换为你的接口地址
method: 'POST', method: 'POST',
data: params, data: params,
@@ -37,7 +38,6 @@ function agentChatStream(params, onChunk) {
complete(res) { complete(res) {
if(res.statusCode !== 200) { if(res.statusCode !== 200) {
console.log("====> ", JSON.stringify(res)) console.log("====> ", JSON.stringify(res))
if (onChunk) { if (onChunk) {
onChunk({ error: true, message: '服务器错误', detail: res }); onChunk({ error: true, message: '服务器错误', detail: res });
} }
@@ -75,18 +75,19 @@ function agentChatStream(params, onChunk) {
}); });
// #endif // #endif
}); });
return {
promise,
requestTask
};
} }
// window.atob兼容性处理 // window.atob兼容性处理
const weAtob = (string) => { const weAtob = (string) => {
const b64re = const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/;
/^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/; const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const b64 =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
// 去除空白字符 // 去除空白字符
string = String(string).replace(/[\t\n\f\r ]+/g, ''); string = String(string).replace(/[\t\n\f\r ]+/g, '');
// 验证 Base64 编码 // 验证 Base64 编码
if (!b64re.test(string)) { if (!b64re.test(string)) {
throw new TypeError( throw new TypeError(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
static/input_stop_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB