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
7807b267
authored
Mar 09, 2023
by
Archer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 流优化
parent
16775430
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
158 additions
and
48 deletions
+158
-48
README.md
+47
-7
src/api/chat.ts
+2
-1
src/api/request.ts
+4
-5
src/components/Layout/navbar.tsx
+1
-0
src/components/Markdown/index.module.scss
+0
-1
src/pages/api/chat/chatGpt.ts
+61
-18
src/pages/api/chat/init.ts
+1
-2
src/pages/chat/index.tsx
+42
-14
No files found.
README.md
View file @
7807b267
...
@@ -15,22 +15,62 @@ TOKEN_KEY=随便填一个,用于生成和校验token
...
@@ -15,22 +15,62 @@ TOKEN_KEY=随便填一个,用于生成和校验token
```
bash
```
bash
pnpm dev
pnpm dev
```
```
Open
[
http://localhost:3000
](
http://localhost:3000
)
with your browser to see the result.
## 部署
## 部署
```
bash
```
bash
# 本地 docker 打包
# 本地 docker 打包
docker build
-t
imageName
.
docker build
-t
imageName:tag
.
docker push imageName
docker push imageName:tag
```
# 服务器拉取部署
服务器请准备好 docker, mongo,nginx和代理。 镜像走本机的代理,所以用 host,port改成代理的端口,clash一般都是7890。
docker pull imageName
```
bash
# 服务器拉取部署, imageName 替换成镜像名
docker pull imageName:tag
# 获取本地旧镜像ID
OLD_IMAGE_ID
=
$(
docker images imageName
-f
"dangling=true"
-q
)
docker stop doc-gpt
||
true
docker stop doc-gpt
||
true
docker rm doc-gpt
||
true
docker rm doc-gpt
||
true
# 运行时才把参数写入
docker run
-d
--network
=
host
--name
doc-gpt
\
docker run
-d
--network
=
host
--name
doc-gpt
-e
AXIOS_PROXY_HOST
=
-e
AXIOS_PROXY_PORT
=
-e
MAILE_CODE
=
-e
TOKEN_KEY
=
-e
MONGODB_URI
=
imageName
-e
MAX_USER
=
50
\
-e
AXIOS_PROXY_HOST
=
127.0.0.1
\
-e
AXIOS_PROXY_PORT
=
7890
\
-e
MY_MAIL
=
your email
\
-e
MAILE_CODE
=
your email code
\
-e
TOKEN_KEY
=
任意一个内容
\
-e
MONGODB_URI
=
"mongodb://aha:ROOT_root123@127.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false"
\
imageName:tag
docker logs doc-gpt
# 删除本地旧镜像
if
[
!
-z
"
$OLD_IMAGE_ID
"
]
;
then
docker rmi
$OLD_IMAGE_ID
fi
```
### docker 安装
```
bash
# 安装docker
curl
-sSL
https://get.daocloud.io/docker | sh
sudo
systemctl start docker
```
### mongo 安装
```
bash
docker pull mongo:6.0.4
docker stop mongo
docker rm mongo
docker run
-d
--name
mongo
\
-e
MONGO_INITDB_ROOT_USERNAME
=
\
-e
MONGO_INITDB_ROOT_PASSWORD
=
\
-v
/root/service/mongo:/data/db
\
mongo:6.0.4
```
```
Open
[
http://localhost:3000
](
http://localhost:3000
)
with your browser to see the result.
# 介绍页
# 介绍页
...
...
src/api/chat.ts
View file @
7807b267
import
{
GET
,
POST
,
DELETE
}
from
'./request'
;
import
{
GET
,
POST
,
DELETE
}
from
'./request'
;
import
{
ChatItemType
,
ChatSiteType
,
ChatSiteItemType
}
from
'@/types/chat'
;
import
{
ChatItemType
,
ChatSiteType
,
ChatSiteItemType
}
from
'@/types/chat'
;
import
axios
from
'axios'
;
/**
/**
* 获取一个聊天框的ID
* 获取一个聊天框的ID
...
@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
...
@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
});
});
/* 获取 Chat 的 Event 对象,进行持续通信 */
/* 获取 Chat 的 Event 对象,进行持续通信 */
export
const
getChatGPTSendEvent
=
(
chatId
:
string
,
windowId
:
string
)
=>
export
const
getChatGPTSendEvent
=
(
chatId
:
string
,
windowId
:
string
)
=>
new
EventSource
(
`/api/chat/chatGpt?chatId=
${
chatId
}
&windowId=
${
windowId
}
`
);
new
EventSource
(
`/api/chat/chatGpt?chatId=
${
chatId
}
&windowId=
${
windowId
}
&date=
${
Date
.
now
()}
`
);
/**
/**
* 删除最后一句
* 删除最后一句
...
...
src/api/request.ts
View file @
7807b267
...
@@ -49,21 +49,20 @@ function responseError(err: any) {
...
@@ -49,21 +49,20 @@ function responseError(err: any) {
console
.
error
(
'请求错误'
,
err
);
console
.
error
(
'请求错误'
,
err
);
if
(
!
err
)
{
if
(
!
err
)
{
return
Promise
.
reject
(
'未知错误'
);
return
Promise
.
reject
(
{
message
:
'未知错误'
}
);
}
}
if
(
typeof
err
===
'string'
)
{
if
(
typeof
err
===
'string'
)
{
return
Promise
.
reject
(
err
);
return
Promise
.
reject
(
{
message
:
err
}
);
}
}
if
(
err
.
response
)
{
if
(
err
.
response
)
{
// 有报错响应
// 有报错响应
const
res
=
err
.
response
;
const
res
=
err
.
response
;
/* token过期,判断请求token与本地是否相同,若不同需要重发 */
if
(
res
.
data
.
code
in
TOKEN_ERROR_CODE
)
{
if
(
res
.
data
.
code
in
TOKEN_ERROR_CODE
)
{
clearToken
();
clearToken
();
return
Promise
.
reject
(
'token过期,重新登录'
);
return
Promise
.
reject
(
{
message
:
'token过期,重新登录'
}
);
}
}
}
}
return
Promise
.
reject
(
'未知错误'
);
return
Promise
.
reject
(
err
);
}
}
/* 创建请求实例 */
/* 创建请求实例 */
...
...
src/components/Layout/navbar.tsx
View file @
7807b267
...
@@ -46,6 +46,7 @@ const Navbar = ({
...
@@ -46,6 +46,7 @@ const Navbar = ({
alignItems=
{
'center'
}
alignItems=
{
'center'
}
justifyContent=
{
'center'
}
justifyContent=
{
'center'
}
onClick=
{
()
=>
onClick=
{
()
=>
!
item
.
activeLink
.
includes
(
router
.
pathname
)
&&
router
.
push
(
item
.
link
,
undefined
,
{
router
.
push
(
item
.
link
,
undefined
,
{
shallow
:
true
shallow
:
true
})
})
...
...
src/components/Markdown/index.module.scss
View file @
7807b267
...
@@ -328,7 +328,6 @@
...
@@ -328,7 +328,6 @@
border-radius
:
3px
3px
3px
3px
;
border-radius
:
3px
3px
3px
3px
;
margin
:
0
2px
;
margin
:
0
2px
;
padding
:
0
5px
;
padding
:
0
5px
;
white-space
:
nowrap
;
}
}
.markdown
pre
>
code
{
.markdown
pre
>
code
{
background
:
none
repeat
scroll
0
0
transparent
;
background
:
none
repeat
scroll
0
0
transparent
;
...
...
src/pages/api/chat/chatGpt.ts
View file @
7807b267
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import
type
{
NextApiRequest
,
NextApiResponse
}
from
'next'
;
import
type
{
NextApiRequest
,
NextApiResponse
}
from
'next'
;
import
{
connectToDatabase
,
Chat
,
ChatWindow
}
from
'@/service/mongo'
;
import
{
Readable
}
from
'stream'
;
import
{
connectToDatabase
,
ChatWindow
}
from
'@/service/mongo'
;
import
type
{
ModelType
}
from
'@/types/model'
;
import
type
{
ModelType
}
from
'@/types/model'
;
import
{
getOpenAIApi
,
authChat
}
from
'@/service/utils/chat'
;
import
{
getOpenAIApi
,
authChat
}
from
'@/service/utils/chat'
;
import
{
openaiProxy
}
from
'@/service/utils/tools'
;
import
{
openaiProxy
}
from
'@/service/utils/tools'
;
...
@@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat';
...
@@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat';
/* 发送提示词 */
/* 发送提示词 */
export
default
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
export
default
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
res
.
writeHead
(
200
,
{
res
.
setHeader
(
'Connection'
,
'keep-alive'
);
Connection
:
'keep-alive'
,
res
.
setHeader
(
'Cache-Control'
,
'no-cache'
);
'Content-Encoding'
:
'none'
,
res
.
setHeader
(
'Content-Type'
,
'text/event-stream'
);
'Cache-Control'
:
'no-cache'
,
'Content-Type'
:
'text/event-stream'
const
responseData
:
string
[]
=
[];
const
stream
=
new
Readable
({
read
(
size
)
{
const
data
=
responseData
.
shift
()
||
null
;
this
.
push
(
data
);
}
});
res
.
on
(
'close'
,
()
=>
{
res
.
end
();
stream
.
destroy
();
});
});
const
{
chatId
,
windowId
}
=
req
.
query
as
{
chatId
:
string
;
windowId
:
string
};
const
{
chatId
,
windowId
}
=
req
.
query
as
{
chatId
:
string
;
windowId
:
string
};
try
{
try
{
...
@@ -47,14 +58,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -47,14 +58,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const
formatPrompts
:
ChatCompletionRequestMessage
[]
=
filterPrompts
.
map
(
const
formatPrompts
:
ChatCompletionRequestMessage
[]
=
filterPrompts
.
map
(
(
item
:
ChatItemType
)
=>
({
(
item
:
ChatItemType
)
=>
({
role
:
map
[
item
.
obj
],
role
:
map
[
item
.
obj
],
content
:
item
.
value
content
:
item
.
value
.
replace
(
/
(\n
|
)
/g
,
''
)
})
})
);
);
// 第一句话,强调代码类型
// 第一句话,强调代码类型
formatPrompts
.
unshift
({
formatPrompts
.
unshift
({
role
:
ChatCompletionRequestMessageRoleEnum
.
System
,
role
:
ChatCompletionRequestMessageRoleEnum
.
System
,
content
:
content
:
'If the content is code or code blocks, please
label the code type as accurately as possible.
'
'If the content is code or code blocks, please
mark the code type as accurately as possible!
'
});
});
// 获取 chatAPI
// 获取 chatAPI
...
@@ -74,8 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -74,8 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const
reg
=
/{"content"
(
.*
)
"}/g
;
const
reg
=
/{"content"
(
.*
)
"}/g
;
// @ts-ignore
// @ts-ignore
const
match
=
chatResponse
.
data
.
match
(
reg
);
const
match
=
chatResponse
.
data
.
match
(
reg
);
if
(
!
match
)
return
;
let
AIResponse
=
''
;
let
AIResponse
=
''
;
if
(
match
)
{
// 循环给 stream push 内容
match
.
forEach
((
item
:
string
,
i
:
number
)
=>
{
match
.
forEach
((
item
:
string
,
i
:
number
)
=>
{
try
{
try
{
const
json
=
JSON
.
parse
(
item
);
const
json
=
JSON
.
parse
(
item
);
...
@@ -83,15 +97,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -83,15 +97,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if
(
i
===
0
&&
json
.
content
?.
startsWith
(
'\n'
))
return
;
if
(
i
===
0
&&
json
.
content
?.
startsWith
(
'\n'
))
return
;
AIResponse
+=
json
.
content
;
AIResponse
+=
json
.
content
;
const
content
=
json
.
content
.
replace
(
/
\n
/g
,
'<br/>'
);
// 无法直接传输\n
const
content
=
json
.
content
.
replace
(
/
\n
/g
,
'<br/>'
);
// 无法直接传输\n
content
&&
res
.
write
(
`data:
${
content
}
\n\n`
);
if
(
content
)
{
responseData
.
push
(
`event: responseData\ndata:
${
content
}
\n\n`
);
// res.write(`event: responseData\n`)
// res.write(`data: ${content}\n\n`)
}
}
catch
(
err
)
{
}
catch
(
err
)
{
err
;
err
;
}
}
});
});
}
res
.
write
(
`data: [DONE]\n\n`
);
responseData
.
push
(
`event: done\ndata: \n\n`
);
// 存入库
// 存入库
(
async
()
=>
{
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
$push
:
{
$push
:
{
content
:
{
content
:
{
...
@@ -101,16 +119,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -101,16 +119,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
},
updateTime
:
Date
.
now
()
updateTime
:
Date
.
now
()
});
});
})();
res
.
end
();
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
console
.
error
(
err
?.
response
?.
data
||
err
);
let
errorText
=
err
;
if
(
err
.
code
===
'ECONNRESET'
)
{
errorText
=
'服务器代理出错'
;
}
else
{
switch
(
err
?.
response
?.
data
?.
error
?.
code
)
{
case
'invalid_api_key'
:
errorText
=
'API-KEY不合法'
;
break
;
case
'context_length_exceeded'
:
errorText
=
'内容超长了,请重置对话'
;
break
;
case
'rate_limit_reached'
:
errorText
=
'同时访问用户过多,请稍后再试'
;
break
;
case
null
:
errorText
=
'OpenAI 服务器访问超时'
;
break
;
default
:
errorText
=
'服务器异常'
;
}
}
console
.
error
(
errorText
);
responseData
.
push
(
`event: serviceError\ndata:
${
errorText
}
\n\n`
);
// 删除最一条数据库记录, 也就是预发送的那一条
// 删除最一条数据库记录, 也就是预发送的那一条
(
async
()
=>
{
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
await
ChatWindow
.
findByIdAndUpdate
(
windowId
,
{
$pop
:
{
content
:
1
},
$pop
:
{
content
:
1
},
updateTime
:
Date
.
now
()
updateTime
:
Date
.
now
()
});
});
})();
res
.
end
();
}
}
// 开启 stream 传输
stream
.
pipe
(
res
);
}
}
src/pages/api/chat/init.ts
View file @
7807b267
...
@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
});
// 安全校验
// 安全校验
if
(
chat
.
loadAmount
===
0
||
chat
.
expiredTime
<
Date
.
now
())
{
if
(
!
chat
||
chat
.
loadAmount
===
0
||
chat
.
expiredTime
<
Date
.
now
())
{
throw
new
Error
(
'聊天框已过期'
);
throw
new
Error
(
'聊天框已过期'
);
}
}
...
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
});
});
}
catch
(
err
)
{
}
catch
(
err
)
{
console
.
error
(
err
);
jsonRes
(
res
,
{
jsonRes
(
res
,
{
code
:
500
,
code
:
500
,
error
:
err
error
:
err
...
...
src/pages/chat/index.tsx
View file @
7807b267
...
@@ -14,7 +14,6 @@ import { useToast } from '@/hooks/useToast';
...
@@ -14,7 +14,6 @@ import { useToast } from '@/hooks/useToast';
import
Icon
from
'@/components/Icon'
;
import
Icon
from
'@/components/Icon'
;
import
{
useScreen
}
from
'@/hooks/useScreen'
;
import
{
useScreen
}
from
'@/hooks/useScreen'
;
import
{
useQuery
}
from
'@tanstack/react-query'
;
import
{
useQuery
}
from
'@tanstack/react-query'
;
import
{
useLoading
}
from
'@/hooks/useLoading'
;
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'
;
...
@@ -75,9 +74,9 @@ const Chat = () => {
...
@@ -75,9 +74,9 @@ const Chat = () => {
scrollToBottom
();
scrollToBottom
();
setLoading
(
false
);
setLoading
(
false
);
},
},
onError
()
{
onError
(
e
:
any
)
{
toast
({
toast
({
title
:
'初始化异常,请刷新
'
,
title
:
e
?.
message
||
'初始化异常,请检查地址
'
,
status
:
'error'
,
status
:
'error'
,
isClosable
:
true
,
isClosable
:
true
,
duration
:
5000
duration
:
5000
...
@@ -124,36 +123,55 @@ const Chat = () => {
...
@@ -124,36 +123,55 @@ const Chat = () => {
return
new
Promise
((
resolve
,
reject
)
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
event
=
getChatGPTSendEvent
(
chatId
,
windowId
);
const
event
=
getChatGPTSendEvent
(
chatId
,
windowId
);
event
.
onmessage
=
({
data
})
=>
{
// 30s 收不到消息就报错
if
(
data
===
'[DONE]'
)
{
let
timer
=
setTimeout
(()
=>
{
event
.
close
();
event
.
close
();
reject
(
'服务器超时'
);
},
300000
);
event
.
addEventListener
(
'responseData'
,
({
data
})
=>
{
/* 重置定时器 */
clearTimeout
(
timer
);
timer
=
setTimeout
(()
=>
{
event
.
close
();
reject
(
'服务器超时'
);
},
300000
);
const
msg
=
data
.
replace
(
/<br
\/
>/g
,
'\n'
);
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
+
msg
};
};
})
})
);
);
resolve
(
''
);
});
}
else
if
(
data
)
{
event
.
addEventListener
(
'done'
,
()
=>
{
const
msg
=
data
.
replace
(
/<br
\/
>/g
,
'\n'
);
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
,
value
:
item
.
value
+
msg
status
:
'finish'
};
};
})
})
);
);
}
resolve
(
''
);
};
});
event
.
onerror
=
(
err
)
=>
{
event
.
addEventListener
(
'serviceError'
,
({
data
:
err
})
=>
{
clearTimeout
(
timer
);
event
.
close
();
console
.
error
(
err
,
'==='
);
console
.
error
(
err
,
'==='
);
reject
(
typeof
err
===
'string'
?
err
:
'对话出现不知名错误~'
);
});
event
.
onerror
=
(
err
)
=>
{
clearTimeout
(
timer
);
event
.
close
();
event
.
close
();
reject
(
'对话出现错误'
);
console
.
error
(
err
);
reject
(
typeof
err
===
'string'
?
err
:
'对话出现不知名错误~'
);
};
};
});
});
},
},
...
@@ -320,6 +338,16 @@ const Chat = () => {
...
@@ -320,6 +338,16 @@ const Chat = () => {
</
Box
>
</
Box
>
))
}
))
}
</
Box
>
</
Box
>
{
/* 空内容提示 */
}
{
/* {
chatList.length === 0 && (
<>
<Card>
内容太长
</Card>
</>
)
} */
}
<
Box
<
Box
m=
{
media
(
'20px auto'
,
'0 auto'
)
}
m=
{
media
(
'20px auto'
,
'0 auto'
)
}
w=
{
media
(
'100vw'
,
'100%'
)
}
w=
{
media
(
'100vw'
,
'100%'
)
}
...
...
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