perf: stream response (#2813)

* perf: stream response

* fold code
This commit is contained in:
Archer
2024-09-26 18:09:01 +08:00
committed by GitHub
parent aee5de29c7
commit 21ab855871
6 changed files with 66 additions and 58 deletions

View File

@@ -84,24 +84,25 @@ weight: 813
## V4.8.11 更新说明 ## V4.8.11 更新说明
1. 1. 新增 - 表单输入节点,允许用户在工作流中让用户输入一些信息。
2. 新增 - 循环运行节点,可传入数组进行批量调用,目前最多支持 50 长度的数组串行执行。 2. 新增 - 循环运行节点,可传入数组进行批量调用,目前最多支持 50 长度的数组串行执行。
3. 新增 - 聊天记录滚动加载,不再只加载 30 条 3. 新增 - 节点支持折叠
4. 新增 - 工作流增加触摸板优先模式 4. 新增 - 聊天记录滚动加载,不再只加载 30 条
5. 新增 - 沙盒增加字符串转 base64 全局方法 5. 新增 - 工作流增加触摸板优先模式,可以通过工作流右下角按键进行切换
6. 新增 - 支持 Openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature``max_tokens``stream`配置o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看 6. 新增 - 沙盒增加字符串转 base64 全局方法(全局变量 strToBase64)
7. 新增 - AI 对话节点知识库引用,支持配置 role=system 和 role=user已配置的过自定义提示词的节点将会保持 user 模式,其余用户将转成 system 模式 7. 新增 - 支持 Openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature``max_tokens``stream`配置o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看
8. 新增 - 插件支持上传系统文件 8. 新增 - AI 对话节点知识库引用,支持配置 role=system 和 role=user已配置的过自定义提示词的节点将会保持 user 模式,其余用户将转成 system 模式
9. 新增 - 插件输出,支持指定字段作为工具响应 9. 新增 - 插件支持上传系统文件
10. 新增 - 支持工作流嵌套子应用时,可以设置`非流模式`,同时简易模式也可以选择工作流作为插件了,简易模式调用子应用时,都将强制使用非流模式 10. 新增 - 插件输出,支持指定字段作为工具响应
11. 新增 - 调试模式下,子应用调用,支持返回详细运行数据 11. 新增 - 支持工作流嵌套子应用时,可以设置`非流模式`,同时简易模式也可以选择工作流作为插件了,简易模式调用子应用时,都将强制使用非流模式
12. 新增 - 保留所有模式下子应用嵌套调用的日志 12. 新增 - 调试模式下子应用调用,支持返回详细运行数据
13. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环 13. 新增 - 保留所有模式下子应用嵌套调用的日志
14. 优化 - 工作流 handler 性能优化 14. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环
15. 优化 - 工作流快捷键,避免调试测试时也会触发 15. 优化 - 工作流 handler 性能优化
16. 优化 - 流输出,切换 tab 时仍可以继续输出 16. 优化 - 工作流快捷键,避免调试测试时也会触发复制和回退
17. 优化 - 完善外部文件知识库相关 API 17. 优化 - 流输出,切换浏览器 Tab 后仍可以继续输出。
18. 修复 - 知识库选择权限问题。 18. 优化 - 完善外部文件知识库相关 API
19. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常 19. 修复 - 知识库选择权限问题
20. 修复 - createDataset 接口intro 为赋值 20. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常
21. 修复 - 对话框渲染性能问题 21. 修复 - createDataset 接口intro 为赋值
22. 修复 - 对话框渲染性能问题。

View File

@@ -20,6 +20,9 @@ type FormInputResponse = DispatchNodeResultType<{
[NodeOutputKeyEnum.formInputResult]?: Record<string, any>; [NodeOutputKeyEnum.formInputResult]?: Record<string, any>;
}>; }>;
/*
用户输入都内容,将会以 JSON 字符串格式进入工作流,可以从 query 的 text 中获取。
*/
export const dispatchFormInput = async (props: Props): Promise<FormInputResponse> => { export const dispatchFormInput = async (props: Props): Promise<FormInputResponse> => {
const { const {
histories, histories,

View File

@@ -199,13 +199,15 @@ const ChatBox = (
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部 // 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
const { run: generatingScroll } = useThrottleFn( const { run: generatingScroll } = useThrottleFn(
() => { (force?: boolean) => {
if (!ChatBoxRef.current) return; if (!ChatBoxRef.current) return;
const isBottom = const isBottom =
ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >= ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >=
ChatBoxRef.current.scrollHeight; ChatBoxRef.current.scrollHeight;
isBottom && scrollToBottom('auto'); if (isBottom || force) {
scrollToBottom('auto');
}
}, },
{ {
wait: 100 wait: 100
@@ -321,7 +323,9 @@ const ChatBox = (
return item; return item;
}) })
); );
generatingScroll();
const forceScroll = event === SseResponseEventEnum.interactive;
generatingScroll(forceScroll);
} }
); );
@@ -529,7 +533,7 @@ const ChatBox = (
}); });
} }
generatingScroll(); generatingScroll(true);
isPc && TextareaDom.current?.focus(); isPc && TextareaDom.current?.focus();
}, 100); }, 100);

View File

