179 lines
4.7 KiB
TypeScript
179 lines
4.7 KiB
TypeScript
/* eslint-disable */
|
|
// @ts-ignore
|
|
import type { CustomRequestOptions } from '@/http/types';
|
|
|
|
type AnyRecord = Record<string, any>;
|
|
|
|
type StreamCallbacks = {
|
|
onToken?: (text: string) => void;
|
|
onDone?: () => void;
|
|
onError?: (err: unknown) => void;
|
|
onStart?: (payload: Record<string, any>) => void;
|
|
onTool?: (payload: Record<string, any>) => void;
|
|
onChunk?: (payload: Record<string, any>) => void;
|
|
onEnd?: (payload: Record<string, any>) => void;
|
|
};
|
|
|
|
export type AiChatStreamQuery = {
|
|
content: string;
|
|
conversationId?: string;
|
|
};
|
|
|
|
function decodeChunk(data: ArrayBuffer | string): string {
|
|
if (typeof data === 'string') return data;
|
|
try {
|
|
return new TextDecoder('utf-8').decode(data);
|
|
} catch (e) {
|
|
const arr = new Uint8Array(data);
|
|
let str = '';
|
|
for (let i = 0; i < arr.length; i += 1) str += String.fromCharCode(arr[i]);
|
|
return decodeURIComponent(escape(str));
|
|
}
|
|
}
|
|
|
|
function parseSsePayload(raw: string): string {
|
|
const line = raw.trim();
|
|
if (!line) return '';
|
|
if (!line.startsWith('data:')) return line;
|
|
const payload = line.slice(5).trim();
|
|
if (!payload || payload === '[DONE]') return '';
|
|
try {
|
|
const json = JSON.parse(payload);
|
|
return (
|
|
json?.content ??
|
|
json?.delta?.content ??
|
|
json?.message ??
|
|
json?.data?.content ??
|
|
''
|
|
);
|
|
} catch (e) {
|
|
return payload;
|
|
}
|
|
}
|
|
|
|
function parseSseEventBlock(block: string): { event: string; payload: Record<string, any> | null } | null {
|
|
const lines = block
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
if (!lines.length) return null;
|
|
|
|
let eventName = 'message';
|
|
const dataLines: string[] = [];
|
|
lines.forEach((line) => {
|
|
if (line.startsWith('event:')) {
|
|
eventName = line.slice(6).trim() || 'message';
|
|
} else if (line.startsWith('data:')) {
|
|
dataLines.push(line.slice(5).trim());
|
|
}
|
|
});
|
|
|
|
if (!dataLines.length) return { event: eventName, payload: null };
|
|
const raw = dataLines.join('\n');
|
|
try {
|
|
return { event: eventName, payload: JSON.parse(raw) };
|
|
} catch (e) {
|
|
return { event: eventName, payload: { text: raw } };
|
|
}
|
|
}
|
|
|
|
function dispatchSseEvent(
|
|
parsed: { event: string; payload: Record<string, any> | null } | null,
|
|
callbacks?: StreamCallbacks,
|
|
) {
|
|
if (!parsed) return;
|
|
const payload = parsed.payload || {};
|
|
switch (parsed.event) {
|
|
case 'start':
|
|
callbacks?.onStart?.(payload);
|
|
break;
|
|
case 'tool':
|
|
callbacks?.onTool?.(payload);
|
|
break;
|
|
case 'chunk':
|
|
callbacks?.onChunk?.(payload);
|
|
if (!callbacks?.onChunk && typeof payload.text === 'string' && payload.text) {
|
|
callbacks?.onToken?.(payload.text);
|
|
}
|
|
break;
|
|
case 'end':
|
|
callbacks?.onEnd?.(payload);
|
|
callbacks?.onDone?.();
|
|
break;
|
|
default:
|
|
// 兼容未知 event 或普通 message
|
|
if (typeof payload.text === 'string' && payload.text) {
|
|
callbacks?.onToken?.(payload.text);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AI 流式对话 GET /app/ai/chat/stream
|
|
* 注意:不同端对 chunk 支持程度不同,不支持时会在 success 中返回完整文本。
|
|
*/
|
|
export function appAiChatStreamGet({
|
|
query,
|
|
callbacks,
|
|
options,
|
|
}: {
|
|
query: AiChatStreamQuery;
|
|
callbacks?: StreamCallbacks;
|
|
options?: CustomRequestOptions;
|
|
}) {
|
|
let buffer = '';
|
|
let gotChunk = false;
|
|
|
|
const task = uni.request({
|
|
url: '/app/ai/chat/stream',
|
|
method: 'GET',
|
|
data: query,
|
|
enableChunked: true as any,
|
|
responseType: 'text',
|
|
timeout: options?.timeout ?? 60000,
|
|
header: {
|
|
...(options?.header || {}),
|
|
},
|
|
success(res) {
|
|
// 部分端不支持 chunk 回调,这里兜底按整段文本处理
|
|
if (gotChunk) {
|
|
return;
|
|
}
|
|
const raw = typeof res.data === 'string' ? res.data : JSON.stringify(res.data || '');
|
|
const normalized = raw.replace(/\r\n/g, '\n');
|
|
const blocks = normalized.split('\n\n').filter(Boolean);
|
|
if (!blocks.length) {
|
|
if (raw) callbacks?.onToken?.(raw);
|
|
callbacks?.onDone?.();
|
|
return;
|
|
}
|
|
blocks.forEach((block) => {
|
|
dispatchSseEvent(parseSseEventBlock(block), callbacks);
|
|
});
|
|
},
|
|
fail(err) {
|
|
callbacks?.onError?.(err);
|
|
},
|
|
});
|
|
|
|
const taskAny = task as any;
|
|
if (taskAny && typeof taskAny.onChunkReceived === 'function') {
|
|
taskAny.onChunkReceived((chunk: { data: ArrayBuffer | string }) => {
|
|
gotChunk = true;
|
|
buffer += decodeChunk(chunk.data).replace(/\r\n/g, '\n');
|
|
const blocks = buffer.split('\n\n');
|
|
buffer = blocks.pop() || '';
|
|
blocks.forEach((block) => {
|
|
dispatchSseEvent(parseSseEventBlock(block), callbacks);
|
|
});
|
|
});
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
// 兼容旧调用名,避免其他页面引用报错
|
|
export const appAiChatStreamPost = appAiChatStreamGet;
|
|
|