@tencentcloud/ai-desk-customer-vue 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 1.7.1 @2026.3.17
2
+
3
+ ### Features
4
+ - 转人工排队时,新增二次确认。
5
+ - 转人工排队时,支持检测用户离开页面行为。
6
+
7
+ ### Fixed
8
+ - 修复已知问题。
9
+
1
10
  ## 1.7.0 @2026.2.9
2
11
 
3
12
  ### Features
@@ -120,10 +120,11 @@ import Log from '../../utils/logger';
120
120
  import MessageToolbarButton from './message-toolbar-button/index.vue';
121
121
  import TUILocales from '../../locales';
122
122
  import state from '../../utils/state.js';
123
- import { switchReadStatus,isNonEmptyObject } from '../../utils/utils';
123
+ import { switchReadStatus, isNonEmptyObject } from '../../utils/utils';
124
124
  import { getCountryForTimezone } from 'countries-and-timezones';
125
125
  import FeedbackModal from './feedback-modal/index.vue';
126
126
  import CustomerQueuePage from './customer-queue-page/index.vue';
127
+ import { startHeartbeat, stopHeartbeat, sendHeartbeat } from '../../utils/heartbeat-handler';
127
128
  const { ref, onMounted, onUnmounted, computed } = vue;
128
129
 
129
130
  interface IProps {
@@ -176,7 +177,8 @@ const currentLanguage = ref('');
176
177
  const languageForShowList = ref<Array<string>>([]);
177
178
  const feedbackModalRef = ref();
178
179
  const showFeedbackModal = ref(false);
179
- const queueNumber = ref(-1);
180
+ const queueNumber = ref(-1);
181
+ const queueLeavePageTimeoutEnable = ref(false);
180
182
  const hasLeftQueue = ref(false);
181
183
  let timezone = '';
182
184
  let countryID = '';
@@ -341,7 +343,9 @@ onMounted(() => {
341
343
  });
342
344
  TUIStore.watch(StoreName.CUSTOM, {
343
345
  isQueuing: onIsQueuingUpdate,
346
+ queueLeavePageTimeoutEnable: onQueueLeavePageTimeoutEnable,
344
347
  });
348
+ document.addEventListener('visibilitychange', handleVisibilityChange);
345
349
  });
346
350
 
347
351
  onUnmounted(() => {
@@ -356,7 +360,10 @@ onUnmounted(() => {
356
360
  });
357
361
  TUIStore.unwatch(StoreName.CUSTOM, {
358
362
  isQueuing: onIsQueuingUpdate,
363
+ queueLeavePageTimeoutEnable: onQueueLeavePageTimeoutEnable,
359
364
  });
365
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
366
+ stopHeartbeat();
360
367
  });
361
368
 
