@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 CHANGED
@@ -1,9 +1,6 @@
1
1
  ## 介绍
2
2
 
3
- 智能客服用户端 Web UIKit。使用此 UIKit,您可以在一天内将智能客服的能力集成到您的 Web 或 Hybrid 项目。极简接入,用 AI 为您的产品降本增效。其流程如下图所示:
4
-
5
- ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100027960326/7de563a1f4cf11efa8355254001c06ec.png)
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: '970px', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
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: '970px', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
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: '970px', margin: '10px auto', boxShadow: '0 11px 20px #ccc' }"
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. 安装支持 composition-api 以及 script setup 的相关依赖,以及 vue2.6 相关依赖。
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
- 3. 在 `vue.config.js `中增加,若没有该文件请新建。
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
- 4. 在 `src/ai-desk-customer-vue/adapter-vue.ts` 文件最后, 替换导出源:
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
- console.log("ai-desk-customer login success");
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: { conversationID: conversationID },
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 } = vue;
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
- // import {TUICustomerServer} from '../../../server';
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
- messageList.value = list.filter((message) => {
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, watchEffect, onMounted } = vue;
14
+ const { ref, computed, withDefaults, defineProps, watch } = vue;
17
15
 
18
16
  interface Props {
19
17
  payload: customerServicePayloadType;
20
18
  }
21
19
 
22
- export default {
23
- props: {
24
- payload: {
25
- type: Object as () => customerServicePayloadType,
26
- default: () => ({}),
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
- setup(props: Props) {
30
- const displayedContent = ref<string>('');
31
- const isFinished = ref<boolean>(false);
32
- let currentIndex = 0;
39
+ });
33
40
 
34
- onMounted(() => {
35
- displayedContent.value = parseMarkdown(props?.payload?.chunks?.join('') ?? '');
36
- currentIndex = displayedContent.value.length;
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
- watchEffect(() => {
40
- const newContent = props?.payload?.chunks?.join('') ?? '';
41
- const parsedContent = parseMarkdown(newContent);
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
- return {
53
- props,
54
- isFinished,
55
- displayedContent,
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>
@@ -118,7 +118,7 @@
118
118
  .message-marked {
119
119
  overflow: hidden;
120
120
  word-break: break-word;
121
- white-space: normal;
121
+ white-space: pre-wrap;
122
122
  display: flex;
123
123
  flex-direction: column;
124
124
  justify-content: flex-start;
@@ -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-montainer',messageItem.flow === 'in'?'file-in' :'']"
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,isWeChat } from '../../../../utils/env';
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 || props.messageItem.flow === 'out') {
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-in{
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
@@ -28,6 +28,7 @@ export const CUSTOM_MESSAGE_SRC = {
28
28
  STREAM_TEXT: '31',
29
29
  MULTI_BRANCH:'32',
30
30
  MULTI_FORM:'33',
31
+ THINKING:'35',
31
32
  };
32
33
 
33
34
  // im message extra type
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.2.1",
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, TUILogin } from '@tencentcloud/tui-core';
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
- console.log("login success");
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
- console.log(error);
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
- return;
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({src: '7', customerServicePlugin: 0}),
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
+ }
@@ -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 = customerServicePayload?.src !== null && customerServicePayload?.src === CUSTOM_MESSAGE_SRC.MULTI_FORM && message.flow === 'out';
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
+ }
@@ -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
+