Преглед на файлове

reactor:对接坐席辅助:

zhangchong преди 1 година
родител
ревизия
d31e34576b
променени са 5 файла, в които са добавени 134 реда и са изтрити 67 реда
  1. 1 1
      .env.development
  2. 2 2
      .env.production
  3. 21 3
      src/utils/websocket.ts
  4. 109 60
      src/views/todo/seats/accept/Voice-assistant.vue
  5. 1 1
      src/views/todo/seats/accept/index.vue

+ 1 - 1
.env.development

@@ -29,7 +29,7 @@ VITE_AMAP_KEY=83f51df235e4008e4eaf515cff63785c
 VITE_CALLCENTER_SOCKET_URL=ws://222.213.23.229:29003/ola_socket
 
 # # 智能客服登录地址
-VITE_VOICE_ASSISTANT_API_URL=http://118.121.198.222:19080
+VITE_VOICE_ASSISTANT_API_URL=http://182.151.41.101:19081
 # # 智能客服socket地址
 VITE_VOICE_ASSISTANT_SOCKET_URL=ws://182.151.41.101:19005
 

+ 2 - 2
.env.production

@@ -28,7 +28,7 @@ VITE_AMAP_KEY=83f51df235e4008e4eaf515cff63785c
 # # 呼叫中心socket地址
 VITE_CALLCENTER_SOCKET_URL=ws://222.213.23.229:29003/ola_socket
 
+# # 智能客服登录地址
+VITE_VOICE_ASSISTANT_API_URL=http://182.151.41.101:19081
 # # 智能客服socket地址
 VITE_VOICE_ASSISTANT_SOCKET_URL=ws://182.151.41.101:19005
-# # 智能客服登录地址
-VITE_VOICE_ASSISTANT_API_URL=http://118.121.198.222:19080

+ 21 - 3
src/utils/websocket.ts