@@ -180,21 +180,12 @@ const NodeCard = (props: Props) => {
}); });
}} }}
> >
{!isFolded ? ( <MyIcon
<MyIcon name={isFolded ? 'core/chat/chevronDown' : 'core/chat/chevronRight'}
name={'core/chat/chevronDown'} w={'24px'}
w={'24px'} h={'24px'}
h={'24px'} color={'myGray.500'}
color={'myGray.500'} />
/>
) : (
<MyIcon
name={'core/chat/chevronRight'}
w={'24px'}
h={'24px'}
color={'myGray.500'}
/>
)}
</Box> </Box>
)} )}
<Avatar src={avatar} borderRadius={'sm'} objectFit={'contain'} w={'30px'} h={'30px'} /> <Avatar src={avatar} borderRadius={'sm'} objectFit={'contain'} w={'30px'} h={'30px'} />

View File

@@ -22,6 +22,16 @@ export type StreamResponseType = {
responseText: string; responseText: string;
[DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[]; [DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[];
}; };
type ResponseQueueItemType =
| { event: SseResponseEventEnum.fastAnswer | SseResponseEventEnum.answer; text: string }
| { event: SseResponseEventEnum.interactive; [key: string]: any }
| {
event:
| SseResponseEventEnum.toolCall
| SseResponseEventEnum.toolParams
| SseResponseEventEnum.toolResponse;
[key: string]: any;
};
class FatalError extends Error {} class FatalError extends Error {}
export const streamFetch = ({ export const streamFetch = ({
@@ -31,22 +41,14 @@ export const streamFetch = ({
abortCtrl abortCtrl
}: StreamFetchProps) => }: StreamFetchProps) =>
new Promise<StreamResponseType>(async (resolve, reject) => { new Promise<StreamResponseType>(async (resolve, reject) => {
// First res
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
abortCtrl.abort('Time out'); abortCtrl.abort('Time out');
}, 60000); }, 60000);
// response data // response data
let responseText = ''; let responseText = '';
let responseQueue: ( let responseQueue: ResponseQueueItemType[] = [];
| { event: SseResponseEventEnum.fastAnswer | SseResponseEventEnum.answer; text: string }
| {
event:
| SseResponseEventEnum.toolCall
| SseResponseEventEnum.toolParams
| SseResponseEventEnum.toolResponse;
[key: string]: any;
}
)[] = [];
let errMsg: string | undefined; let errMsg: string | undefined;
let responseData: ChatHistoryItemResType[] = []; let responseData: ChatHistoryItemResType[] = [];
let finished = false; let finished = false;
@@ -84,7 +86,7 @@ export const streamFetch = ({
} }
if (responseQueue.length > 0) { if (responseQueue.length > 0) {
const fetchCount = Math.max(1, Math.round(responseQueue.length / 20)); const fetchCount = Math.max(1, Math.round(responseQueue.length / 30));
for (let i = 0; i < fetchCount; i++) { for (let i = 0; i < fetchCount; i++) {
const item = responseQueue[i]; const item = responseQueue[i];
onMessage(item); onMessage(item);
@@ -100,13 +102,20 @@ export const streamFetch = ({
return finish(); return finish();
} }
document.hidden requestAnimationFrame(animateResponseText);
? setTimeout(animateResponseText, 16)
: requestAnimationFrame(animateResponseText);
} }
// start animation // start animation
animateResponseText(); animateResponseText();
const pushDataToQueue = (data: ResponseQueueItemType) => {
// If the document is hidden, the data is directly sent to the front end
responseQueue.push(data);
if (document.hidden) {
animateResponseText();
}
};
try { try {
// auto complete variables // auto complete variables
const variables = data?.variables || {}; const variables = data?.variables || {};
@@ -171,14 +180,14 @@ export const streamFetch = ({
if (event === SseResponseEventEnum.answer) { if (event === SseResponseEventEnum.answer) {
const text = parseJson.choices?.[0]?.delta?.content || ''; const text = parseJson.choices?.[0]?.delta?.content || '';
for (const item of text) { for (const item of text) {
responseQueue.push({ pushDataToQueue({
event, event,
text: item text: item
}); });
} }
} else if (event === SseResponseEventEnum.fastAnswer) { } else if (event === SseResponseEventEnum.fastAnswer) {
const text = parseJson.choices?.[0]?.delta?.content || ''; const text = parseJson.choices?.[0]?.delta?.content || '';
responseQueue.push({ pushDataToQueue({
event, event,
text text
}); });
@@ -187,7 +196,7 @@ export const streamFetch = ({
event === SseResponseEventEnum.toolParams || event === SseResponseEventEnum.toolParams ||
event === SseResponseEventEnum.toolResponse event === SseResponseEventEnum.toolResponse
) { ) {
responseQueue.push({ pushDataToQueue({
event, event,
...parseJson ...parseJson
}); });
@@ -204,7 +213,7 @@ export const streamFetch = ({
variables: parseJson variables: parseJson
}); });
} else if (event === SseResponseEventEnum.interactive) { } else if (event === SseResponseEventEnum.interactive) {
responseQueue.push({ pushDataToQueue({
event, event,
...parseJson ...parseJson
}); });

View File

@@ -200,7 +200,7 @@ const ChatContextProvider = ({
); );
} else { } else {
// Chat history not exists // Chat history not exists
loadHistories(); loadHistories(true);
} }
}, },
[histories, loadHistories, setHistories] [histories, loadHistories, setHistories]