* perf: redirect request and err log replace perf: dataset openapi feat: session fix: retry input error feat: 468 doc sub page feat: standard sub perf: rerank tip perf: rerank tip perf: api sdk perf: openapi sub plan perf: sub ui fix: ts * perf: init log * fix: variable select * sub page * icon * perf: llm model config * perf: menu ux * perf: system store * perf: publish app name * fix: init data * perf: flow edit ux * fix: value type format and ux * fix prompt editor default value (#13) * fix prompt editor default value * fix prompt editor update when not focus * add key with variable --------- Co-authored-by: Archer <545436317@qq.com> * fix: value type * doc * i18n * import path * home page * perf: mongo session running * fix: ts * perf: use toast * perf: flow edit * perf: sse response * slider ui * fetch error * fix prompt editor rerender when not focus by key defaultvalue (#14) * perf: prompt editor * feat: dataset search concat * perf: doc * fix:ts * perf: doc * fix json editor onblur value (#15) * faq * vector model default config * ipv6 --------- Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
183 lines
4.9 KiB
TypeScript
183 lines
4.9 KiB
TypeScript
import { sseResponseEventEnum } from '@fastgpt/service/common/response/constant';
|
|
import { getErrText } from '@fastgpt/global/common/error/utils';
|
|
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
|
import { StartChatFnProps } from '@/components/ChatBox';
|
|
import { getToken } from '@/web/support/user/auth';
|
|
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
// refer to https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web
|
|
EventStreamContentType,
|
|
fetchEventSource
|
|
} from '@fortaine/fetch-event-source';
|
|
|
|
type StreamFetchProps = {
|
|
url?: string;
|
|
data: Record<string, any>;
|
|
onMessage: StartChatFnProps['generatingMessage'];
|
|
abortCtrl: AbortController;
|
|
};
|
|
type StreamResponseType = {
|
|
responseText: string;
|
|
[ModuleOutputKeyEnum.responseData]: ChatHistoryItemResType[];
|
|
};
|
|
export const streamFetch = ({
|
|
url = '/api/v1/chat/completions',
|
|
data,
|
|
onMessage,
|
|
abortCtrl
|
|
}: StreamFetchProps) =>
|
|
new Promise<StreamResponseType>(async (resolve, reject) => {
|
|
const timeoutId = setTimeout(() => {
|
|
abortCtrl.abort('Time out');
|
|
}, 60000);
|
|
|
|
// response data
|
|
let responseText = '';
|
|
let remainText = '';
|
|
let errMsg = '';
|
|
let responseData: ChatHistoryItemResType[] = [];
|
|
let finished = false;
|
|
|
|
const finish = () => {
|
|
if (errMsg) {
|
|
return failedFinish();
|
|
}
|
|
return resolve({
|
|
responseText,
|
|
responseData
|
|
});
|
|
};
|
|
const failedFinish = (err?: any) => {
|
|
finished = true;
|
|
reject({
|
|
message: getErrText(err, errMsg || '响应过程出现异常~'),
|
|
responseText
|
|
});
|
|
};
|
|
|
|
// animate response to make it looks smooth
|
|
function animateResponseText() {
|
|
// abort message
|
|
if (abortCtrl.signal.aborted) {
|
|
onMessage({ text: remainText });
|
|
responseText += remainText;
|
|
return finish();
|
|
}
|
|
|
|
if (remainText) {
|
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
|
const fetchText = remainText.slice(0, fetchCount);
|
|
|
|
onMessage({ text: fetchText });
|
|
|
|
responseText += fetchText;
|
|
remainText = remainText.slice(fetchCount);
|
|
}
|
|
|
|
if (finished && !remainText) {
|
|
return finish();
|
|
}
|
|
|
|
requestAnimationFrame(animateResponseText);
|
|
}
|
|
// start animation
|
|
animateResponseText();
|
|
|
|
try {
|
|
// auto complete variables
|
|
const variables = data?.variables || {};
|
|
variables.cTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
const requestData = {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
token: getToken()
|
|
},
|
|
signal: abortCtrl.signal,
|
|
body: JSON.stringify({
|
|
...data,
|
|
variables,
|
|
detail: true,
|
|
stream: true
|
|
})
|
|
};
|
|
|
|
// send request
|
|
await fetchEventSource(url, {
|
|
...requestData,
|
|
async onopen(res) {
|
|
clearTimeout(timeoutId);
|
|
const contentType = res.headers.get('content-type');
|
|
|
|
// not stream
|
|
if (contentType?.startsWith('text/plain')) {
|
|
return failedFinish(await res.clone().text());
|
|
}
|
|
|
|
// failed stream
|
|
if (
|
|
!res.ok ||
|
|
!res.headers.get('content-type')?.startsWith(EventStreamContentType) ||
|
|
res.status !== 200
|
|
) {
|
|
try {
|
|
failedFinish(await res.clone().json());
|
|
} catch {
|
|
failedFinish(await res.clone().text());
|
|
}
|
|
}
|
|
},
|
|
onmessage({ event, data }) {
|
|
if (data === '[DONE]') {
|
|
return;
|
|
}
|
|
|
|
// parse text to json
|
|
const parseJson = (() => {
|
|
try {
|
|
return JSON.parse(data);
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
})();
|
|
|
|
if (event === sseResponseEventEnum.answer) {
|
|
const answer: string = parseJson?.choices?.[0]?.delta?.content || '';
|
|
remainText += answer;
|
|
} else if (
|
|
event === sseResponseEventEnum.moduleStatus &&
|
|
parseJson?.name &&
|
|
parseJson?.status
|
|
) {
|
|
onMessage(parseJson);
|
|
} else if (event === sseResponseEventEnum.appStreamResponse && Array.isArray(parseJson)) {
|
|
responseData = parseJson;
|
|
} else if (event === sseResponseEventEnum.error) {
|
|
errMsg = getErrText(parseJson, '流响应错误');
|
|
}
|
|
},
|
|
onclose() {
|
|
finished = true;
|
|
},
|
|
onerror(e) {
|
|
clearTimeout(timeoutId);
|
|
failedFinish(getErrText(e));
|
|
},
|
|
openWhenHidden: true
|
|
});
|
|
} catch (err: any) {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (abortCtrl.signal.aborted) {
|
|
finished = true;
|
|
|
|
return;
|
|
}
|
|
console.log(err, 'fetch error');
|
|
|
|
failedFinish(err);
|
|
}
|
|
});
|