af-mobile-client-vue3 1.4.56 → 1.4.57
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/package.json +1 -1
- package/src/App.vue +1 -0
- package/src/components/common/MateChat/components/MateChatContent.vue +3 -11
- package/src/components/common/MateChat/components/MateChatHeader.vue +51 -12
- package/src/components/common/MateChat/composables/useMateChat.ts +50 -70
- package/src/components/common/MateChat/index.vue +1 -0
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -6,7 +6,6 @@ import MateChatHeader from '@af-mobile-client-vue3/components/common/MateChat/co
|
|
|
6
6
|
import { PromptList } from '@af-mobile-client-vue3/components/common/MateChat/components/PromptList'
|
|
7
7
|
import { useMateChat } from '@af-mobile-client-vue3/components/common/MateChat/composables/useMateChat'
|
|
8
8
|
import { useDebounceFn } from '@vueuse/core'
|
|
9
|
-
import { Button as VanButton } from 'vant'
|
|
10
9
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
11
10
|
import 'vant/es/image-preview/style'
|
|
12
11
|
|
|
@@ -110,6 +109,7 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
|
|
|
110
109
|
:app-id="props.config.appId"
|
|
111
110
|
:app-key="props.config.appKey"
|
|
112
111
|
@select-session="handleSelectSession"
|
|
112
|
+
@new-conversation="newConversation"
|
|
113
113
|
/>
|
|
114
114
|
<McLayoutContent
|
|
115
115
|
v-if="startPage"
|
|
@@ -161,20 +161,13 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
|
|
|
161
161
|
style="flex: 1"
|
|
162
162
|
@item-click="onSubmit($event.label)"
|
|
163
163
|
/>
|
|
164
|
-
<VanButton
|
|
165
|
-
style="margin-left: auto"
|
|
166
|
-
icon="add"
|
|
167
|
-
title="新建对话"
|
|
168
|
-
size="small"
|
|
169
|
-
round
|
|
170
|
-
@click="newConversation"
|
|
171
|
-
/>
|
|
172
164
|
</div>
|
|
173
165
|
<McLayoutSender>
|
|
174
166
|
<McInput
|
|
175
167
|
:value="inputValue"
|
|
176
168
|
placeholder="请输入您的问题,我会为您解答"
|
|
177
169
|
:max-length="2000"
|
|
170
|
+
:autosize="true"
|
|
178
171
|
@change="(e) => (inputValue = e)"
|
|
179
172
|
@submit="onSubmit"
|
|
180
173
|
/>
|
|
@@ -186,7 +179,7 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
|
|
|
186
179
|
.chat-card {
|
|
187
180
|
width: 100%;
|
|
188
181
|
max-width: 1200px;
|
|
189
|
-
height:
|
|
182
|
+
height: 100%;
|
|
190
183
|
background: #ffffff;
|
|
191
184
|
border-radius: 24px;
|
|
192
185
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
@@ -205,7 +198,6 @@ function handleSelectSession(session: { chatId: string, title: string, lastTime:
|
|
|
205
198
|
/* 移动端适配 */
|
|
206
199
|
@media (max-width: 768px) {
|
|
207
200
|
.chat-card {
|
|
208
|
-
height: calc(100% - 16px);
|
|
209
201
|
border-radius: 16px;
|
|
210
202
|
padding: 8px;
|
|
211
203
|
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import type { ChatHistoryItem } from '@af-mobile-client-vue3/components/common/MateChat/types'
|
|
3
3
|
import { getHistories } from '@af-mobile-client-vue3/components/common/MateChat/apiService'
|
|
4
4
|
import { useChatHistoryCache } from '@af-mobile-client-vue3/components/common/MateChat/composables/useChatHistoryCache'
|
|
5
|
-
import { Empty, Icon, Loading, Popup } from 'vant'
|
|
5
|
+
import { Empty, Icon, Loading, Popup, Popover as VanPopover } from 'vant'
|
|
6
6
|
import { ref } from 'vue'
|
|
7
7
|
import 'vant/es/popup/style'
|
|
8
8
|
import 'vant/es/empty/style'
|
|
9
9
|
import 'vant/es/loading/style'
|
|
10
10
|
import 'vant/es/icon/style'
|
|
11
|
+
import 'vant/es/popover/style'
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
/**
|
|
@@ -47,6 +48,10 @@ interface Emits {
|
|
|
47
48
|
* 选择某条历史会话
|
|
48
49
|
*/
|
|
49
50
|
(e: 'selectSession', session: SessionItem): void
|
|
51
|
+
/**
|
|
52
|
+
* 新建对话
|
|
53
|
+
*/
|
|
54
|
+
(e: 'newConversation'): void
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -58,6 +63,12 @@ const emit = defineEmits<Emits>()
|
|
|
58
63
|
const showHistory = ref(false)
|
|
59
64
|
const isLoading = ref(false)
|
|
60
65
|
const sessionList = ref<SessionItem[]>([])
|
|
66
|
+
const showMenu = ref(false)
|
|
67
|
+
|
|
68
|
+
const menuActions: { text: string, key: 'new' | 'history', icon: string }[] = [
|
|
69
|
+
{ text: '新对话', key: 'new', icon: 'add-o' },
|
|
70
|
+
{ text: '历史对话', key: 'history', icon: 'clock-o' },
|
|
71
|
+
]
|
|
61
72
|
|
|
62
73
|
// 使用历史会话缓存
|
|
63
74
|
const { getCachedHistory, setCachedHistory } = useChatHistoryCache()
|
|
@@ -72,6 +83,20 @@ function handleSelectSession(session: SessionItem) {
|
|
|
72
83
|
showHistory.value = false
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 处理下拉菜单点击
|
|
88
|
+
*/
|
|
89
|
+
function handleMenuSelect(action: { text: string, key: 'new' | 'history', icon?: string }) {
|
|
90
|
+
showMenu.value = false
|
|
91
|
+
if (action.key === 'new') {
|
|
92
|
+
emit('newConversation')
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (action.key === 'history') {
|
|
96
|
+
handleOpenHistory()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
/**
|
|
76
101
|
* 格式化时间字符串为日期时间字符串
|
|
77
102
|
*/
|
|
@@ -139,11 +164,24 @@ async function fetchSessions() {
|
|
|
139
164
|
|
|
140
165
|
<template>
|
|
141
166
|
<div class="matechat-header-wrapper">
|
|
142
|
-
<McHeader
|
|
167
|
+
<McHeader
|
|
168
|
+
:logo-img="showTitle ? props.logoImage : ''"
|
|
169
|
+
:title="showTitle ? props.title : ''"
|
|
170
|
+
:logo-clickable="false"
|
|
171
|
+
>
|
|
143
172
|
<template #operationArea>
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
<VanPopover
|
|
174
|
+
v-model:show="showMenu"
|
|
175
|
+
placement="bottom-end"
|
|
176
|
+
:actions="menuActions"
|
|
177
|
+
@select="handleMenuSelect"
|
|
178
|
+
>
|
|
179
|
+
<template #reference>
|
|
180
|
+
<div class="matechat-header-history-btn">
|
|
181
|
+
<Icon name="ellipsis" size="16" />
|
|
182
|
+
</div>
|
|
183
|
+
</template>
|
|
184
|
+
</VanPopover>
|
|
147
185
|
</template>
|
|
148
186
|
</McHeader>
|
|
149
187
|
|
|
@@ -204,15 +242,17 @@ async function fetchSessions() {
|
|
|
204
242
|
justify-content: center;
|
|
205
243
|
width: 32px;
|
|
206
244
|
height: 32px;
|
|
207
|
-
border-radius:
|
|
208
|
-
background-color: rgba(255, 255, 255, 0.
|
|
245
|
+
border-radius: 8px;
|
|
246
|
+
background-color: rgba(255, 255, 255, 0.9);
|
|
247
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
248
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);
|
|
209
249
|
cursor: pointer;
|
|
210
250
|
transition: all 0.2s ease;
|
|
211
251
|
|
|
212
252
|
&:hover {
|
|
213
|
-
background-color:
|
|
214
|
-
transform: translateY(-
|
|
215
|
-
box-shadow: 0 2px
|
|
253
|
+
background-color: #ffffff;
|
|
254
|
+
transform: translateY(-0.5px);
|
|
255
|
+
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16);
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
|
|
@@ -221,7 +261,6 @@ async function fetchSessions() {
|
|
|
221
261
|
width: 320px;
|
|
222
262
|
max-width: 80vw;
|
|
223
263
|
background-color: #ffffff;
|
|
224
|
-
border-radius: 16px;
|
|
225
264
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
|
226
265
|
padding: 16px 16px 12px;
|
|
227
266
|
box-sizing: border-box;
|
|
@@ -260,7 +299,7 @@ async function fetchSessions() {
|
|
|
260
299
|
|
|
261
300
|
&__item {
|
|
262
301
|
padding: 10px 8px;
|
|
263
|
-
border-radius:
|
|
302
|
+
border-radius: 0;
|
|
264
303
|
cursor: pointer;
|
|
265
304
|
transition: all 0.2s ease;
|
|
266
305
|
|
|
@@ -138,97 +138,77 @@ export function useMateChat(config: MateChatConfig) {
|
|
|
138
138
|
return
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// 流式:使用 FastGPT SSE
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
// 流式:使用 FastGPT SSE,增量更新最后一条模型消息内容
|
|
142
|
+
// 规则:
|
|
143
|
+
// 1)每次收到 chunk 都立即累加到 msg.content,保证实时流式效果
|
|
144
|
+
// 2)在累加后的内容上做一次“是否为转人工 JSON 消息”的前缀粗判:以 {"msgType 开头
|
|
145
|
+
// 3)如果判断为转人工,则保持 loading 状态,等待 onComplete 再统一处理整条消息
|
|
146
|
+
// 4)如果判断不是转人工,则当作普通文本流式展示,onComplete 时仅结束 loading
|
|
147
|
+
const transferPrefix = '{"msgType'
|
|
148
|
+
let checkedTransfer = false // 是否已经做过类型判定
|
|
149
|
+
let isTransferMessage = false // 是否判定为转人工消息
|
|
146
150
|
|
|
147
151
|
const callbacks: ChatStreamCallbacks = {
|
|
148
152
|
onMessage(chunk) {
|
|
149
|
-
|
|
153
|
+
const msg = messages.value[loadingMessageIndex]
|
|
154
|
+
if (!msg) {
|
|
150
155
|
return
|
|
151
156
|
}
|
|
157
|
+
// 1. 实时累加内容,保证流式体验
|
|
158
|
+
msg.content += chunk
|
|
152
159
|
|
|
153
|
-
//
|
|
160
|
+
// 2. 如果还没有做过类型判定,基于当前累积内容做一次前缀粗判
|
|
154
161
|
if (!checkedTransfer) {
|
|
155
|
-
const trimmed =
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
const trimmed = msg.content.trimStart()
|
|
163
|
+
// 内容太短,无法判断,继续等待更多 chunk
|
|
164
|
+
if (!trimmed || trimmed.length < transferPrefix.length) {
|
|
158
165
|
return
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const msg = messages.value[loadingMessageIndex]
|
|
165
|
-
if (!msg) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
msg.content += chunk
|
|
169
|
-
msg.loading = true
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 有可能是 JSON,累积前缀做精准匹配
|
|
174
|
-
prefixBuffer += trimmed
|
|
175
|
-
|
|
176
|
-
// 如果当前前缀还是 transferPrefix 的前缀,继续等后续 chunk
|
|
177
|
-
if (transferPrefix.startsWith(prefixBuffer)) {
|
|
178
|
-
// 还没完整匹配上整个标识,继续等待
|
|
179
|
-
if (prefixBuffer.length < transferPrefix.length) {
|
|
180
|
-
return
|
|
181
|
-
}
|
|
168
|
+
if (trimmed.startsWith(transferPrefix)) {
|
|
169
|
+
// 以 {"msgType 开头,标记为“可能是转人工消息”
|
|
170
|
+
isTransferMessage = true
|
|
182
171
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
messages.value.push({
|
|
188
|
-
from: 'service',
|
|
189
|
-
content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。',
|
|
190
|
-
})
|
|
191
|
-
transferHandled = true
|
|
192
|
-
checkedTransfer = true
|
|
193
|
-
// 更新缓存:追加用户消息和客服消息(在 onComplete 中统一处理)
|
|
194
|
-
return
|
|
172
|
+
else {
|
|
173
|
+
// 不以 {"msgType 开头,当作普通文本处理
|
|
174
|
+
isTransferMessage = false
|
|
175
|
+
msg.loading = false
|
|
195
176
|
}
|
|
196
|
-
|
|
197
|
-
// 前缀与约定不匹配,当作普通内容处理,并不再尝试转人工识别
|
|
198
177
|
checkedTransfer = true
|
|
199
|
-
const msg = messages.value[loadingMessageIndex]
|
|
200
|
-
if (!msg) {
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
msg.content += prefixBuffer
|
|
204
|
-
msg.loading = true
|
|
205
|
-
prefixBuffer = ''
|
|
206
|
-
return
|
|
207
178
|
}
|
|
208
|
-
|
|
209
|
-
// 已经判断过不会转人工,正常流式追加内容
|
|
210
|
-
const msg = messages.value[loadingMessageIndex]
|
|
211
|
-
if (!msg) {
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
msg.content += chunk
|
|
215
|
-
msg.loading = true
|
|
216
179
|
},
|
|
217
180
|
onComplete() {
|
|
218
|
-
if (transferHandled) {
|
|
219
|
-
// 转人工情况:更新缓存
|
|
220
|
-
appendMessages(chatId.value, [
|
|
221
|
-
{ from: 'user', content: evt },
|
|
222
|
-
{ from: 'service', content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。' },
|
|
223
|
-
])
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
181
|
const msg = messages.value[loadingMessageIndex]
|
|
227
182
|
if (!msg) {
|
|
228
183
|
return
|
|
229
184
|
}
|
|
185
|
+
// 根据前面判定结果决定如何处理整条消息
|
|
186
|
+
if (isTransferMessage) {
|
|
187
|
+
// 尝试按 JSON 解析,判断是否真的是转人工指令
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(msg.content.trim())
|
|
190
|
+
if (parsed && parsed.msgType === 'transfer') {
|
|
191
|
+
// 确认为转人工:移除模型气泡,插入客服气泡
|
|
192
|
+
messages.value.splice(loadingMessageIndex, 1)
|
|
193
|
+
messages.value.push({
|
|
194
|
+
from: 'service',
|
|
195
|
+
content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。',
|
|
196
|
+
})
|
|
197
|
+
// 转人工情况:更新缓存
|
|
198
|
+
appendMessages(chatId.value, [
|
|
199
|
+
{ from: 'user', content: evt },
|
|
200
|
+
{ from: 'service', content: '您好,客服xxx工号xxx为你服务。功能开发中,敬请期待。' },
|
|
201
|
+
])
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// 解析失败则回退为普通文本处理
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 普通消息:结束 loading,按完整文本渲染并写入缓存
|
|
230
211
|
msg.loading = false
|
|
231
|
-
// 流式完成后更新缓存:追加用户消息和完整的模型回复
|
|
232
212
|
appendMessages(chatId.value, [
|
|
233
213
|
{ from: 'user', content: evt },
|
|
234
214
|
{ from: 'model', content: msg.content, loading: false },
|