From 935c66220301c5df01a2a9ac9351df35a56ebfcc Mon Sep 17 00:00:00 2001 From: dzq Date: Fri, 19 Dec 2025 17:19:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=B7=BB=E5=8A=A0=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=AF=B9=E8=AF=9DAPI=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现基于SSE的流式对话接口,支持消息发送和回调处理 添加@microsoft/fetch-event-source依赖 --- package.json | 1 + pnpm-lock.yaml | 8 ++++ src/api/agent/index.ts | 98 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 src/api/agent/index.ts diff --git a/package.json b/package.json index e1a150d..ae0130c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "prepare": "" }, "dependencies": { + "@microsoft/fetch-event-source": "^2.0.1", "@unhead/vue": "2.0.19", "@vant/touch-emulator": "^1.4.0", "@vant/use": "^1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d00887..ad09512 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: dependencies: + '@microsoft/fetch-event-source': + specifier: ^2.0.1 + version: 2.0.1 '@unhead/vue': specifier: 2.0.19 version: 2.0.19(vue@3.5.25(typescript@5.9.3)) @@ -1017,6 +1020,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@microsoft/fetch-event-source@2.0.1': + resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -5053,6 +5059,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@microsoft/fetch-event-source@2.0.1': {} + '@noble/hashes@1.8.0': {} '@paralleldrive/cuid2@2.3.1': diff --git a/src/api/agent/index.ts b/src/api/agent/index.ts new file mode 100644 index 0000000..d60291a --- /dev/null +++ b/src/api/agent/index.ts @@ -0,0 +1,98 @@ +const AGENT_BASE_URL = ''; + +import { fetchEventSource } from '@microsoft/fetch-event-source'; + +export const baseInfoApi = () => { + return fetch(`${AGENT_BASE_URL}`, { + method: 'GET', + }); +} + +/** + * 流式对话接口,返回流式 JSON 数据,形如 {"text":"我来"} + * @param message 用户消息 + * @param callbacks 回调函数对象 + * @param options 可选配置,包含 abort signal + * @example + * // 使用示例: + * streamApi('你好', { + * onopen(response) { + * console.log('连接已建立', response); + * }, + * onmessage(data) { + * // data 是服务器发送的原始字符串,如 '{"text":"我来"}' + * console.log('收到消息:', data); + * }, + * onclose() { + * console.log('连接已关闭'); + * }, + * onerror(error) { + * console.error('连接错误:', error); + * } + * }, { signal: abortController.signal }); + */ +export const streamApi = ( + message: string, + callbacks: { + onopen?: (response: Response) => void; + onmessage?: (data: string) => void; + onclose?: () => void; + onerror?: (error: Error) => void; + }, + options?: { signal?: AbortSignal } +) => { + // 启动 fetchEventSource + fetchEventSource(`${AGENT_BASE_URL}/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message }), + signal: options?.signal, + async onopen(response) { + // 检查响应是否正常 + if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) { + // 连接成功,调用 onopen 回调 + callbacks.onopen?.(response); + } else { + // 如果响应不正常,抛出错误 + throw new Error(`SSE 连接失败: ${response.status} ${response.statusText}`); + } + }, + onmessage(msg) { + // 处理服务器发送的消息 + // msg.data 应该是 JSON 字符串,如 {"text":"..."} + try { + const data = msg.data; + console.log('Received message:', data); + if (data && data !== '[DONE]') { + callbacks.onmessage?.(data); + } + } catch (error) { + console.warn('处理 SSE 消息时出错:', error); + } + }, + onclose() { + // 连接关闭 + console.log('SSE 连接已关闭'); + callbacks.onclose?.(); + }, + onerror(err) { + // 发生错误 + console.error('SSE 连接错误:', err); + callbacks.onerror?.(err instanceof Error ? err : new Error(String(err))); + + // 重新抛出错误以停止重试 + throw err; + }, + }).catch(error => { + // 捕获 fetchEventSource 的 Promise 拒绝 + if (error.name !== 'AbortError') { + console.error('fetchEventSource 错误:', error); + callbacks.onerror?.(error instanceof Error ? error : new Error(String(error))); + } else { + // AbortError 是正常的取消操作,不需要调用 onerror + console.log('请求已被取消'); + } + }); +} \ No newline at end of file