Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
6244f6c1
authored
Mar 14, 2023
by
Archer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
perf: 流响应
parent
e12f97a7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
159 additions
and
166 deletions
+159
-166
src/api/chat.ts
+3
-21
src/api/fetch.ts
+47
-0
src/pages/api/chat/chatGpt.ts
+37
-65
src/pages/api/chat/saveChat.ts
+13
-16
src/pages/chat/index.tsx
+49
-59
src/service/errorCode.ts
+4
-0
src/service/response.ts
+6
-5
No files found.
src/api/chat.ts
View file @
6244f6c1
...
@@ -36,28 +36,10 @@ export const postGPT3SendPrompt = ({
...
@@ -36,28 +36,10 @@ export const postGPT3SendPrompt = ({
});
});
/**
/**
*
预发 prompt 进行存储
*
存储一轮对话
*/
*/
export
const
postChatGptPrompt
=
({
export
const
postSaveChat
=
(
data
:
{
windowId
:
string
;
prompts
:
ChatItemType
[]
})
=>
prompt
,
POST
(
'/chat/saveChat'
,
data
);
windowId
,
chatId
}:
{
prompt
:
ChatSiteItemType
;
windowId
:
string
;
chatId
:
string
;
})
=>
POST
<
string
>
(
`/chat/preChat`
,
{
windowId
,
prompt
:
{
obj
:
prompt
.
obj
,
value
:
prompt
.
value
},
chatId
});
/* 获取 Chat 的 Event 对象,进行持续通信 */
export
const
getChatGPTSendEvent
=
(
chatId
:
string
,
windowId
:
string
)
=>
new
EventSource
(
`/api/chat/chatGpt?chatId=
${
chatId
}
&windowId=
${
windowId
}
&date=
${
Date
.
now
()}
`
);
/**
/**
* 删除最后一句
* 删除最后一句
...
...
src/api/fetch.ts
0 → 100644
View file @
6244f6c1
interface
StreamFetchProps
{
url
:
string
;
data
:
any
;
onMessage
:
(
text
:
string
)
=>
void
;
}
export
const
streamFetch
=
({
url
,
data
,
onMessage
}:
StreamFetchProps
)
=>
new
Promise
(
async
(
resolve
,
reject
)
=>
{
try
{
const
res
=
await
fetch
(
url
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
(
data
)
});
const
reader
=
res
.
body
?.
getReader
();
if
(
!
reader
)
return
;
const
decoder
=
new
TextDecoder
();
let
responseText
=
''
;
const
read
=
async
()
=>
{
const
{
done
,
value
}
=
await
reader
?.
read
();
if
(
done
)
{
if
(
res
.
status
===
200
)
{
resolve
(
responseText
);
}
else
{
try
{
const
parseError
=
JSON
.
parse
(
responseText
);
reject
(
parseError
?.
message
||
'请求异常'
);
}
catch
(
err
)
{
reject
(
'请求异常'
);
}
}
return
;
}
const
text
=
decoder
.
decode
(
value
).
replace
(
/<br
\/
>/g
,
'\n'
);
res
.
status
===
200
&&
onMessage
(
text
);
responseText
+=
text
;
read
();
};
read
();
}
catch
(
err
:
any
)
{
console
.
log
(
err
,
'===='
);
reject
(
typeof
err
===
'string'
?
err
:
err
?.
message
||
'请求异常'
);
}
});
src/pages/api/chat/chatGpt.ts
View file @
6244f6c1
...
@@ -6,26 +6,19 @@ import { getOpenAIApi, authChat } from '@/service/utils/chat';
...
@@ -6,26 +6,19 @@ import { getOpenAIApi, authChat } from '@/service/utils/chat';
import
{
httpsAgent
}
from
'@/service/utils/tools'
;
import
{
httpsAgent
}
from
'@/service/utils/tools'
;
import
{
ChatCompletionRequestMessage
,
ChatCompletionRequestMessageRoleEnum
}
from
'openai'
;
import
{
ChatCompletionRequestMessage
,
ChatCompletionRequestMessageRoleEnum
}
from
'openai'
;
import
{
ChatItemType
}
from
'@/types/chat'
;
import
{
ChatItemType
}
from
'@/types/chat'
;
import
{
openaiError
}
from
'@/service/errorCode'
;
import
{
jsonRes
}
from
'@/service/response'
;
import
{
PassThrough
}
from
'stream'
;
/* 发送提示词 */
/* 发送提示词 */
export
default
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
export
default
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
res
.
setHeader
(
'Content-Type'
,
'text/event-stream;charset-utf-8'
);
const
{
chatId
,
windowId
,
prompt
}
=
req
.
body
as
{
res
.
setHeader
(
'Access-Control-Allow-Origin'
,
'*'
);
prompt
:
ChatItemType
;
res
.
setHeader
(
'X-Accel-Buffering'
,
'no'
);
windowId
:
string
;
res
.
setHeader
(
'Cache-Control'
,
'no-cache, no-transform'
);
chatId
:
string
;
};
res
.
on
(
'close'
,
()
=>
{
res
.
end
();
});
req
.
on
(
'error'
,
()
=>
{
res
.
end
();
});
const
{
chatId
,
windowId
}
=
req
.
query
as
{
chatId
:
string
;
windowId
:
string
};
try
{
try
{
if
(
!
windowId
||
!
chatId
)
{
if
(
!
windowId
||
!
chatId
||
!
prompt
)
{
throw
new
Error
(
'缺少参数'
);
throw
new
Error
(
'缺少参数'
);
}
}
...
@@ -35,15 +28,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -35,15 +28,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const
model
:
ModelType
=
chat
.
modelId
;
const
model
:
ModelType
=
chat
.
modelId
;
const
map
=
{
Human
:
ChatCompletionRequestMessageRoleEnum
.
User
,
AI
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
SYSTEM
:
ChatCompletionRequestMessageRoleEnum
.
System
};
// 读取对话内容
// 读取对话内容
const
prompts
:
ChatItemType
[]
=
(
await
ChatWindow
.
findById
(
windowId
)).
content
;
const
prompts
:
ChatItemType
[]
=
(
await
ChatWindow
.
findById
(
windowId
)).
content
;
prompts
.
push
(
prompt
);
// 长度过滤
//
上下文
长度过滤
const
maxContext
=
model
.
security
.
contextMaxLen
;
const
maxContext
=
model
.
security
.
contextMaxLen
;
const
filterPrompts
=
const
filterPrompts
=
prompts
.
length
>
maxContext
+
2
prompts
.
length
>
maxContext
+
2
...
@@ -51,6 +40,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -51,6 +40,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
:
prompts
.
slice
(
0
,
prompts
.
length
);
:
prompts
.
slice
(
0
,
prompts
.
length
);
// 格式化文本内容
// 格式化文本内容
const
map
=
{
Human
:
ChatCompletionRequestMessageRoleEnum
.
User
,
AI
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
SYSTEM
:
ChatCompletionRequestMessageRoleEnum
.
System
};
const
formatPrompts
:
ChatCompletionRequestMessage
[]
=
filterPrompts
.
map
(
const
formatPrompts
:
ChatCompletionRequestMessage
[]
=
filterPrompts
.
map
(
(
item
:
ChatItemType
)
=>
({
(
item
:
ChatItemType
)
=>
({
role
:
map
[
item
.
obj
],
role
:
map
[
item
.
obj
],
...
@@ -62,9 +56,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -62,9 +56,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
role
:
ChatCompletionRequestMessageRoleEnum
.
System
,
role
:
ChatCompletionRequestMessageRoleEnum
.
System
,
content
:
'如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
content
:
'如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
});
});
// 获取 chatAPI
// 获取 chatAPI
const
chatAPI
=
getOpenAIApi
(
userApiKey
);
const
chatAPI
=
getOpenAIApi
(
userApiKey
);
// 发出请求
const
chatResponse
=
await
chatAPI
.
createChatCompletion
(
const
chatResponse
=
await
chatAPI
.
createChatCompletion
(
{
{
model
:
model
.
service
.
chatModel
,
model
:
model
.
service
.
chatModel
,
...
@@ -84,58 +79,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -84,58 +79,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
'response success'
'response success'
);
);
let
AIResponse
=
''
;
// 创建响应流
const
pass
=
new
PassThrough
();
pass
.
pipe
(
res
);
// 解析数据
const
decoder
=
new
TextDecoder
();
const
onParse
=
async
(
event
:
ParsedEvent
|
ReconnectInterval
)
=>
{
const
onParse
=
async
(
event
:
ParsedEvent
|
ReconnectInterval
)
=>
{
if
(
event
.
type
===
'event'
)
{
if
(
event
.
type
!==
'event'
)
return
;
const
data
=
event
.
data
;
const
data
=
event
.
data
;
if
(
data
===
'[DONE]'
)
{
if
(
data
===
'[DONE]'
)
return
;
// 存入库
try
{
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
const
json
=
JSON
.
parse
(
data
);
$push
:
{
const
content
:
string
=
json
?.
choices
?.[
0
].
delta
.
content
||
''
;
content
:
{
if
(
!
content
)
return
;
obj
:
'AI'
,
// console.log('content:', content)
value
:
AIResponse
pass
.
push
(
content
.
replace
(
/
\n
/g
,
'<br/>'
));
}
}
catch
(
error
)
{
},
error
;
updateTime
:
Date
.
now
()
});
res
.
write
(
'event: done\ndata: \n\n'
);
return
;
}
try
{
const
json
=
JSON
.
parse
(
data
);
const
content
:
string
=
json
?.
choices
?.[
0
].
delta
.
content
||
'\n'
;
// console.log('content:', content)
res
.
write
(
`event: responseData\ndata:
${
content
.
replace
(
/
\n
/g
,
'<br/>'
)}
\n\n`
);
AIResponse
+=
content
;
}
catch
(
error
)
{
error
;
}
}
}
};
};
for
await
(
const
chunk
of
chatResponse
.
data
as
any
)
{
for
await
(
const
chunk
of
chatResponse
.
data
as
any
)
{
const
parser
=
createParser
(
onParse
);
const
parser
=
createParser
(
onParse
);
parser
.
feed
(
decode
r
.
decode
(
chunk
));
parser
.
feed
(
decode
URIComponent
(
chunk
));
}
}
pass
.
push
(
null
);
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
console
.
log
(
'error->'
,
err
?.
response
,
'==='
);
res
.
status
(
500
);
let
errorText
=
'OpenAI 服务器访问超时'
;
jsonRes
(
res
,
{
if
(
err
.
code
===
'ECONNRESET'
||
err
?.
response
?.
status
===
502
)
{
code
:
500
,
errorText
=
'服务器代理出错'
;
error
:
err
}
else
if
(
err
?.
response
?.
statusText
&&
openaiError
[
err
.
response
.
statusText
])
{
errorText
=
openaiError
[
err
.
response
.
statusText
];
}
console
.
log
(
'error->'
,
errorText
);
res
.
write
(
`event: serviceError\ndata:
${
errorText
}
\n\n`
);
// 删除最一条数据库记录, 也就是预发送的那一条
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
$pop
:
{
content
:
1
},
updateTime
:
Date
.
now
()
});
});
res
.
end
();
}
}
}
}
src/pages/api/chat/
pr
eChat.ts
→
src/pages/api/chat/
sav
eChat.ts
View file @
6244f6c1
...
@@ -2,34 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next';
...
@@ -2,34 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import
{
jsonRes
}
from
'@/service/response'
;
import
{
jsonRes
}
from
'@/service/response'
;
import
{
ChatItemType
}
from
'@/types/chat'
;
import
{
ChatItemType
}
from
'@/types/chat'
;
import
{
connectToDatabase
,
ChatWindow
}
from
'@/service/mongo'
;
import
{
connectToDatabase
,
ChatWindow
}
from
'@/service/mongo'
;
import
type
{
ModelType
}
from
'@/types/model'
;
import
{
authChat
}
from
'@/service/utils/chat'
;
/* 聊天
预请求,存储聊天内容
*/
/* 聊天
内容存存储
*/
export
default
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
export
default
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
try
{
try
{
const
{
windowId
,
prompt
,
chatId
}
=
req
.
body
as
{
const
{
windowId
,
prompt
s
}
=
req
.
body
as
{
windowId
:
string
;
windowId
:
string
;
prompt
:
ChatItemType
;
prompts
:
ChatItemType
[];
chatId
:
string
;
};
};
if
(
!
windowId
||
!
prompt
||
!
chatId
)
{
if
(
!
windowId
||
!
prompt
s
)
{
throw
new
Error
(
'缺少参数'
);
throw
new
Error
(
'缺少参数'
);
}
}
await
connectToDatabase
();
await
connectToDatabase
();
const
{
chat
}
=
await
authChat
(
chatId
);
// 存入库
// 长度校验
const
model
:
ModelType
=
chat
.
modelId
;
if
(
prompt
.
value
.
length
>
model
.
security
.
contentMaxLen
)
{
throw
new
Error
(
'输入内容超长'
);
}
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
$push
:
{
content
:
prompt
},
$push
:
{
content
:
{
$each
:
prompts
.
map
((
item
)
=>
({
obj
:
item
.
obj
,
value
:
item
.
value
}))
}
},
updateTime
:
Date
.
now
()
updateTime
:
Date
.
now
()
});
});
...
...
src/pages/chat/index.tsx
View file @
6244f6c1
import
React
,
{
useCallback
,
useState
,
useRef
,
useMemo
}
from
'react'
;
import
React
,
{
useCallback
,
useState
,
useRef
,
useMemo
}
from
'react'
;
import
{
useRouter
}
from
'next/router'
;
import
{
useRouter
}
from
'next/router'
;
import
Image
from
'next/image'
;
import
Image
from
'next/image'
;
import
{
import
{
getInitChatSiteInfo
,
postGPT3SendPrompt
,
delLastMessage
,
postSaveChat
}
from
'@/api/chat'
;
getInitChatSiteInfo
,
postGPT3SendPrompt
,
getChatGPTSendEvent
,
postChatGptPrompt
,
delLastMessage
}
from
'@/api/chat'
;
import
{
ChatSiteItemType
,
ChatSiteType
}
from
'@/types/chat'
;
import
{
ChatSiteItemType
,
ChatSiteType
}
from
'@/types/chat'
;
import
{
Textarea
,
Box
,
Flex
,
Button
}
from
'@chakra-ui/react'
;
import
{
Textarea
,
Box
,
Flex
,
Button
}
from
'@chakra-ui/react'
;
import
{
useToast
}
from
'@/hooks/useToast'
;
import
{
useToast
}
from
'@/hooks/useToast'
;
...
@@ -17,6 +11,7 @@ import { useQuery } from '@tanstack/react-query';
...
@@ -17,6 +11,7 @@ import { useQuery } from '@tanstack/react-query';
import
{
OpenAiModelEnum
}
from
'@/constants/model'
;
import
{
OpenAiModelEnum
}
from
'@/constants/model'
;
import
dynamic
from
'next/dynamic'
;
import
dynamic
from
'next/dynamic'
;
import
{
useGlobalStore
}
from
'@/store/global'
;
import
{
useGlobalStore
}
from
'@/store/global'
;
import
{
streamFetch
}
from
'@/api/fetch'
;
const
Markdown
=
dynamic
(()
=>
import
(
'@/components/Markdown'
));
const
Markdown
=
dynamic
(()
=>
import
(
'@/components/Markdown'
));
...
@@ -128,69 +123,64 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
...
@@ -128,69 +123,64 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
const
chatGPTPrompt
=
useCallback
(
const
chatGPTPrompt
=
useCallback
(
async
(
newChatList
:
ChatSiteItemType
[])
=>
{
async
(
newChatList
:
ChatSiteItemType
[])
=>
{
if
(
!
windowId
)
return
;
if
(
!
windowId
)
return
;
/* 预请求,把消息存入库 */
const
prompt
=
{
await
postChatGptPrompt
({
obj
:
newChatList
[
newChatList
.
length
-
1
].
obj
,
windowId
,
value
:
newChatList
[
newChatList
.
length
-
1
].
value
prompt
:
newChatList
[
newChatList
.
length
-
1
],
};
chatId
// 流请求,获取数据
});
const
res
=
await
streamFetch
({
url
:
'/api/chat/chatGpt'
,
return
new
Promise
((
resolve
,
reject
)
=>
{
data
:
{
const
event
=
getChatGPTSendEvent
(
chatId
,
windowId
);
windowId
,
// 30s 收不到消息就报错
prompt
,
let
timer
=
setTimeout
(()
=>
{
chatId
event
.
close
();
},
reject
(
'服务器超时'
);
onMessage
:
(
text
:
string
)
=>
{
},
30000
);
event
.
addEventListener
(
'responseData'
,
({
data
})
=>
{
/* 重置定时器 */
clearTimeout
(
timer
);
timer
=
setTimeout
(()
=>
{
event
.
close
();
reject
(
'服务器超时'
);
},
30000
);
const
msg
=
data
.
replace
(
/<br
\/
>/g
,
'\n'
);
setChatList
((
state
)
=>
state
.
map
((
item
,
index
)
=>
{
if
(
index
!==
state
.
length
-
1
)
return
item
;
return
{
...
item
,
value
:
item
.
value
+
msg
};
})
);
});
event
.
addEventListener
(
'done'
,
()
=>
{
console
.
log
(
'done'
);
clearTimeout
(
timer
);
event
.
close
();
setChatList
((
state
)
=>
setChatList
((
state
)
=>
state
.
map
((
item
,
index
)
=>
{
state
.
map
((
item
,
index
)
=>
{
if
(
index
!==
state
.
length
-
1
)
return
item
;
if
(
index
!==
state
.
length
-
1
)
return
item
;
return
{
return
{
...
item
,
...
item
,
status
:
'finish'
value
:
item
.
value
+
text
};
};
})
})
);
);
resolve
(
''
);
}
});
// 保存对话信息
try
{
await
postSaveChat
({
windowId
,
prompts
:
[
prompt
,
{
obj
:
'AI'
,
value
:
res
as
string
}
]
});
});
event
.
addEventListener
(
'serviceError'
,
({
data
:
err
})
=>
{
}
catch
(
err
)
{
clearTimeout
(
timer
);
toast
({
event
.
close
();
title
:
'存储对话出现异常, 继续对话会导致上下文丢失,请刷新页面'
,
console
.
log
(
'error->'
,
err
,
'==='
);
status
:
'warning'
,
reject
(
typeof
err
===
'string'
?
err
:
'对话出现不知名错误~'
);
duration
:
3000
,
isClosable
:
true
});
});
event
.
onerror
=
(
err
)
=>
{
}
clearTimeout
(
timer
);
event
.
close
();
// 设置完成状态
console
.
log
(
'error->'
,
err
);
setChatList
((
state
)
=>
reject
(
typeof
err
===
'string'
?
err
:
'对话出现不知名错误~'
);
state
.
map
((
item
,
index
)
=>
{
};
if
(
index
!==
state
.
length
-
1
)
return
item
;
});
return
{
...
item
,
status
:
'finish'
};
})
);
},
},
[
chatId
,
windowId
]
[
chatId
,
toast
,
windowId
]
);
);
/**
/**
...
...
src/service/errorCode.ts
View file @
6244f6c1
...
@@ -4,3 +4,7 @@ export const openaiError: Record<string, string> = {
...
@@ -4,3 +4,7 @@ export const openaiError: Record<string, string> = {
rate_limit_reached
:
'同时访问用户过多,请稍后再试'
,
rate_limit_reached
:
'同时访问用户过多,请稍后再试'
,
'Bad Request'
:
'上下文太多了,请重开对话~'
'Bad Request'
:
'上下文太多了,请重开对话~'
};
};
export
const
proxyError
:
Record
<
string
,
boolean
>
=
{
ECONNABORTED
:
true
,
ECONNRESET
:
true
};
src/service/response.ts
View file @
6244f6c1
import
{
NextApiResponse
}
from
'next'
;
import
{
NextApiResponse
}
from
'next'
;
import
{
openaiError
}
from
'./errorCode'
;
import
{
openaiError
,
proxyError
}
from
'./errorCode'
;
export
interface
ResponseType
<
T
=
any
>
{
export
interface
ResponseType
<
T
=
any
>
{
code
:
number
;
code
:
number
;
...
@@ -23,12 +23,13 @@ export const jsonRes = (
...
@@ -23,12 +23,13 @@ export const jsonRes = (
msg
=
error
?.
message
||
'请求错误'
;
msg
=
error
?.
message
||
'请求错误'
;
if
(
typeof
error
===
'string'
)
{
if
(
typeof
error
===
'string'
)
{
msg
=
error
;
msg
=
error
;
}
else
if
(
error
?.
response
?.
data
?.
message
in
openaiError
)
{
}
else
if
(
proxyError
[
error
?.
code
])
{
msg
=
openaiError
[
error
?.
response
?.
data
?.
message
];
msg
=
'服务器代理出错'
;
}
else
if
(
openaiError
[
error
?.
response
?.
statusText
])
{
msg
=
openaiError
[
error
.
response
.
statusText
];
}
}
console
.
log
(
'error->'
,
error
);
console
.
log
(
'error->'
,
error
.
code
,
error
?.
response
?.
statusText
,
msg
);
console
.
log
(
'error->'
,
msg
);
}
}
res
.
json
({
res
.
json
({
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment