@base-web-kits/base-tools-ts 1.3.4 → 1.3.5
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/dist/buffer/PolyfillTextDecoder.d.ts +12 -0
- package/dist/buffer/PolyfillTextDecoder.d.ts.map +1 -0
- package/dist/buffer/SSEParser.d.ts +81 -0
- package/dist/buffer/SSEParser.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ts/buffer/PolyfillTextDecoder.ts +141 -0
- package/src/ts/buffer/SSEParser.ts +210 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模拟 TextDecoder 的简单实现
|
|
3
|
+
* @description 用于在不支持 TextDecoder 的环境(如小程序)中进行流式解码
|
|
4
|
+
*/
|
|
5
|
+
declare class PolyfillTextDecoder {
|
|
6
|
+
private leftOver;
|
|
7
|
+
decode(input?: ArrayBuffer | Uint8Array, options?: {
|
|
8
|
+
stream?: boolean;
|
|
9
|
+
}): string;
|
|
10
|
+
}
|
|
11
|
+
export default PolyfillTextDecoder;
|
|
12
|
+
//# sourceMappingURL=PolyfillTextDecoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PolyfillTextDecoder.d.ts","sourceRoot":"","sources":["../../src/ts/buffer/PolyfillTextDecoder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAM,mBAAmB;IACvB,OAAO,CAAC,QAAQ,CAAiC;IAEjD,MAAM,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM;CAmIjF;AAED,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** 流式数据消息 */
|
|
2
|
+
export type SSEMessage = {
|
|
3
|
+
/**
|
|
4
|
+
* SSE 消息类型
|
|
5
|
+
* - 'DONE' 流式数据接收完毕
|
|
6
|
+
* - 'thinking' 思考中
|
|
7
|
+
* - 'text' 文本内容
|
|
8
|
+
* - 其他业务类型
|
|
9
|
+
*/
|
|
10
|
+
type?: string;
|
|
11
|
+
/** SSE 事件 ID (如 'id:123') */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** SSE 建议重连时间 (毫秒, 如 'retry:5000') */
|
|
14
|
+
retry?: number;
|
|
15
|
+
/** SSE 事件类型 (如 'event:customEvent') */
|
|
16
|
+
event?: string;
|
|
17
|
+
/**
|
|
18
|
+
* 除type之外的其他data数据
|
|
19
|
+
* - 思考中 'data:{"type": "thinking", "content": "xx"}' -> {type: 'thinking', content: 'xx'}
|
|
20
|
+
* - JSON数据 'data:{"type": "text", "content": "xx"}' -> {type: 'text', content: 'xx'}
|
|
21
|
+
* - 非JSON数据 'data:xx' -> {raw: 'xx'}
|
|
22
|
+
* - 接收完毕 'data:[DONE]' -> {type: 'DONE'}
|
|
23
|
+
*/
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
};
|
|
26
|
+
/** 流式数据消息回调 */
|
|
27
|
+
export type MessageCallback = (msg: SSEMessage) => void;
|
|
28
|
+
/**
|
|
29
|
+
* 流式数据解析器
|
|
30
|
+
* - 对于不支持TextDecoder的环境,使用PolyfillTextDecoder (bufferToText流式增强版)
|
|
31
|
+
* @description 用于解析 SSE 流式数据,将其转换为 SSEMessage 格式并调用回调函数处理
|
|
32
|
+
* @param onMessage 消息回调
|
|
33
|
+
* @example
|
|
34
|
+
* const parser = new SSEParser((msg) => {
|
|
35
|
+
* console.log(msg);
|
|
36
|
+
* });
|
|
37
|
+
* parser.receive(arrayBuffer);
|
|
38
|
+
*/
|
|
39
|
+
export declare class SSEParser {
|
|
40
|
+
private buffer;
|
|
41
|
+
private onMessage;
|
|
42
|
+
private decoder;
|
|
43
|
+
private eventDataLines;
|
|
44
|
+
private eventType?;
|
|
45
|
+
private eventId?;
|
|
46
|
+
private eventRetry?;
|
|
47
|
+
constructor(onMessage: MessageCallback);
|
|
48
|
+
/**
|
|
49
|
+
* 接收流式数据
|
|
50
|
+
* @param buffer ArrayBuffer
|
|
51
|
+
*/
|
|
52
|
+
receive(buffer: ArrayBuffer): void;
|
|
53
|
+
/**
|
|
54
|
+
* 刷新解码器残留数据并处理尾部未换行的内容
|
|
55
|
+
*/
|
|
56
|
+
flush(): void;
|
|
57
|
+
/**
|
|
58
|
+
* 追加文本并按行拆分处理,保留不完整尾行
|
|
59
|
+
* @param text 新到达的文本片段
|
|
60
|
+
*/
|
|
61
|
+
private appendText;
|
|
62
|
+
/**
|
|
63
|
+
* 处理缓冲区中剩余的尾行内容
|
|
64
|
+
*/
|
|
65
|
+
private flushRemainder;
|
|
66
|
+
/**
|
|
67
|
+
* 解析每行 SSE 数据并触发回调
|
|
68
|
+
* @param lines 以换行切分后的行内容
|
|
69
|
+
*/
|
|
70
|
+
private processLines;
|
|
71
|
+
/**
|
|
72
|
+
* 将当前缓存的一次 SSE 事件分发给回调
|
|
73
|
+
* @description 以空行作为事件边界,将多行 data 合并后再解析;处理 "[DONE]" 结束标记;分发完成后会重置事件缓存
|
|
74
|
+
*/
|
|
75
|
+
private dispatchEvent;
|
|
76
|
+
/** 安全调用 onMessage 回调,捕获并打印错误 */
|
|
77
|
+
private safeOnMessage;
|
|
78
|
+
/** 重置当前 SSE 事件的临时缓存 */
|
|
79
|
+
private resetEvent;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=SSEParser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SSEParser.d.ts","sourceRoot":"","sources":["../../src/ts/buffer/SSEParser.ts"],"names":[],"mappings":"AAGA,aAAa;AACb,MAAM,MAAM,UAAU,GAAG;IACvB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,6BAA6B;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAEF,eAAe;AACf,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,CAAC;AAExD;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,UAAU,CAAC,CAAS;gBAEhB,SAAS,EAAE,eAAe;IAMtC;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW;IAK3B;;OAEG;IACH,KAAK;IAOL;;;OAGG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAqCpB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAyCrB,gCAAgC;IAChC,OAAO,CAAC,aAAa;IAQrB,uBAAuB;IACvB,OAAO,CAAC,UAAU;CAMnB"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模拟 TextDecoder 的简单实现
|
|
3
|
+
* @description 用于在不支持 TextDecoder 的环境(如小程序)中进行流式解码
|
|
4
|
+
*/
|
|
5
|
+
class PolyfillTextDecoder {
|
|
6
|
+
private leftOver: Uint8Array = new Uint8Array(0);
|
|
7
|
+
|
|
8
|
+
decode(input?: ArrayBuffer | Uint8Array, options?: { stream?: boolean }): string {
|
|
9
|
+
const stream = options?.stream ?? false;
|
|
10
|
+
let bytes: Uint8Array;
|
|
11
|
+
|
|
12
|
+
if (!input) {
|
|
13
|
+
bytes = new Uint8Array(0);
|
|
14
|
+
} else if (input instanceof ArrayBuffer) {
|
|
15
|
+
bytes = new Uint8Array(input);
|
|
16
|
+
} else {
|
|
17
|
+
bytes = input;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.leftOver.length > 0) {
|
|
21
|
+
const merged = new Uint8Array(this.leftOver.length + bytes.length);
|
|
22
|
+
merged.set(this.leftOver);
|
|
23
|
+
merged.set(bytes, this.leftOver.length);
|
|
24
|
+
bytes = merged;
|
|
25
|
+
this.leftOver = new Uint8Array(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const len = bytes.length;
|
|
29
|
+
if (len === 0) return '';
|
|
30
|
+
|
|
31
|
+
const parts: string[] = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
const replacement = '\uFFFD';
|
|
34
|
+
const isContinuationByte = (b: number) => (b & 0xc0) === 0x80;
|
|
35
|
+
|
|
36
|
+
while (i < len) {
|
|
37
|
+
const byte1 = bytes[i];
|
|
38
|
+
|
|
39
|
+
// 1字节字符 (0xxxxxxx)
|
|
40
|
+
if (byte1 < 0x80) {
|
|
41
|
+
parts.push(String.fromCharCode(byte1));
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
// 2字节字符 (110xxxxx 10xxxxxx)
|
|
45
|
+
else if (byte1 >= 0xc2 && byte1 < 0xe0) {
|
|
46
|
+
if (i + 1 >= len) {
|
|
47
|
+
if (stream) {
|
|
48
|
+
this.leftOver = bytes.slice(i);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
parts.push(replacement);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const byte2 = bytes[i + 1];
|
|
55
|
+
if (!isContinuationByte(byte2)) {
|
|
56
|
+
parts.push(replacement);
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
parts.push(String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f)));
|
|
61
|
+
i += 2;
|
|
62
|
+
}
|
|
63
|
+
// 3字节字符 (1110xxxx 10xxxxxx 10xxxxxx)
|
|
64
|
+
else if (byte1 >= 0xe0 && byte1 < 0xf0) {
|
|
65
|
+
if (i + 2 >= len) {
|
|
66
|
+
if (stream) {
|
|
67
|
+
this.leftOver = bytes.slice(i);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
parts.push(replacement);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
const byte2 = bytes[i + 1];
|
|
74
|
+
const byte3 = bytes[i + 2];
|
|
75
|
+
if (!isContinuationByte(byte2) || !isContinuationByte(byte3)) {
|
|
76
|
+
parts.push(replacement);
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (byte1 === 0xe0 && byte2 < 0xa0) {
|
|
81
|
+
parts.push(replacement);
|
|
82
|
+
i += 3;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (byte1 === 0xed && byte2 >= 0xa0) {
|
|
86
|
+
parts.push(replacement);
|
|
87
|
+
i += 3;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const codeUnit = ((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f);
|
|
91
|
+
parts.push(String.fromCharCode(codeUnit));
|
|
92
|
+
i += 3;
|
|
93
|
+
}
|
|
94
|
+
// 4字节字符 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) - 处理emoji等
|
|
95
|
+
else if (byte1 >= 0xf0 && byte1 <= 0xf4) {
|
|
96
|
+
if (i + 3 >= len) {
|
|
97
|
+
if (stream) {
|
|
98
|
+
this.leftOver = bytes.slice(i);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
parts.push(replacement);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const byte2 = bytes[i + 1];
|
|
105
|
+
const byte3 = bytes[i + 2];
|
|
106
|
+
const byte4 = bytes[i + 3];
|
|
107
|
+
if (
|
|
108
|
+
!isContinuationByte(byte2) ||
|
|
109
|
+
!isContinuationByte(byte3) ||
|
|
110
|
+
!isContinuationByte(byte4)
|
|
111
|
+
) {
|
|
112
|
+
parts.push(replacement);
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (byte1 === 0xf0 && byte2 < 0x90) {
|
|
117
|
+
parts.push(replacement);
|
|
118
|
+
i += 4;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (byte1 === 0xf4 && byte2 >= 0x90) {
|
|
122
|
+
parts.push(replacement);
|
|
123
|
+
i += 4;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const codepoint =
|
|
127
|
+
((byte1 & 0x07) << 18) | ((byte2 & 0x3f) << 12) | ((byte3 & 0x3f) << 6) | (byte4 & 0x3f);
|
|
128
|
+
const offset = codepoint - 0x10000;
|
|
129
|
+
parts.push(String.fromCharCode(0xd800 + (offset >> 10), 0xdc00 + (offset & 0x3ff)));
|
|
130
|
+
i += 4;
|
|
131
|
+
} else {
|
|
132
|
+
parts.push(replacement);
|
|
133
|
+
i += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return parts.join('');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default PolyfillTextDecoder;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { isPlainObject } from 'es-toolkit';
|
|
2
|
+
import PolyfillTextDecoder from './PolyfillTextDecoder';
|
|
3
|
+
|
|
4
|
+
/** 流式数据消息 */
|
|
5
|
+
export type SSEMessage = {
|
|
6
|
+
/**
|
|
7
|
+
* SSE 消息类型
|
|
8
|
+
* - 'DONE' 流式数据接收完毕
|
|
9
|
+
* - 'thinking' 思考中
|
|
10
|
+
* - 'text' 文本内容
|
|
11
|
+
* - 其他业务类型
|
|
12
|
+
*/
|
|
13
|
+
type?: string;
|
|
14
|
+
|
|
15
|
+
/** SSE 事件 ID (如 'id:123') */
|
|
16
|
+
id?: string;
|
|
17
|
+
|
|
18
|
+
/** SSE 建议重连时间 (毫秒, 如 'retry:5000') */
|
|
19
|
+
retry?: number;
|
|
20
|
+
|
|
21
|
+
/** SSE 事件类型 (如 'event:customEvent') */
|
|
22
|
+
event?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 除type之外的其他data数据
|
|
26
|
+
* - 思考中 'data:{"type": "thinking", "content": "xx"}' -> {type: 'thinking', content: 'xx'}
|
|
27
|
+
* - JSON数据 'data:{"type": "text", "content": "xx"}' -> {type: 'text', content: 'xx'}
|
|
28
|
+
* - 非JSON数据 'data:xx' -> {raw: 'xx'}
|
|
29
|
+
* - 接收完毕 'data:[DONE]' -> {type: 'DONE'}
|
|
30
|
+
*/
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** 流式数据消息回调 */
|
|
35
|
+
export type MessageCallback = (msg: SSEMessage) => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 流式数据解析器
|
|
39
|
+
* - 对于不支持TextDecoder的环境,使用PolyfillTextDecoder (bufferToText流式增强版)
|
|
40
|
+
* @description 用于解析 SSE 流式数据,将其转换为 SSEMessage 格式并调用回调函数处理
|
|
41
|
+
* @param onMessage 消息回调
|
|
42
|
+
* @example
|
|
43
|
+
* const parser = new SSEParser((msg) => {
|
|
44
|
+
* console.log(msg);
|
|
45
|
+
* });
|
|
46
|
+
* parser.receive(arrayBuffer);
|
|
47
|
+
*/
|
|
48
|
+
export class SSEParser {
|
|
49
|
+
private buffer: string = '';
|
|
50
|
+
private onMessage: MessageCallback;
|
|
51
|
+
private decoder: TextDecoder | PolyfillTextDecoder;
|
|
52
|
+
private eventDataLines: string[] = [];
|
|
53
|
+
private eventType?: string;
|
|
54
|
+
private eventId?: string;
|
|
55
|
+
private eventRetry?: number;
|
|
56
|
+
|
|
57
|
+
constructor(onMessage: MessageCallback) {
|
|
58
|
+
this.onMessage = onMessage;
|
|
59
|
+
this.decoder =
|
|
60
|
+
typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8') : new PolyfillTextDecoder();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 接收流式数据
|
|
65
|
+
* @param buffer ArrayBuffer
|
|
66
|
+
*/
|
|
67
|
+
receive(buffer: ArrayBuffer) {
|
|
68
|
+
const text = this.decoder.decode(new Uint8Array(buffer), { stream: true });
|
|
69
|
+
this.appendText(text);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 刷新解码器残留数据并处理尾部未换行的内容
|
|
74
|
+
*/
|
|
75
|
+
flush() {
|
|
76
|
+
const tail = this.decoder.decode(undefined, { stream: false });
|
|
77
|
+
if (tail) this.appendText(tail);
|
|
78
|
+
|
|
79
|
+
this.flushRemainder();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 追加文本并按行拆分处理,保留不完整尾行
|
|
84
|
+
* @param text 新到达的文本片段
|
|
85
|
+
*/
|
|
86
|
+
private appendText(text: string) {
|
|
87
|
+
this.buffer += text;
|
|
88
|
+
|
|
89
|
+
const lines = this.buffer.split(/\r?\n/);
|
|
90
|
+
this.buffer = lines.pop() || '';
|
|
91
|
+
|
|
92
|
+
this.processLines(lines);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 处理缓冲区中剩余的尾行内容
|
|
97
|
+
*/
|
|
98
|
+
private flushRemainder() {
|
|
99
|
+
if (!this.buffer.trim()) {
|
|
100
|
+
this.buffer = '';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const rest = this.buffer;
|
|
104
|
+
this.buffer = '';
|
|
105
|
+
this.processLines([rest, '']);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 解析每行 SSE 数据并触发回调
|
|
110
|
+
* @param lines 以换行切分后的行内容
|
|
111
|
+
*/
|
|
112
|
+
private processLines(lines: string[]) {
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
if (!line.trim()) {
|
|
115
|
+
this.dispatchEvent();
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (line.startsWith(':')) continue;
|
|
120
|
+
|
|
121
|
+
const colonIndex = line.indexOf(':');
|
|
122
|
+
const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
|
|
123
|
+
let value = colonIndex === -1 ? '' : line.slice(colonIndex + 1);
|
|
124
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
125
|
+
|
|
126
|
+
if (field === 'data') {
|
|
127
|
+
this.eventDataLines.push(value);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (field === 'event') {
|
|
132
|
+
this.eventType = value || undefined;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (field === 'id') {
|
|
137
|
+
this.eventId = value || undefined;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (field === 'retry') {
|
|
142
|
+
const retry = Number(value);
|
|
143
|
+
this.eventRetry = Number.isFinite(retry) ? retry : undefined;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 将当前缓存的一次 SSE 事件分发给回调
|
|
151
|
+
* @description 以空行作为事件边界,将多行 data 合并后再解析;处理 "[DONE]" 结束标记;分发完成后会重置事件缓存
|
|
152
|
+
*/
|
|
153
|
+
private dispatchEvent() {
|
|
154
|
+
if (!this.eventDataLines.length) {
|
|
155
|
+
this.resetEvent();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const data = this.eventDataLines.join('\n');
|
|
160
|
+
|
|
161
|
+
if (!data) {
|
|
162
|
+
this.resetEvent();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (data.trim() === '[DONE]') {
|
|
167
|
+
this.safeOnMessage({ type: 'DONE' });
|
|
168
|
+
this.resetEvent();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let msg: SSEMessage;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// 尝试解析 JSON 数据
|
|
176
|
+
const json = JSON.parse(data);
|
|
177
|
+
// 'data:{"type": "text", "content": "xx"}' -> {type: 'text', content: 'xx'}
|
|
178
|
+
// 'data:[1,2,3]' -> {data: [1,2,3]}
|
|
179
|
+
msg = isPlainObject(json) ? json : { data: json };
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// 解析失败时,返回原始数据。'data:xx' -> {raw: 'xx'}
|
|
182
|
+
msg = { raw: data };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (this.eventType && msg.event === undefined) msg.event = this.eventType;
|
|
186
|
+
if (this.eventType && msg.type === undefined) msg.type = this.eventType;
|
|
187
|
+
if (this.eventId && msg.id === undefined) msg.id = this.eventId;
|
|
188
|
+
if (this.eventRetry !== undefined && msg.retry === undefined) msg.retry = this.eventRetry;
|
|
189
|
+
|
|
190
|
+
this.safeOnMessage(msg);
|
|
191
|
+
this.resetEvent();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 安全调用 onMessage 回调,捕获并打印错误 */
|
|
195
|
+
private safeOnMessage(msg: SSEMessage) {
|
|
196
|
+
try {
|
|
197
|
+
this.onMessage(msg);
|
|
198
|
+
} catch (onMessageError) {
|
|
199
|
+
console.error('SSEParser onMessage error:', onMessageError);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 重置当前 SSE 事件的临时缓存 */
|
|
204
|
+
private resetEvent() {
|
|
205
|
+
this.eventDataLines = [];
|
|
206
|
+
this.eventType = undefined;
|
|
207
|
+
this.eventId = undefined;
|
|
208
|
+
this.eventRetry = undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|