|
@@ -1,6 +1,6 @@
|
|
|
<template>
|
|
|
<view class="page" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
|
|
|
- <u-navbar title="对话" bgColor="transparent" :titleStyle="{'font-size':'32rpx','font-weight':'bold'}">
|
|
|
+ <u-navbar title="对话" bgColor="#F6F6F8" :titleStyle="{'font-size':'32rpx','font-weight':'bold'}">
|
|
|
<view class="u-nav-slot" slot="left" style="display: flex;background-color: transparent;">
|
|
|
<image src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/10/2f7cc90a-848c-4369-b573-4448125591a9.png" style="width: 45rpx;height: 45rpx;" @tap="toBack"></image>
|
|
|
<!-- <image src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/10/3580b8d2-8ddb-4385-8516-fb6f47ce5fea.png" style="width: 42rpx;height: 42rpx;margin-left: 40rpx;" @tap="startNewDialog"></image> -->
|
|
@@ -40,7 +40,7 @@
|
|
|
</view>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
- <u-parse :content="item.answer"></u-parse>
|
|
|
+ <u-parse :content="item.answer" style="font-size: 28rpx;line-height: 54rpx;color: #252525;"></u-parse>
|
|
|
</template>
|
|
|
<div class="dc_btns adfacjb" v-if="item.answer">
|
|
|
<div class="db_l">
|
|
@@ -49,7 +49,7 @@
|
|
|
<image :src="item.comment?require('@/static/comment_active.png'):require('@/static/comment.png')" @tap="toComment(item,index)"></image>
|
|
|
</div>
|
|
|
<div class="db_r">
|
|
|
- <!-- <image :src="item.share?require('@/static/share_active.png'):require('@/static/share.png')" @tap="toShare(item,index)"></image> -->
|
|
|
+ <image :src="item.share?require('@/static/share_active.png'):require('@/static/share.png')" @tap="toShare(item,index)"></image>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -81,166 +81,198 @@
|
|
|
let requestTask = null;
|
|
|
import { BaseApi } from '../http/baseApi.js'
|
|
|
import * as TextEncoding from "text-encoding-shim";
|
|
|
+ import { webSocketConnect } from '@/common/xiaoZhiConnect.js';
|
|
|
export default {
|
|
|
data(){
|
|
|
return {
|
|
|
- isDialog:false,
|
|
|
- retryCount: 3, // 最大重试次数
|
|
|
- currentRetry: 0, // 当前重试次数
|
|
|
- isRequesting: false, // 请求状态锁
|
|
|
question:'',
|
|
|
- streamingResponse:'',
|
|
|
- receivedData:'',
|
|
|
dialogList:[],
|
|
|
- windex:0,
|
|
|
commentShow:false,
|
|
|
content:'',
|
|
|
cindex:'',
|
|
|
chzq:false,
|
|
|
lwss:false,
|
|
|
- fixBottom: 0
|
|
|
+ fixBottom: 0,
|
|
|
+ // 对话
|
|
|
+ config: {
|
|
|
+ deviceMac: '',
|
|
|
+ deviceName: '小智AI设备',
|
|
|
+ clientId: 'uniapp_xiaozhi_client',
|
|
|
+ token: 'xiaozhi-ai',
|
|
|
+ otaUrl: 'http://i.ringzle.com:8002/xiaozhi/ota/',
|
|
|
+ serverUrl: 'ws://i.ringzle.com:8000/xiaozhi/v1/'
|
|
|
+ },
|
|
|
+ isConnected: false,
|
|
|
+ textMessage: '',
|
|
|
+ socketTask: null,
|
|
|
}
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ displayMac() {
|
|
|
+ return this.config.deviceMac;
|
|
|
+ },
|
|
|
+ displayClient() {
|
|
|
+ return this.config.clientId;
|
|
|
+ }
|
|
|
},
|
|
|
onReady() {
|
|
|
uni.onKeyboardHeightChange(res => {
|
|
|
this.fixBottom = res.height||0;
|
|
|
});
|
|
|
},
|
|
|
+ onLoad(option) {
|
|
|
+ this.config.deviceMac = option?.deviceMac;
|
|
|
+ this.connectToServer();
|
|
|
+ },
|
|
|
onUnload() {
|
|
|
+ this.disconnectFromServer();
|
|
|
uni.offKeyboardHeightChange();
|
|
|
this.fixBottom = 0;
|
|
|
},
|
|
|
methods:{
|
|
|
- toBack(){
|
|
|
- uni.navigateBack();
|
|
|
- },
|
|
|
- startNewDialog(){
|
|
|
- clearInterval(timer)
|
|
|
- this.dialogList = [];
|
|
|
- this.question = '';
|
|
|
- this.streamingResponse = '';
|
|
|
- },
|
|
|
- // 封装带重试机制的请求方法
|
|
|
- async sendRequestWithRetry() {
|
|
|
- if (this.isRequesting) return;
|
|
|
- this.isRequesting = true;
|
|
|
- this.currentRetry = 0;
|
|
|
- try {
|
|
|
- await this._executeRequest();
|
|
|
- } catch (error) {
|
|
|
- this.$showToast('请求失败,请稍后重试')
|
|
|
- } finally {
|
|
|
- this.isRequesting = false;
|
|
|
+ async connectToServer() {
|
|
|
+ if (this.socketTask) return;
|
|
|
+ console.log('开始连接流程...');
|
|
|
+ const task = await webSocketConnect(
|
|
|
+ this.config.otaUrl,
|
|
|
+ this.config.serverUrl,
|
|
|
+ this.config
|
|
|
+ );
|
|
|
+
|
|
|
+ if(task) {
|
|
|
+ this.socketTask = task;
|
|
|
+ this.setupSocketListeners();
|
|
|
}
|
|
|
},
|
|
|
- _executeRequest2(){
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- this.$api.post('/core/chat/sendChatMessageStream',{query:this.question},false).then(res=>{
|
|
|
- if(res.data.code!==0) return this.$showToast(res.data.msg)
|
|
|
- let answer = this.dialogList[this.dialogList.length-1].answer+res.data.data;
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'answer',answer);
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'think',false);
|
|
|
- setTimeout(()=>{
|
|
|
- this.scrollToBottom();
|
|
|
- },100)
|
|
|
- resolve()
|
|
|
- })
|
|
|
- })
|
|
|
+ setupSocketListeners() {
|
|
|
+ this.socketTask.onOpen(() => {
|
|
|
+ this.isConnected = true;
|
|
|
+ console.log('WebSocket连接已打开', 'success');
|
|
|
+ this.sendHelloMessage();
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketTask.onClose(() => {
|
|
|
+ this.isConnected = false;
|
|
|
+ console.log('WebSocket连接已关闭', 'info');
|
|
|
+ this.socketTask = null;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketTask.onError((err) => {
|
|
|
+ this.isConnected = false;
|
|
|
+ console.log(`WebSocket错误: ${JSON.stringify(err)}`, 'error');
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socketTask.onMessage((res) => {
|
|
|
+ if (typeof res.data === 'string') {
|
|
|
+ this.handleTextMessage(res.data);
|
|
|
+ } else {
|
|
|
+ this.handleBinaryMessage(res.data);
|
|
|
+ }
|
|
|
+ });
|
|
|
},
|
|
|
- // 实际执行请求的方法
|
|
|
- _executeRequest() {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- requestTask = uni.request({
|
|
|
- url: `${BaseApi}/core/chat/sendChatMessageStream`,
|
|
|
- method: 'POST',
|
|
|
- timeout: 10000,
|
|
|
- data:{
|
|
|
- query: this.question,
|
|
|
- identity:'被教练者',
|
|
|
- },
|
|
|
- header: {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'token': uni.getStorageSync('token') || ''
|
|
|
- },
|
|
|
- enableChunked: true, // 启用流式接收
|
|
|
- responseType:'text',
|
|
|
- success: (res) => {
|
|
|
- if (res.statusCode === 200) {
|
|
|
- this._handleSuccess(res.data);
|
|
|
- resolve();
|
|
|
- } else {
|
|
|
- this._handleError(`状态码异常: ${res.statusCode}`, resolve, reject);
|
|
|
- }
|
|
|
- },
|
|
|
- fail: (err) => {
|
|
|
- this._handleError(err.errMsg, resolve, reject);
|
|
|
- },
|
|
|
- complete: (com) => {
|
|
|
- console.log('请求完成',com)
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- requestTask.onChunkReceived(async (res) => {
|
|
|
- const uint8Array = new Uint8Array(res.data);
|
|
|
- const decoder = new TextEncoding.TextDecoder("utf-8");
|
|
|
- const decodedString = decoder.decode(uint8Array);
|
|
|
- try {
|
|
|
- let newtext = decodedString.replaceAll('data:','').replaceAll(':keepAlive','');
|
|
|
- let ntArr = newtext.split('\n\n');
|
|
|
- if(ntArr.length){
|
|
|
- ntArr.forEach(n=>{
|
|
|
- if(!n.trim()) return
|
|
|
- let nj = JSON.parse(n);
|
|
|
- if(nj.event=='message'){
|
|
|
- let answer = this.dialogList[this.dialogList.length-1].answer+nj.answer?.replace(/(\r\n|\n|\r)+/g, '<br>');
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'answer',answer);
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'id',nj.id);
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'task_id',nj.task_id);
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'message_id',nj.message_id);
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'conversation_id',nj.conversation_id);
|
|
|
- this.$set(this.dialogList[this.dialogList.length-1],'think',false);
|
|
|
- }
|
|
|
- })
|
|
|
- setTimeout(()=>{
|
|
|
- this.scrollToBottom();
|
|
|
- },100)
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- console.error('解析失败', e, '原始数据:', decodedString);
|
|
|
- }
|
|
|
+ disconnectFromServer() {
|
|
|
+ if (this.socketTask) {
|
|
|
+ this.socketTask.close({
|
|
|
+ code: 1000,
|
|
|
+ reason: 'manual disconnection'
|
|
|
});
|
|
|
+ console.log('手动断开连接', 'info');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ sendSocketMessage(data) {
|
|
|
+ if (!this.isConnected || !this.socketTask) {
|
|
|
+ console.log('无法发送消息,连接未建立', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ uni.sendSocketMessage({
|
|
|
+ data: data,
|
|
|
+ fail: (err) => {
|
|
|
+ console.log(`发送消息失败: ${JSON.stringify(err)}`, 'error');
|
|
|
+ }
|
|
|
});
|
|
|
},
|
|
|
- // 成功处理
|
|
|
- _handleSuccess(data) {
|
|
|
- if (data) {
|
|
|
- this.streamingResponse += data;
|
|
|
+ sendHelloMessage() {
|
|
|
+ const helloMessage = {
|
|
|
+ type: 'hello',
|
|
|
+ device_id: this.config.deviceMac,
|
|
|
+ device_name: this.config.deviceName,
|
|
|
+ device_mac: this.config.deviceMac,
|
|
|
+ token: this.config.token,
|
|
|
+ features: { mcp: true }
|
|
|
+ };
|
|
|
+ console.log('发送hello握手消息', 'info');
|
|
|
+ this.sendSocketMessage(JSON.stringify(helloMessage));
|
|
|
+ },
|
|
|
+ addConversationMessage(text, isUser = false) {
|
|
|
+ console.log(text,isUser);
|
|
|
+ text = text+'<br>'
|
|
|
+ if(!isUser){
|
|
|
+ let answer = this.dialogList[this.dialogList.length-1].answer+text?.replace(/(\r\n|\n|\r)+/g, '<br>');
|
|
|
+ this.$set(this.dialogList[this.dialogList.length-1],'answer',answer);
|
|
|
+ this.$set(this.dialogList[this.dialogList.length-1],'think',false);
|
|
|
+ setTimeout(()=>{
|
|
|
+ this.scrollToBottom();
|
|
|
+ },100)
|
|
|
}
|
|
|
- this.currentRetry = 0; // 重置重试计数器
|
|
|
},
|
|
|
- // 错误处理
|
|
|
- _handleError(errorMsg, resolve, reject) {
|
|
|
- if (this._shouldRetry(errorMsg)) {
|
|
|
- this.currentRetry++;
|
|
|
- setTimeout(() => {
|
|
|
- this._executeRequest().then(resolve).catch(reject);
|
|
|
- }, this._getRetryDelay());
|
|
|
- } else {
|
|
|
- reject(errorMsg);
|
|
|
+ sendTextMessage() {
|
|
|
+ const message = this.question.trim();
|
|
|
+ if (!message) return;
|
|
|
+ const listenMessage = {
|
|
|
+ type: 'listen',
|
|
|
+ mode: 'manual',
|
|
|
+ state: 'detect',
|
|
|
+ text: message
|
|
|
+ };
|
|
|
+ this.sendSocketMessage(JSON.stringify(listenMessage));
|
|
|
+ this.addConversationMessage(message, true);
|
|
|
+ console.log(`发送文本: ${message}`, 'info');
|
|
|
+ },
|
|
|
+ handleTextMessage(jsonString) {
|
|
|
+ try {
|
|
|
+ const message = JSON.parse(jsonString);
|
|
|
+ console.log(`收到文本消息: ${message.type}`, 'debug');
|
|
|
+ switch (message.type) {
|
|
|
+ case 'hello':
|
|
|
+ console.log(`服务器回应: Session ID ${message.session_id}`, 'success');
|
|
|
+ break;
|
|
|
+ case 'tts':
|
|
|
+ if(message.state === 'sentence_start' && message.text) {
|
|
|
+ this.addConversationMessage(message.text);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 'llm':
|
|
|
+ if(message.text && message.text !== '😊') {
|
|
|
+ this.addConversationMessage(message.text);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ console.log(`收到消息: ${jsonString}`, 'info');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.log(`解析JSON失败: ${e.message}`, 'error');
|
|
|
+ console.log(`原始数据: ${jsonString}`, 'debug');
|
|
|
}
|
|
|
},
|
|
|
- // 判断是否需要重试
|
|
|
- _shouldRetry(errorMsg) {
|
|
|
- const retryableErrors = [
|
|
|
- 'timeout',
|
|
|
- 'request:fail',
|
|
|
- 'Network Error'
|
|
|
- ];
|
|
|
- return this.currentRetry < this.retryCount && retryableErrors.some(e => errorMsg.includes(e));
|
|
|
+ handleBinaryMessage(arrayBuffer) {
|
|
|
+ console.log(`收到二进制数据,大小: ${arrayBuffer.byteLength}字节`, 'debug');
|
|
|
},
|
|
|
- // 获取指数退避延迟时间
|
|
|
- _getRetryDelay() {
|
|
|
- return Math.min(1000 * Math.pow(2, this.currentRetry), 10000);
|
|
|
+
|
|
|
+ toBack(){
|
|
|
+ uni.navigateBack();
|
|
|
+ },
|
|
|
+ startNewDialog(){
|
|
|
+ uni.showModal({
|
|
|
+ title:'温馨提示',
|
|
|
+ content:'是否开启新的一轮对话?',
|
|
|
+ success: (res) => {
|
|
|
+ if(res.confirm){
|
|
|
+ clearInterval(timer)
|
|
|
+ this.dialogList = [];
|
|
|
+ this.question = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
},
|
|
|
sendQuestion(){
|
|
|
if(!this.question) return this.$showToast('请输入您的问题');
|
|
@@ -257,7 +289,7 @@
|
|
|
this.dialogList = [...this.dialogList,...[qa]];
|
|
|
this.$nextTick(()=>{
|
|
|
this.scrollToBottom();
|
|
|
- this.sendRequestWithRetry();
|
|
|
+ this.sendTextMessage();
|
|
|
this.question = '';
|
|
|
})
|
|
|
},
|
|
@@ -321,7 +353,7 @@
|
|
|
}
|
|
|
|
|
|
.page{
|
|
|
- background: linear-gradient( 227deg, #EEEFF8 0%, #F6ECF4 100%, #F6ECF4 100%);
|
|
|
+ background: #F6F6F8;
|
|
|
padding: 0 30rpx 200rpx;
|
|
|
box-sizing: border-box;
|
|
|
|
|
@@ -356,6 +388,7 @@
|
|
|
padding-top: 34rpx;
|
|
|
box-sizing: border-box;
|
|
|
overflow-y: auto;
|
|
|
+ padding-bottom: 50rpx;
|
|
|
.d_answer{
|
|
|
margin-top: 40rpx;
|
|
|
&.init{
|
|
@@ -409,13 +442,13 @@
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
.dq_text{
|
|
|
- background: #833478;
|
|
|
+ background: #D9F159;
|
|
|
border-radius: 24rpx 4rpx 24rpx 24rpx;
|
|
|
font-family: PingFangSC, PingFang SC;
|
|
|
font-weight: 400;
|
|
|
- font-size: 30rpx;
|
|
|
- color: #FFFFFF;
|
|
|
- line-height: 48rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 40rpx;
|
|
|
// text-align: right;
|
|
|
padding: 26rpx 30rpx;
|
|
|
}
|
|
@@ -424,7 +457,7 @@
|
|
|
.ask_box{
|
|
|
width: 100%;
|
|
|
min-height: 176rpx;
|
|
|
- background: linear-gradient( 227deg, #EEEFF8 0%, #F6ECF4 100%, #F6ECF4 100%);
|
|
|
+ background: #F6F6F8;
|
|
|
padding: 0 30rpx 60rpx;
|
|
|
position: fixed;
|
|
|
left: 0;
|