@tencentcloud/ai-desk-customer-vue 0.2.1 → 0.7.0
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/README.md +23 -25
- package/assets/loading_message.svg +1 -0
- package/components/CustomerServiceChat/index-web.vue +23 -4
- package/components/CustomerServiceChat/message-list/index-web.vue +16 -7
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +51 -52
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/styles/common.scss +1 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/type-writer.ts +189 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-file.vue +6 -80
- package/components/CustomerServiceChat/message-list/message-elements/message-thinking.vue +59 -0
- package/components/CustomerServiceChat/message-toolbar-button/index.vue +69 -0
- package/constant.ts +1 -0
- package/interface.ts +8 -0
- package/package.json +4 -5
- package/server.ts +35 -14
- package/utils/console.js +28 -0
- package/utils/index.ts +49 -2
- package/utils/logger.js +178 -0
- package/utils/time.js +15 -0
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
## 介绍
|
|
2
2
|
|
|
3
|
-
智能客服用户端 Web UIKit。使用此 UIKit,您可以在一天内将智能客服的能力集成到您的 Web 或 Hybrid 项目。极简接入,用 AI
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
3
|
+
智能客服用户端 Web UIKit。使用此 UIKit,您可以在一天内将智能客服的能力集成到您的 Web 或 Hybrid 项目。极简接入,用 AI 为您的产品增收提效。
|
|
7
4
|
|
|
8
5
|
## 效果展示
|
|
9
6
|
|
|
@@ -107,13 +104,25 @@ npm install
|
|
|
107
104
|
npm i -D sass sass-loader
|
|
108
105
|
```
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
>
|
|
107
|
+
清除项目默认的样式,避免样式问题:
|
|
112
108
|
|
|
113
|
-
> 请删除 **src/style.css** 内的项目默认样式,避免样式问题。
|
|
114
|
-
>
|
|
115
109
|
|
|
116
110
|
|
|
111
|
+
【macOS 端】
|
|
112
|
+
``` bash
|
|
113
|
+
echo -n > src/style.css
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
【Windows 端(PowerShell)】
|
|
117
|
+
``` bash
|
|
118
|
+
Clear-Content -Path src/style.css
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
【Windows 端(CMD)】
|
|
122
|
+
``` bash
|
|
123
|
+
echo. > src\style.css
|
|
124
|
+
```
|
|
125
|
+
|
|
117
126
|
### 步骤2:下载 UI 组件
|
|
118
127
|
|
|
119
128
|
通过 npm 方式下载 UI 组件,并将 UI 组件复制到自己工程的 src 目录下:
|
|
@@ -170,7 +179,7 @@ xcopy .\node_modules\@tencentcloud\ai-desk-customer-vue .\src\ai-desk-customer-v
|
|
|
170
179
|
:SDKAppID=""
|
|
171
180
|
userID=""
|
|
172
181
|
userSig=""
|
|
173
|
-
:style="{ width: '600px', height: '
|
|
182
|
+
:style="{ width: '600px', height: '80vh', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
|
|
174
183
|
/>
|
|
175
184
|
</template>
|
|
176
185
|
<script setup lang="ts">
|
|
@@ -188,7 +197,7 @@ xcopy .\node_modules\@tencentcloud\ai-desk-customer-vue .\src\ai-desk-customer-v
|
|
|
188
197
|
:SDKAppID=""
|
|
189
198
|
userID=""
|
|
190
199
|
userSig=""
|
|
191
|
-
:style="{ width: '600px', height: '
|
|
200
|
+
:style="{ width: '600px', height: '80vh', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
|
|
192
201
|
/>
|
|
193
202
|
</div>
|
|
194
203
|
</template>
|
|
@@ -210,7 +219,7 @@ body {
|
|
|
210
219
|
:SDKAppID=""
|
|
211
220
|
userID=""
|
|
212
221
|
userSig=""
|
|
213
|
-
:style="{ width: '600px', height: '
|
|
222
|
+
:style="{ width: '600px', height: '80vh', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
|
|
214
223
|
/>
|
|
215
224
|
</div>
|
|
216
225
|
</template>
|
|
@@ -223,18 +232,13 @@ body {
|
|
|
223
232
|
}
|
|
224
233
|
</style>
|
|
225
234
|
```
|
|
226
|
-
1.
|
|
227
|
-
|
|
228
|
-
``` javascript
|
|
229
|
-
npm i @vue/composition-api unplugin-vue2-script-setup vue@2.6.14 vue-template-compiler@2.6.14
|
|
230
|
-
```
|
|
231
|
-
2. 在 `main.ts/mian.js `中引入 VueCompositionAPI。
|
|
235
|
+
1. 在 `main.ts/mian.js `中引入 VueCompositionAPI。
|
|
232
236
|
|
|
233
237
|
``` javascript
|
|
234
238
|
import VueCompositionAPI from "@vue/composition-api";
|
|
235
239
|
Vue.use(VueCompositionAPI);
|
|
236
240
|
```
|
|
237
|
-
|
|
241
|
+
2. 在 `vue.config.js `中增加,若没有该文件请新建。
|
|
238
242
|
|
|
239
243
|
``` javascript
|
|
240
244
|
const ScriptSetup = require("unplugin-vue2-script-setup/webpack").default;
|
|
@@ -253,7 +257,7 @@ module.exports = {
|
|
|
253
257
|
},
|
|
254
258
|
};
|
|
255
259
|
```
|
|
256
|
-
|
|
260
|
+
3. 在 `src/ai-desk-customer-vue/adapter-vue-web.ts` 文件最后, 替换导出源:
|
|
257
261
|
|
|
258
262
|
``` javascript
|
|
259
263
|
// 初始写法
|
|
@@ -317,12 +321,6 @@ export * from "@vue/composition-api";
|
|
|
317
321
|
> 在 tsconfig.json 中关闭 ai-desk-customer-vue 的ts检测。
|
|
318
322
|
>
|
|
319
323
|
> `{`
|
|
320
|
-
> ` "compilerOptions": {`
|
|
321
|
-
> ` ...`
|
|
322
|
-
> ` "preserveValueImports": false,`
|
|
323
|
-
> ` "importsNotUsedAsValues": "preserve",`
|
|
324
|
-
> ` "noImplicitAny": false,`
|
|
325
|
-
> ` },`
|
|
326
324
|
> ` "exclude": [`
|
|
327
325
|
> ` "node_modules",`
|
|
328
326
|
> ` "src/ai-desk-customer-vue",`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#CCCCCC"><path d="M480.2-240Q380-240 310-309.8q-70-69.8-70-170T309.8-650q69.8-70 170-70T650-650.2q70 69.8 70 170T650.2-310q-69.8 70-170 70Z"/></svg>
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
@handleEditor="handleEditor"
|
|
19
19
|
@closeInputToolBar="() => changeToolbarDisplayType('none')"
|
|
20
20
|
/>
|
|
21
|
+
<MessageToolbarButton
|
|
22
|
+
:toolbarButtonList="props.toolbarButtonList"
|
|
23
|
+
/>
|
|
21
24
|
<MessageInputToolbar
|
|
22
25
|
v-if="isInputToolbarShow"
|
|
23
26
|
:class="[
|
|
@@ -87,8 +90,10 @@ import MessageInput from './message-input/index-web.vue';
|
|
|
87
90
|
import MessageInputToolbar from './message-input-toolbar/index-web.vue';
|
|
88
91
|
import EmojiDialog from './message-input-toolbar/emoji-dialog-mobile/emoji-dialog-mobile.vue';
|
|
89
92
|
import { isPC, isH5} from '../../utils/env';
|
|
90
|
-
import { ToolbarDisplayType } from '../../interface';
|
|
91
|
-
|
|
93
|
+
import { ToolbarButtonModel, ToolbarDisplayType } from '../../interface';
|
|
94
|
+
import { isSupportedLang } from '../../utils/';
|
|
95
|
+
import Log from '../../utils/logger';
|
|
96
|
+
import MessageToolbarButton from './message-toolbar-button/index.vue';
|
|
92
97
|
|
|
93
98
|
const { ref, onMounted, onUnmounted, computed } = vue;
|
|
94
99
|
|
|
@@ -97,6 +102,8 @@ interface IProps {
|
|
|
97
102
|
SDKAppID?:number;
|
|
98
103
|
userID?:string;
|
|
99
104
|
userSig?:string;
|
|
105
|
+
robotLang?:string;
|
|
106
|
+
toolbarButtonList?:ToolbarButtonModel[];
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
const emits = defineEmits(['closeChat']);
|
|
@@ -112,6 +119,8 @@ const props = withDefaults(defineProps<IProps>(), {
|
|
|
112
119
|
SDKAppID: 0,
|
|
113
120
|
userID: '',
|
|
114
121
|
userSig: '',
|
|
122
|
+
robotLang: '',
|
|
123
|
+
toolbarButtonList:() => [] as ToolbarButtonModel[]
|
|
115
124
|
});
|
|
116
125
|
|
|
117
126
|
const loginCustomerUIKit = () => {
|
|
@@ -121,7 +130,7 @@ const loginCustomerUIKit = () => {
|
|
|
121
130
|
userSig: props.userSig,
|
|
122
131
|
useUploadPlugin: true,
|
|
123
132
|
}).then(() => {
|
|
124
|
-
|
|
133
|
+
Log.i(`login success, robotLang:${props.robotLang}`);
|
|
125
134
|
let conversationID = 'C2C@customer_service_account';
|
|
126
135
|
TUIConversationService.switchConversation(conversationID);
|
|
127
136
|
TUIChatEngine.chat.callExperimentalAPI('isFeatureEnabledForStat', Math.pow(2, 42));
|
|
@@ -134,6 +143,9 @@ try {
|
|
|
134
143
|
const userContext = TUILogin.getContext();
|
|
135
144
|
if(userContext.userID == '' && props.SDKAppID!==0 && props.userID!=='' && props.userSig!==''){
|
|
136
145
|
loginCustomerUIKit();
|
|
146
|
+
if (props.robotLang && !isSupportedLang(props.robotLang)) {
|
|
147
|
+
Log.w(`robotLang:${props.robotLang} is not supported`);
|
|
148
|
+
}
|
|
137
149
|
}
|
|
138
150
|
} catch (e) {
|
|
139
151
|
console.log(e)
|
|
@@ -200,6 +212,10 @@ function scrollToLatestMessage() {
|
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
function onCurrentConversationIDUpdate(conversationID: string) {
|
|
215
|
+
if (!conversationID) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
if (currentConversationID.value === conversationID) {
|
|
204
220
|
return;
|
|
205
221
|
}
|
|
@@ -210,7 +226,10 @@ function onCurrentConversationIDUpdate(conversationID: string) {
|
|
|
210
226
|
TUICore.callService({
|
|
211
227
|
serviceName: TUIConstants.TUICustomerServicePlugin.SERVICE.NAME,
|
|
212
228
|
method: TUIConstants.TUICustomerServicePlugin.SERVICE.METHOD.ACTIVE_CONVERSATION,
|
|
213
|
-
params: {
|
|
229
|
+
params: {
|
|
230
|
+
conversationID: conversationID,
|
|
231
|
+
robotLang: props.robotLang && isSupportedLang(props.robotLang) ? props.robotLang : undefined,
|
|
232
|
+
},
|
|
214
233
|
});
|
|
215
234
|
}
|
|
216
235
|
|
|
@@ -68,8 +68,9 @@
|
|
|
68
68
|
@resendMessage="resendMessage(item)"
|
|
69
69
|
>
|
|
70
70
|
<template #messageElement>
|
|
71
|
+
<MessageThinking v-if="isThinkingMessage(item)"/>
|
|
71
72
|
<MessageText
|
|
72
|
-
v-if="item.type === TYPES.MSG_TEXT"
|
|
73
|
+
v-else-if="item.type === TYPES.MSG_TEXT"
|
|
73
74
|
:content="item.getMessageContent()"
|
|
74
75
|
/>
|
|
75
76
|
<ProgressMessage
|
|
@@ -175,7 +176,7 @@
|
|
|
175
176
|
|
|
176
177
|
<script lang="ts" setup>
|
|
177
178
|
import vue from '../../../adapter-vue';
|
|
178
|
-
const { ref, nextTick, computed, onMounted, onUnmounted }
|
|
179
|
+
const { ref, nextTick, computed, onMounted, onUnmounted } = vue;
|
|
179
180
|
import TUIChatEngine, {
|
|
180
181
|
IMessageModel,
|
|
181
182
|
TUIStore,
|
|
@@ -203,6 +204,7 @@ import MessageVideo from './message-elements/message-video-web.vue';
|
|
|
203
204
|
import MessageTool from './message-tool/index-web.vue';
|
|
204
205
|
import MessageRevoked from './message-tool/message-revoked.vue';
|
|
205
206
|
import MessagePlugin from '../message-list/message-elements/message-desk/message-plugin-web.vue';
|
|
207
|
+
import MessageThinking from './message-elements/message-thinking.vue';
|
|
206
208
|
import ScrollButton from './scroll-button/index.vue';
|
|
207
209
|
import { isPluginMessage } from './message-elements/message-desk/index';
|
|
208
210
|
import Dialog from '../../common/Dialog/index.vue';
|
|
@@ -214,9 +216,8 @@ import {
|
|
|
214
216
|
isEnabledMessageReadReceiptGlobal,
|
|
215
217
|
deepCopy,
|
|
216
218
|
} from '../../../utils/utils';
|
|
217
|
-
import { isMessageInvisible } from '../../../utils/index';
|
|
218
|
-
|
|
219
|
-
import {isCustomerConversation} from '../../../index';
|
|
219
|
+
import { isMessageInvisible, isThinkingMessage, isThinkingMessageOverTime } from '../../../utils/index';
|
|
220
|
+
import { isCustomerConversation } from '../../../index';
|
|
220
221
|
|
|
221
222
|
interface ScrollConfig {
|
|
222
223
|
scrollToMessage?: IMessageModel;
|
|
@@ -328,19 +329,26 @@ onUnmounted(() => {
|
|
|
328
329
|
});
|
|
329
330
|
|
|
330
331
|
async function onMessageListUpdated(list: IMessageModel[]) {
|
|
331
|
-
if(!isCustomerConversation(currentConversationID.value)){
|
|
332
|
+
if (!isCustomerConversation(currentConversationID.value)) {
|
|
332
333
|
return;
|
|
333
334
|
}
|
|
334
335
|
observer?.disconnect();
|
|
335
336
|
const oldLastMessage = currentLastMessage.value;
|
|
336
337
|
let hasEmojiReaction = false;
|
|
337
338
|
allMessageList.value = list;
|
|
338
|
-
|
|
339
|
+
|
|
340
|
+
messageList.value = list.filter((message, index) => {
|
|
339
341
|
if (message.reactionList?.length && !message.isDeleted) {
|
|
340
342
|
hasEmojiReaction = true;
|
|
341
343
|
}
|
|
344
|
+
|
|
345
|
+
// 判断是否结束
|
|
346
|
+
if (isThinkingMessage(message)) {
|
|
347
|
+
return isThinkingMessageOverTime(message);
|
|
348
|
+
}
|
|
342
349
|
return !message.isDeleted && !isMessageInvisible(message as any);
|
|
343
350
|
});
|
|
351
|
+
|
|
344
352
|
if (!messageList.value?.length) {
|
|
345
353
|
currentLastMessage.value = {};
|
|
346
354
|
return;
|
|
@@ -695,6 +703,7 @@ function setAudioPlayed(messageID: string) {
|
|
|
695
703
|
};
|
|
696
704
|
}
|
|
697
705
|
|
|
706
|
+
|
|
698
707
|
defineExpose({
|
|
699
708
|
scrollToLatestMessage,
|
|
700
709
|
});
|
|
@@ -1,61 +1,74 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="message-stream">
|
|
3
3
|
<pre :class="['message-marked']" v-html="displayedContent" />
|
|
4
|
-
<span
|
|
5
|
-
v-if="!isFinished"
|
|
6
|
-
class="blinking-cursor"
|
|
7
|
-
/>
|
|
8
4
|
</div>
|
|
9
5
|
</template>
|
|
10
6
|
|
|
11
|
-
<script lang="ts">
|
|
7
|
+
<script lang="ts" setup>
|
|
12
8
|
import vue from '../../../../../../adapter-vue';
|
|
13
9
|
import { customerServicePayloadType } from '../../../../../../interface';
|
|
14
10
|
import { parseMarkdown } from './marked'
|
|
11
|
+
import { TypeWriter } from "./type-writer";
|
|
12
|
+
import { JSONToObject } from "../../../../../../utils";
|
|
15
13
|
|
|
16
|
-
const { ref,
|
|
14
|
+
const { ref, computed, withDefaults, defineProps, watch } = vue;
|
|
17
15
|
|
|
18
16
|
interface Props {
|
|
19
17
|
payload: customerServicePayloadType;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
payload: () => '',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const isStreaming = ref<boolean>(false);
|
|
25
|
+
const chunks = ref<string>('');
|
|
26
|
+
const isFinished = ref<boolean>(true);
|
|
27
|
+
const prevChunksLength = ref<number>(0);
|
|
28
|
+
const streamContent = ref<string>('');
|
|
29
|
+
const displayedContent = computed(() => parseMarkdown(streamContent.value));
|
|
30
|
+
|
|
31
|
+
const typeWriter = new TypeWriter({
|
|
32
|
+
onTyping: (item: string) => {
|
|
33
|
+
streamContent.value += item;
|
|
34
|
+
// emits('onStreaming', item, streamContent.value);
|
|
35
|
+
},
|
|
36
|
+
onComplete() {
|
|
37
|
+
isStreaming.value = false;
|
|
28
38
|
},
|
|
29
|
-
|
|
30
|
-
const displayedContent = ref<string>('');
|
|
31
|
-
const isFinished = ref<boolean>(false);
|
|
32
|
-
let currentIndex = 0;
|
|
39
|
+
});
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
function startStreaming(content: string[]) {
|
|
42
|
+
if (!isStreaming.value) {
|
|
43
|
+
isStreaming.value = true;
|
|
44
|
+
typeWriter.add(content);
|
|
45
|
+
typeWriter.start();
|
|
46
|
+
} else {
|
|
47
|
+
typeWriter.add(content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (parsedContent.length > currentIndex) {
|
|
43
|
-
displayedContent.value = parsedContent;
|
|
44
|
-
currentIndex = parsedContent.length;
|
|
51
|
+
watch(() => props.payload, (newValue: string, oldValue: string) => {
|
|
52
|
+
if (newValue === oldValue) {
|
|
53
|
+
return;
|
|
45
54
|
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
watchEffect(() => {
|
|
49
|
-
isFinished.value = props?.payload?.isFinished === 1;
|
|
50
|
-
});
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
isFinished
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
56
|
+
const _payloadObject = JSONToObject(props.payload);
|
|
57
|
+
chunks.value = Array.isArray(_payloadObject.chunks) ? _payloadObject.chunks.join('') : _payloadObject.chunks;
|
|
58
|
+
isFinished.value = _payloadObject.isFinished === 1;
|
|
59
|
+
if (newValue && !oldValue && isFinished.value) {
|
|
60
|
+
// disable typeWriter style or history message first load
|
|
61
|
+
streamContent.value = chunks.value;
|
|
62
|
+
} else {
|
|
63
|
+
const _newChunksToAdd = chunks.value?.slice(prevChunksLength.value);
|
|
64
|
+
startStreaming([_newChunksToAdd]);
|
|
65
|
+
}
|
|
66
|
+
prevChunksLength.value = chunks.value?.length;
|
|
67
|
+
}, {
|
|
68
|
+
deep: true,
|
|
69
|
+
immediate: true,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
59
72
|
</script>
|
|
60
73
|
<style lang="scss" scoped>
|
|
61
74
|
.message-stream {
|
|
@@ -63,19 +76,5 @@ export default {
|
|
|
63
76
|
word-break: keep-all;
|
|
64
77
|
white-space: normal;
|
|
65
78
|
font-size: 14px;
|
|
66
|
-
|
|
67
|
-
.blinking-cursor {
|
|
68
|
-
display: inline-block;
|
|
69
|
-
width: 1px;
|
|
70
|
-
height: 1em;
|
|
71
|
-
background-color: black;
|
|
72
|
-
animation: blink 1s step-end infinite;
|
|
73
|
-
vertical-align: sub;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@keyframes blink {
|
|
77
|
-
0%, 100% { opacity: 1; }
|
|
78
|
-
50% { opacity: 0; }
|
|
79
|
-
}
|
|
80
79
|
}
|
|
81
80
|
</style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const chineseRegex = /[\u4e00-\u9fa5]/;
|
|
2
|
+
const wordAndNonWordRegex = /\b\w+\b|[^\w]+/g;
|
|
3
|
+
const isStringArray = (test: any): boolean => {
|
|
4
|
+
return Array.isArray(test) && !test.some(value => typeof value !== 'string');
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class TypeWriter {
|
|
8
|
+
/**
|
|
9
|
+
* @property {array} strings strings to be typed
|
|
10
|
+
*/
|
|
11
|
+
public strings: string[] = [];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @property {boolean} isTyping current typing status
|
|
15
|
+
*/
|
|
16
|
+
public isTyping: boolean = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @property {number} typeSpeed type speed in milliseconds. If empty, using dynamic speed.
|
|
20
|
+
*/
|
|
21
|
+
public typeSpeed?: number = 0;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @property {number} curArrayPos current typing string's position of all strings.
|
|
25
|
+
*/
|
|
26
|
+
private curArrayPos: number = 0;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @property {number} curCharPos current typing character's position in current strings.
|
|
30
|
+
*/
|
|
31
|
+
private curCharPos: number = 0;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @property {ReturnType<typeof setTimeout>} timer timer for type writer animation
|
|
35
|
+
*/
|
|
36
|
+
private timer?: ReturnType<typeof setTimeout>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* On string is typing
|
|
40
|
+
* @param {string} curStr
|
|
41
|
+
* @param {number} arrayPos
|
|
42
|
+
* @param {number} characterPos
|
|
43
|
+
* @param {Typed} self
|
|
44
|
+
*/
|
|
45
|
+
public onTyping?: (curStr: string, arrayPos: number, characterPos: number, self: any) => void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* After start
|
|
49
|
+
* @param {number} arrayPos
|
|
50
|
+
* @param {number} characterPos
|
|
51
|
+
* @param {TypeWriter} self
|
|
52
|
+
*/
|
|
53
|
+
public onStart?: (arrayPos: number, characterPos: number, self: any) => void;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* After stop
|
|
57
|
+
* @param {number} arrayPos
|
|
58
|
+
* @param {number} characterPos
|
|
59
|
+
* @param {TypeWriter} self
|
|
60
|
+
*/
|
|
61
|
+
public onStop?: (arrayPos: number, characterPos: number, self: any) => void;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* All typing is complete
|
|
65
|
+
* @param {Typed} self
|
|
66
|
+
*/
|
|
67
|
+
public onComplete?: (self: any) => void;
|
|
68
|
+
|
|
69
|
+
constructor(options: {
|
|
70
|
+
defaultStrings?: string[];
|
|
71
|
+
typeSpeed?: number;
|
|
72
|
+
onTyping?: (curStr: string, arrayPos: number, characterPos: number, self: any) => void;
|
|
73
|
+
onComplete?: (self: any) => void;
|
|
74
|
+
onStart?: (arrayPos: number, characterPos: number, self: any) => void;
|
|
75
|
+
onStop?: (arrayPos: number, characterPos: number, self: any) => void;
|
|
76
|
+
}) {
|
|
77
|
+
const { defaultStrings, typeSpeed, onTyping, onComplete, onStart, onStop } = options;
|
|
78
|
+
if (defaultStrings && isStringArray(defaultStrings)) {
|
|
79
|
+
this.add(defaultStrings);
|
|
80
|
+
}
|
|
81
|
+
if (typeof typeSpeed === 'number') {
|
|
82
|
+
this.typeSpeed = typeSpeed;
|
|
83
|
+
}
|
|
84
|
+
if (typeof onTyping === 'function') {
|
|
85
|
+
this.onTyping = onTyping;
|
|
86
|
+
}
|
|
87
|
+
if (typeof onComplete === 'function') {
|
|
88
|
+
this.onComplete = onComplete;
|
|
89
|
+
}
|
|
90
|
+
if (typeof onStart === 'function') {
|
|
91
|
+
this.onStart = onStart;
|
|
92
|
+
}
|
|
93
|
+
if (typeof onStop === 'function') {
|
|
94
|
+
this.onStop = onStop;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
add(addStrings: string[]) {
|
|
99
|
+
if (!addStrings || !addStrings.length) return;
|
|
100
|
+
addStrings.forEach((item: string) => {
|
|
101
|
+
if (chineseRegex.test(item)) {
|
|
102
|
+
const newValueArray = item.split('');
|
|
103
|
+
this.strings.push(...newValueArray);
|
|
104
|
+
} else {
|
|
105
|
+
const newValueArray = item.match(wordAndNonWordRegex) || item.split('');
|
|
106
|
+
this.strings.push(...newValueArray);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
start() {
|
|
112
|
+
if (this.isTyping) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.isTyping = true;
|
|
116
|
+
this.onStart && this.onStart(this.curArrayPos, this.curCharPos, this);
|
|
117
|
+
this._next();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
stop() {
|
|
121
|
+
if (!this.isTyping) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.isTyping = false;
|
|
125
|
+
clearTimeout(this.timer);
|
|
126
|
+
this.onStop && this.onStop(this.curArrayPos, this.curCharPos, this);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
done() {
|
|
130
|
+
this.stop();
|
|
131
|
+
let _str = this.strings[this.curArrayPos].slice(this.curCharPos);
|
|
132
|
+
_str += this.strings.slice(this.curArrayPos + 1).join('');
|
|
133
|
+
this.curArrayPos = this.strings.length - 1;
|
|
134
|
+
this.curCharPos = this.strings[this.curArrayPos]?.length - 1;
|
|
135
|
+
this.onTyping && this.onTyping(_str, this.curArrayPos, this.curCharPos, this);
|
|
136
|
+
this.strings = [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_consume() {
|
|
140
|
+
if (!this.strings.length) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ((this.curArrayPos >= this.strings.length)) {
|
|
145
|
+
this.isTyping = false;
|
|
146
|
+
this.onComplete?.(this);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const item = this.strings[this.curArrayPos]?.[this.curCharPos];
|
|
151
|
+
if (item) {
|
|
152
|
+
this.onTyping && this.onTyping(item, this.curArrayPos, this.curCharPos, this);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.curCharPos < this.strings[this.curArrayPos]?.length - 1) {
|
|
156
|
+
this.curCharPos++;
|
|
157
|
+
} else {
|
|
158
|
+
this.curArrayPos++;
|
|
159
|
+
this.curCharPos = 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_next() {
|
|
164
|
+
this._consume();
|
|
165
|
+
this.timer = setTimeout(() => {
|
|
166
|
+
this._consume();
|
|
167
|
+
if (this.isTyping) {
|
|
168
|
+
this._next();
|
|
169
|
+
}
|
|
170
|
+
}, this.typeSpeed || this._dynamicSpeed());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_dynamicSpeed() {
|
|
174
|
+
let length = 0;
|
|
175
|
+
length += (this.strings[this.curArrayPos]?.length || 0) - this.curCharPos - 1;
|
|
176
|
+
for (let i = this.curArrayPos + 1; i < this.strings.length; i++) {
|
|
177
|
+
length += this.strings[i]?.length || 0;
|
|
178
|
+
}
|
|
179
|
+
if (length <= 0) {
|
|
180
|
+
length = 10;
|
|
181
|
+
}
|
|
182
|
+
const speed = 1500 / length;
|
|
183
|
+
if (speed >= 150) {
|
|
184
|
+
return 150;
|
|
185
|
+
} else {
|
|
186
|
+
return speed;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
:class="['file-message-
|
|
3
|
+
:class="['file-message-container']"
|
|
4
4
|
:title="TUITranslateService.t('TUIChat.单击下载')"
|
|
5
5
|
@click="download"
|
|
6
6
|
>
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from '@tencentcloud/chat-uikit-engine';
|
|
24
24
|
import Icon from '../../../common/Icon.vue';
|
|
25
25
|
import files from '../../../../assets/files.png';
|
|
26
|
-
import { isUniFrameWork
|
|
26
|
+
import { isUniFrameWork } from '../../../../utils/env';
|
|
27
27
|
import type { IFileMessageContent } from '../../../../interface';
|
|
28
28
|
const { withDefaults } = vue;
|
|
29
29
|
|
|
@@ -39,10 +39,10 @@ const props = withDefaults(
|
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
const download = () => {
|
|
42
|
-
if (props.messageItem.hasRiskContent
|
|
42
|
+
if (props.messageItem.hasRiskContent) {
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
// If the browser supports fetch, use blob to download, so as to avoid the browser clicking the a tag and jumping to the preview of the new page
|
|
47
47
|
if (!isUniFrameWork && (window as any)?.fetch) {
|
|
48
48
|
const option = {
|
|
@@ -60,89 +60,15 @@ const download = () => {
|
|
|
60
60
|
a.download = props.content.name;
|
|
61
61
|
a.click();
|
|
62
62
|
});
|
|
63
|
-
} else if(isWeChat){
|
|
64
|
-
console.log("isWechat",props.content.url)
|
|
65
|
-
|
|
66
|
-
wx.downloadFile({
|
|
67
|
-
url: props.content.url,
|
|
68
|
-
filePath: wx.env.USER_DATA_PATH + '/' + props.content.name,
|
|
69
|
-
success: function (res) {
|
|
70
|
-
var filePath = res.filePath;
|
|
71
|
-
const lastIndex = filePath.lastIndexOf('.');
|
|
72
|
-
const fileType = filePath.substring(lastIndex + 1);
|
|
73
|
-
console.log(fileType)
|
|
74
|
-
wx.openDocument({
|
|
75
|
-
filePath: filePath,
|
|
76
|
-
showMenu:true,
|
|
77
|
-
fileType:fileType,
|
|
78
|
-
success: function (res) {
|
|
79
|
-
console.log('打开文档成功');
|
|
80
|
-
},
|
|
81
|
-
fail:function(){
|
|
82
|
-
console.log("fail")
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}else if(isUniFrameWork){
|
|
88
|
-
const lastIndex = props.content.url.lastIndexOf('.');
|
|
89
|
-
const fileType = props.content.url.substring(lastIndex + 1);
|
|
90
|
-
uni.downloadFile({
|
|
91
|
-
url:props.content.url,
|
|
92
|
-
success:function(res){
|
|
93
|
-
if(res.statusCode == 200){
|
|
94
|
-
console.log(res)
|
|
95
|
-
const tempFilePaths = res.tempFilePath;
|
|
96
|
-
uni.showToast({
|
|
97
|
-
title: '下载成功'+tempFilePaths,
|
|
98
|
-
icon: 'success',
|
|
99
|
-
duration: 2000
|
|
100
|
-
});
|
|
101
|
-
console.log(tempFilePaths);
|
|
102
|
-
uni.openDocument({
|
|
103
|
-
filePath: tempFilePaths,
|
|
104
|
-
fileType: fileType,
|
|
105
|
-
success: function () {
|
|
106
|
-
console.log('打开文档成功');
|
|
107
|
-
},
|
|
108
|
-
fail: function () {
|
|
109
|
-
console.log('打开文档失败');
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
// uni.openDocument({
|
|
116
|
-
// filePath: tempFilePaths[0],
|
|
117
|
-
// fileType: 'pdf',
|
|
118
|
-
// success: function () {
|
|
119
|
-
// console.log('打开文档成功');
|
|
120
|
-
// },
|
|
121
|
-
// fail: function () {
|
|
122
|
-
// console.log('打开文档失败');
|
|
123
|
-
// }
|
|
124
|
-
// });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
else {
|
|
128
|
-
console.log("no window here")
|
|
129
|
-
const a = document.createElement('a');
|
|
130
|
-
a.href = props.content.url;
|
|
131
|
-
a.target = '_blank';
|
|
132
|
-
a.download = props.content.name;
|
|
133
|
-
a.click();
|
|
134
63
|
}
|
|
135
64
|
};
|
|
136
65
|
</script>
|
|
137
66
|
<style lang="scss" scoped>
|
|
138
67
|
@import "../../style/common";
|
|
139
|
-
.file-
|
|
140
|
-
cursor: pointer;
|
|
141
|
-
}
|
|
142
|
-
.file-message-montainer {
|
|
68
|
+
.file-message-container {
|
|
143
69
|
display: flex;
|
|
144
70
|
flex-direction: row;
|
|
145
|
-
|
|
71
|
+
cursor: pointer;
|
|
146
72
|
|
|
147
73
|
.file-icon {
|
|
148
74
|
margin: auto 8px;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="message-thinking">
|
|
3
|
+
<div v-for="(icon, index) in icons" :key="index">
|
|
4
|
+
<transition name="fade">
|
|
5
|
+
<Icon v-if="icon" :file="loading_message" width="16px" height="16px"/>
|
|
6
|
+
</transition>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import vue from '../../../../adapter-vue';
|
|
12
|
+
import Icon from '../../../common/Icon.vue';
|
|
13
|
+
import loading_message from '../../../../assets/loading_message.svg';
|
|
14
|
+
const { ref, watchEffect,onMounted,onUnmounted} = vue;
|
|
15
|
+
export default {
|
|
16
|
+
components:{
|
|
17
|
+
Icon
|
|
18
|
+
},
|
|
19
|
+
setup() {
|
|
20
|
+
const icons = ref([false, false, false]);
|
|
21
|
+
const index = ref(0);
|
|
22
|
+
|
|
23
|
+
let intervalId:any;
|
|
24
|
+
|
|
25
|
+
onMounted(() => {
|
|
26
|
+
intervalId = setInterval(() => {
|
|
27
|
+
if (index.value < icons.value.length) {
|
|
28
|
+
icons.value = icons.value.map((v, i) => (i === index.value ? true : v));
|
|
29
|
+
index.value += 1;
|
|
30
|
+
} else {
|
|
31
|
+
icons.value = icons.value.map(() => false);
|
|
32
|
+
index.value = 0;
|
|
33
|
+
}
|
|
34
|
+
}, 500);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
onUnmounted(() => {
|
|
38
|
+
intervalId && clearInterval(intervalId)
|
|
39
|
+
intervalId = null;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { icons,loading_message };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
<style lang="scss" scoped>
|
|
47
|
+
.message-thinking{
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: row;
|
|
50
|
+
width: 45px;
|
|
51
|
+
height: 16px;
|
|
52
|
+
}
|
|
53
|
+
.fade-enter-active, .fade-leave-active {
|
|
54
|
+
transition: opacity .5s;
|
|
55
|
+
}
|
|
56
|
+
.fade-enter, .fade-leave-to {
|
|
57
|
+
opacity: 0;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="toolbar-button-container">
|
|
3
|
+
<template v-for="(item, index) in props.toolbarButtonList">
|
|
4
|
+
<div v-if="item.renderCondition()" :key="index"
|
|
5
|
+
:class="['toolbar-button', isH5 ? 'toolbar-button-h5' : '']" @click="onClick(index)">
|
|
6
|
+
<Icon v-if="item.icon" class="toolbar-button-icon" :file="item.icon" width="18px" height="18px"/>
|
|
7
|
+
<div class="toolbar-button-text">
|
|
8
|
+
{{ item.title }}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import { isH5 } from '../../../utils/env';
|
|
17
|
+
import { ToolbarButtonModel } from '../../../interface';
|
|
18
|
+
import Icon from '../../common/Icon.vue';
|
|
19
|
+
|
|
20
|
+
interface IProps {
|
|
21
|
+
toolbarButtonList?: ToolbarButtonModel[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const props = withDefaults(defineProps<IProps>(), {
|
|
25
|
+
toolbarButtonList: () => [] as ToolbarButtonModel[]
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function onClick(index: number) {
|
|
29
|
+
props.toolbarButtonList[index].clickEvent();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
</script>
|
|
33
|
+
<style>
|
|
34
|
+
.toolbar-button-container {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: row !important;
|
|
37
|
+
margin: 5px !important;
|
|
38
|
+
display: flex ;
|
|
39
|
+
align-items: center ;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.toolbar-button {
|
|
43
|
+
border: 1px solid #E7EAEF;
|
|
44
|
+
padding: 5px;
|
|
45
|
+
border-radius: 20px;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
margin-left: 10px;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.toolbar-button-h5 {
|
|
54
|
+
border: none;
|
|
55
|
+
background-color: #fff;
|
|
56
|
+
box-shadow: 0px 2px 2px 0px rgba(70, 98, 140, 0.06);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.toolbar-button-icon {
|
|
60
|
+
margin-right: 3px;
|
|
61
|
+
}
|
|
62
|
+
.toolbar-button-text {
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
text-overflow: ellipsis;
|
|
65
|
+
max-width: 100px;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
font-family: PingFangSC-Regular;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
package/constant.ts
CHANGED
package/interface.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface customerServicePayloadType {
|
|
|
8
8
|
chunks?: string[];
|
|
9
9
|
status?:number;
|
|
10
10
|
nodeStatus?:number;
|
|
11
|
+
thinkingStatus?:number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface IMenuItem {
|
|
@@ -174,3 +175,10 @@ export interface ISendMessagePayload {
|
|
|
174
175
|
file?: any;
|
|
175
176
|
atUserList?: string[];
|
|
176
177
|
}
|
|
178
|
+
|
|
179
|
+
export interface ToolbarButtonModel {
|
|
180
|
+
title:string,
|
|
181
|
+
icon?:string,
|
|
182
|
+
renderCondition:()=>{},
|
|
183
|
+
clickEvent:()=>void
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tencentcloud/ai-desk-customer-vue",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "chat uikit ai-desk-customer",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "chat uikit ai-desk-customer web-mui-uikit",
|
|
5
5
|
"main": "index",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"customer service",
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
"chatbot",
|
|
10
10
|
"vue",
|
|
11
11
|
"tencentcloud",
|
|
12
|
-
"客服",
|
|
13
12
|
"智能客服",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"智能机器人",
|
|
14
|
+
"deepseek"
|
|
16
15
|
],
|
|
17
16
|
"scripts": {
|
|
18
17
|
"sync": "node ./script/fileCopy.js",
|
package/server.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import TUICore, { TUIConstants
|
|
1
|
+
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
|
|
2
2
|
import {
|
|
3
3
|
isCustomerServiceMessage,
|
|
4
|
+
isThinkingMessage,
|
|
4
5
|
isMessageInvisible,
|
|
6
|
+
clearChatStorage,
|
|
5
7
|
} from './utils/index';
|
|
6
8
|
import TUIChatEngine, { TUIChatService, TUIConversationService,IMessageModel } from '@tencentcloud/chat-uikit-engine';
|
|
9
|
+
import Log from './utils/logger';
|
|
7
10
|
|
|
8
11
|
export default class TUICustomerServer {
|
|
9
12
|
static isInitialized: boolean;
|
|
10
13
|
static instance: TUICustomerServer;
|
|
11
14
|
private customerServiceAccounts: any[];
|
|
15
|
+
static loggedInUserID: string;
|
|
12
16
|
constructor() {
|
|
13
|
-
console.log('TUICustomerServer.init ok');
|
|
14
17
|
TUICore.registerService(TUIConstants.TUICustomerServicePlugin.SERVICE.NAME, this);
|
|
15
18
|
TUICore.registerExtension(TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID, this);
|
|
16
19
|
this.customerServiceAccounts = ['@customer_service_account'];
|
|
20
|
+
Log.i('TUICustomerServer.init ok');
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
static getInstance(): TUICustomerServer {
|
|
@@ -24,34 +28,43 @@ export default class TUICustomerServer {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
static loginCustomerUIKit(SDKAppID:number, userID:string, userSig:string) {
|
|
31
|
+
clearChatStorage(SDKAppID, userID);
|
|
27
32
|
TUIChatEngine.login({
|
|
28
33
|
SDKAppID,
|
|
29
34
|
userID,
|
|
30
35
|
userSig,
|
|
31
36
|
useUploadPlugin: true,
|
|
32
37
|
}).then(() => {
|
|
33
|
-
|
|
38
|
+
Log.i(`login success. userID:${userID}`);
|
|
39
|
+
TUICustomerServer.loggedInUserID = userID;
|
|
34
40
|
TUIConversationService.switchConversation('C2C@customer_service_account');
|
|
35
41
|
TUIChatEngine.chat.callExperimentalAPI('isFeatureEnabledForStat', Math.pow(2, 42));
|
|
36
42
|
})
|
|
37
43
|
.catch((error) => {
|
|
38
|
-
|
|
44
|
+
Log.l(error);
|
|
39
45
|
})
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
/**
|
|
43
|
-
* init
|
|
44
|
-
*/
|
|
45
48
|
public static init(SDKAppID:number, userID:string, userSig:string) {
|
|
49
|
+
Log.l(`TUICustomerServer.init SDKAppID:${SDKAppID} userID:${userID} isInitialized:${TUICustomerServer.isInitialized} loggedInUserID:${TUICustomerServer.loggedInUserID}`);
|
|
46
50
|
// Backward compatibility, the new version executes the init operation by default in index.ts
|
|
47
51
|
if (TUICustomerServer.isInitialized) {
|
|
48
|
-
|
|
52
|
+
if (TUICustomerServer.loggedInUserID === userID) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
TUICustomerServer.unInit().finally(() => {
|
|
56
|
+
this.loginCustomerUIKit(SDKAppID, userID, userSig);
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
TUICustomerServer.isInitialized = true;
|
|
60
|
+
// Execute call server when native plugin TUICallKit exists
|
|
61
|
+
this.loginCustomerUIKit(SDKAppID, userID, userSig);
|
|
49
62
|
}
|
|
50
|
-
TUICustomerServer.isInitialized = true;
|
|
51
|
-
// Execute call server when native plugin TUICallKit exists
|
|
52
|
-
this.loginCustomerUIKit(SDKAppID, userID, userSig);
|
|
53
63
|
}
|
|
54
64
|
|
|
65
|
+
public static async unInit() {
|
|
66
|
+
return TUIChatEngine.logout();
|
|
67
|
+
}
|
|
55
68
|
|
|
56
69
|
// Determine if the current session is a customer service session
|
|
57
70
|
public isCustomerConversation(conversationID: string) {
|
|
@@ -64,6 +77,9 @@ export default class TUICustomerServer {
|
|
|
64
77
|
if (!message || !this.isCustomerConversation(message.conversationID)) {
|
|
65
78
|
return false;
|
|
66
79
|
}
|
|
80
|
+
if (isThinkingMessage(message)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
67
83
|
return isCustomerServiceMessage(message) || isMessageInvisible(message);
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -73,7 +89,7 @@ export default class TUICustomerServer {
|
|
|
73
89
|
{
|
|
74
90
|
weight: 0,
|
|
75
91
|
icon: '',
|
|
76
|
-
text: '
|
|
92
|
+
text: '智能客服',
|
|
77
93
|
data: {
|
|
78
94
|
name: 'customer',
|
|
79
95
|
accountList: this.customerServiceAccounts,
|
|
@@ -84,16 +100,21 @@ export default class TUICustomerServer {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
public onCall(method: string, params: any) {
|
|
103
|
+
Log.l(`TUICustomerServer.onCall method:${method} params:`, params);
|
|
87
104
|
if (method === TUIConstants.TUICustomerServicePlugin.SERVICE.METHOD.ACTIVE_CONVERSATION) {
|
|
88
105
|
if (this.isCustomerConversation(params.conversationID)) {
|
|
89
106
|
TUIChatService.sendCustomMessage({
|
|
90
107
|
to: params.conversationID.slice(3),
|
|
91
108
|
conversationType: TUIChatEngine.TYPES.CONV_C2C,
|
|
92
109
|
payload: {
|
|
93
|
-
data: JSON.stringify({
|
|
110
|
+
data: JSON.stringify({
|
|
111
|
+
src: '7',
|
|
112
|
+
customerServicePlugin: 0,
|
|
113
|
+
triggeredContent: typeof params.robotLang === 'undefined' ? undefined : { language: params.robotLang }
|
|
114
|
+
}),
|
|
94
115
|
},
|
|
95
116
|
}, { onlineUserOnly: true});
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
119
|
}
|
|
99
|
-
}
|
|
120
|
+
}
|
package/utils/console.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
let _console; let method;
|
|
2
|
+
if (typeof console !== 'undefined') {
|
|
3
|
+
_console = console;
|
|
4
|
+
} else if (typeof global !== 'undefined' && global.console) {
|
|
5
|
+
_console = global.console;
|
|
6
|
+
} else if (typeof window !== 'undefined' && window.console) {
|
|
7
|
+
_console = window.console;
|
|
8
|
+
} else {
|
|
9
|
+
_console = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const noop = function() {};
|
|
13
|
+
const methods = [
|
|
14
|
+
'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'group',
|
|
15
|
+
'groupCollapsed', 'groupEnd', 'info', 'log', 'profile', 'profileEnd',
|
|
16
|
+
'table', 'time', 'timeEnd', 'timeStamp', 'trace', 'warn',
|
|
17
|
+
];
|
|
18
|
+
let length = methods.length;
|
|
19
|
+
|
|
20
|
+
while (length--) {
|
|
21
|
+
method = methods[length];
|
|
22
|
+
|
|
23
|
+
if (!console[method]) {
|
|
24
|
+
_console[method] = noop;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default _console;
|
package/utils/index.ts
CHANGED
|
@@ -37,6 +37,22 @@ export const isMessageRating = (message: IMessageModel): boolean => {
|
|
|
37
37
|
return isCustomerServiceMessage(message) && customerServicePayload.src === CUSTOM_MESSAGE_SRC.MENU;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
export const isThinkingMessage = (message: IMessageModel): boolean => {
|
|
41
|
+
const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
|
|
42
|
+
const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
|
|
43
|
+
return isCustomerMessage && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.THINKING && customerServicePayload?.thinkingStatus === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const isThinkingMessageOverTime = (message: IMessageModel): boolean => {
|
|
47
|
+
const messageTime = message.time * 1000;
|
|
48
|
+
const minute = 60 * 1000;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (now - messageTime > minute) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
export const isMessageInvisible = (message: IMessageModel): boolean => {
|
|
41
57
|
const customerServicePayload: customerServicePayloadType = JSONToObject(message?.payload?.data);
|
|
42
58
|
const robotCommandArray = ['feedback', 'updateBotStatus'];
|
|
@@ -50,11 +66,42 @@ export const isMessageInvisible = (message: IMessageModel): boolean => {
|
|
|
50
66
|
CUSTOM_MESSAGE_SRC.RICH_TEXT,
|
|
51
67
|
CUSTOM_MESSAGE_SRC.STREAM_TEXT,
|
|
52
68
|
CUSTOM_MESSAGE_SRC.MULTI_BRANCH,
|
|
53
|
-
CUSTOM_MESSAGE_SRC.MULTI_FORM
|
|
69
|
+
CUSTOM_MESSAGE_SRC.MULTI_FORM,
|
|
54
70
|
];
|
|
55
71
|
const isCustomerMessage = message?.type === TYPES.MSG_CUSTOM;
|
|
56
72
|
const isCustomerInvisible = customerServicePayload?.src && !whiteList.includes(customerServicePayload?.src);
|
|
57
|
-
const isMultiFormMessage:boolean =
|
|
73
|
+
const isMultiFormMessage: boolean = customerServicePayload?.src !== null && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.MULTI_FORM && message.flow === 'out';
|
|
58
74
|
const isRobot = customerServicePayload?.src === CUSTOM_MESSAGE_SRC.ROBOT && robotCommandArray.indexOf(customerServicePayload?.content?.command) !== -1;
|
|
59
75
|
return isCustomerMessage && (isCustomerInvisible || isRobot || isMultiFormMessage);
|
|
60
76
|
};
|
|
77
|
+
|
|
78
|
+
export const isSupportedLang = (lang: string): boolean => {
|
|
79
|
+
return [
|
|
80
|
+
'zh', // Simplified Chinese中文简体:zh
|
|
81
|
+
'zh-TW', // Traditional Chinese中文繁体:zh-TW
|
|
82
|
+
'en', // English英语:en
|
|
83
|
+
'id', // Indonesian印度尼西亚语:id
|
|
84
|
+
'vi', // Vietnamese越南语:vi
|
|
85
|
+
'ja', // Japanese日语:ja
|
|
86
|
+
'fil' // Filipino菲律宾语:fil
|
|
87
|
+
].indexOf(lang) !== -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 如果用户选择 block cookies,此时访问 localStorage 浏览器会抛错
|
|
91
|
+
// Uncaught SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document
|
|
92
|
+
// 通过 navigator.cookieEnabled 短路逻辑规避
|
|
93
|
+
const canIUseCookies = () => {
|
|
94
|
+
// When the browser is configured to block third-party cookies, and navigator.cookieEnabled is invoked inside a third-party iframe,
|
|
95
|
+
// it returns true in Safari, Edge Spartan and IE (while trying to set a cookie in such scenario would fail).
|
|
96
|
+
// It returns false in Firefox and Chromium-based browsers.
|
|
97
|
+
if (typeof window !== 'undefined') {
|
|
98
|
+
return window.navigator?.cookieEnabled && localStorage;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const clearChatStorage = (SDKAppID, userID) => {
|
|
104
|
+
if (canIUseCookies()) {
|
|
105
|
+
localStorage.removeItem(`TIM_${SDKAppID}_${userID}_conversationMap`);
|
|
106
|
+
}
|
|
107
|
+
}
|
package/utils/logger.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import console from './console';
|
|
2
|
+
import { getDate } from './time';
|
|
3
|
+
|
|
4
|
+
const USER_AGENT = window && window.navigator && window.navigator.userAgent || '';
|
|
5
|
+
const IS_IE = /MSIE/.test(USER_AGENT) || (USER_AGENT.indexOf('Trident') > -1 && USER_AGENT.indexOf('rv:11.0') > -1);
|
|
6
|
+
|
|
7
|
+
const canIUseConsoleStyle = function() {
|
|
8
|
+
// ie 浏览器不支持 console css style
|
|
9
|
+
// 小程序仅部分支持 console css style (console.warn/console.error 不支持)
|
|
10
|
+
return !IS_IE;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getType = function(input) {
|
|
14
|
+
return Object.prototype.toString
|
|
15
|
+
.call(input)
|
|
16
|
+
.match(/^\[object (.*)\]$/)[1]
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isArray = function(input) {
|
|
21
|
+
if (typeof Array.isArray === 'function') {
|
|
22
|
+
return Array.isArray(input);
|
|
23
|
+
}
|
|
24
|
+
return getType(input) === 'array';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isObject = function(input) {
|
|
28
|
+
// null is object, hence the extra check
|
|
29
|
+
return input !== null && typeof input === 'object';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 检测input是否为Error的实例
|
|
34
|
+
* @param {*} input 任意类型的输入
|
|
35
|
+
* @returns {Boolean} true->input is an instance of Error
|
|
36
|
+
*/
|
|
37
|
+
const isInstanceOfError = function(input) {
|
|
38
|
+
return (input instanceof Error);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 检测input类型是否为数组或者object
|
|
43
|
+
* @param {*} input 任意类型的输入
|
|
44
|
+
* @returns {Boolean} true->input is an array or an object
|
|
45
|
+
*/
|
|
46
|
+
const isArrayOrObject = function(input) {
|
|
47
|
+
return isArray(input) || isObject(input);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 对齐毫秒字符串
|
|
52
|
+
* @param {*} ms 毫秒
|
|
53
|
+
* @returns {String} 对齐后的毫秒时间字符串
|
|
54
|
+
*/
|
|
55
|
+
function padMs(ms) {
|
|
56
|
+
const len = ms.toString().length;
|
|
57
|
+
let ret;
|
|
58
|
+
switch (len) {
|
|
59
|
+
case 1:
|
|
60
|
+
ret = '00' + ms;
|
|
61
|
+
break;
|
|
62
|
+
case 2:
|
|
63
|
+
ret = '0' + ms;
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
ret = ms;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return ret;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* log前缀
|
|
75
|
+
* @returns {String} 日志前缀
|
|
76
|
+
*/
|
|
77
|
+
function getPrefix() {
|
|
78
|
+
if (!canIUseConsoleStyle()) {
|
|
79
|
+
return 'ai-desk-customer';
|
|
80
|
+
}
|
|
81
|
+
return '%c ai-desk-customer %c';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getPrefixStyle() {
|
|
85
|
+
return 'background:#0052d9; padding:1px; border-radius:3px; color: #fff';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getBgStyle() {
|
|
89
|
+
return 'background:transparent';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getTime() {
|
|
93
|
+
const date = getDate();
|
|
94
|
+
return date.toLocaleTimeString('en-US', { hour12: false }) + '.' + padMs(date.getMilliseconds());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const Log = {
|
|
98
|
+
// 将函数参数拼成字符串
|
|
99
|
+
arguments2String(args) {
|
|
100
|
+
let s = '';
|
|
101
|
+
if (args.length === 1) {
|
|
102
|
+
s = args[0];
|
|
103
|
+
} else {
|
|
104
|
+
for (let i = 0, length = args.length; i < length; i++) {
|
|
105
|
+
if (isArrayOrObject(args[i])) {
|
|
106
|
+
try {
|
|
107
|
+
s += isInstanceOfError(args[i]) ? JSON.stringify(args[i], ['message', 'code']) : JSON.stringify(args[i]);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
s += error ? error.message : '';
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
s += args[i];
|
|
114
|
+
}
|
|
115
|
+
s += ' ';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return s;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
_exec(api, log) {
|
|
122
|
+
if (!canIUseConsoleStyle()) {
|
|
123
|
+
console[api](`${getPrefix()} ${getTime()} ${log}`);
|
|
124
|
+
} else {
|
|
125
|
+
console[api](getPrefix(), getPrefixStyle(), getBgStyle(), getTime(), log);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 打印调试日志
|
|
131
|
+
*/
|
|
132
|
+
d: function() {
|
|
133
|
+
const s = this.arguments2String(arguments);
|
|
134
|
+
this._exec('debug', s);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 打印普通日志
|
|
139
|
+
*/
|
|
140
|
+
l: function() {
|
|
141
|
+
const s = this.arguments2String(arguments);
|
|
142
|
+
this._exec('log', s);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 打印普通日志,等同于 Log.i,为了兼容低版本的本地审核插件(其内部调用了 Logger.log)
|
|
147
|
+
*/
|
|
148
|
+
log: function() {
|
|
149
|
+
const s = this.arguments2String(arguments);
|
|
150
|
+
this._exec('log', s);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 打印release日志
|
|
155
|
+
*/
|
|
156
|
+
i: function() {
|
|
157
|
+
const s = this.arguments2String(arguments);
|
|
158
|
+
this._exec('info', s);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 打印告警日志
|
|
163
|
+
*/
|
|
164
|
+
w: function() {
|
|
165
|
+
const s = this.arguments2String(arguments);
|
|
166
|
+
this._exec('warn', s);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 打印错误日志
|
|
171
|
+
*/
|
|
172
|
+
e: function() {
|
|
173
|
+
const s = this.arguments2String(arguments);
|
|
174
|
+
this._exec('error', s);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default Log;
|
package/utils/time.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// user system clock may be inaccurate, so we need to adjust the timestamp
|
|
2
|
+
// based on the baseTime received from login server
|
|
3
|
+
|
|
4
|
+
let _offset = 0;
|
|
5
|
+
|
|
6
|
+
export const getTimestamp = function() {
|
|
7
|
+
return new Date().getTime() + _offset;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getDate = function() {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
now.setTime(getTimestamp());
|
|
13
|
+
return now;
|
|
14
|
+
};
|
|
15
|
+
|