Skip to content

Commit

Permalink
feat: add chat record
Browse files Browse the repository at this point in the history
feat: persistent prompts
feat: add custom system role
feat: add GeminiPro option
  • Loading branch information
jamebal committed Jan 11, 2024
2 parents 46d819f + 3d20329 commit 3771a52
Show file tree
Hide file tree
Showing 32 changed files with 1,210 additions and 102 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: build_docker

on:
push:
branches: [main]
branches: [master]
release:
types: [created] # 表示在创建新的 Release 时触发

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name: CI
on:
push:
branches:
- main
- master

pull_request:
branches:
- main
- master

jobs:
lint:
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

package-lock.json
node_modules
.DS_Store
dist
Expand All @@ -30,4 +30,4 @@ coverage

# Environment variables files
/service/.env
/docker-compose/nginx/html
/docker-compose/nginx/html
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
[] 用户管理

[] 多 Key 随机

[] 持久化提示词
</br>

## 截图
Expand Down Expand Up @@ -228,14 +230,14 @@ http://localhost:3002/

#### Docker compose

[Hub 地址](https://hub.docker.com/repository/docker/kerwin1202/chatgpt-web/general)
[Hub 地址](https://hub.docker.com/r/jmal/chatgpt-web)

```yml
version: '3'

services:
app:
image: kerwin1202/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
image: jmal/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
container_name: chatgptweb
restart: unless-stopped
ports:
Expand Down
22 changes: 17 additions & 5 deletions service/src/chatgpt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getCacheApiKeys, getCacheConfig, getOriginConfig } from '../storage/con
import { sendResponse } from '../utils'
import { hasAnyRole, isNotEmptyString } from '../utils/is'
import type { ChatContext, ChatGPTUnofficialProxyAPIOptions, JWT, ModelConfig } from '../types'
import { getChatByMessageId, updateRoomAccountId } from '../storage/mongo'
import { getChatByMessageId, getUserById, updateRoomAccountId, updateRoomChatModel } from '../storage/mongo'
import type { RequestOptions } from './types'

const { HttpsProxyAgent } = httpsProxyAgent
Expand Down Expand Up @@ -118,7 +118,7 @@ async function chatReplyProcess(options: RequestOptions) {
const userId = options.user._id.toString()
const maxContextCount = options.user.advanced.maxContextCount ?? 20
const messageId = options.messageId
if (key == null || key === undefined)
if (key == null)
throw new Error('没有可用的配置。请再试一次 | No available configuration. Please try again.')

if (key.keyModel === 'ChatGPTUnofficialProxyAPI') {
Expand All @@ -131,17 +131,29 @@ async function chatReplyProcess(options: RequestOptions) {
}

const { message, lastContext, process, systemMessage, temperature, top_p } = options

Check warning on line 133 in service/src/chatgpt/index.ts

View workflow job for this annotation

GitHub Actions / lint

'temperature' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 133 in service/src/chatgpt/index.ts

View workflow job for this annotation

GitHub Actions / lint

'top_p' is assigned a value but never used. Allowed unused vars must match /^_/u
updateRoomChatModel(userId, options.room.roomId, model)

try {
const timeoutMs = (await getCacheConfig()).timeoutMs
let options: SendMessageOptions = { timeoutMs }

const user = await getUserById(userId)
if (key.keyModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage))
if (isNotEmptyString(systemMessage)) {
options.systemMessage = systemMessage
options.completionParams = { model, temperature, top_p }
}
}
else {
if (isNotEmptyString(user.systemRole))
options.systemMessage = user.systemRole
else
options.systemMessage = '你是一个大型语言模型。请仔细按照用户的指示进行操作。使用中文并且使用Markdown格式进行回复。'
}

const temperature: number = user.temperature ?? 0.8
const top_p: number = user.top_p ?? 0.9
const presence_penalty: number = user.presencePenalty ?? 0.6
options.completionParams = { model, temperature, top_p, presence_penalty }
}
if (lastContext != null) {
if (key.keyModel === 'ChatGPTAPI')
options.parentMessageId = lastContext.parentMessageId
Expand Down
156 changes: 150 additions & 6 deletions service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,30 @@ import type { ChatMessage } from './chatgpt'
import { abortChatProcess, chatConfig, chatReplyProcess, containsSensitiveWords, initAuditService } from './chatgpt'
import { auth, getUserId } from './middleware/auth'
import { clearApiKeyCache, clearConfigCache, getApiKeys, getCacheApiKeys, getCacheConfig, getOriginConfig } from './storage/config'
import type { AuditConfig, ChatInfo, ChatOptions, Config, KeyConfig, MailConfig, SiteConfig, UserConfig, UserInfo } from './storage/model'
import type { AuditConfig, ChatInfo, ChatOptions, Config, KeyConfig, MailConfig, SiteConfig, UserConfig, UserInfo, UserPrompt } from './storage/model'
import { AdvancedConfig, Status, UsageResponse, UserRole } from './storage/model'
import {
clearChat,
clearUserPrompt,
createChatRoom,
createUser,
deleteAllChatRooms,
deleteChat,
deleteChatRoom,
deleteUserPrompt,
disableUser2FA,
existsChatRoom,
getChat,
getChatRoom,
getChatRooms,
getChatRoomsCount,
getChats,
getUser,
getUserById,
getUserPromptList,
getUserStatisticsByDay,
getUsers,
importUserPrompt,
insertChat,
insertChatUsage,
renameChatRoom,
Expand All @@ -47,6 +52,7 @@ import {
updateUserPasswordWithVerifyOld,
updateUserStatus,
upsertKey,
upsertUserPrompt,
verifyUser,
} from './storage/mongo'
import { authLimiter, limiter } from './middleware/limiter'
Expand Down Expand Up @@ -93,6 +99,43 @@ router.get('/chatrooms', auth, async (req, res) => {
}
})

function formatTimestamp(timestamp: number) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')

return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

router.get('/chatrooms-count', auth, async (req, res) => {
try {
const userId = req.query.userId as string
const page = +req.query.page
const size = +req.query.size
const rooms = await getChatRoomsCount(userId, page, size)
const result = []
rooms.data.forEach((r) => {
result.push({
uuid: r.roomId,
title: r.title,
userId: r.userId,
name: r.username,
lastTime: formatTimestamp(r.dateTime),
chatCount: r.chatCount,
})
})
res.send({ status: 'Success', message: null, data: { data: result, total: rooms.total } })
}
catch (error) {
console.error(error)
res.send({ status: 'Fail', message: 'Load error', data: [] })
}
})

router.post('/room-create', auth, async (req, res) => {
try {
const userId = req.headers.userId as string
Expand Down Expand Up @@ -195,12 +238,35 @@ router.get('/chat-history', auth, async (req, res) => {
const userId = req.headers.userId as string
const roomId = +req.query.roomId
const lastId = req.query.lastId as string
if (!roomId || !await existsChatRoom(userId, roomId)) {
const all = req.query.all as string
if ((!roomId || !await existsChatRoom(userId, roomId)) && (all === null || all === 'undefined' || all === undefined || all.trim().length === 0)) {
res.send({ status: 'Success', message: null, data: [] })
return
}
const chats = await getChats(roomId, !isNotEmptyString(lastId) ? null : Number.parseInt(lastId))

if (all !== null && all !== 'undefined' && all !== undefined && all.trim().length !== 0) {
const config = await getCacheConfig()
if (config.siteConfig.loginEnabled) {
try {
const token = req.header('Authorization').replace('Bearer ', '')
const info = jwt.verify(token, config.siteConfig.loginSalt.trim())
req.headers.userId = info.userId
const user = await getUserById(info.userId)
if (user == null || user.status !== Status.Normal || !user.roles.includes(UserRole.Admin)) {
res.send({ status: 'Fail', message: '无权限 | No permission.', data: null })
return
}
}
catch (error) {
res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
}
}
else {
res.send({ status: 'Fail', message: '无权限 | No permission.', data: null })
}
}

const chats = await getChats(roomId, !isNotEmptyString(lastId) ? null : Number.parseInt(lastId), all)
const result = []
chats.forEach((c) => {
if (c.status !== Status.InversionDeleted) {
Expand Down Expand Up @@ -595,12 +661,16 @@ router.post('/session', async (req, res) => {
}
})

let userInfo: { name: string; description: string; avatar: string; userId: string; root: boolean; roles: UserRole[]; config: UserConfig; advanced: AdvancedConfig }
let userInfo: { name: string; description: string; temperature: number; top_p: number; presencePenalty: number; avatar: string; systemRole: string; userId: string; root: boolean; roles: UserRole[]; config: UserConfig; advanced: AdvancedConfig }
if (userId != null) {
const user = await getUserById(userId)
userInfo = {
name: user.name,
description: user.description,
temperature: user.temperature,
top_p: user.top_p,
presencePenalty: user.presencePenalty,
systemRole: user.systemRole,
avatar: user.avatar,
userId: user._id.toString(),
root: user.roles.includes(UserRole.Admin),
Expand Down Expand Up @@ -741,13 +811,13 @@ router.post('/user-reset-password', authLimiter, async (req, res) => {

router.post('/user-info', auth, async (req, res) => {
try {
const { name, avatar, description } = req.body as UserInfo
const { name, avatar, description, temperature, top_p, presencePenalty, systemRole } = req.body as UserInfo
const userId = req.headers.userId.toString()

const user = await getUserById(userId)
if (user == null || user.status !== Status.Normal)
throw new Error('用户不存在 | User does not exist.')
await updateUserInfo(userId, { name, avatar, description } as UserInfo)
await updateUserInfo(userId, { name, avatar, description, temperature, top_p, presencePenalty, systemRole } as UserInfo)
res.send({ status: 'Success', message: '更新成功 | Update successfully' })
}
catch (error) {
Expand Down Expand Up @@ -1175,6 +1245,80 @@ router.post('/statistics/by-day', auth, async (req, res) => {
}
})

router.get('/prompt-list', auth, async (req, res) => {
try {
const userId = req.headers.userId as string
const prompts = await getUserPromptList(userId)
const result = []
prompts.data.forEach((p) => {
result.push({
_id: p._id,
title: p.title,
value: p.value,
})
})
res.send({ status: 'Success', message: null, data: { data: result, total: prompts.total } })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})

router.post('/prompt-upsert', auth, async (req, res) => {
try {
const userId = req.headers.userId as string
const userPrompt = req.body as UserPrompt
if (userPrompt._id !== undefined)
userPrompt._id = new ObjectId(userPrompt._id)
userPrompt.userId = userId
await upsertUserPrompt(userPrompt)
res.send({ status: 'Success', message: '成功 | Successfully' })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})

router.post('/prompt-delete', auth, async (req, res) => {
try {
const { id } = req.body as { id: string }
await deleteUserPrompt(id)
res.send({ status: 'Success', message: '成功 | Successfully' })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})

router.post('/prompt-clear', auth, async (req, res) => {
try {
const userId = req.headers.userId as string
await clearUserPrompt(userId)
res.send({ status: 'Success', message: '成功 | Successfully' })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})

router.post('/prompt-import', auth, async (req, res) => {
try {
const userId = req.headers.userId as string
const userPrompt = req.body as UserPrompt[]
const updatedUserPrompt = userPrompt.map((prompt) => {
return {
...prompt,
userId,
}
})
await importUserPrompt(updatedUserPrompt)
res.send({ status: 'Success', message: '成功 | Successfully' })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})

app.use('', router)
app.use('/api', router)

Expand Down
2 changes: 1 addition & 1 deletion service/src/storage/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export async function getOriginConfig() {
}

if (!isNotEmptyString(config.siteConfig.chatModels))
config.siteConfig.chatModels = 'gpt-3.5-turbo,gpt-3.5-turbo-1106,gpt-3.5-turbo-16k,gpt-3.5-turbo-16k-0613,gpt-4,gpt-4-0613,gpt-4-32k,gpt-4-32k-0613,text-davinci-002-render-sha-mobile,text-embedding-ada-002,gpt-4-mobile,gpt-4-browsing,gpt-4-1106-preview,gpt-4-vision-preview'
config.siteConfig.chatModels = 'gpt-3.5-turbo,gpt-3.5-turbo-1106,gpt-3.5-turbo-16k,gpt-3.5-turbo-16k-0613,gpt-4,gpt-4-0613,gpt-4-32k,gpt-4-32k-0613,text-davinci-002-render-sha-mobile,text-embedding-ada-002,gpt-4-mobile,gpt-4-browsing,gpt-4-1106-preview,gpt-4-vision-preview,gemini-pro'

return config
}
Expand Down
16 changes: 16 additions & 0 deletions service/src/storage/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export enum UserRole {
export class UserInfo {
_id: ObjectId
name: string
temperature: number
top_p: number
presencePenalty: number
systemRole: string
email: string
password: string
status: Status
Expand Down Expand Up @@ -239,4 +243,16 @@ export class KeyConfig {
}
}

export class UserPrompt {
_id: ObjectId
userId: string
title: string
value: string
constructor(userId: string, title: string, value: string) {
this.userId = userId
this.title = title
this.value = value
}
}

export type APIMODEL = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI'
Loading

0 comments on commit 3771a52

Please sign in to comment.