Files
map-client-vue/web/src/services/SSETransport.ts
2025-10-14 14:18:20 +08:00

247 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { v4 as uuidv4 } from 'uuid';
/**
* SSE (Server-Sent Events) 传输层实现
* 用于MCP协议的单向数据流传输
*/
export class SSETransport {
private eventSource: EventSource | null = null;
private url: string;
private pendingRequests = new Map<string, { resolve: Function; reject: Function; timeout: NodeJS.Timeout }>();
private listeners = new Map<string, Function[]>();
private connected = false;
constructor(url: string) {
this.url = url;
}
async connect(): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
// 首先建立SSE连接获取sessionId
console.log('📡 连接SSE端点:', this.url);
// 第一步连接SSE获取endpoint信息
this.eventSource = new EventSource(this.url);
let resolveTimeout: NodeJS.Timeout;
this.eventSource.addEventListener('endpoint', (event: any) => {
const endpointData = event.data;
console.log('✅ 收到SSE endpoint:', endpointData);
// 提取sessionId格式: /message?sessionId=xxx
const match = endpointData.match(/sessionId=([^&]+)/);
if (match) {
const sessionId = match[1];
console.log('📝 SSE sessionId:', sessionId);
// 保存sessionId以便后续请求使用
(this as any).sessionId = sessionId;
}
this.connected = true;
resolve();
});
this.eventSource.onopen = () => {
console.log('📡 SSE连接已打开');
// 设置超时如果10秒内没有收到endpoint事件则认为失败
resolveTimeout = setTimeout(() => {
if (!this.connected) {
reject(new Error('SSE连接超时未收到endpoint'));
}
}, 10000);
};
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('📨 收到SSE消息:', data);
this.handleMessage(data);
} catch (error) {
// 如果不是JSON可能是普通文本消息
console.log('📨 收到SSE文本消息:', event.data);
}
};
this.eventSource.onerror = (error) => {
console.error('❌ SSE连接错误:', error);
this.connected = false;
this.emit('disconnected');
if (this.eventSource?.readyState === EventSource.CLOSED) {
reject(new Error('SSE连接失败'));
}
};
// 监听message事件MCP响应
this.eventSource.addEventListener('message', (event: any) => {
try {
const message = JSON.parse(event.data);
console.log('📨 收到MCP消息:', message);
this.handleMessage(message);
} catch (error) {
console.error('❌ MCP消息解析失败:', error, event.data);
}
});
// 清理resolve超时
this.eventSource.addEventListener('endpoint', () => {
if (resolveTimeout) {
clearTimeout(resolveTimeout);
}
});
} catch (error) {
console.error('❌ 创建SSE连接失败:', error);
reject(error);
}
});
}
async sendRequest(method: string, params?: any): Promise<any> {
const id = uuidv4();
const request = {
jsonrpc: '2.0',
id,
method,
params: params || {}
};
return new Promise(async (resolve, reject) => {
// 设置超时
const timeout = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`SSE请求超时: ${method}`));
}, 30000); // 30秒超时
this.pendingRequests.set(id, { resolve, reject, timeout });
try {
console.log(`📤 发送SSE请求 (${method}):`, request);
// 获取sessionId
const sessionId = (this as any).sessionId;
if (!sessionId) {
throw new Error('SSE sessionId未就绪');
}
// 根据服务器endpoint构建URL
// 例如: http://localhost:3200/message?sessionId=xxx
const baseUrl = this.url.replace('/sse', '');
const messageUrl = `${baseUrl}/message?sessionId=${sessionId}`;
console.log(`📤 发送到: ${messageUrl}`);
// SSE模式通过HTTP POST发送请求到/message端点响应通过SSE事件流返回
const response = await fetch(messageUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(request)
});
if (!response.ok) {
clearTimeout(timeout);
this.pendingRequests.delete(id);
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
}
// 对于某些简单请求可能直接返回JSON响应
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
clearTimeout(timeout);
this.pendingRequests.delete(id);
const result = await response.json();
if (result.error) {
reject(new Error(result.error.message || '请求失败'));
} else {
resolve(result.result);
}
}
// 否则等待SSE响应
} catch (error) {
const pending = this.pendingRequests.get(id);
if (pending) {
clearTimeout(pending.timeout);
this.pendingRequests.delete(id);
}
reject(error);
}
});
}
private handleMessage(message: any): void {
if (message.id && this.pendingRequests.has(message.id)) {
const pending = this.pendingRequests.get(message.id)!;
clearTimeout(pending.timeout);
this.pendingRequests.delete(message.id);
if (message.error) {
pending.reject(new Error(message.error.message || '请求失败'));
} else {
pending.resolve(message.result);
}
} else if (!message.id && message.method) {
// 处理通知消息
console.log('📢 收到通知:', message.method, message.params);
this.emit('notification', message);
}
}
on(event: string, callback: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
off(event: string, callback: Function): void {
const callbacks = this.listeners.get(event) || [];
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
private emit(event: string, data?: any): void {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('❌ SSE事件回调错误:', error);
}
});
}
async disconnect(): Promise<void> {
console.log('🔌 断开SSE连接');
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.connected = false;
// 清理待处理的请求
this.pendingRequests.forEach(pending => {
clearTimeout(pending.timeout);
pending.reject(new Error('连接已断开'));
});
this.pendingRequests.clear();
this.listeners.clear();
}
get isConnected(): boolean {
return this.connected && this.eventSource?.readyState === EventSource.OPEN;
}
}