byt-lingxiao-ai 0.3.23 → 0.3.25
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 +57 -71
- package/components/ChatMessageList.vue +12 -31
- package/components/ChatWindow.vue +37 -29
- package/components/mixins/messageMixin.js +19 -30
- package/components/mixins/webSocketMixin.js +2 -0
- package/components/utils/StreamParser.js +144 -98
- package/dist/index.common.js +64257 -26511
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.umd.js +64107 -26361
- 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 +5 -2
package/components/AiMessage.vue
CHANGED
|
@@ -1,107 +1,93 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="chat-window-message-ai">
|
|
3
3
|
<div class="ai-render">
|
|
4
|
-
<div
|
|
4
|
+
<div v-if="message.loading" class="ai-loading">
|
|
5
5
|
<div class="dot"></div>
|
|
6
6
|
<div class="dot"></div>
|
|
7
7
|
<div class="dot"></div>
|
|
8
8
|
</div>
|
|
9
|
-
<div
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
<div
|
|
10
|
+
v-for="item in message.timeline"
|
|
11
|
+
:key="item.id"
|
|
12
|
+
class="ai-message-item"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
v-if="item.type === 'thinking'"
|
|
16
|
+
class="ai-thinking"
|
|
17
|
+
@click="$emit('thinking-toggle')"
|
|
18
|
+
>
|
|
19
|
+
<div class="ai-thinking-time">
|
|
20
|
+
{{
|
|
21
|
+
item.duration
|
|
22
|
+
? `思考用时 ${item.duration} 秒`
|
|
23
|
+
: '思考中...'
|
|
24
|
+
}}
|
|
25
|
+
</div>
|
|
26
|
+
<div
|
|
27
|
+
class="ai-thinking-content"
|
|
28
|
+
v-if="thinkingExpanded"
|
|
29
|
+
>
|
|
30
|
+
{{ item.content }}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div
|
|
34
|
+
v-else-if="item.type === 'tool_call'"
|
|
35
|
+
class="ai-tool-call"
|
|
36
|
+
>
|
|
37
|
+
<div class="tool-title">🔧 {{ item.content }}{{ item.name }}</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
v-else-if="item.type === 'tool_result'"
|
|
41
|
+
class="ai-tool-result"
|
|
42
|
+
>
|
|
43
|
+
<div class="tool-title">📦 {{ item.content }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
v-else-if="item.type === 'content'"
|
|
47
|
+
class="ai-content markdown-body"
|
|
48
|
+
v-html="renderMarkdown(item.content)"
|
|
49
|
+
></div>
|
|
12
50
|
</div>
|
|
13
|
-
<div class="ai-content markdown-body" @click="handleLinkClick" v-html="renderedContent"></div>
|
|
14
51
|
</div>
|
|
15
52
|
</div>
|
|
16
53
|
</template>
|
|
17
54
|
|
|
18
55
|
<script>
|
|
19
|
-
import { marked } from 'marked'
|
|
20
|
-
import hljs from 'highlight.js'
|
|
21
|
-
import 'highlight.js/styles/github.css'
|
|
22
|
-
|
|
23
|
-
// 创建自定义渲染器
|
|
24
|
-
const renderer = new marked.Renderer()
|
|
25
|
-
|
|
26
|
-
// 自定义链接渲染
|
|
27
|
-
renderer.link = function ({ href, text }) {
|
|
28
|
-
let dataPath = ''
|
|
29
|
-
const url = new URL(href, window.location.origin)
|
|
30
|
-
const pathname = url.pathname
|
|
31
|
-
|
|
32
|
-
if (pathname.startsWith('/portal/')) {
|
|
33
|
-
dataPath = pathname.substring('/portal'.length)
|
|
34
|
-
return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
|
|
35
|
-
} else {
|
|
36
|
-
dataPath = href
|
|
37
|
-
return `<a href="${dataPath}" target="_blank">${text}</a>`
|
|
38
|
-
}
|
|
39
|
-
}
|
|
56
|
+
import { marked } from 'marked'
|
|
57
|
+
import hljs from 'highlight.js'
|
|
58
|
+
import 'highlight.js/styles/github.css'
|
|
59
|
+
|
|
40
60
|
marked.setOptions({
|
|
41
|
-
|
|
42
|
-
highlight: function(code, lang) {
|
|
61
|
+
highlight(code, lang) {
|
|
43
62
|
if (lang && hljs.getLanguage(lang)) {
|
|
44
|
-
return hljs.highlight(code, { language: lang }).value
|
|
63
|
+
return hljs.highlight(code, { language: lang }).value
|
|
45
64
|
}
|
|
46
|
-
return hljs.highlightAuto(code).value
|
|
65
|
+
return hljs.highlightAuto(code).value
|
|
47
66
|
},
|
|
48
67
|
breaks: true,
|
|
49
68
|
gfm: true
|
|
50
|
-
})
|
|
69
|
+
})
|
|
51
70
|
|
|
52
71
|
export default {
|
|
53
|
-
name: 'AiMessage',
|
|
54
72
|
props: {
|
|
55
|
-
message:
|
|
56
|
-
|
|
57
|
-
required: true
|
|
58
|
-
}
|
|
73
|
+
message: Object,
|
|
74
|
+
thinkStatus: Boolean
|
|
59
75
|
},
|
|
60
76
|
computed: {
|
|
61
77
|
thinkingExpanded() {
|
|
62
|
-
return this.message.thinkingExpanded !== false
|
|
63
|
-
},
|
|
64
|
-
renderedContent() {
|
|
65
|
-
return marked.parse(this.message.content || '');
|
|
66
|
-
},
|
|
67
|
-
isLoading() {
|
|
68
|
-
return this.message.loading === true;
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
watch: {
|
|
72
|
-
thinkStatus(newVal, oldVal) {
|
|
73
|
-
console.log('thinkStatus 变化:', newVal, oldVal);
|
|
78
|
+
return this.message.thinkingExpanded !== false
|
|
74
79
|
}
|
|
75
80
|
},
|
|
76
81
|
methods: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!link) return
|
|
80
|
-
|
|
81
|
-
const routePath = link.getAttribute('data-path')
|
|
82
|
-
const linkText = link.getAttribute('data-text')
|
|
83
|
-
|
|
84
|
-
if (routePath && linkText) {
|
|
85
|
-
event.preventDefault()
|
|
86
|
-
|
|
87
|
-
if (this.$appOptions?.store?.dispatch) {
|
|
88
|
-
this.$appOptions.store.dispatch('tags/addTagview', {
|
|
89
|
-
path: routePath,
|
|
90
|
-
fullPath: routePath,
|
|
91
|
-
label: linkText,
|
|
92
|
-
name: linkText,
|
|
93
|
-
meta: { title: linkText }
|
|
94
|
-
})
|
|
95
|
-
this.$appOptions.router.push({ path: routePath })
|
|
96
|
-
} else {
|
|
97
|
-
this.$router.push(routePath).catch(() => {})
|
|
98
|
-
}
|
|
99
|
-
}
|
|
82
|
+
renderMarkdown(text) {
|
|
83
|
+
return marked.parse(text || '')
|
|
100
84
|
}
|
|
101
85
|
}
|
|
102
86
|
}
|
|
103
87
|
</script>
|
|
104
88
|
|
|
89
|
+
|
|
90
|
+
|
|
105
91
|
<style scoped>
|
|
106
92
|
/* Loading 容器 */
|
|
107
93
|
.ai-loading {
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
v-for="message in messages"
|
|
7
7
|
:key="message.id"
|
|
8
8
|
>
|
|
9
|
-
<UserMessage
|
|
9
|
+
<UserMessage
|
|
10
10
|
v-if="message.type === 'user'"
|
|
11
11
|
:content="message.content"
|
|
12
12
|
/>
|
|
13
|
-
<AiMessage
|
|
13
|
+
<AiMessage
|
|
14
14
|
v-else
|
|
15
15
|
:message="message"
|
|
16
16
|
:think-status="thinkStatus"
|
|
@@ -25,53 +25,34 @@ import UserMessage from './UserMessage.vue'
|
|
|
25
25
|
import AiMessage from './AiMessage.vue'
|
|
26
26
|
|
|
27
27
|
export default {
|
|
28
|
-
|
|
29
|
-
components: {
|
|
30
|
-
UserMessage,
|
|
31
|
-
AiMessage
|
|
32
|
-
},
|
|
28
|
+
components: { UserMessage, AiMessage },
|
|
33
29
|
props: {
|
|
34
|
-
messages:
|
|
35
|
-
|
|
36
|
-
required: true
|
|
37
|
-
},
|
|
38
|
-
thinkStatus: {
|
|
39
|
-
type: Boolean,
|
|
40
|
-
default: true
|
|
41
|
-
}
|
|
30
|
+
messages: Array,
|
|
31
|
+
thinkStatus: Boolean
|
|
42
32
|
},
|
|
43
33
|
computed: {
|
|
44
34
|
lastMessageObject() {
|
|
45
|
-
|
|
46
|
-
if (len === 0) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
return this.messages[len - 1];
|
|
35
|
+
return this.messages[this.messages.length - 1] || null
|
|
50
36
|
}
|
|
51
37
|
},
|
|
52
38
|
methods: {
|
|
53
39
|
handleThinkingToggle(message) {
|
|
54
|
-
|
|
55
|
-
this.$set(message, 'thinkingExpanded', !message.thinkingExpanded);
|
|
40
|
+
this.$set(message, 'thinkingExpanded', !message.thinkingExpanded)
|
|
56
41
|
},
|
|
57
42
|
scrollToBottom() {
|
|
58
43
|
this.$nextTick(() => {
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
chatArea.scrollTop = chatArea.scrollHeight
|
|
62
|
-
}
|
|
44
|
+
const el = this.$refs.chatArea
|
|
45
|
+
if (el) el.scrollTop = el.scrollHeight
|
|
63
46
|
})
|
|
64
47
|
}
|
|
65
48
|
},
|
|
66
49
|
watch: {
|
|
67
50
|
lastMessageObject: {
|
|
68
|
-
handler(
|
|
69
|
-
|
|
70
|
-
this.scrollToBottom();
|
|
71
|
-
}
|
|
51
|
+
handler() {
|
|
52
|
+
this.scrollToBottom()
|
|
72
53
|
},
|
|
73
54
|
deep: true,
|
|
74
|
-
immediate: true
|
|
55
|
+
immediate: true
|
|
75
56
|
}
|
|
76
57
|
}
|
|
77
58
|
}
|
|
@@ -94,6 +94,7 @@ export default {
|
|
|
94
94
|
initialX: 10,
|
|
95
95
|
initialY: 20,
|
|
96
96
|
hasMoved: false,
|
|
97
|
+
timeJumpPoints: []
|
|
97
98
|
}
|
|
98
99
|
},
|
|
99
100
|
computed: {
|
|
@@ -117,6 +118,8 @@ export default {
|
|
|
117
118
|
this.appendToBodyHandler()
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
this.fetchTimeJumpPoints()
|
|
122
|
+
|
|
120
123
|
this.$nextTick(() => {
|
|
121
124
|
const chatEl = this.$el;
|
|
122
125
|
const style = window.getComputedStyle(chatEl);
|
|
@@ -219,42 +222,47 @@ export default {
|
|
|
219
222
|
}
|
|
220
223
|
})
|
|
221
224
|
},
|
|
225
|
+
async fetchTimeJumpPoints() {
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch(TIME_JUMP_POINTS_URL)
|
|
228
|
+
const data = await res.json()
|
|
229
|
+
this.timeJumpPoints = Array.isArray(data) ? data : []
|
|
230
|
+
console.log('时间跳转点加载完成:', this.timeJumpPoints)
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error('获取时间跳转点失败:', err)
|
|
233
|
+
this.timeJumpPoints = []
|
|
234
|
+
}
|
|
235
|
+
},
|
|
222
236
|
// 音频时间更新处理
|
|
223
237
|
onTimeUpdate() {
|
|
224
238
|
const audio = this.$refs.audioPlayer
|
|
225
239
|
const currentTime = audio.currentTime
|
|
226
240
|
|
|
227
|
-
if (!this.
|
|
228
|
-
|
|
229
|
-
|
|
241
|
+
if (!this.timeJumpPoints.length) return
|
|
242
|
+
|
|
243
|
+
this.timeJumpPoints.forEach(point => {
|
|
244
|
+
if (
|
|
245
|
+
currentTime >= point.time &&
|
|
246
|
+
currentTime < point.time + 1 &&
|
|
247
|
+
!this.jumpedTimePoints.has(point.time)
|
|
248
|
+
) {
|
|
249
|
+
console.log('触发跳转:', point.url)
|
|
230
250
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log('跳转时间范围:', currentTime >= point.time && currentTime < point.time + 1)
|
|
242
|
-
if (currentTime >= point.time && currentTime < point.time + 1 && !this.jumpedTimePoints.has(point.time)) {
|
|
243
|
-
this.jumpedTimePoints.add(point.time)
|
|
244
|
-
this.$appOptions.store.dispatch('tags/addTagview', {
|
|
245
|
-
path: point.url,
|
|
246
|
-
fullPath: point.url,
|
|
247
|
-
label: point.title,
|
|
248
|
-
name: point.title,
|
|
249
|
-
meta: { title: point.title },
|
|
250
|
-
query: {},
|
|
251
|
-
params: {}
|
|
252
|
-
})
|
|
253
|
-
this.$appOptions.router.push({ path: point.url })
|
|
254
|
-
}
|
|
251
|
+
this.jumpedTimePoints.add(point.time)
|
|
252
|
+
|
|
253
|
+
this.$appOptions.store.dispatch('tags/addTagview', {
|
|
254
|
+
path: point.url,
|
|
255
|
+
fullPath: point.url,
|
|
256
|
+
label: point.title,
|
|
257
|
+
name: point.title,
|
|
258
|
+
meta: { title: point.title },
|
|
259
|
+
query: {},
|
|
260
|
+
params: {}
|
|
255
261
|
})
|
|
256
|
-
|
|
257
|
-
|
|
262
|
+
|
|
263
|
+
this.$appOptions.router.push({ path: point.url })
|
|
264
|
+
}
|
|
265
|
+
})
|
|
258
266
|
},
|
|
259
267
|
onAudioEnded() {
|
|
260
268
|
this.robotStatus = 'leaving'
|
|
@@ -7,7 +7,14 @@ import { getCookie } from '../utils/Cookie.js'
|
|
|
7
7
|
export default {
|
|
8
8
|
data() {
|
|
9
9
|
return {
|
|
10
|
-
streamParser: null
|
|
10
|
+
streamParser: null,
|
|
11
|
+
aiMessage: {
|
|
12
|
+
id: Date.now(),
|
|
13
|
+
type: 'ai',
|
|
14
|
+
timeline: [],
|
|
15
|
+
loading: true,
|
|
16
|
+
thinkingExpanded: true
|
|
17
|
+
}
|
|
11
18
|
}
|
|
12
19
|
},
|
|
13
20
|
|
|
@@ -21,20 +28,9 @@ export default {
|
|
|
21
28
|
|
|
22
29
|
methods: {
|
|
23
30
|
createAiMessage() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
sender: 'AI',
|
|
28
|
-
time: '',
|
|
29
|
-
thinking: '',
|
|
30
|
-
charts: [],
|
|
31
|
-
content: '',
|
|
32
|
-
loading: true,
|
|
33
|
-
thinkingExpanded: true
|
|
34
|
-
};
|
|
35
|
-
this.messages.push(message);
|
|
36
|
-
this.currentMessage = message;
|
|
37
|
-
return message;
|
|
31
|
+
this.messages.push(this.aiMessage);
|
|
32
|
+
this.currentMessage = this.aiMessage;
|
|
33
|
+
return this.aiMessage;
|
|
38
34
|
},
|
|
39
35
|
|
|
40
36
|
createUserMessage(content) {
|
|
@@ -86,10 +82,8 @@ export default {
|
|
|
86
82
|
await this.consumeStream(response.body);
|
|
87
83
|
|
|
88
84
|
// 完成解析
|
|
89
|
-
|
|
90
|
-
this.
|
|
91
|
-
self.handleStreamUpdate(result);
|
|
92
|
-
});
|
|
85
|
+
this.streamParser.finish(() => {});
|
|
86
|
+
this.aiMessage.loading = false;
|
|
93
87
|
|
|
94
88
|
// 记录耗时
|
|
95
89
|
const duration = Date.now() - startTime;
|
|
@@ -120,7 +114,6 @@ export default {
|
|
|
120
114
|
async consumeStream(readableStream) {
|
|
121
115
|
const reader = readableStream.getReader();
|
|
122
116
|
const decoder = new TextDecoder('utf-8');
|
|
123
|
-
const self = this;
|
|
124
117
|
|
|
125
118
|
try {
|
|
126
119
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -131,9 +124,11 @@ export default {
|
|
|
131
124
|
const chunk = decoder.decode(value, { stream: true });
|
|
132
125
|
console.log('收到数据块:', chunk);
|
|
133
126
|
// 使用解析器处理数据块,确保this指向正确
|
|
134
|
-
this.streamParser.processChunk(chunk,
|
|
135
|
-
console.log('处理数据块:',
|
|
136
|
-
|
|
127
|
+
this.streamParser.processChunk(chunk, (e) => {
|
|
128
|
+
console.log('处理数据块:', e);
|
|
129
|
+
if (e.type === 'create') {
|
|
130
|
+
this.aiMessage.timeline.push(e.event)
|
|
131
|
+
}
|
|
137
132
|
});
|
|
138
133
|
}
|
|
139
134
|
} finally {
|
|
@@ -174,11 +169,5 @@ export default {
|
|
|
174
169
|
this.$forceUpdate();
|
|
175
170
|
}
|
|
176
171
|
},
|
|
177
|
-
|
|
178
|
-
beforeDestroy() {
|
|
179
|
-
// 清理解析器
|
|
180
|
-
if (this.streamParser) {
|
|
181
|
-
this.streamParser.destroy();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
172
|
+
beforeDestroy() {}
|
|
184
173
|
}
|
|
@@ -14,7 +14,9 @@ export default {
|
|
|
14
14
|
try {
|
|
15
15
|
// this.ws = new WebSocket('ws://10.2.233.41:9999');
|
|
16
16
|
// 测试
|
|
17
|
+
// console.log('WS_URL:', WS_URL)
|
|
17
18
|
this.ws = new WebSocket(WS_URL);
|
|
19
|
+
// this.ws = new WebSocket('ws://192.168.8.87/audio/ws/');
|
|
18
20
|
this.ws.binaryType = 'arraybuffer';
|
|
19
21
|
|
|
20
22
|
this.ws.onopen = async () => {
|