362
369
  const isInputToolbarShow = computed<boolean>(() => {
@@ -503,6 +510,27 @@ function onIsQueuingUpdate(data: {conversationID: string, value: number}) {
503
510
  if (data && data.conversationID === currentConversationID.value) {
504
511
  queueNumber.value = data.value;
505
512
  hasLeftQueue.value = false;
513
+ if (queueNumber.value >= 0) {
514
+ startHeartbeat();
515
+ } else {
516
+ stopHeartbeat();
517
+ }
518
+ }
519
+ }
520
+
521
+ function onQueueLeavePageTimeoutEnable(data: boolean) {
522
+ queueLeavePageTimeoutEnable.value = data;
523
+ startHeartbeat();
524
+ }
525
+
526
+ const handleVisibilityChange = () => {
527
+ if (document.visibilityState === 'hidden') {
528
+ if (queueLeavePageTimeoutEnable.value && queueNumber.value >= 0) {
529
+ sendHeartbeat(currentConversationID.value, "leaveSessionPage");
530
+ }
531
+ stopHeartbeat();
532
+ } else if (document.visibilityState === 'visible') {
533
+ startHeartbeat();
506
534
  }
507
535
  }
508
536
 
@@ -69,6 +69,7 @@ import audioIcon from '../../../assets/audio-blue.svg';
69
69
  import keyboardIcon from '../../../assets/keyboard-icon.svg';
70
70
  import { INPUT_TOOLBAR_TYPE } from '../../../constant';
71
71
  import Log from '../../../utils/logger';
72
+ import { Toast, TOAST_TYPE } from '../../common/Toast/index-web';
72
73
  const { ref, onMounted, onBeforeUnmount, computed, onUnmounted } = vue;
73
74
 
74
75
  const props = defineProps({
@@ -169,14 +170,30 @@ const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => {
169
170
  const sendMessage = async () => {
170
171
  const _editorContentList = editor.value?.getEditorContent();
171
172
  if (!_editorContentList || !currentConversation.value) return;
173
+ let hasValidContent = false;
172
174
  const editorContentList = _editorContentList.map((editor: any) => {
173
175
  if (editor.type === 'text') {
174
176
  editor.payload.text = transformTextWithEmojiNamesToKeys(
175
177
  editor.payload.text,
176
178
  );
179
+ const isEmptyText = !editor.payload.text || editor.payload.text.trim() === '';
180
+ if (!isEmptyText) {
181
+ hasValidContent = true;
182
+ }
183
+ return !isEmptyText ? editor : undefined;
177
184
  }
185
+ hasValidContent = true;
178
186
  return editor;
179
- });
187
+ }).filter(Boolean);
188
+ if (!hasValidContent) {
189
+ editor.value?.resetEditor();
190
+ Toast({
191
+ message: TUITranslateService.t('AIDesk.不能发送空白消息'),
192
+ type: TOAST_TYPE.ERROR,
193
+ duration: 3000,
194
+ });
195
+ return;
196
+ }
180
197
  await sendMessages(editorContentList, currentConversation.value, quoteMessageCloudCustomData);
181
198
  // 注意这里不要 emit 'sendMessage',避免写出死循环
182
199
  emit('messageSent');
@@ -410,8 +410,9 @@ function onNewMessageList(list: IMessageModel[]) {
410
410
  } else if (data.src === CUSTOM_MESSAGE_SRC.NO_SEAT_ONLINE || data.src === CUSTOM_MESSAGE_SRC.TIMEOUT || data.src === CUSTOM_MESSAGE_SRC.END) {
411
411
  updateCustomStore("canEndConversation", { conversationID, value: false });
412
412
  updateCustomStore("isQueuing", { conversationID, value: -1 });
413
- } else if (data.src === CUSTOM_MESSAGE_SRC.GET_FEEDBACK_MENU) {
413
+ } else if (data.src === CUSTOM_MESSAGE_SRC.GET_SETTINGS) {
414
414
  TUIStore.update(StoreName.CUSTOM, "feedbackTags", data.content.menu);
415
+ TUIStore.update(StoreName.CUSTOM, "queueLeavePageTimeoutEnable", data.content.queueLeavePageTimeoutEnable);
415
416
  }
416
417
  }
417
418
  }
@@ -427,7 +428,7 @@ async function onMessageListUpdated(list: IMessageModel[]) {
427
428
  const oldLastMessage = currentLastMessage.value;
428
429
  let hasEmojiReaction = false;
429
430
  allMessageList.value = list;
430
-
431
+
431
432
  messageList.value = list.filter((message, index) => {
432
433
  if (message.reactionList?.length && !message.isDeleted) {
433
434
  hasEmojiReaction = true;
@@ -1,5 +1,5 @@
1
1
  import {Marked} from 'marked';
2
- import Log from '../../../../../../utils/logger';
2
+ import { getStyledATagFromText } from '../../../../../../utils/utils';
3
3
  import DOMPurify from 'dompurify';
4
4
 
5
5
  export const marked = new Marked(
@@ -32,19 +32,7 @@ export const marked = new Marked(
32
32
  },
33
33
  link(this: any, href: string | null, title: string | null, text: string) {
34
34
  if (href) {
35
- // 匹配以 http:// https:// 开头,所有 URL 主体字符,遇到第一个非主体字符(如中文括号、空格、表情符号等)时停止
36
- let ret = href.replace(/&amp;/g, '&').replace(/https?:\/\/[\w\-./?=&:#]+(?=[^\w\-./?=&:#]|$)/g, (matchedUrl) => {
37
- let isURLInText = false;
38
- if (matchedUrl !== href) {
39
- // 如果 text 里包含 url,我们就用 matchedUrl 作为 a 标签的值;否则用 text 作为值
40
- isURLInText = true;
41
- }
42
- return `<a target="_blank" rel="noreferrer noopenner" class="message-marked_link" href="${matchedUrl || ''}" title="${title || ''}">${isURLInText ? matchedUrl : text}</a>`;
43
- });
44
- if (ret === href) {
45
- Log.w(`Unable to extract url, href:${href}`);
46
- }
47
- return ret;
35
+ return getStyledATagFromText(href, "message-marked_link", undefined, title || '');
48
36
  }
49
37
  return text;
50
38
  },
@@ -74,6 +74,12 @@
74
74
  <div v-if="payload.src === CUSTOM_MESSAGE_SRC.TRANSFER_TO_HUMAN || payload.src === CUSTOM_MESSAGE_SRC.TRANSFER_TO_TASK_FLOW">
75
75
  <MessageTransferWithDesc :payload="payload" />
76
76
  </div>
77
+ <div v-if="payload.src === CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION">
78
+ <MessageQueueConfirmation
79
+ :payload="payload"
80
+ @sendMessage="sendCustomMessage"
81
+ />
82
+ </div>
77
83
  </div>
78
84
  </div>
79
85
  </template>
@@ -95,6 +101,7 @@ import MessageMultiForm from './message-multi-form/index.vue';
95
101
  import MessageConcurrencyLimit from "./message-concurrency-limit.vue";
96
102
  import MessageOrder from './message-order.vue';
97
103
  import MessageTransferWithDesc from './message-transfer-with-desc.vue';
104
+ import MessageQueueConfirmation from './message-queue-confirmation.vue';
98
105
  import {
99
106
  IMessageModel,
100
107
  TUIChatService,
@@ -116,7 +123,8 @@ export default {
116
123
  MessageRating,
117
124
  MessageConcurrencyLimit,
118
125
  MessageOrder,
119
- MessageTransferWithDesc
126
+ MessageTransferWithDesc,
127
+ MessageQueueConfirmation
120
128
  },
121
129
  props: {
122
130
  message: {
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <div class="message-queue-confirmation">
3
+ <div class="message-queue-confirmation-tip">
4
+ <span v-for="(part, index) in formattedTips" :key="index">
5
+ <span v-if="part.isNumber" class="message-queue-confirmation-tip-number">{{ part.content }}</span>
6
+ <span v-else>{{ part.content }}</span>
7
+ </span>
8
+ </div>
9
+ <div v-if="!props.payload.status && !isConfirmed" class="message-queue-confirmation-button" @click="sendMessage">
10
+ {{ TUITranslateService.t('AIDesk.确认转人工') }}
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script lang="ts">
16
+ import { TUITranslateService } from '../../../../../../@aidesk/uikit-engine';
17
+ import { CUSTOM_MESSAGE_SRC } from '../../../../../../constant';
18
+ import vue from '../../../../../../adapter-vue';
19
+ const { ref, computed } = vue;
20
+ import { customerServicePayloadType } from '../../../../../../interface';
21
+
22
+ interface Props {
23
+ payload: customerServicePayloadType;
24
+ }
25
+
26
+ export default {
27
+ props: {
28
+ payload: {
29
+ type: Object as () => customerServicePayloadType,
30
+ default: () => ({}),
31
+ },
32
+ },
33
+ emits:['sendMessage'],
34
+ setup(props: Props, {emit}) {
35
+ const isConfirmed = ref<boolean>(false);
36
+ const { transferConfirmTips, waitingQueueLength, sessionId, skillGroupId, transferConfirmId } = props.payload.content;
37
+ const formattedTips = computed(() => {
38
+ if (!transferConfirmTips) return [];
39
+
40
+ const parts = transferConfirmTips.split('${WaitNo}');
41
+ const result: { content: any; isNumber: boolean; }[] = [];
42
+
43
+ parts.forEach((part, index) => {
44
+ if (part) {
45
+ result.push({ content: part, isNumber: false });
46
+ }
47
+ if (index < parts.length - 1) {
48
+ result.push({ content: waitingQueueLength, isNumber: true });
49
+ }
50
+ });
51
+
52
+ return result;
53
+ });
54
+
55
+ const sendMessage = () => {
56
+ let data = {
57
+ data: JSON.stringify({
58
+ src: CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION,
59
+ content: {
60
+ sessionId: sessionId,
61
+ skillGroupId: skillGroupId,
62
+ transferConfirmId: transferConfirmId,
63
+ }
64
+ })
65
+ };
66
+ emit('sendMessage', data);
67
+ isConfirmed.value = true;
68
+ }
69
+
70
+ return {
71
+ props,
72
+ formattedTips,
73
+ isConfirmed,
74
+ sendMessage,
75
+ TUITranslateService
76
+ };
77
+ },
78
+ };
79
+ </script>
80
+ <style lang="scss" scoped>
81
+ .message-queue-confirmation-tip-number {
82
+ color: #006AF6;
83
+ }
84
+ .message-queue-confirmation-button {
85
+ color: #006AF6;
86
+ cursor: pointer;
87
+ margin-top: 12px;
88
+ border: 1px solid #adcfff;
89
+ width: fit-content;
90
+ padding: 0px 16px;
91
+ height: 32px;
92
+ border-radius: 999px;
93
+ line-height: 32px;
94
+ background-color: #fff;
95
+ }
96
+ </style>
@@ -29,6 +29,7 @@ import {
29
29
  CUSTOM_BASIC_EMOJI_URL_MAPPING,
30
30
  } from '../../emoji-config';
31
31
  import { isPC } from '../../../../utils/env';
32
+ import { getStyledATagFromText } from '../../../../utils/utils';
32
33
  import state from '../../../../utils/state.js';
33
34
  const {ref, computed } = vue;
34
35
  interface IProps {
@@ -64,10 +65,7 @@ const textMessageData = computed(() => {
64
65
  + CUSTOM_BASIC_EMOJI_URL_MAPPING[item.emojiKey];
65
66
  }
66
67
  } else if (item.name === 'text' && enableURLDetection.value) {
67
- // 开启 url 识别时先移除 html 标签,避免 xss 漏洞
68
- item.text = item.text.replace(/<[^>]+>/g, '').replace(/https?:\/\/[\w\-./?=&:#]+(?=[^\w\-./?=&:#]|$)/g, (url) => {
69
- return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="message-text-link" style="color: ${linkColor.value}; text-decoration: underline;">${url}</a>`;
70
- }) || '';
68
+ item.text = getStyledATagFromText(item.text || '', "message-text-link", linkColor.value, '');
71
69
  }
72
70
  },
73
71
  );
package/constant.ts CHANGED
@@ -37,8 +37,10 @@ export const CUSTOM_MESSAGE_SRC = {
37
37
  CONCURRENCY_LIMIT: '36',
38
38
  TIMEOUT_WARNING: '37',
39
39
  TRANSFER_TO_HUMAN: '39',
40
- GET_FEEDBACK_MENU: '42',
40
+ GET_SETTINGS: '42',
41
41
  SEND_FEEDBACK: '43',
42
+ SEND_HEARTBEAT: '44',
43
+ QUEUE_CONFIRMATION: '45',
42
44
  };
43
45
 
44
46
  // im message extra type
@@ -149,6 +151,12 @@ export const WHITE_LIST = [
149
151
  CUSTOM_MESSAGE_SRC.TIMEOUT_WARNING,
150
152
  CUSTOM_MESSAGE_SRC.TRANSFER_TO_TASK_FLOW,
151
153
  CUSTOM_MESSAGE_SRC.TRANSFER_TO_HUMAN,
154
+ CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION,
155
+ ];
156
+
157
+ export const SHOW_IF_FLOW_IN = [
158
+ CUSTOM_MESSAGE_SRC.MULTI_FORM,
159
+ CUSTOM_MESSAGE_SRC.QUEUE_CONFIRMATION,
152
160
  ];
153
161
 
154
162
  export const TOOLBAR_BUTTON_TYPE = {
@@ -40,5 +40,7 @@ const AIDesk = {
40
40
  "当前前方排队人数": "Queue Position",
41
41
  "结束排队": "Leave Queue",
42
42
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Once a review has been submitted for more than 3 minutes, it cannot be modified.",
43
+ "不能发送空白消息": "Unable to send blank message",
44
+ "确认转人工": "Confirm transfer to agent",
43
45
  }
44
46
  export default AIDesk;
@@ -40,5 +40,7 @@ const AIDesk = {
40
40
  "当前前方排队人数": "Posición en la cola",
41
41
  "结束排队": "Salir de la cola",
42
42
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Una valoración enviada hace más de 3 minutos no puede modificarse.",
43
+ "不能发送空白消息": "No se puede enviar un mensaje vacío",
44
+ "确认转人工": "Confirmar transferencia a agente",
43
45
  }
44
46
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "Bilang ng mga tao sa unahan sa pila",
40
40
  "结束排队": "Umalis sa pila",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Kapag lumipas na ang 3 minuto mula sa pagsusumite ng review, hindi na ito mababago.",
42
+ "不能发送空白消息": "Hindi maaaring magpadala ng blangkong mensahe",
43
+ "确认转人工": "Kumpirmahin ang paglipat sa ahente",
42
44
  }
43
45
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "Jumlah orang di depan antrian saat ini",
40
40
  "结束排队": "Keluar dari antrian",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Ulasan yang dikirim lebih dari 3 menit tidak dapat diubah.",
42
+ "不能发送空白消息": "Tidak dapat mengirim pesan kosong",
43
+ "确认转人工": "Konfirmasi transfer ke agen",
42
44
  }
43
45
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "現在の前方待機人数",
40
40
  "结束排队": "待機を終了する",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "レビューは投稿から3分経過すると変更できません。",
42
+ "不能发送空白消息": "空白のメッセージは送信できません",
43
+ "确认转人工": "人工サービスに転送する",
42
44
  }
43
45
  export default AIDesk;
@@ -40,5 +40,7 @@ const AIDesk = {
40
40
  "当前前方排队人数": "현재 대기 인원",
41
41
  "结束排队": "대기 취소",
42
42
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "리뷰는 제출 후 3분이 지나면 수정할 수 없습니다.",
43
+ "不能发送空白消息": "빈 메시지를 보낼 수 없습니다",
44
+ "确认转人工": "상담사 연결",
43
45
  }
44
46
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "Bilangan orang di hadapan dalam barisan sekarang",
40
40
  "结束排队": "Tinggalkan barisan",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Ulasan yang dihantar lebih dari 3 minit tidak boleh diubah.",
42
+ "不能发送空白消息": "Tidak dapat menghantar mesej kosong",
43
+ "确认转人工": "Sahkan pindah kepada ejen",
42
44
  }
43
45
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "Текущее количество человек впереди в очереди",
40
40
  "结束排队": "Выйти из очереди",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Отзыв нельзя изменить после истечения 3 минут с момента отправки.",
42
+ "不能发送空白消息": "Невозможно отправить пустое сообщение",
43
+ "确认转人工": "Подтвердить перевод на оператора",
42
44
  }
43
45
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "จำนวนผู้รอคิวด้านหน้าในขณะนี้",
40
40
  "结束排队": "ออกจากคิว",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "รีวิวที่ส่งไปแล้วเกิน 3 นาทีจะไม่สามารถแก้ไขได้",
42
+ "不能发送空白消息": "ไม่สามารถส่งข้อความว่างได้",
43
+ "确认转人工": "ยืนยันการโอนย้ายไปยังเจ้าหน้าที่",
42
44
  }
43
45
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "Số người đang chờ trước bạn trong hàng",
40
40
  "结束排队": "Rời khỏi hàng",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "Đánh giá sau khi gửi hơn 3 phút sẽ không thể chỉnh sửa.",
42
+ "不能发送空白消息": "Không thể gửi tin nhắn trống",
43
+ "确认转人工": "Xác nhận chuyển tiếp sang nhân viên",
42
44
  }
43
45
  export default AIDesk;
@@ -40,5 +40,7 @@ const AIDesk = {
40
40
  "当前前方排队人数": "当前前方排队人数",
41
41
  "结束排队": "结束排队",
42
42
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "评价提交超过3分钟,无法更改。感谢您的反馈!",
43
+ "不能发送空白消息": "不能发送空白消息",
44
+ "确认转人工": "确认转人工",
43
45
  }
44
46
  export default AIDesk;
@@ -39,5 +39,7 @@ const AIDesk = {
39
39
  "当前前方排队人数": "目前前方排隊人數",
40
40
  "结束排队": "結束排隊",
41
41
  "评价提交超过3分钟,无法更改。感谢您的反馈!": "評價提交超過3分鐘,無法更改。感謝您的反饋!",
42
+ "不能发送空白消息": "不能發送空白訊息",
43
+ "确认转人工": "確認轉人工",
42
44
  }
43
45
  export default AIDesk;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencentcloud/ai-desk-customer-vue",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Vue2/Vue3 UIKit for AI Desk",
5
5
  "main": "index",
6
6
  "keywords": [
@@ -0,0 +1,65 @@
1
+ import { CUSTOM_MESSAGE_SRC } from "../constant";
2
+ import TUIChatEngine, { TUIStore, StoreName, TUIChatService } from "../@aidesk/uikit-engine";
3
+
4
+ let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
5
+ let isHeartbeatRunning = false;
6
+ let currentConversationID = TUIStore.getData(StoreName.CONV, 'currentConversationID');
7
+ const HEARTBEAT_INTERVAL = 30000; // 30 秒
8
+
9
+ // 在人工排队时发送心跳
10
+ export async function sendHeartbeat(conversationID: string, eventName: string) {
11
+ await TUIChatService.sendCustomMessage({
12
+ to: conversationID.slice(3),
13
+ conversationType: TUIChatEngine.TYPES.CONV_C2C,
14
+ payload: {
15
+ data: JSON.stringify({
16
+ customerServicePlugin: 0,
17
+ src: CUSTOM_MESSAGE_SRC.SEND_HEARTBEAT,
18
+ userClientEvent: {
19
+ event: eventName,
20
+ },
21
+ }),
22
+ },
23
+ needReadReceipt: false,
24
+ },{ onlineUserOnly: true });
25
+ }
26
+
27
+ function canSendHeartbeat() {
28
+ const isQueuing = TUIStore.getData(StoreName.CUSTOM, 'isQueuing');
29
+ const queueLeavePageTimeoutEnable = TUIStore.getData(StoreName.CUSTOM, 'queueLeavePageTimeoutEnable');
30
+ currentConversationID = TUIStore.getData(StoreName.CONV, 'currentConversationID');
31
+ const queueNumber = isQueuing && isQueuing.conversationID === currentConversationID ? isQueuing.value : -1;
32
+ if (queueNumber < 0 || !queueLeavePageTimeoutEnable || !currentConversationID) {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ export function startHeartbeat() {
39
+ if (!canSendHeartbeat() || isHeartbeatRunning) {
40
+ stopHeartbeat();
41
+ return;
42
+ }
43
+
44
+ isHeartbeatRunning = true;
45
+ (async function loop() {
46
+ if (!canSendHeartbeat() || !isHeartbeatRunning) {
47
+ stopHeartbeat();
48
+ return;
49
+ }
50
+ await sendHeartbeat(currentConversationID, "sessionPageHeartbeat");
51
+
52
+ heartbeatTimer = setTimeout(() => {
53
+ heartbeatTimer = null;
54
+ loop();
55
+ }, HEARTBEAT_INTERVAL);
56
+ })();
57
+ }
58
+
59
+ export function stopHeartbeat() {
60
+ if (heartbeatTimer !== null) {
61
+ clearTimeout(heartbeatTimer!);
62
+ heartbeatTimer = null;
63
+ }
64
+ isHeartbeatRunning = false;
65
+ }
package/utils/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { customerServicePayloadType } from '../interface';
2
- import { CUSTOM_MESSAGE_SRC, TYPES, WHITE_LIST } from '../constant';
2
+ import { CUSTOM_MESSAGE_SRC, SHOW_IF_FLOW_IN, TYPES, WHITE_LIST } from '../constant';
3
3
  import { IMessageModel } from '../@aidesk/uikit-engine';
4
4
 
5
5
  // Determine if it is a JSON string
@@ -71,9 +71,9 @@ export const isMessageInvisible = (message: IMessageModel): boolean => {
71
71
  const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
72
72
  const isGroupTipMessage = message?.type === TYPES.MSG_GROUP_TIP;
73
73
  const isCustomerInvisible = customerServicePayload?.src && !WHITE_LIST.includes(customerServicePayload?.src);
74
- const isMultiFormMessage: boolean = customerServicePayload?.src !== null && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.MULTI_FORM && message.flow === 'out';
74
+ const isShowIfFlowInMessage: boolean = customerServicePayload?.src !== null && SHOW_IF_FLOW_IN.includes(customerServicePayload?.src as string) && message.flow === 'out';
75
75
  const isRobot = customerServicePayload?.src === CUSTOM_MESSAGE_SRC.ROBOT && robotCommandArray.indexOf(customerServicePayload?.content?.command) !== -1;
76
- return (isCustomerMessage && (isCustomerInvisible || isRobot || isMultiFormMessage)) || isGroupTipMessage;
76
+ return (isCustomerMessage && (isCustomerInvisible || isRobot || isShowIfFlowInMessage)) || isGroupTipMessage;
77
77
  };
78
78
 
79
79
  // 如果用户选择 block cookies,此时访问 localStorage 浏览器会抛错
package/utils/utils.ts CHANGED
@@ -420,4 +420,32 @@ export function throttle(
420
420
  func.apply(this, args);
421
421
  }
422
422
  };
423
+ }
424
+
425
+ export function getStyledATagFromText(text, className, linkColor, title) {
426
+ // 开启 url 识别时先移除 html 标签,避免 xss 漏洞
427
+ // 预处理文本:移除换行符和制表符
428
+ const preprocessedText = text.replace(/[\n\t]/g, '').trim();
429
+
430
+ // 先移除HTML标签避免XSS漏洞
431
+ const sanitizedText = preprocessedText.replace(/<[^>]+>/g, '');
432
+
433
+ // 匹配URL:从http://或https://开始,匹配尽可能长的有效URL字符序列
434
+ // 包括:字母数字、-._~:/?#[]@!$&'()*+,;=% 等URL允许的字符
435
+ const ret = sanitizedText.replace(/https?:\/\/[\w\-._~:/?#\[\]@!$&'()*+,;=%]+/g, (matchedUrl) => {
436
+ // 检查URL是否包含中文字符,如果包含则跳过处理
437
+ const chineseRegex = /[\u4e00-\u9fff]/;
438
+ if (chineseRegex.test(matchedUrl)) {
439
+ return matchedUrl; // 不处理包含中文的URL
440
+ }
441
+
442
+ let isURLInText = false;
443
+ if (matchedUrl !== preprocessedText) {
444
+ // 如果 text 里包含 url,我们就用 matchedUrl 作为 a 标签的值;否则用 text 作为值
445
+ isURLInText = true;
446
+ }
447
+ const encodedUrl = encodeURI(matchedUrl);
448
+ return `<a target="_blank" rel="noreferrer noopener" class="${className}" style="color:${linkColor}; text-decoration:underline" href="${encodedUrl}" title="${title || ''}">${isURLInText ? matchedUrl : text}</a>`;
449
+ });
450
+ return ret;
423
451
  }