@@ -4,6 +4,8 @@ interface SocketOptions {
 	heartbeatInterval?: number;
 	reconnectInterval?: number;
 	maxReconnectAttempts?: number;
+	uid?: string;
+	subscribe?: string;
 }
 
 class Socket {
@@ -17,9 +19,11 @@ class Socket {
 	constructor(url: string, opts: SocketOptions = {}) {
 		this.url = url;
 		this.opts = {
-			heartbeatInterval: 10 * 1000, // 心跳间隔
+			heartbeatInterval: 2 * 1000, // 心跳间隔
 			reconnectInterval: 10 * 1000, // 重连间隔
 			maxReconnectAttempts: 5, // 最大重连次数
+			uid: '', // 用户id
+			subscribe: '', // 订阅的频道
 			...opts,
 		};
 
@@ -37,6 +41,7 @@ class Socket {
 
 	onOpen(event: Event) {
 		// this.reconnectAttempts=0;
+		this.startSubscribe();
 		this.startHeartbeat();
 		this.emit('open', event);
 	}
@@ -76,15 +81,28 @@ class Socket {
 				this.send({
 					id: '',
 					type: 2,
-					from: '',
+					from: this.opts.uid,
 					to: 'sys',
-					timestamps: 1,
+					timestamps: new Date().getTime(),
 					body: 'PING',
 				});
 			}
 		}, this.opts.heartbeatInterval);
 	}
 
+	startSubscribe() {
+		setTimeout(() => {
+			if (!this.opts.subscribe) return;
+			this.send({
+				id: '',
+				type: 8,
+				from: this.opts.uid,
+				to: this.opts.subscribe ,
+				timestamps: new Date().getTime(),
+				body: 'subscribe',
+			});
+		},this.opts.heartbeatInterval+100)
+	}
 	stopHeartbeat() {
 		if (this.heartbeatInterval) {
 			clearInterval(this.heartbeatInterval);

+ 109 - 60
src/views/todo/seats/accept/Voice-assistant.vue

@@ -3,50 +3,51 @@
 		<!-- 聊天框 -->
 		<el-scrollbar class="h100" noresize ref="scrollbarRef" max-height="400px" v-if="messageList.length">
 			<div class="chat-box" ref="chatBoxRef">
-				<div v-for="(item, index) in messageList" :key="index" class="chat-item" :class="item.type">
-					<div v-if="item.type === 'agent'" class="agent">
+				<div v-for="(item, index) in messageList" :key="index" class="chat-item" :class="item.callSentenceInfo?.role">
+					<div v-if="item.callSentenceInfo?.role === 'agent'" class="agent">
 						<img v-lazy="getImageUrl('order/service.png')" alt="" class="agent-avatar" src="" />
-						<div class="agent-name">{{ item.name }}</div>
-						<div class="agent-content">{{ item.content }}</div>
-						<div class="agent-date">{{ item.date }}</div>
+						<div class="agent-name">{{ item.calledNumber }}</div>
+						<div class="agent-content">{{ item.callSentenceInfo.text }}</div>
+						<div class="agent-date">{{ formatDate(item.callStartTime, 'YYYY-mm-dd HH:MM:SS') }}</div>
 					</div>
 					<div v-else class="user">
 						<img v-lazy="getImageUrl('order/user.png')" alt="" class="user-avatar" src="" />
-						<div class="user-name">{{ item.name }}</div>
-						<div class="user-content">{{ item.content }}</div>
-						<div class="user-date">{{ item.date }}</div>
+						<div class="user-name">{{ item.callerNumber }}</div>
+						<div class="user-content">{{ item.callSentenceInfo.text }}</div>
+						<div class="user-date">{{ formatDate(item.callStartTime, 'YYYY-mm-dd HH:MM:SS') }}</div>
 					</div>
 				</div>
+				<el-text class="end-call" tag="p" v-if="talkEnd">-- 通话结束 --</el-text>
 			</div>
+      <!-- 识别内容 通话结束才展示-->
+      <div class="recognize-box" v-if="talkEnd">
+        <transition name="el-zoom-in-bottom">
+          <div v-show="!searchCol" class="transition-box">
+            <el-button link @click="closeSearch" class="transition-box-close">
+              <SvgIcon name="ele-Close" class="ml3" size="18px" />
+            </el-button>
+            <div class="transition-box-title">
+              <el-text tag="b" size="large">识别内容</el-text>
+            </div>
+            <el-scrollbar class="h100 transition-box-content" noresize ref="scrollbarRef" max-height="180px">
 
-			<!-- 识别内容 -->
-			<div class="recognize-box" v-if="messageList.length">
-				<transition name="el-zoom-in-bottom">
-					<div v-show="!searchCol" class="transition-box">
-						<el-button link @click="closeSearch" class="transition-box-close">
-							<SvgIcon name="ele-Close" class="ml3" size="18px" />
-						</el-button>
-						<div class="transition-box-title">
-							<el-text tag="b" size="large">识别内容</el-text>
-						</div>
-						<el-scrollbar class="h100 transition-box-content" noresize ref="scrollbarRef" max-height="180px">
-							<div class="transition-box-content-item" v-for="(item, index) in recognizeList" :key="index">
-								<div class="transition-box-content-item-title">{{ item.title }}</div>
-								<div class="transition-box-content-item-content">{{ item.content }}</div>
-							</div>
-						</el-scrollbar>
-						<div class="flex-end mt20">
-							<el-button @click="closeSearch" class="default-button"> 取消</el-button>
-							<el-button type="primary" @click="fillSingle">一键填单</el-button>
-						</div>
-					</div>
-				</transition>
-				<el-button type="primary" @click="closeSearch" v-show="searchCol">
-					<SvgIcon name="ele-ArrowUp" class="ml3" size="18px" />
-				</el-button>
-			</div>
+            <pre>
+              {{recognizeList}}
+            </pre>
+            </el-scrollbar>
+            <div class="flex-end mt20">
+              <el-button @click="closeSearch" class="default-button"> 取消</el-button>
+              <el-button type="primary" @click="fillSingle">一键填单</el-button>
+            </div>
+          </div>
+        </transition>
+        <el-button type="primary" @click="closeSearch" v-show="searchCol">
+          <SvgIcon name="ele-ArrowUp" class="ml3" size="18px" />
+        </el-button>
+      </div>
 		</el-scrollbar>
 		<Empty v-else />
+
 	</div>
 </template>
 <script setup lang="ts">
@@ -57,47 +58,79 @@ import { getImageUrl } from '/@/utils/tools';
 import axios from 'axios';
 import { storeToRefs } from 'pinia';
 import { useTelStatus } from '/@/stores/telStatus';
+import { useRoute } from 'vue-router';
+import { formatDate } from '/@/utils/formatTime';
+import Empty from '/@/components/Empty/index.vue';
 // 消息列表
-const messageList = ref([]); // 消息列表
-const recognizeList = ref([]); // 识别信息
+const messageList = ref<any>([]); // 消息列表
+const recognizeList = ref<any>([]); // 识别信息
 
 const useTelStatusStore = useTelStatus();
 const { telStatusInfo } = storeToRefs(useTelStatusStore); // 电话状态
 console.log(telStatusInfo.value);
 
+const socket = ref<any>(null);
 // 打开websocket链接
 const seatAssistOn = () => {
+	console.log(telStatusInfo.value.telsNo);
 	if (telStatusInfo.value.telsNo) {
-		axios.get(`${import.meta.env.VITE_VOICE_ASSISTANT_API_URL}/rtserver-api/users/getUserByAgentId/${telStatusInfo.value.telsNo}`).then((res) => {
-      console.log(res,'获取到的用户信息')
-			const { socket, send, on, off } = useSocket(import.meta.env.VITE_VOICE_ASSISTANT_SOCKET_URL);
-			on('open', () => {
-				console.log('坐席辅助链接已打开');
-				send({
-					id: '',
-					type: 1,
-					from: '',
-					to: 'sys',
-					timestamps: 1,
-					body: '',
+		axios
+			.get(`${import.meta.env.VITE_VOICE_ASSISTANT_API_URL}/users/getUserByAgentId/${telStatusInfo.value.telsNo}`)
+			.then((res) => {
+				const { data } = res;
+				const { result } = data;
+				const uid = `8#User-${result.uid}`;
+				const subscribe = `/trans/${result.orgCode}/${result.groupUid}/${uid}`;
+				console.log(result, '获取到的用户信息');
+				socket.value = useSocket(import.meta.env.VITE_VOICE_ASSISTANT_SOCKET_URL, { uid, subscribe });
+				console.log(socket.value);
+				socket.value.on('open', () => {
+					console.log('坐席辅助链接已打开');
+					socket.value.send({
+						// 调用登录
+						id: '',
+						type: 1,
+						from: uid,
+						to: 'sys',
+						timestamps: new Date().getTime(),
+						body: result.userName,
+					});
 				});
+				socket.value.on('message', wsReceive); // 接收消息
+				socket.value.on('error', onError); // 错误
+				socket.value.on('close', onClose); // 关闭
+			})
+			.catch((err) => {
+				console.log(err, '获取用户信息失败');
 			});
-			on('message', wsReceive); // 接收消息
-			on('error', onError); // 错误
-			on('close', onClose); // 关闭
-		}).catch((err) => {
-      console.log(err,'获取用户信息失败');
-    });
 	}
 };
 // 设置初始化,防止刷新时恢复默认
 onMounted(() => {
-  seatAssistOn();
+	seatAssistOn();
 });
+const route = useRoute();
+const talkEnd = ref(false); // 通话结束
 const wsReceive = (message: any) => {
 	try {
 		const data = JSON.parse(message.data);
-		console.log('坐席辅助收到消息:', data);
+		if (data.body.bisType === 3) {
+			// console.log('坐席辅助收到转写消息:', data);
+			if (route.params.callId === data.body.content.callId) {
+				// 判断是不是当前通话
+				if (data.body.content.callSentenceInfo) {
+					//通话中才显示
+					messageList.value.push(data.body.content);
+					scrollToBottom();
+				}
+				if (data.body.content.callEndInfo) {
+					//通话结束
+          talkEnd.value = true;
+          console.log('通话结束了。')
+				}
+			}
+			console.log(messageList.value, '消息内容');
+		}
 	} catch (error) {
 		console.log('坐席辅助收到消息', message);
 	}
@@ -123,13 +156,24 @@ const searchCol = ref(true); // 展开/收起
 // 展开/收起
 const closeSearch = () => {
 	searchCol.value = !searchCol.value;
-	scrollToBottom();
+  if(!searchCol.value){
+    const callId = route.params.callId;
+    axios.get(`${import.meta.env.VITE_VOICE_ASSISTANT_API_URL}/monitor/remote/details/${callId}`).then((res) => {
+      const { data } = res;
+      const { result } = data;
+      recognizeList.value = result.call_detail_content;
+      const str =  result.call_detail_content.split('\n');
+
+      console.log(str)
+      console.log(recognizeList.value)
+    });
+  }
 };
 // 滚动到底部
 const scrollToBottom = async () => {
 	await nextTick(); // 等待 DOM 更新
-	const max = chatBoxRef.value!.clientHeight;
-	scrollbarRef.value!.setScrollTop(max);
+	const max = chatBoxRef.value?.clientHeight;
+	scrollbarRef.value?.setScrollTop(max);
 };
 
 // 一键填单
@@ -160,8 +204,8 @@ watch(messageList.value, (val) => {
 .voice-assistant {
 	width: 100%;
 	height: 100%;
+  min-height: 400px;
 	position: relative;
-
 	.chat-box {
 		width: 100%;
 		padding-bottom: 50px;
@@ -275,6 +319,11 @@ watch(messageList.value, (val) => {
 				}
 			}
 		}
+		.end-call {
+			width: 100%;
+			text-align: center;
+			margin-top: 20px;
+		}
 	}
 
 	.recognize-box {

+ 1 - 1
src/views/todo/seats/accept/index.vue

@@ -514,10 +514,10 @@ import { orderRepeatEvent } from '/@/api/business/repeatEvent';
 import { removeDuplicate } from '/@/utils/arrayOperation';
 
 // 引入组件
+const VoiceAssistant = defineAsyncComponent(() => import('/@/views/todo/seats/accept/Voice-assistant.vue')); // 语音助手
 const Knowledge = defineAsyncComponent(() => import('/@/views/todo/seats/accept/Knowledge.vue')); // 知识库
 const HistoryOrder = defineAsyncComponent(() => import('/@/views/todo/seats/accept/History.vue')); // 历史工单
 const RepeatEvent = defineAsyncComponent(() => import('/@/views/todo/seats/accept/Repeat-event.vue')); // 重复事件
-const VoiceAssistant = defineAsyncComponent(() => import('/@/views/todo/seats/accept/Voice-assistant.vue')); // 重复事件
 const CitizenPortrait = defineAsyncComponent(() => import('/@/views/todo/seats/accept/Citizen-portrait.vue')); // 市民坏画像
 const ExpandForm = defineAsyncComponent(() => import('/@/views/todo/seats/accept/Expand-form.vue')); // 拓展表单
 const OrderHistory = defineAsyncComponent(() => import('/@/views/business/order/components/Order-history.vue')); // 历史工单弹窗列表