byt-lingxiao-ai 0.3.5 → 0.3.8
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 +26 -144
- package/components/ChatWindow.vue +1 -0
- package/components/ChatWindowHeader.vue +1 -1
- package/components/config/index.js +5 -7
- package/components/mixins/audioMixin.js +17 -24
- package/components/mixins/messageMixin.js +5 -3
- package/components/mixins/webSocketMixin.js +6 -0
- package/components/utils/StreamParser.js +50 -229
- package/dist/index.common.js +101 -378
- package/dist/index.common.js.map +1 -1
- package/dist/index.css +2 -2
- package/dist/index.umd.js +172 -449
- 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 +3 -2
|
@@ -1,314 +1,135 @@
|
|
|
1
1
|
export class StreamParser {
|
|
2
2
|
constructor(options = {}) {
|
|
3
3
|
this.options = {
|
|
4
|
-
updateInterval: 16, //
|
|
5
|
-
|
|
6
|
-
debug: false, // 是否开启调试
|
|
4
|
+
updateInterval: 16, // 每16ms刷新一次(约60fps)
|
|
5
|
+
debug: false,
|
|
7
6
|
...options
|
|
8
7
|
};
|
|
9
|
-
|
|
10
8
|
this.reset();
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
* 重置解析器状态
|
|
15
|
-
*/
|
|
11
|
+
// 重置解析器状态
|
|
16
12
|
reset() {
|
|
17
13
|
this.buffer = '';
|
|
18
|
-
this.contentBuffer = [];
|
|
14
|
+
this.contentBuffer = [];
|
|
19
15
|
this.thinkingBuffer = [];
|
|
20
16
|
this.inTag = false;
|
|
21
17
|
this.tagBuffer = '';
|
|
22
18
|
this.updateTimer = null;
|
|
23
19
|
this.status = 'output'; // thinking | output
|
|
24
|
-
this.metrics = {
|
|
25
|
-
startTime: Date.now(),
|
|
26
|
-
chunks: 0,
|
|
27
|
-
events: 0,
|
|
28
|
-
chars: 0
|
|
29
|
-
};
|
|
30
20
|
}
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
* 处理流数据块
|
|
34
|
-
* @param {string} chunk - 接收到的数据块
|
|
35
|
-
* @param {Function} callback - 更新回调函数
|
|
36
|
-
*/
|
|
22
|
+
// 处理接收到的流数据块
|
|
37
23
|
processChunk(chunk, callback) {
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
if (this.options.debug) {
|
|
42
|
-
console.log('[StreamParser] 收到chunk:', chunk.substring(0, 100));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!this.buffer.includes('data:') && !this.buffer.includes('\n\n')) {
|
|
46
|
-
// 纯文本流,直接处理
|
|
47
|
-
if (this.options.debug) {
|
|
48
|
-
console.log('[StreamParser] 检测到纯文本流');
|
|
49
|
-
}
|
|
50
|
-
this.processPlainText(callback);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 尝试解析为 SSE 格式
|
|
55
|
-
if (this.buffer.includes('data:')) {
|
|
56
|
-
this.processSSEFormat(callback);
|
|
57
|
-
} else {
|
|
58
|
-
// 直接处理纯文本流
|
|
59
|
-
this.processPlainText(callback);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 处理标准 SSE 格式
|
|
65
|
-
*/
|
|
66
|
-
processSSEFormat(callback) {
|
|
67
|
-
// SSE 格式:事件由双换行符分隔
|
|
68
|
-
const events = this.buffer.split('\n\n');
|
|
69
|
-
|
|
70
|
-
// 保留最后一个可能不完整的事件
|
|
71
|
-
this.buffer = events.pop() || '';
|
|
72
|
-
|
|
73
|
-
// 处理完整的事件
|
|
74
|
-
for (const event of events) {
|
|
75
|
-
if (event.trim()) {
|
|
76
|
-
this.metrics.events++;
|
|
77
|
-
this.processSSEEvent(event, callback);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 处理纯文本流格式
|
|
84
|
-
*/
|
|
85
|
-
processPlainText(callback) {
|
|
86
|
-
const content = this.buffer;
|
|
87
|
-
this.buffer = ''; // 清空缓冲区
|
|
88
|
-
|
|
89
|
-
if (content) {
|
|
90
|
-
this.metrics.chars += content.length;
|
|
91
|
-
|
|
92
|
-
if (this.options.debug) {
|
|
93
|
-
console.log('[StreamParser] 处理纯文本:', content);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
this.parseContent(content, callback);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 处理单个SSE事件
|
|
102
|
-
*/
|
|
103
|
-
processSSEEvent(eventStr, callback) {
|
|
104
|
-
const lines = eventStr.split('\n');
|
|
105
|
-
|
|
106
|
-
for (const line of lines) {
|
|
107
|
-
// 解析 data: 行
|
|
108
|
-
if (line.startsWith('data:')) {
|
|
109
|
-
const data = line.substring(5).trim();
|
|
110
|
-
|
|
111
|
-
if (data === '[DONE]') {
|
|
112
|
-
this.flush(callback);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const parsed = JSON.parse(data);
|
|
118
|
-
const content = parsed?.choices?.[0]?.delta?.content;
|
|
119
|
-
|
|
120
|
-
if (content) {
|
|
121
|
-
this.metrics.chars += content.length;
|
|
122
|
-
|
|
123
|
-
if (this.options.debug) {
|
|
124
|
-
console.log('[StreamParser] 解析SSE内容:', content);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.parseContent(content, callback);
|
|
128
|
-
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (this.options.debug) {
|
|
131
|
-
console.warn('[StreamParser] JSON解析失败:', data, error);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
24
|
+
if (!chunk) return;
|
|
25
|
+
this.parseContent(chunk, callback);
|
|
136
26
|
}
|
|
137
27
|
|
|
138
|
-
|
|
139
|
-
* 使用状态机解析内容和标签
|
|
140
|
-
*/
|
|
28
|
+
// 核心内容解析,支持 <think> 标签
|
|
141
29
|
parseContent(content, callback) {
|
|
142
30
|
let i = 0;
|
|
143
|
-
|
|
31
|
+
|
|
144
32
|
while (i < content.length) {
|
|
145
33
|
if (this.inTag) {
|
|
146
|
-
// 在标签内部,查找标签结束
|
|
147
34
|
const endIndex = content.indexOf('>', i);
|
|
148
|
-
|
|
149
35
|
if (endIndex !== -1) {
|
|
150
|
-
// 找到标签结束
|
|
151
36
|
this.tagBuffer += content.substring(i, endIndex + 1);
|
|
152
37
|
this.handleTag(this.tagBuffer);
|
|
153
38
|
this.tagBuffer = '';
|
|
154
39
|
this.inTag = false;
|
|
155
40
|
i = endIndex + 1;
|
|
156
41
|
} else {
|
|
157
|
-
//
|
|
42
|
+
// 标签未闭合,超过50字符强制输出,防止阻塞
|
|
158
43
|
this.tagBuffer += content.substring(i);
|
|
44
|
+
if (this.tagBuffer.length > 50) {
|
|
45
|
+
this.appendText(this.tagBuffer);
|
|
46
|
+
this.tagBuffer = '';
|
|
47
|
+
this.inTag = false;
|
|
48
|
+
}
|
|
159
49
|
break;
|
|
160
50
|
}
|
|
161
51
|
} else {
|
|
162
|
-
// 不在标签内,查找标签开始
|
|
163
52
|
const startIndex = content.indexOf('<', i);
|
|
164
|
-
|
|
165
53
|
if (startIndex !== -1) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.appendText(content.substring(i, startIndex));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 检查是否是完整标签
|
|
54
|
+
if (startIndex > i) this.appendText(content.substring(i, startIndex));
|
|
55
|
+
|
|
172
56
|
const endIndex = content.indexOf('>', startIndex);
|
|
173
57
|
if (endIndex !== -1) {
|
|
174
|
-
// 完整标签
|
|
175
58
|
const tag = content.substring(startIndex, endIndex + 1);
|
|
176
59
|
this.handleTag(tag);
|
|
177
60
|
i = endIndex + 1;
|
|
178
61
|
} else {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
62
|
+
const nextChar = content[startIndex + 1];
|
|
63
|
+
if (!/[a-zA-Z/]/.test(nextChar)) {
|
|
64
|
+
// 很可能不是标签,直接当文本输出
|
|
65
|
+
this.appendText('<');
|
|
66
|
+
i = startIndex + 1;
|
|
67
|
+
} else {
|
|
68
|
+
this.inTag = true;
|
|
69
|
+
this.tagBuffer = content.substring(startIndex);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
183
72
|
}
|
|
184
73
|
} else {
|
|
185
|
-
// 没有标签,全部是文本
|
|
186
74
|
this.appendText(content.substring(i));
|
|
187
75
|
break;
|
|
188
76
|
}
|
|
189
77
|
}
|
|
190
78
|
}
|
|
191
79
|
|
|
192
|
-
// 定时批量更新
|
|
193
80
|
this.scheduleUpdate(callback);
|
|
194
81
|
}
|
|
195
82
|
|
|
196
|
-
|
|
197
|
-
* 处理标签
|
|
198
|
-
*/
|
|
83
|
+
// 处理标签
|
|
199
84
|
handleTag(tag) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
if (this.
|
|
203
|
-
console.log('[StreamParser] 处理标签:', tag);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (tagName === '<think>') {
|
|
207
|
-
this.status = 'thinking';
|
|
208
|
-
} else if (tagName === '</think>') {
|
|
209
|
-
this.status = 'output';
|
|
210
|
-
}
|
|
211
|
-
// 可扩展:支持更多标签类型
|
|
85
|
+
const t = tag.toLowerCase();
|
|
86
|
+
if (t === '<think>') this.status = 'thinking';
|
|
87
|
+
else if (t === '</think>') this.status = 'output';
|
|
212
88
|
}
|
|
213
89
|
|
|
214
|
-
|
|
215
|
-
* 添加文本到缓冲区
|
|
216
|
-
*/
|
|
90
|
+
// 添加文本到缓冲区
|
|
217
91
|
appendText(text) {
|
|
218
92
|
if (!text) return;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.thinkingBuffer.push(text);
|
|
222
|
-
} else {
|
|
223
|
-
this.contentBuffer.push(text);
|
|
224
|
-
}
|
|
93
|
+
if (this.status === 'thinking') this.thinkingBuffer.push(text);
|
|
94
|
+
else this.contentBuffer.push(text);
|
|
225
95
|
}
|
|
226
96
|
|
|
227
|
-
|
|
228
|
-
* 计划更新(防抖)
|
|
229
|
-
*/
|
|
97
|
+
// 防抖刷新
|
|
230
98
|
scheduleUpdate(callback) {
|
|
231
99
|
if (this.updateTimer) return;
|
|
232
|
-
|
|
233
100
|
this.updateTimer = setTimeout(() => {
|
|
234
101
|
this.flush(callback);
|
|
235
102
|
this.updateTimer = null;
|
|
236
103
|
}, this.options.updateInterval);
|
|
237
104
|
}
|
|
238
105
|
|
|
239
|
-
|
|
240
|
-
* 立即刷新缓冲区
|
|
241
|
-
*/
|
|
106
|
+
// 刷新缓冲区
|
|
242
107
|
flush(callback) {
|
|
243
|
-
if (!callback)
|
|
244
|
-
console.warn('[StreamParser] flush: callback 为空');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const hasThinking = this.thinkingBuffer.length > 0;
|
|
249
|
-
const hasContent = this.contentBuffer.length > 0;
|
|
250
|
-
|
|
251
|
-
if (!hasThinking && !hasContent) return;
|
|
252
|
-
|
|
253
|
-
// 使用 join 比字符串拼接性能更好
|
|
108
|
+
if (!callback) return;
|
|
254
109
|
const result = {
|
|
255
|
-
thinking:
|
|
256
|
-
content:
|
|
110
|
+
thinking: this.thinkingBuffer.length ? this.thinkingBuffer.join('') : null,
|
|
111
|
+
content: this.contentBuffer.length ? this.contentBuffer.join('') : null,
|
|
257
112
|
status: this.status
|
|
258
113
|
};
|
|
259
|
-
|
|
260
|
-
if (this.options.debug) {
|
|
261
|
-
console.log('[StreamParser] 刷新缓冲区:', {
|
|
262
|
-
thinking: result.thinking?.length || 0,
|
|
263
|
-
content: result.content?.length || 0,
|
|
264
|
-
status: result.status,
|
|
265
|
-
thinkingPreview: result.thinking?.substring(0, 50),
|
|
266
|
-
contentPreview: result.content?.substring(0, 50)
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// 清空缓冲区
|
|
271
114
|
this.thinkingBuffer = [];
|
|
272
115
|
this.contentBuffer = [];
|
|
273
|
-
|
|
274
|
-
// 回调更新
|
|
275
|
-
try {
|
|
276
|
-
callback(result);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
console.error('[StreamParser] 回调执行错误:', error);
|
|
279
|
-
}
|
|
116
|
+
callback(result);
|
|
280
117
|
}
|
|
281
118
|
|
|
282
|
-
|
|
283
|
-
* 完成解析
|
|
284
|
-
*/
|
|
119
|
+
// 完成解析
|
|
285
120
|
finish(callback) {
|
|
286
|
-
this.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
this.updateTimer = null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (this.options.debug) {
|
|
294
|
-
const duration = Date.now() - this.metrics.startTime;
|
|
295
|
-
console.log('[StreamParser] 解析完成:', {
|
|
296
|
-
耗时: `${duration}ms`,
|
|
297
|
-
数据块: this.metrics.chunks,
|
|
298
|
-
事件数: this.metrics.events,
|
|
299
|
-
字符数: this.metrics.chars,
|
|
300
|
-
平均速度: `${(this.metrics.chars / duration * 1000).toFixed(0)} chars/s`
|
|
301
|
-
});
|
|
121
|
+
if (this.inTag && this.tagBuffer) {
|
|
122
|
+
this.appendText(this.tagBuffer);
|
|
123
|
+
this.tagBuffer = '';
|
|
124
|
+
this.inTag = false;
|
|
302
125
|
}
|
|
126
|
+
this.flush(callback);
|
|
127
|
+
if (this.updateTimer) clearTimeout(this.updateTimer);
|
|
303
128
|
}
|
|
304
129
|
|
|
305
|
-
|
|
306
|
-
* 销毁解析器
|
|
307
|
-
*/
|
|
130
|
+
// 销毁解析器
|
|
308
131
|
destroy() {
|
|
309
|
-
if (this.updateTimer)
|
|
310
|
-
clearTimeout(this.updateTimer);
|
|
311
|
-
}
|
|
132
|
+
if (this.updateTimer) clearTimeout(this.updateTimer);
|
|
312
133
|
this.reset();
|
|
313
134
|
}
|
|
314
|
-
}
|
|
135
|
+
}
|