Compare commits

4 Commits

Author SHA1 Message Date
ChenZhaoYu
10058f151c chore: version 2.7.0 2023-02-22 23:08:00 +08:00
Redon
09359c3c46 feat: 流式输出内容 (#93)
* feat: 流式输出内容

* fix: 修复异常状态

* feat: markdown 链接颜色
2023-02-22 23:03:20 +08:00
ChenZhaoYu
ba83856173 chore: v2.6.2 2023-02-22 22:57:09 +08:00
ChenZhaoYu
b84f7e4c72 fix: 还原配置 2023-02-22 22:55:53 +08:00
14 changed files with 179 additions and 52 deletions

4
.env
View File

@@ -1,5 +1,7 @@
# Glob API URL # Glob API URL
VITE_GLOB_API_URL=http://localhost:3002 VITE_GLOB_API_URL=/api
VITE_APP_API_BASE_URL=http://localhost:3002/
# Glob API Timeout (ms) # Glob API Timeout (ms)
VITE_GLOB_API_TIMEOUT=100000 VITE_GLOB_API_TIMEOUT=100000

View File

@@ -1 +0,0 @@
VITE_GLOB_HTTP_PROXY=Y

View File

@@ -1,3 +1,19 @@
## v2.7.0
`2023-02-23`
### Feature
- 使用消息流返回信息,反应更迅速
### Enhancement
- 样式的一点小改动
## v2.6.2
`2023-02-22`
### BugFix
- 还原修改代理导致的异常问题
## v2.6.1 ## v2.6.1
`2023-02-22` `2023-02-22`

View File

@@ -1,6 +1,6 @@
{ {
"name": "chatgpt-web", "name": "chatgpt-web",
"version": "2.6.1", "version": "2.7.0",
"private": false, "private": false,
"description": "ChatGPT Web", "description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>", "author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",

View File

@@ -15,6 +15,7 @@
}, },
"scripts": { "scripts": {
"start": "esno ./src/index.ts", "start": "esno ./src/index.ts",
"dev": "esno watch ./src/index.ts",
"prod": "esno ./build/index.js", "prod": "esno ./build/index.js",
"build": "pnpm clean && tsup", "build": "pnpm clean && tsup",
"clean": "rimraf build", "clean": "rimraf build",

View File

@@ -1,6 +1,6 @@
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import 'isomorphic-fetch' import 'isomorphic-fetch'
import type { ChatGPTAPI, SendMessageOptions } from 'chatgpt' import type { ChatGPTAPI, ChatMessage, SendMessageOptions } from 'chatgpt'
import { ChatGPTUnofficialProxyAPI } from 'chatgpt' import { ChatGPTUnofficialProxyAPI } from 'chatgpt'
import { sendResponse } from './utils' import { sendResponse } from './utils'
@@ -8,7 +8,7 @@ dotenv.config()
let apiModel: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined let apiModel: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
export interface ChatContext { interface ChatContext {
conversationId?: string conversationId?: string
parentMessageId?: string parentMessageId?: string
} }
@@ -65,6 +65,34 @@ async function chatReply(
} }
} }
async function chatReplyProcess(
message: string,
lastContext?: { conversationId?: string; parentMessageId?: string },
process?: (chat: ChatMessage) => void,
) {
if (!message)
return sendResponse({ type: 'Fail', message: 'Message is empty' })
try {
let options: SendMessageOptions = { timeoutMs }
if (lastContext)
options = { ...lastContext }
const response = await api.sendMessage(message, {
...options,
onProgress: (partialResponse) => {
process?.(partialResponse)
},
})
return sendResponse({ type: 'Success', data: response })
}
catch (error: any) {
return sendResponse({ type: 'Fail', message: error.message })
}
}
async function chatConfig() { async function chatConfig() {
return sendResponse({ return sendResponse({
type: 'Success', type: 'Success',
@@ -76,4 +104,6 @@ async function chatConfig() {
}) })
} }
export { chatReply, chatConfig } export type { ChatContext, ChatMessage }
export { chatReply, chatReplyProcess, chatConfig }

View File

@@ -1,6 +1,6 @@
import express from 'express' import express from 'express'
import type { ChatContext } from './chatgpt' import type { ChatContext, ChatMessage } from './chatgpt'
import { chatConfig, chatReply } from './chatgpt' import { chatConfig, chatReply, chatReplyProcess } from './chatgpt'
const app = express() const app = express()
const router = express.Router() const router = express.Router()
@@ -26,6 +26,23 @@ router.post('/chat', async (req, res) => {
} }
}) })
router.post('/chat-process', async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream')
try {
const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext }
await chatReplyProcess(prompt, options, (chat: ChatMessage) => {
res.write(JSON.stringify(chat))
})
}
catch (error) {
res.write(JSON.stringify(error))
}
finally {
res.end()
}
})
router.post('/config', async (req, res) => { router.post('/config', async (req, res) => {
try { try {
const response = await chatConfig() const response = await chatConfig()

View File

@@ -1,4 +1,4 @@
import type { GenericAbortSignal } from 'axios' import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { post } from '@/utils/request' import { post } from '@/utils/request'
export function fetchChatAPI<T = any>( export function fetchChatAPI<T = any>(
@@ -13,6 +13,21 @@ export function fetchChatAPI<T = any>(
}) })
} }
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chat-process',
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
export function fetchChatConfig<T = any>() { export function fetchChatConfig<T = any>() {
return post<T>({ return post<T>({
url: '/config', url: '/config',

View File

@@ -3,5 +3,5 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_GLOB_API_URL: string; readonly VITE_GLOB_API_URL: string;
readonly VITE_GLOB_API_TIMEOUT: string; readonly VITE_GLOB_API_TIMEOUT: string;
readonly VITE_GLOB_HTTP_PROXY: 'Y' | 'N'; readonly VITE_APP_API_BASE_URL: string;
} }

View File

@@ -1,7 +1,7 @@
import axios, { type AxiosResponse } from 'axios' import axios, { type AxiosResponse } from 'axios'
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_GLOB_HTTP_PROXY === 'Y' ? '/api' : import.meta.env.VITE_GLOB_API_URL, baseURL: import.meta.env.VITE_GLOB_API_URL,
timeout: !isNaN(+import.meta.env.VITE_GLOB_API_TIMEOUT) ? Number(import.meta.env.VITE_GLOB_API_TIMEOUT) : 60 * 1000, timeout: !isNaN(+import.meta.env.VITE_GLOB_API_TIMEOUT) ? Number(import.meta.env.VITE_GLOB_API_TIMEOUT) : 60 * 1000,
}) })

View File

@@ -1,4 +1,4 @@
import type { AxiosResponse, GenericAbortSignal } from 'axios' import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'
import request from './axios' import request from './axios'
export interface HttpOption { export interface HttpOption {
@@ -6,6 +6,7 @@ export interface HttpOption {
data?: any data?: any
method?: string method?: string
headers?: any headers?: any
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
signal?: GenericAbortSignal signal?: GenericAbortSignal
beforeRequest?: () => void beforeRequest?: () => void
afterRequest?: () => void afterRequest?: () => void
@@ -17,9 +18,11 @@ export interface Response<T = any> {
status: string status: string
} }
function http<T = any>({ url, data, method, headers, signal, beforeRequest, afterRequest }: HttpOption) { function http<T = any>(
{ url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
) {
const successHandler = (res: AxiosResponse<Response<T>>) => { const successHandler = (res: AxiosResponse<Response<T>>) => {
if (res.data.status === 'Success') if (res.data.status === 'Success' || typeof res.data === 'string')
return res.data return res.data
return Promise.reject(res.data) return Promise.reject(res.data)
@@ -37,17 +40,18 @@ function http<T = any>({ url, data, method, headers, signal, beforeRequest, afte
const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
return method === 'GET' return method === 'GET'
? request.get(url, { params, signal }).then(successHandler, failHandler) ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler)
: request.post(url, params, { headers, signal }).then(successHandler, failHandler) : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler)
} }
export function get<T = any>( export function get<T = any>(
{ url, data, method = 'GET', signal, beforeRequest, afterRequest }: HttpOption, { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
): Promise<Response<T>> { ): Promise<Response<T>> {
return http<T>({ return http<T>({
url, url,
method, method,
data, data,
onDownloadProgress,
signal, signal,
beforeRequest, beforeRequest,
afterRequest, afterRequest,
@@ -55,13 +59,14 @@ export function get<T = any>(
} }
export function post<T = any>( export function post<T = any>(
{ url, data, method = 'POST', headers, signal, beforeRequest, afterRequest }: HttpOption, { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
): Promise<Response<T>> { ): Promise<Response<T>> {
return http<T>({ return http<T>({
url, url,
method, method,
data, data,
headers, headers,
onDownloadProgress,
signal, signal,
beforeRequest, beforeRequest,
afterRequest, afterRequest,

View File

@@ -15,7 +15,7 @@ const props = defineProps<Props>()
const wrapClass = computed(() => { const wrapClass = computed(() => {
return [ return [
'text-wrap', 'text-wrap',
'p-2', 'p-3',
'min-w-[20px]', 'min-w-[20px]',
'rounded-md', 'rounded-md',
props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]', props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',
@@ -51,6 +51,9 @@ const text = computed(() => {
max-width: 100%; max-width: 100%;
vertical-align: middle; vertical-align: middle;
} }
a {
color: #2d5cf6
}
} }
.hljs { .hljs {

View File

@@ -8,7 +8,7 @@ import { useChat } from './hooks/useChat'
import { HoverButton, SvgIcon } from '@/components/common' import { HoverButton, SvgIcon } from '@/components/common'
import { useBasicLayout } from '@/hooks/useBasicLayout' import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useChatStore } from '@/store' import { useChatStore } from '@/store'
import { fetchChatAPI } from '@/api' import { fetchChatAPIProcess } from '@/api'
let controller = new AbortController() let controller = new AbortController()
@@ -80,22 +80,39 @@ async function onConversation() {
) )
scrollToBottom() scrollToBottom()
let offset = 0
try { try {
const { data } = await fetchChatAPI<Chat.ConversationResponse>(message, options, controller.signal) await fetchChatAPIProcess<Chat.ConversationResponse>({
updateChat( prompt: message,
+uuid, options,
dataSources.value.length - 1, signal: controller.signal,
{ onDownloadProgress: ({ event }) => {
dateTime: new Date().toLocaleString(), const xhr = event.target
text: data.text ?? '', const { responseText } = xhr
inversion: false, const chunk = responseText.substring(offset)
error: false, offset = responseText.length
loading: false, try {
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, const data = JSON.parse(chunk)
requestOptions: { prompt: message, options: { ...options } }, updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: data.text ?? '',
inversion: false,
error: false,
loading: false,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottom()
}
catch (error) {
//
}
}, },
) })
scrollToBottom()
} }
catch (error: any) { catch (error: any) {
let errorMessage = error?.message ?? 'Something went wrong, please try again later.' let errorMessage = error?.message ?? 'Something went wrong, please try again later.'
@@ -119,6 +136,7 @@ async function onConversation() {
scrollToBottom() scrollToBottom()
} }
finally { finally {
offset = 0
loading.value = false loading.value = false
} }
} }
@@ -154,24 +172,41 @@ async function onRegenerate(index: number) {
}, },
) )
let offset = 0
try { try {
const { data } = await fetchChatAPI<Chat.ConversationResponse>(message, options, controller.signal) await fetchChatAPIProcess<Chat.ConversationResponse>({
updateChat( prompt: message,
+uuid, options,
index, signal: controller.signal,
{ onDownloadProgress: ({ event }) => {
dateTime: new Date().toLocaleString(), const xhr = event.target
text: data.text ?? '', const { responseText } = xhr
inversion: false, const chunk = responseText.substring(offset)
error: false, offset = responseText.length
loading: false, try {
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, const data = JSON.parse(chunk)
requestOptions: { prompt: message, ...options }, updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: data.text ?? '',
inversion: false,
error: false,
loading: false,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, ...options },
},
)
}
catch (error) {
//
}
}, },
) })
} }
catch (error: any) { catch (error: any) {
let errorMessage = 'Something went wrong, please try again later.' let errorMessage = error?.message ?? 'Something went wrong, please try again later.'
if (error.message === 'canceled') if (error.message === 'canceled')
errorMessage = 'Request canceled. Please try again.' errorMessage = 'Request canceled. Please try again.'
@@ -192,6 +227,7 @@ async function onRegenerate(index: number) {
} }
finally { finally {
loading.value = false loading.value = false
offset = 0
} }
} }

View File

@@ -1,13 +1,10 @@
import path from 'path' import path from 'path'
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { createViteProxy } from './config'
export default defineConfig((env) => { export default defineConfig((env) => {
const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv
const isOpenProxy = viteEnv.VITE_GLOB_HTTP_PROXY === 'Y'
return { return {
resolve: { resolve: {
alias: { alias: {
@@ -19,7 +16,13 @@ export default defineConfig((env) => {
host: '0.0.0.0', host: '0.0.0.0',
port: 1002, port: 1002,
open: false, open: false,
proxy: createViteProxy(isOpenProxy, viteEnv), proxy: {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true, // 允许跨域
rewrite: path => path.replace('/api/', '/'),
},
},
}, },
build: { build: {
reportCompressedSize: false, reportCompressedSize: false,