byt-lingxiao-ai 0.3.25 → 0.3.27
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/components/AiMessage.vue +63 -9
- package/components/ChatMessageList.vue +12 -5
- package/components/ChatWindow.vue +2 -0
- package/components/ChatWindowDialog.vue +5 -5
- package/components/mixins/messageMixin.js +19 -19
- package/components/utils/StreamParser.js +63 -34
- package/dist/index.common.js +800 -321
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.umd.js +800 -321
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/package.json +1 -1
package/components/AiMessage.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="chat-window-message-ai">
|
|
3
3
|
<div class="ai-render">
|
|
4
|
-
<div v-if="
|
|
4
|
+
<div v-if="messageLoading" class="ai-loading">
|
|
5
5
|
<div class="dot"></div>
|
|
6
6
|
<div class="dot"></div>
|
|
7
7
|
<div class="dot"></div>
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
<div
|
|
15
15
|
v-if="item.type === 'thinking'"
|
|
16
16
|
class="ai-thinking"
|
|
17
|
-
@click="$emit('thinking-toggle')"
|
|
18
17
|
>
|
|
19
|
-
<div class="ai-thinking-time">
|
|
18
|
+
<div class="ai-thinking-time" :class="{'thinking-expanded': !item.thinkingExpanded}" @click="handleThinkingToggle(item)">
|
|
20
19
|
{{
|
|
21
20
|
item.duration
|
|
22
21
|
? `思考用时 ${item.duration} 秒`
|
|
@@ -25,7 +24,7 @@
|
|
|
25
24
|
</div>
|
|
26
25
|
<div
|
|
27
26
|
class="ai-thinking-content"
|
|
28
|
-
v-if="thinkingExpanded"
|
|
27
|
+
v-if="item.thinkingExpanded"
|
|
29
28
|
>
|
|
30
29
|
{{ item.content }}
|
|
31
30
|
</div>
|
|
@@ -34,7 +33,7 @@
|
|
|
34
33
|
v-else-if="item.type === 'tool_call'"
|
|
35
34
|
class="ai-tool-call"
|
|
36
35
|
>
|
|
37
|
-
<div class="tool-title">🔧 {{ item.
|
|
36
|
+
<div class="tool-title">🔧 {{ item.name }}</div>
|
|
38
37
|
</div>
|
|
39
38
|
<div
|
|
40
39
|
v-else-if="item.type === 'tool_result'"
|
|
@@ -45,6 +44,7 @@
|
|
|
45
44
|
<div
|
|
46
45
|
v-else-if="item.type === 'content'"
|
|
47
46
|
class="ai-content markdown-body"
|
|
47
|
+
@click="handleLinkClick"
|
|
48
48
|
v-html="renderMarkdown(item.content)"
|
|
49
49
|
></div>
|
|
50
50
|
</div>
|
|
@@ -57,20 +57,40 @@ import { marked } from 'marked'
|
|
|
57
57
|
import hljs from 'highlight.js'
|
|
58
58
|
import 'highlight.js/styles/github.css'
|
|
59
59
|
|
|
60
|
+
// 创建自定义渲染器
|
|
61
|
+
const renderer = new marked.Renderer()
|
|
62
|
+
|
|
63
|
+
// 自定义链接渲染
|
|
64
|
+
renderer.link = function ({ href, text }) {
|
|
65
|
+
let dataPath = ''
|
|
66
|
+
const url = new URL(href, window.location.origin)
|
|
67
|
+
const pathname = url.pathname
|
|
68
|
+
const search = url.search
|
|
69
|
+
|
|
70
|
+
if (pathname.startsWith('/portal/')) {
|
|
71
|
+
dataPath = pathname.substring('/portal'.length) + search
|
|
72
|
+
return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
|
|
73
|
+
} else {
|
|
74
|
+
dataPath = href
|
|
75
|
+
return `<a href="${dataPath}" target="_blank">${text}</a>`
|
|
76
|
+
}
|
|
77
|
+
}
|
|
60
78
|
marked.setOptions({
|
|
61
|
-
|
|
79
|
+
renderer: renderer,
|
|
80
|
+
highlight: function(code, lang) {
|
|
62
81
|
if (lang && hljs.getLanguage(lang)) {
|
|
63
|
-
return hljs.highlight(code, { language: lang }).value
|
|
82
|
+
return hljs.highlight(code, { language: lang }).value;
|
|
64
83
|
}
|
|
65
|
-
return hljs.highlightAuto(code).value
|
|
84
|
+
return hljs.highlightAuto(code).value;
|
|
66
85
|
},
|
|
67
86
|
breaks: true,
|
|
68
87
|
gfm: true
|
|
69
|
-
})
|
|
88
|
+
});
|
|
70
89
|
|
|
71
90
|
export default {
|
|
72
91
|
props: {
|
|
73
92
|
message: Object,
|
|
93
|
+
messageLoading: Boolean,
|
|
74
94
|
thinkStatus: Boolean
|
|
75
95
|
},
|
|
76
96
|
computed: {
|
|
@@ -81,6 +101,34 @@ export default {
|
|
|
81
101
|
methods: {
|
|
82
102
|
renderMarkdown(text) {
|
|
83
103
|
return marked.parse(text || '')
|
|
104
|
+
},
|
|
105
|
+
handleThinkingToggle(item) {
|
|
106
|
+
this.$set(item, 'thinkingExpanded', !item.thinkingExpanded)
|
|
107
|
+
},
|
|
108
|
+
handleLinkClick(event) {
|
|
109
|
+
const link = event.target.closest('a')
|
|
110
|
+
if (!link) return
|
|
111
|
+
|
|
112
|
+
const routePath = link.getAttribute('data-path')
|
|
113
|
+
const linkText = link.getAttribute('data-text')
|
|
114
|
+
|
|
115
|
+
if (routePath && linkText) {
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
|
|
118
|
+
if (this.$appOptions?.store?.dispatch) {
|
|
119
|
+
this.$appOptions.store.dispatch('tags/addTagview', {
|
|
120
|
+
path: routePath,
|
|
121
|
+
fullPath: routePath,
|
|
122
|
+
label: linkText,
|
|
123
|
+
name: linkText,
|
|
124
|
+
meta: { title: linkText }
|
|
125
|
+
})
|
|
126
|
+
this.$appOptions.router.push({ path: routePath })
|
|
127
|
+
} else {
|
|
128
|
+
console.log('路由跳转:', routePath)
|
|
129
|
+
this.$router.push(routePath).catch(() => {})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
84
132
|
}
|
|
85
133
|
}
|
|
86
134
|
}
|
|
@@ -187,6 +235,11 @@ export default {
|
|
|
187
235
|
height: 16px;
|
|
188
236
|
background: url('./assets/arrow.png') no-repeat;
|
|
189
237
|
background-size: cover;
|
|
238
|
+
/* 添加过度动画 */
|
|
239
|
+
transition: all 0.2s ease-in-out;
|
|
240
|
+
}
|
|
241
|
+
.thinking-expanded::after {
|
|
242
|
+
transform: translateY(-50%) rotate(180deg);
|
|
190
243
|
}
|
|
191
244
|
|
|
192
245
|
.ai-thinking-content {
|
|
@@ -206,6 +259,7 @@ export default {
|
|
|
206
259
|
font-style: normal;
|
|
207
260
|
font-weight: 400;
|
|
208
261
|
line-height: 24px;
|
|
262
|
+
margin-bottom: 10px;
|
|
209
263
|
}
|
|
210
264
|
|
|
211
265
|
/* Markdown 样式 */
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<AiMessage
|
|
14
14
|
v-else
|
|
15
15
|
:message="message"
|
|
16
|
+
:message-loading="messageLoading"
|
|
16
17
|
:think-status="thinkStatus"
|
|
17
|
-
@thinking-toggle="handleThinkingToggle(message)"
|
|
18
18
|
/>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
@@ -28,17 +28,24 @@ export default {
|
|
|
28
28
|
components: { UserMessage, AiMessage },
|
|
29
29
|
props: {
|
|
30
30
|
messages: Array,
|
|
31
|
+
messageLoading: Boolean,
|
|
31
32
|
thinkStatus: Boolean
|
|
32
33
|
},
|
|
33
34
|
computed: {
|
|
34
35
|
lastMessageObject() {
|
|
35
36
|
return this.messages[this.messages.length - 1] || null
|
|
37
|
+
},
|
|
38
|
+
timelineContents() {
|
|
39
|
+
if (!this.lastMessageObject || !this.lastMessageObject.timeline) {
|
|
40
|
+
return ''
|
|
41
|
+
}
|
|
42
|
+
// 将所有 content 拼接成一个字符串用于监听变化
|
|
43
|
+
return this.lastMessageObject.timeline
|
|
44
|
+
.map(item => item.content || '')
|
|
45
|
+
.join('')
|
|
36
46
|
}
|
|
37
47
|
},
|
|
38
48
|
methods: {
|
|
39
|
-
handleThinkingToggle(message) {
|
|
40
|
-
this.$set(message, 'thinkingExpanded', !message.thinkingExpanded)
|
|
41
|
-
},
|
|
42
49
|
scrollToBottom() {
|
|
43
50
|
this.$nextTick(() => {
|
|
44
51
|
const el = this.$refs.chatArea
|
|
@@ -47,7 +54,7 @@ export default {
|
|
|
47
54
|
}
|
|
48
55
|
},
|
|
49
56
|
watch: {
|
|
50
|
-
|
|
57
|
+
timelineContents: {
|
|
51
58
|
handler() {
|
|
52
59
|
this.scrollToBottom()
|
|
53
60
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
<ChatWindowDialog
|
|
32
32
|
v-model="visible"
|
|
33
33
|
:messages="messages"
|
|
34
|
+
:message-loading="messageLoading"
|
|
34
35
|
:input-message="inputMessage"
|
|
35
36
|
:think-status="thinkStatus"
|
|
36
37
|
:chat-id="chatId"
|
|
@@ -77,6 +78,7 @@ export default {
|
|
|
77
78
|
inputMessage: '',
|
|
78
79
|
visible: false,
|
|
79
80
|
messages: [],
|
|
81
|
+
messageLoading: true,
|
|
80
82
|
robotStatus: 'leaving',
|
|
81
83
|
avaterStatus: 'normal',
|
|
82
84
|
currentMessage: null,
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
<ChatMessageList
|
|
10
10
|
ref="messageList"
|
|
11
11
|
:messages="messages"
|
|
12
|
+
:message-loading="messageLoading"
|
|
12
13
|
:think-status="thinkStatus"
|
|
13
|
-
:loading="loading"
|
|
14
14
|
@thinking-click="$emit('thinking-click')"
|
|
15
15
|
/>
|
|
16
16
|
|
|
@@ -45,6 +45,10 @@ export default {
|
|
|
45
45
|
type: Array,
|
|
46
46
|
required: true
|
|
47
47
|
},
|
|
48
|
+
messageLoading: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: false
|
|
51
|
+
},
|
|
48
52
|
inputMessage: {
|
|
49
53
|
type: String,
|
|
50
54
|
default: ''
|
|
@@ -53,10 +57,6 @@ export default {
|
|
|
53
57
|
type: Boolean,
|
|
54
58
|
default: true
|
|
55
59
|
},
|
|
56
|
-
loading: {
|
|
57
|
-
type: Boolean,
|
|
58
|
-
default: false
|
|
59
|
-
},
|
|
60
60
|
chatId: {
|
|
61
61
|
type: String,
|
|
62
62
|
default: ''
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StreamParser } from '../utils/StreamParser'
|
|
2
2
|
import { API_URL } from '../config/index.js'
|
|
3
3
|
import { getCookie } from '../utils/Cookie.js'
|
|
4
|
+
import generateUuid from 'components/utils/Uuid'
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
|
|
@@ -8,13 +9,7 @@ export default {
|
|
|
8
9
|
data() {
|
|
9
10
|
return {
|
|
10
11
|
streamParser: null,
|
|
11
|
-
|
|
12
|
-
id: Date.now(),
|
|
13
|
-
type: 'ai',
|
|
14
|
-
timeline: [],
|
|
15
|
-
loading: true,
|
|
16
|
-
thinkingExpanded: true
|
|
17
|
-
}
|
|
12
|
+
currentMessage: null
|
|
18
13
|
}
|
|
19
14
|
},
|
|
20
15
|
|
|
@@ -28,14 +23,22 @@ export default {
|
|
|
28
23
|
|
|
29
24
|
methods: {
|
|
30
25
|
createAiMessage() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
const aiMessage = {
|
|
27
|
+
id: generateUuid(),
|
|
28
|
+
type: 'ai',
|
|
29
|
+
sender: 'AI',
|
|
30
|
+
timeline: [],
|
|
31
|
+
loading: true,
|
|
32
|
+
thinkingExpanded: true
|
|
33
|
+
}
|
|
34
|
+
this.messages.push(aiMessage);
|
|
35
|
+
this.currentMessage = aiMessage;
|
|
36
|
+
return aiMessage;
|
|
34
37
|
},
|
|
35
38
|
|
|
36
39
|
createUserMessage(content) {
|
|
37
40
|
const message = {
|
|
38
|
-
id:
|
|
41
|
+
id: generateUuid(),
|
|
39
42
|
type: 'user',
|
|
40
43
|
sender: '用户',
|
|
41
44
|
time: '',
|
|
@@ -61,6 +64,8 @@ export default {
|
|
|
61
64
|
const controller = new AbortController();
|
|
62
65
|
|
|
63
66
|
try {
|
|
67
|
+
this.messageLoading = true;
|
|
68
|
+
|
|
64
69
|
const startTime = Date.now();
|
|
65
70
|
const token = getCookie('bonyear-access_token') || `44e7f112-63f3-429d-908d-2c97ec380de2`;
|
|
66
71
|
|
|
@@ -83,7 +88,6 @@ export default {
|
|
|
83
88
|
|
|
84
89
|
// 完成解析
|
|
85
90
|
this.streamParser.finish(() => {});
|
|
86
|
-
this.aiMessage.loading = false;
|
|
87
91
|
|
|
88
92
|
// 记录耗时
|
|
89
93
|
const duration = Date.now() - startTime;
|
|
@@ -98,13 +102,9 @@ export default {
|
|
|
98
102
|
console.error('发送消息失败:', error);
|
|
99
103
|
if (this.currentMessage) {
|
|
100
104
|
this.currentMessage.content = '抱歉,发生了错误,请重试。';
|
|
101
|
-
this.$forceUpdate();
|
|
102
105
|
}
|
|
103
106
|
} finally {
|
|
104
|
-
|
|
105
|
-
if (this.currentMessage) {
|
|
106
|
-
this.currentMessage.loading = false;
|
|
107
|
-
}
|
|
107
|
+
this.messageLoading = false;
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
|
|
@@ -125,9 +125,9 @@ export default {
|
|
|
125
125
|
console.log('收到数据块:', chunk);
|
|
126
126
|
// 使用解析器处理数据块,确保this指向正确
|
|
127
127
|
this.streamParser.processChunk(chunk, (e) => {
|
|
128
|
-
|
|
128
|
+
if (!this.currentMessage) return;
|
|
129
129
|
if (e.type === 'create') {
|
|
130
|
-
this.
|
|
130
|
+
this.currentMessage.timeline.push(e.event)
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
}
|
|
@@ -60,37 +60,69 @@ export class StreamParser {
|
|
|
60
60
|
this.scheduleFlush(emit)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
const
|
|
63
|
+
parseTag(tag) {
|
|
64
|
+
const isClosing = tag.startsWith('</')
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
66
|
+
const content = tag
|
|
67
|
+
.replace(/^<\/?|>$/g, '')
|
|
68
|
+
.trim()
|
|
69
|
+
|
|
70
|
+
const [tagName, ...attrParts] = content.split(/\s+/)
|
|
71
|
+
|
|
72
|
+
const attrs = {}
|
|
73
|
+
|
|
74
|
+
attrParts.forEach(part => {
|
|
75
|
+
const [key, rawValue] = part.split('=')
|
|
76
|
+
if (!key || !rawValue) return
|
|
77
|
+
|
|
78
|
+
attrs[key] = rawValue.replace(/^['"]|['"]$/g, '')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
tag: tagName.replace('/', ''),
|
|
83
|
+
attrs,
|
|
84
|
+
isClosing
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
handleTag(tag, emit) {
|
|
90
|
+
const { tag: tagName, attrs, isClosing } = this.parseTag(tag.toLowerCase())
|
|
91
|
+
|
|
92
|
+
if (tagName === 'think') {
|
|
93
|
+
if (!isClosing) {
|
|
94
|
+
this.startThinking(attrs, emit)
|
|
95
|
+
} else {
|
|
96
|
+
this.closeThinking()
|
|
97
|
+
this.currentType = 'content'
|
|
98
|
+
this.currentEvent = null
|
|
99
|
+
}
|
|
80
100
|
}
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
|
|
102
|
+
else if (tagName === 'tool_call') {
|
|
103
|
+
if (!isClosing) {
|
|
104
|
+
this.switchType('tool_call', emit, attrs)
|
|
105
|
+
} else {
|
|
106
|
+
this.switchType('content', emit)
|
|
107
|
+
}
|
|
83
108
|
}
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
|
|
110
|
+
else if (tagName === 'tool_result') {
|
|
111
|
+
if (!isClosing) {
|
|
112
|
+
this.switchType('tool_result', emit, attrs)
|
|
113
|
+
} else {
|
|
114
|
+
this.switchType('content', emit)
|
|
115
|
+
}
|
|
86
116
|
}
|
|
117
|
+
|
|
87
118
|
else {
|
|
88
119
|
this.append(tag, emit)
|
|
89
120
|
}
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
|
|
93
|
-
|
|
124
|
+
|
|
125
|
+
startThinking(attrs={}, emit) {
|
|
94
126
|
// 如果上一个 thinking 还没结算,先结算
|
|
95
127
|
this.closeThinking()
|
|
96
128
|
|
|
@@ -98,8 +130,10 @@ export class StreamParser {
|
|
|
98
130
|
this.currentEvent = {
|
|
99
131
|
id: this.uuid(),
|
|
100
132
|
type: 'thinking',
|
|
133
|
+
name: attrs.name || null,
|
|
101
134
|
content: '',
|
|
102
135
|
startTime: Date.now(),
|
|
136
|
+
thinkingExpanded: true,
|
|
103
137
|
endTime: null,
|
|
104
138
|
duration: null
|
|
105
139
|
}
|
|
@@ -121,14 +155,21 @@ export class StreamParser {
|
|
|
121
155
|
}
|
|
122
156
|
}
|
|
123
157
|
|
|
124
|
-
switchType(type) {
|
|
158
|
+
switchType(type, emit, attrs={}) {
|
|
125
159
|
// 如果是从 thinking 切走,结算时间
|
|
126
160
|
if (this.currentEvent?.type === 'thinking') {
|
|
127
161
|
this.closeThinking()
|
|
128
162
|
}
|
|
129
163
|
|
|
130
164
|
this.currentType = type
|
|
131
|
-
this.currentEvent =
|
|
165
|
+
this.currentEvent = {
|
|
166
|
+
id: this.uuid(),
|
|
167
|
+
type,
|
|
168
|
+
name: attrs.name || null,
|
|
169
|
+
content: ''
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
emit({ type: 'create', event: this.currentEvent })
|
|
132
173
|
}
|
|
133
174
|
|
|
134
175
|
append(text, emit) {
|
|
@@ -147,18 +188,6 @@ export class StreamParser {
|
|
|
147
188
|
emit({ type: 'append', event: this.currentEvent })
|
|
148
189
|
}
|
|
149
190
|
|
|
150
|
-
parseTagAttributes(tag) {
|
|
151
|
-
const attrRegex = /(\w+)=['"]([^'"]+)['"]/g
|
|
152
|
-
const attrs = {}
|
|
153
|
-
let match
|
|
154
|
-
|
|
155
|
-
while ((match = attrRegex.exec(tag)) !== null) {
|
|
156
|
-
attrs[match[1]] = match[2]
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return attrs
|
|
160
|
-
}
|
|
161
|
-
|
|
162
191
|
scheduleFlush(emit) {
|
|
163
192
|
if (this.updateTimer) return
|
|
164
193
|
this.updateTimer = setTimeout(() => {
|