byt-lingxiao-ai 0.2.5 → 0.2.7

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * SSE (Server-Sent Events) 流解析器
3
+ * 优化点:
4
+ * 1. 使用状态机模式处理标签解析
5
+ * 2. 批量更新减少DOM操作
6
+ * 3. 内存优化:避免频繁字符串拼接
7
+ * 4. 可配置的更新频率
8
+ */
9
+ export class StreamParser {
10
+ constructor(options = {}) {
11
+ this.options = {
12
+ updateInterval: 16, // 更新间隔(ms),默认约60fps
13
+ batchSize: 100, // 批处理大小
14
+ debug: false, // 是否开启调试
15
+ ...options
16
+ };
17
+
18
+ this.reset();
19
+ }
20
+
21
+ /**
22
+ * 重置解析器状态
23
+ */
24
+ reset() {
25
+ this.buffer = '';
26
+ this.contentBuffer = []; // 使用数组缓冲,避免频繁字符串拼接
27
+ this.thinkingBuffer = [];
28
+ this.inTag = false;
29
+ this.tagBuffer = '';
30
+ this.updateTimer = null;
31
+ this.status = 'output'; // thinking | output
32
+ this.metrics = {
33
+ startTime: Date.now(),
34
+ chunks: 0,
35
+ events: 0,
36
+ chars: 0
37
+ };
38
+ }
39
+
40
+ /**
41
+ * 处理流数据块
42
+ * @param {string} chunk - 接收到的数据块
43
+ * @param {Function} callback - 更新回调函数
44
+ */
45
+ processChunk(chunk, callback) {
46
+ this.metrics.chunks++;
47
+ this.buffer += chunk;
48
+
49
+ // SSE 格式:事件由双换行符分隔
50
+ const events = this.buffer.split('\n\n');
51
+
52
+ // 保留最后一个可能不完整的事件
53
+ this.buffer = events.pop() || '';
54
+
55
+ // 处理完整的事件
56
+ for (const event of events) {
57
+ if (event.trim()) {
58
+ this.metrics.events++;
59
+ this.processEvent(event, callback);
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 处理单个SSE事件
66
+ */
67
+ processEvent(eventStr, callback) {
68
+ const lines = eventStr.split('\n');
69
+
70
+ for (const line of lines) {
71
+ // 解析 data: 行
72
+ if (line.startsWith('data:')) {
73
+ const data = line.substring(5).trim();
74
+
75
+ if (data === '[DONE]') {
76
+ this.flush(callback);
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const parsed = JSON.parse(data);
82
+ const content = parsed?.choices?.[0]?.delta?.content;
83
+
84
+ if (content) {
85
+ this.metrics.chars += content.length;
86
+ this.parseContent(content, callback);
87
+ }
88
+ } catch (error) {
89
+ if (this.options.debug) {
90
+ console.warn('[StreamParser] JSON解析失败:', data);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 使用状态机解析内容和标签
99
+ */
100
+ parseContent(content, callback) {
101
+ let i = 0;
102
+
103
+ while (i < content.length) {
104
+ if (this.inTag) {
105
+ // 在标签内部,查找标签结束
106
+ const endIndex = content.indexOf('>', i);
107
+
108
+ if (endIndex !== -1) {
109
+ // 找到标签结束
110
+ this.tagBuffer += content.substring(i, endIndex + 1);
111
+ this.handleTag(this.tagBuffer);
112
+ this.tagBuffer = '';
113
+ this.inTag = false;
114
+ i = endIndex + 1;
115
+ } else {
116
+ // 标签未结束,缓存剩余部分
117
+ this.tagBuffer += content.substring(i);
118
+ break;
119
+ }
120
+ } else {
121
+ // 不在标签内,查找标签开始
122
+ const startIndex = content.indexOf('<', i);
123
+
124
+ if (startIndex !== -1) {
125
+ // 找到标签开始,先处理前面的文本
126
+ if (startIndex > i) {
127
+ this.appendText(content.substring(i, startIndex));
128
+ }
129
+
130
+ // 检查是否是完整标签
131
+ const endIndex = content.indexOf('>', startIndex);
132
+ if (endIndex !== -1) {
133
+ // 完整标签
134
+ const tag = content.substring(startIndex, endIndex + 1);
135
+ this.handleTag(tag);
136
+ i = endIndex + 1;
137
+ } else {
138
+ // 不完整标签,标记进入标签状态
139
+ this.inTag = true;
140
+ this.tagBuffer = content.substring(startIndex);
141
+ break;
142
+ }
143
+ } else {
144
+ // 没有标签,全部是文本
145
+ this.appendText(content.substring(i));
146
+ break;
147
+ }
148
+ }
149
+ }
150
+
151
+ // 定时批量更新
152
+ this.scheduleUpdate(callback);
153
+ }
154
+
155
+ /**
156
+ * 处理标签
157
+ */
158
+ handleTag(tag) {
159
+ const tagName = tag.toLowerCase();
160
+
161
+ if (tagName === '<think>') {
162
+ this.status = 'thinking';
163
+ } else if (tagName === '</think>') {
164
+ this.status = 'output';
165
+ }
166
+ // 可扩展:支持更多标签类型
167
+ // else if (tagName.startsWith('<code')) {
168
+ // this.status = 'code';
169
+ // }
170
+ }
171
+
172
+ /**
173
+ * 添加文本到缓冲区
174
+ */
175
+ appendText(text) {
176
+ if (!text) return;
177
+
178
+ if (this.status === 'thinking') {
179
+ this.thinkingBuffer.push(text);
180
+ } else {
181
+ this.contentBuffer.push(text);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 计划更新(防抖)
187
+ */
188
+ scheduleUpdate(callback) {
189
+ if (this.updateTimer) return;
190
+
191
+ this.updateTimer = setTimeout(() => {
192
+ this.flush(callback);
193
+ this.updateTimer = null;
194
+ }, this.options.updateInterval);
195
+ }
196
+
197
+ /**
198
+ * 立即刷新缓冲区
199
+ */
200
+ flush(callback) {
201
+ if (!callback) return;
202
+
203
+ const hasThinking = this.thinkingBuffer.length > 0;
204
+ const hasContent = this.contentBuffer.length > 0;
205
+
206
+ if (!hasThinking && !hasContent) return;
207
+
208
+ // 使用 join 比字符串拼接性能更好
209
+ const result = {
210
+ thinking: hasThinking ? this.thinkingBuffer.join('') : null,
211
+ content: hasContent ? this.contentBuffer.join('') : null,
212
+ status: this.status
213
+ };
214
+
215
+ // 清空缓冲区
216
+ this.thinkingBuffer = [];
217
+ this.contentBuffer = [];
218
+
219
+ // 回调更新
220
+ callback(result);
221
+ }
222
+
223
+ /**
224
+ * 完成解析
225
+ */
226
+ finish(callback) {
227
+ this.flush(callback);
228
+
229
+ if (this.updateTimer) {
230
+ clearTimeout(this.updateTimer);
231
+ this.updateTimer = null;
232
+ }
233
+
234
+ if (this.options.debug) {
235
+ const duration = Date.now() - this.metrics.startTime;
236
+ console.log('[StreamParser] 解析完成:', {
237
+ 耗时: `${duration}ms`,
238
+ 数据块: this.metrics.chunks,
239
+ 事件数: this.metrics.events,
240
+ 字符数: this.metrics.chars,
241
+ 平均速度: `${(this.metrics.chars / duration * 1000).toFixed(0)} chars/s`
242
+ });
243
+ }
244
+ }
245
+
246
+ /**
247
+ * 销毁解析器
248
+ */
249
+ destroy() {
250
+ if (this.updateTimer) {
251
+ clearTimeout(this.updateTimer);
252
+ }
253
+ this.reset();
254
+ }
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
+ */