byt-lingxiao-ai 0.2.8 → 0.3.2
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 +357 -8
- package/components/ChatAvatar.vue +9 -2
- package/components/ChatMessageList.vue +29 -5
- package/components/ChatWindow.vue +110 -5
- package/components/ChatWindowDialog.vue +5 -0
- package/components/ChatWindowHeader.vue +1 -0
- package/components/assets/arrow.png +0 -0
- package/components/assets/empty.png +0 -0
- package/components/assets/entering.png +0 -0
- package/components/assets/logo.png +0 -0
- package/components/assets/normal.png +0 -0
- package/components/assets/output.png +0 -0
- package/components/assets/speaking.png +0 -0
- package/components/assets/think.png +0 -0
- package/components/assets/thinking.png +0 -0
- package/components/assets/waiting.png +0 -0
- package/components/config/index.js +4 -0
- package/components/mixins/messageMixin.js +25 -8
- package/components/mixins/webSocketMixin.js +3 -1
- package/components/utils/StreamParser.js +67 -39
- package/dist/img/empty.f36cb82e.png +0 -0
- package/dist/img/entering.4ef198fb.png +0 -0
- package/dist/img/normal.30197a82.png +0 -0
- package/dist/img/output.1dfa94eb.png +0 -0
- package/dist/img/speaking.fa87fedb.png +0 -0
- package/dist/img/thinking.21ad5ca5.png +0 -0
- package/dist/img/waiting.460478ef.png +0 -0
- package/dist/index.common.js +29750 -2967
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +11 -1
- package/dist/index.umd.js +29742 -2959
- 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 -3
- package/components/assets/byt.mp3 +0 -0
- package/dist/img/entering.42f05909.png +0 -0
- package/dist/img/normal.13f08ecb.png +0 -0
- package/dist/img/output.85c6bd8b.png +0 -0
- package/dist/img/speaking.3ce8b666.png +0 -0
- package/dist/img/thinking.05f29a84.png +0 -0
- package/dist/img/waiting.ac21d76e.png +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const API_URL = 'http://192.168.8.87:3100/lingxiao-byt/api/v1/mcp/ask';
|
|
2
|
+
export const WS_URL = 'ws://192.168.8.9:9999/ai_model/ws/voice-stream';
|
|
3
|
+
export const AUDIO_URL = '/minio/lingxiaoai/byt.mp3';
|
|
4
|
+
export const TIME_JUMP_POINTS_URL = '/minio/lingxiaoai/timeJumpPoints.json';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StreamParser } from '../utils/StreamParser'
|
|
2
|
+
import { API_URL } from '../config/index.js'
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
data() {
|
|
@@ -25,6 +26,8 @@ export default {
|
|
|
25
26
|
thinking: '',
|
|
26
27
|
charts: [],
|
|
27
28
|
content: '',
|
|
29
|
+
loading: true,
|
|
30
|
+
thinkingExpanded: true
|
|
28
31
|
};
|
|
29
32
|
this.messages.push(message);
|
|
30
33
|
this.currentMessage = message;
|
|
@@ -59,12 +62,9 @@ export default {
|
|
|
59
62
|
try {
|
|
60
63
|
const startTime = Date.now();
|
|
61
64
|
const controller = new AbortController();
|
|
62
|
-
const token = `Bearer
|
|
63
|
-
|
|
64
|
-
const baseUrl = window.location.protocol + '//' + window.location.hostname + ':3100';
|
|
65
|
-
const apiUrl = baseUrl + '/lingxiao-byt/api/chat/completions';
|
|
65
|
+
const token = `Bearer e298f087-85bc-48c2-afb9-7c69ffc911aa`;
|
|
66
66
|
|
|
67
|
-
const response = await fetch(
|
|
67
|
+
const response = await fetch(API_URL, {
|
|
68
68
|
method: 'POST',
|
|
69
69
|
signal: controller.signal,
|
|
70
70
|
headers: {
|
|
@@ -82,7 +82,10 @@ export default {
|
|
|
82
82
|
await this.consumeStream(response.body);
|
|
83
83
|
|
|
84
84
|
// 完成解析
|
|
85
|
-
this
|
|
85
|
+
const self = this;
|
|
86
|
+
this.streamParser.finish(function(result) {
|
|
87
|
+
self.handleStreamUpdate(result);
|
|
88
|
+
});
|
|
86
89
|
|
|
87
90
|
// 记录耗时
|
|
88
91
|
const duration = Date.now() - startTime;
|
|
@@ -91,6 +94,7 @@ export default {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
console.log(`流处理完成,总耗时: ${duration}ms`);
|
|
97
|
+
this.avaterStatus = 'normal';
|
|
94
98
|
|
|
95
99
|
} catch (error) {
|
|
96
100
|
console.error('发送消息失败:', error);
|
|
@@ -98,6 +102,11 @@ export default {
|
|
|
98
102
|
this.currentMessage.content = '抱歉,发生了错误,请重试。';
|
|
99
103
|
this.$forceUpdate();
|
|
100
104
|
}
|
|
105
|
+
} finally {
|
|
106
|
+
// 确保加载状态关闭
|
|
107
|
+
if (this.currentMessage) {
|
|
108
|
+
this.currentMessage.loading = false;
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
},
|
|
103
112
|
|
|
@@ -107,6 +116,7 @@ export default {
|
|
|
107
116
|
async consumeStream(readableStream) {
|
|
108
117
|
const reader = readableStream.getReader();
|
|
109
118
|
const decoder = new TextDecoder('utf-8');
|
|
119
|
+
const self = this;
|
|
110
120
|
|
|
111
121
|
try {
|
|
112
122
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -116,8 +126,10 @@ export default {
|
|
|
116
126
|
|
|
117
127
|
const chunk = decoder.decode(value, { stream: true });
|
|
118
128
|
|
|
119
|
-
//
|
|
120
|
-
this.streamParser.processChunk(chunk,
|
|
129
|
+
// 使用解析器处理数据块,确保this指向正确
|
|
130
|
+
this.streamParser.processChunk(chunk, function(result) {
|
|
131
|
+
self.handleStreamUpdate(result);
|
|
132
|
+
});
|
|
121
133
|
}
|
|
122
134
|
} finally {
|
|
123
135
|
reader.releaseLock();
|
|
@@ -130,6 +142,11 @@ export default {
|
|
|
130
142
|
handleStreamUpdate(result) {
|
|
131
143
|
if (!this.currentMessage) return;
|
|
132
144
|
|
|
145
|
+
console.log('收到更新:', result);
|
|
146
|
+
|
|
147
|
+
if (this.currentMessage.loading) {
|
|
148
|
+
this.currentMessage.loading = false;
|
|
149
|
+
}
|
|
133
150
|
// 更新思考内容
|
|
134
151
|
if (result.thinking) {
|
|
135
152
|
this.currentMessage.thinking += result.thinking;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { WS_URL } from '../config/index.js'
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
data() {
|
|
3
5
|
return {
|
|
4
6
|
ws: null,
|
|
5
|
-
wsUrl:
|
|
7
|
+
wsUrl: WS_URL,
|
|
6
8
|
isConnected: false,
|
|
7
9
|
reconnectCount: 0, // 重连尝试次数
|
|
8
10
|
maxReconnectAttempts: 3 // 最大重连尝试次数
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SSE (Server-Sent Events) 流解析器
|
|
3
|
-
* 优化点:
|
|
4
|
-
* 1. 使用状态机模式处理标签解析
|
|
5
|
-
* 2. 批量更新减少DOM操作
|
|
6
|
-
* 3. 内存优化:避免频繁字符串拼接
|
|
7
|
-
* 4. 可配置的更新频率
|
|
8
|
-
*/
|
|
9
1
|
export class StreamParser {
|
|
10
2
|
constructor(options = {}) {
|
|
11
3
|
this.options = {
|
|
@@ -46,6 +38,23 @@ export class StreamParser {
|
|
|
46
38
|
this.metrics.chunks++;
|
|
47
39
|
this.buffer += chunk;
|
|
48
40
|
|
|
41
|
+
if (this.options.debug) {
|
|
42
|
+
console.log('[StreamParser] 收到chunk:', chunk.substring(0, 100));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 尝试解析为 SSE 格式
|
|
46
|
+
if (this.buffer.includes('data:')) {
|
|
47
|
+
this.processSSEFormat(callback);
|
|
48
|
+
} else {
|
|
49
|
+
// 直接处理纯文本流
|
|
50
|
+
this.processPlainText(callback);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 处理标准 SSE 格式
|
|
56
|
+
*/
|
|
57
|
+
processSSEFormat(callback) {
|
|
49
58
|
// SSE 格式:事件由双换行符分隔
|
|
50
59
|
const events = this.buffer.split('\n\n');
|
|
51
60
|
|
|
@@ -56,15 +65,33 @@ export class StreamParser {
|
|
|
56
65
|
for (const event of events) {
|
|
57
66
|
if (event.trim()) {
|
|
58
67
|
this.metrics.events++;
|
|
59
|
-
this.
|
|
68
|
+
this.processSSEEvent(event, callback);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 处理纯文本流格式
|
|
75
|
+
*/
|
|
76
|
+
processPlainText(callback) {
|
|
77
|
+
const content = this.buffer;
|
|
78
|
+
this.buffer = ''; // 清空缓冲区
|
|
79
|
+
|
|
80
|
+
if (content) {
|
|
81
|
+
this.metrics.chars += content.length;
|
|
82
|
+
|
|
83
|
+
if (this.options.debug) {
|
|
84
|
+
console.log('[StreamParser] 处理纯文本:', content);
|
|
60
85
|
}
|
|
86
|
+
|
|
87
|
+
this.parseContent(content, callback);
|
|
61
88
|
}
|
|
62
89
|
}
|
|
63
90
|
|
|
64
91
|
/**
|
|
65
92
|
* 处理单个SSE事件
|
|
66
93
|
*/
|
|
67
|
-
|
|
94
|
+
processSSEEvent(eventStr, callback) {
|
|
68
95
|
const lines = eventStr.split('\n');
|
|
69
96
|
|
|
70
97
|
for (const line of lines) {
|
|
@@ -83,11 +110,16 @@ export class StreamParser {
|
|
|
83
110
|
|
|
84
111
|
if (content) {
|
|
85
112
|
this.metrics.chars += content.length;
|
|
113
|
+
|
|
114
|
+
if (this.options.debug) {
|
|
115
|
+
console.log('[StreamParser] 解析SSE内容:', content);
|
|
116
|
+
}
|
|
117
|
+
|
|
86
118
|
this.parseContent(content, callback);
|
|
87
119
|
}
|
|
88
120
|
} catch (error) {
|
|
89
121
|
if (this.options.debug) {
|
|
90
|
-
console.warn('[StreamParser] JSON解析失败:', data);
|
|
122
|
+
console.warn('[StreamParser] JSON解析失败:', data, error);
|
|
91
123
|
}
|
|
92
124
|
}
|
|
93
125
|
}
|
|
@@ -158,15 +190,16 @@ export class StreamParser {
|
|
|
158
190
|
handleTag(tag) {
|
|
159
191
|
const tagName = tag.toLowerCase();
|
|
160
192
|
|
|
193
|
+
if (this.options.debug) {
|
|
194
|
+
console.log('[StreamParser] 处理标签:', tag);
|
|
195
|
+
}
|
|
196
|
+
|
|
161
197
|
if (tagName === '<think>') {
|
|
162
198
|
this.status = 'thinking';
|
|
163
199
|
} else if (tagName === '</think>') {
|
|
164
200
|
this.status = 'output';
|
|
165
201
|
}
|
|
166
202
|
// 可扩展:支持更多标签类型
|
|
167
|
-
// else if (tagName.startsWith('<code')) {
|
|
168
|
-
// this.status = 'code';
|
|
169
|
-
// }
|
|
170
203
|
}
|
|
171
204
|
|
|
172
205
|
/**
|
|
@@ -198,7 +231,10 @@ export class StreamParser {
|
|
|
198
231
|
* 立即刷新缓冲区
|
|
199
232
|
*/
|
|
200
233
|
flush(callback) {
|
|
201
|
-
if (!callback)
|
|
234
|
+
if (!callback) {
|
|
235
|
+
console.warn('[StreamParser] flush: callback 为空');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
202
238
|
|
|
203
239
|
const hasThinking = this.thinkingBuffer.length > 0;
|
|
204
240
|
const hasContent = this.contentBuffer.length > 0;
|
|
@@ -212,12 +248,26 @@ export class StreamParser {
|
|
|
212
248
|
status: this.status
|
|
213
249
|
};
|
|
214
250
|
|
|
251
|
+
if (this.options.debug) {
|
|
252
|
+
console.log('[StreamParser] 刷新缓冲区:', {
|
|
253
|
+
thinking: result.thinking?.length || 0,
|
|
254
|
+
content: result.content?.length || 0,
|
|
255
|
+
status: result.status,
|
|
256
|
+
thinkingPreview: result.thinking?.substring(0, 50),
|
|
257
|
+
contentPreview: result.content?.substring(0, 50)
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
215
261
|
// 清空缓冲区
|
|
216
262
|
this.thinkingBuffer = [];
|
|
217
263
|
this.contentBuffer = [];
|
|
218
264
|
|
|
219
265
|
// 回调更新
|
|
220
|
-
|
|
266
|
+
try {
|
|
267
|
+
callback(result);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('[StreamParser] 回调执行错误:', error);
|
|
270
|
+
}
|
|
221
271
|
}
|
|
222
272
|
|
|
223
273
|
/**
|
|
@@ -252,26 +302,4 @@ export class StreamParser {
|
|
|
252
302
|
}
|
|
253
303
|
this.reset();
|
|
254
304
|
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 使用示例:
|
|
259
|
-
*
|
|
260
|
-
* const parser = new StreamParser({ debug: true });
|
|
261
|
-
*
|
|
262
|
-
* // 处理流
|
|
263
|
-
* for await (const chunk of stream) {
|
|
264
|
-
* parser.processChunk(chunk, (result) => {
|
|
265
|
-
* if (result.thinking) {
|
|
266
|
-
* message.thinking += result.thinking;
|
|
267
|
-
* }
|
|
268
|
-
* if (result.content) {
|
|
269
|
-
* message.content += result.content;
|
|
270
|
-
* }
|
|
271
|
-
* this.$forceUpdate();
|
|
272
|
-
* });
|
|
273
|
-
* }
|
|
274
|
-
*
|
|
275
|
-
* // 完成
|
|
276
|
-
* parser.finish();
|
|
277
|
-
*/
|
|
305
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|