This commit is contained in:
xiamuceer
2025-10-30 11:14:43 +08:00
parent b97410d973
commit 0f6c2d344a
91 changed files with 22309 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
export interface SSEMessage {
type: 'progress' | 'chunk' | 'result' | 'error' | 'done';
message?: string;
progress?: number;
status?: 'processing' | 'success' | 'error' | 'warning';
content?: string;
data?: any;
error?: string;
code?: number;
}
export interface SSEClientOptions {
onProgress?: (message: string, progress: number, status: string) => void;
onChunk?: (content: string) => void;
onResult?: (data: any) => void;
onError?: (error: string, code?: number) => void;
onComplete?: () => void;
onConnectionError?: (error: Event) => void;
}
export class SSEClient {
private eventSource: EventSource | null = null;
private url: string;
private options: SSEClientOptions;
private accumulatedContent: string = '';
constructor(url: string, options: SSEClientOptions = {}) {
this.url = url;
this.options = options;
}
connect(): Promise<any> {
return new Promise((resolve, reject) => {
try {
this.eventSource = new EventSource(this.url);
this.eventSource.onmessage = (event) => {
try {
const message: SSEMessage = JSON.parse(event.data);
this.handleMessage(message, resolve, reject);
} catch (error) {
console.error('解析SSE消息失败:', error);
}
};
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
if (this.options.onConnectionError) {
this.options.onConnectionError(error);
}
this.close();
reject(new Error('SSE连接失败'));
};
} catch (error) {
reject(error);
}
});
}
private handleMessage(message: SSEMessage, resolve: Function, reject: Function) {
switch (message.type) {
case 'progress':
if (this.options.onProgress && message.message && message.progress !== undefined) {
this.options.onProgress(message.message, message.progress, message.status || 'processing');
}
break;
case 'chunk':
if (message.content) {
this.accumulatedContent += message.content;
if (this.options.onChunk) {
this.options.onChunk(message.content);
}
}
break;
case 'result':
if (this.options.onResult && message.data) {
this.options.onResult(message.data);
}
break;
case 'error':
if (this.options.onError) {
this.options.onError(message.error || '未知错误', message.code);
}
this.close();
reject(new Error(message.error || '未知错误'));
break;
case 'done':
if (this.options.onComplete) {
this.options.onComplete();
}
this.close();
if (!this.options.onResult && this.accumulatedContent) {
resolve({ content: this.accumulatedContent });
} else {
resolve(true);
}
break;
}
}
close() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
getAccumulatedContent(): string {
return this.accumulatedContent;
}
}
export class SSEPostClient {
private url: string;
private data: any;
private options: SSEClientOptions;
private abortController: AbortController | null = null;
private accumulatedContent: string = '';
constructor(url: string, data: any, options: SSEClientOptions = {}) {
this.url = url;
this.data = data;
this.options = options;
}
async connect(): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
this.abortController = new AbortController();
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.data),
signal: this.abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('无法获取响应流');
}
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '' || line.startsWith(':')) {
continue;
}
try {
const dataMatch = line.match(/^data: (.+)$/m);
if (dataMatch) {
const message: SSEMessage = JSON.parse(dataMatch[1]);
await this.handleMessage(message, resolve, reject);
}
} catch (error) {
console.error('解析SSE消息失败:', error, line);
}
}
}
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('SSE POST请求失败:', error);
if (this.options.onError) {
this.options.onError(error.message || '请求失败');
}
reject(error);
}
}
});
}
private async handleMessage(message: SSEMessage, resolve: Function, reject: Function) {
switch (message.type) {
case 'progress':
if (this.options.onProgress && message.message && message.progress !== undefined) {
this.options.onProgress(message.message, message.progress, message.status || 'processing');
}
break;
case 'chunk':
if (message.content) {
this.accumulatedContent += message.content;
if (this.options.onChunk) {
this.options.onChunk(message.content);
}
}
break;
case 'result':
if (this.options.onResult && message.data) {
this.options.onResult(message.data);
}
(this as any).resultData = message.data;
break;
case 'error':
if (this.options.onError) {
this.options.onError(message.error || '未知错误', message.code);
}
reject(new Error(message.error || '未知错误'));
break;
case 'done':
if (this.options.onComplete) {
this.options.onComplete();
}
if ((this as any).resultData) {
resolve((this as any).resultData);
} else if (this.accumulatedContent) {
resolve({ content: this.accumulatedContent });
} else {
resolve(true);
}
break;
}
}
abort() {
if (this.abortController) {
this.abortController.abort();
}
}
getAccumulatedContent(): string {
return this.accumulatedContent;
}
}
export async function ssePost<T = any>(
url: string,
data: any,
options: SSEClientOptions = {}
): Promise<T> {
const client = new SSEPostClient(url, data, options);
try {
return await client.connect();
} finally {
client.abort();
}
}