first commit

This commit is contained in:
1iaan
2026-04-02 17:13:39 +08:00
commit de99cb2806
154 changed files with 40725 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
docs

20
README.md Normal file
View File

@@ -0,0 +1,20 @@
```shell
# chatgpt-web-backend
go mod tidy
go run cmd/main.go \
--frontend-path www \
--openapi-key $MOONSHOT_API_KEY \
--openapi-base-url https://api.moonshot.cn/v1 \
--openai-model kimi-k2.5 \
--openai-temperature 100 \
--openai-presence-penalty 0 \
--openai-frequency-penalty 0
# chatgpt-web-frontend
HUSKY=0 pnpm bootstrap
pnpm dev
```

2
chatgpt-web-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
dist/
__pycache__/

View File

@@ -0,0 +1,11 @@
FROM chenzhaoyu94/chatgpt-web:v2.10.9 as frontend
FROM arvintian/chatgpt-web-base:v1
COPY --from=frontend /app/public /app/public
ADD dist/server /app/server
EXPOSE 7080
CMD ["/app/server"]

View File

@@ -0,0 +1,8 @@
FROM python:3.10-alpine
WORKDIR app
ADD tokenizer.py /app/tokenizer.py
ADD requirements.txt /app/requirements.txt
RUN pip install -i https://mirrors.aliyun.com/pypi/simple --upgrade pip
RUN pip install --root-user-action=ignore -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt

View File

@@ -0,0 +1,31 @@
REGISTRY = arvintian
PROJECT = chatgpt-web
BASE_VERSION = v1
GIT_VERSION = $(shell git rev-parse --short HEAD)
.PHONY: build-local
build-local:
go build -v --ldflags="-w -X main.Version=$(GIT_VERSION)" -o dist/server cmd/*.go
.PHONY: build
build:
mkdir -p dist && docker run --rm -ti -e GOPROXY=https://goproxy.cn,direct -v $(GOPATH):/go -v `pwd`:/app -w /app golang:1.19-alpine \
go build -v --ldflags="-w -X main.Version=$(GIT_VERSION)" -o dist/server cmd/*.go
package: build
docker build -t $(REGISTRY)/$(PROJECT):$(GIT_VERSION) .
release: package
docker tag $(REGISTRY)/$(PROJECT):$(GIT_VERSION) $(REGISTRY)/$(PROJECT):latest
docker push $(REGISTRY)/$(PROJECT):$(GIT_VERSION)
docker push $(REGISTRY)/$(PROJECT):latest
base:
docker build -t $(REGISTRY)/$(PROJECT)-base:$(BASE_VERSION) -f Dockerfile.base .
release-base:
docker push $(REGISTRY)/$(PROJECT)-base:$(BASE_VERSION)
clean:
rm -rf dist
docker images | grep -E "$(REGISTRY)/$(PROJECT)" | grep -v "base" | awk '{print $$3}' | uniq | xargs -I {} docker rmi --force {}

View File

@@ -0,0 +1,38 @@
# ChatGPT-Web
[English](https://github.com/Arvintian/chatgpt-web/blob/main/README_en.md)
使用[Gin](https://github.com/gin-gonic/gin)搭建ChatGPT服务,使用[ChatGPT Web](https://github.com/Chanzhaoyu/chatgpt-web)作为前端
## Usage
[Docker Hub](https://hub.docker.com/repository/docker/arvintian/chatgpt-web/general)
```
docker run --restart unless-stopped -d --log-opt max-size=50m -p 7080:7080 \
-e OPENAI_KEY=openai-key \
-e BASIC_AUTH_USER=user1,user2 \
-e BASIC_AUTH_PASSWORD=passwd1,passwd2 \
arvintian/chatgpt-web
```
- SERVER_PORT 服务端口,默认7080
- SERVER_HOST 服务监听地址,默认0.0.0.0
- SOCKS_PROXY socks代理URL,例如socks5://user:password@127.0.0.1:1080
- BASIC_AUTH_USER 认证用户,多用户英文逗号分隔
- BASIC_AUTH_PASSWORD 认证用户密码,多用户英文逗号分隔
- CHAT_SESSION_TTL 会话上下文保持时间,默认30分钟
- CHAT_MIN_RESPONSE_TOKENS 预留给会话响应的token数,可能导致截断最久的上下文,默认600
- OPENAI_KEY openai api key,参考OpenAI文档
- OPENAI_BASE_URL openai api base url,默认https://api.openai.com/v1
- OPENAI_MODEL 调用模型,默认gpt-3.5-turbo-0301
- OPENAI_MAX_TOKENS 模型max_tokens参数,参考OpenAI文档
- OPENAI_TEMPERATURE 模型temperature参数,参考OpenAI文档
- OPENAI_PRESENCE_PENALTY 模型presence_penalty参数,参考OpenAI文档
- OPENAI_FREQUENCY_PENALTY 模型frequency_penalty参数,参考OpenAI文档
更详细参数参考: [启动函数](https://github.com/Arvintian/chatgpt-web/blob/main/cmd/main.go#L21)
Tips:
- 模型float32参数使用(整型/100)设置,例如: temperature设置0.8,需要设置为80
- 内置支持了对OPENAI_BASE_URL的正向代理,可以作为OpenAI接口的代理服务器

View File

@@ -0,0 +1,34 @@
Built ChatGPT service using [Gin](https://github.com/gin-gonic/gin) and [ChatGPT Web](https://github.com/Chanzhaoyu/chatgpt-web) as the front-end.
## Usage
[Docker Hub](https://hub.docker.com/repository/docker/arvintian/chatgpt-web/general)
```
docker run --restart unless-stopped -d --log-opt max-size=50m -p 7080:7080 \
-e OPENAI_KEY=openai-key \
-e BASIC_AUTH_USER=user1,user2 \
-e BASIC_AUTH_PASSWORD=passwd1,passwd2 \
arvintian/chatgpt-web
```
- SERVER_PORT: Server port, default 7080.
- SERVER_HOST: Server listen address, default 0.0.0.0.
- SOCKS_PROXY: Socks proxy URL, for example socks5://user:password@127.0.0.1:1080.
- BASIC_AUTH_USER: Authentication user, multiple users separated by English commas.
- BASIC_AUTH_PASSWORD: Authentication user passwords, multiple users separated by English commas.
- CHAT_SESSION_TTL: Session context retention time, default 30 minutes.
- CHAT_MIN_RESPONSE_TOKENS: Tokens reserved for session response, may lead to truncation of the longest context, default 600.
- OPENAI_KEY: OpenAI API key, refer to OpenAI documentation.
- OPENAI_BASE_URL: OpenAI API base URL, default https://api.openai.com/v1.
- OPENAI_MODEL: Model called, default gpt-3.5-turbo-0301.
- OPENAI_MAX_TOKENS: Model max_tokens parameter, refer to OpenAI documentation.
- OPENAI_TEMPERATURE: Model temperature parameter, refer to OpenAI documentation.
- OPENAI_PRESENCE_PENALTY: Model presence_penalty parameter, refer to OpenAI documentation.
- OPENAI_FREQUENCY_PENALTY: Model frequency_penalty parameter, refer to OpenAI documentation.
For more detailed parameters, please refer to the [start function](https://github.com/Arvintian/chatgpt-web/blob/main/cmd/main.go#L21).
Tips:
- Use (integer/100) to set the float32 model parameters. For example, if temperature is set to 0.8, it needs to be set to 80.
- The built-in support for a forward proxy of OPENAI_BASE_URL enables it to function as a proxy server for the OpenAI API.

View File

@@ -0,0 +1,196 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/Arvintian/chatgpt-web/pkg/controllers"
"github.com/Arvintian/chatgpt-web/pkg/middlewares"
"github.com/Arvintian/chatgpt-web/pkg/utils"
"github.com/Arvintian/go-utils/cmdutil"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
)
type ChatGPTWebServer struct {
Host string `name:"host" env:"SERVER_HOST" usage:"http bind host" default:"0.0.0.0"`
Port int `name:"port" env:"SERVER_PORT" usage:"http bind port" default:"7080"`
BasicAuthUser string `name:"auth-user" env:"BASIC_AUTH_USER" usage:"http basic auth user"`
BasicAuthPassword string `name:"auth-password" env:"BASIC_AUTH_PASSWORD" usage:"http basic auth password"`
FrontendPath string `name:"frontend-path" env:"FRONTEND_PATH" default:"/app/public" usage:"frontend path"`
SocksProxy string `name:"socks-proxy" env:"SOCKS_PROXY" usage:"socks proxy url"`
ChatSessionTTL int `name:"chat-session-ttl" env:"CHAT_SESSION_TTL" default:"30" usage:"chat session ttl minute"`
ChatMinResponseTokens int `name:"chat-min-response-tokens" env:"CHAT_MIN_RESPONSE_TOKENS" default:"600" usage:"chat min response tokens"`
OpenAIKey string `name:"openapi-key" env:"OPENAI_KEY" usage:"openai key"`
OpenAIBaseURL string `name:"openapi-base-url" env:"OPENAI_BASE_URL" default:"https://api.openai.com/v1" usage:"openai base url"`
OpenAIModel string `name:"openai-model" env:"OPENAI_MODEL" default:"gpt-3.5-turbo-0301" usage:"openai params model"`
OpenAIMaxTokens int `name:"openai-max-tokens" env:"OPENAI_MAX_TOKENS" default:"4096" usage:"openai params max-tokens"`
OpenAITemperature int `name:"openai-temperature" env:"OPENAI_TEMPERATURE" default:"80" usage:"openai params temperature"`
OpenAIPresencePenalty int `name:"openai-presence-penalty" env:"OPENAI_PRESENCE_PENALTY" default:"100" usage:"openai params presence-penalty"`
OpenAIFrequencyPenalty int `name:"openai-frequency-penalty" env:"OPENAI_FREQUENCY_PENALTY" default:"0" usage:"openai params frequency-penalty"`
Version bool `name:"version" usage:"show version"`
}
var Version = "0.0.0-dev"
func (r *ChatGPTWebServer) Run(cmd *cobra.Command, args []string) error {
if r.Version {
return r.ShowVersion()
}
gin.SetMode(gin.ReleaseMode)
if err := r.updateAssetsFiles(); err != nil {
return err
}
go r.startTokenizer(cmd.Context())
go r.httpServer(cmd.Context())
<-cmd.Context().Done()
return nil
}
func (r *ChatGPTWebServer) httpServer(ctx context.Context) {
chatService, err := controllers.NewChatService(r.OpenAIKey, r.OpenAIBaseURL, r.SocksProxy, controllers.ChatCompletionParams{
Model: r.OpenAIModel,
MaxTokens: r.OpenAIMaxTokens,
Temperature: float32(r.OpenAITemperature) / 100.0,
PresencePenalty: float32(r.OpenAIPresencePenalty) / 100.0,
FrequencyPenalty: float32(r.OpenAIFrequencyPenalty) / 100.0,
ChatSessionTTL: time.Duration(r.ChatSessionTTL) * time.Minute,
ChatMinResponseTokens: r.ChatMinResponseTokens,
})
if err != nil {
klog.Fatal(err)
}
addr := fmt.Sprintf("%s:%d", r.Host, r.Port)
klog.Infof("ChatGPT Web Server on: %s", addr)
server := &http.Server{
Addr: addr,
}
entry, proxy := gin.New(), gin.New()
entry.Use(gin.Logger())
entry.Use(gin.Recovery())
chat := entry.Group("/api")
if len(r.BasicAuthUser) > 0 {
accounts := gin.Accounts{}
users := strings.Split(r.BasicAuthUser, ",")
passwords := strings.Split(r.BasicAuthPassword, ",")
if len(users) != len(passwords) {
panic("basic auth setting error")
}
for i := 0; i < len(users); i++ {
accounts[users[i]] = passwords[i]
}
chat.POST("/chat-process", gin.BasicAuth(accounts), middlewares.RateLimitMiddleware(1, 2), chatService.ChatProcess)
} else {
chat.POST("/chat-process", middlewares.RateLimitMiddleware(1, 2), chatService.ChatProcess)
}
chat.POST("/config", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"status": "Success",
"data": map[string]string{
"apiModel": "ChatGPTAPI",
"socksProxy": r.SocksProxy,
},
})
})
chat.POST("/session", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"status": "Success",
"message": "",
"data": gin.H{
"auth": false,
},
})
})
upstreamURL, err := url.Parse(strings.TrimSuffix(r.OpenAIBaseURL, "/v1"))
if err != nil {
klog.Fatal(err)
}
upstream := httputil.NewSingleHostReverseProxy(upstreamURL)
if r.SocksProxy != "" {
proxyUrl, err := url.Parse(r.SocksProxy)
if err != nil {
klog.Fatal(err)
}
upstream.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
}
}
apis := proxy.Group("/v1")
apis.Any("/*relativePath", func(ctx *gin.Context) {
ctx.Request.Host = upstreamURL.Host
upstream.ServeHTTP(ctx.Writer, ctx.Request)
})
proxy.NoRoute(func(ctx *gin.Context) {
http.FileServer(http.Dir(r.FrontendPath)).ServeHTTP(ctx.Writer, ctx.Request)
})
entry.NoRoute(func(ctx *gin.Context) {
proxy.ServeHTTP(ctx.Writer, ctx.Request)
})
server.Handler = entry
go func(ctx context.Context) {
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server shutdown with error %v", err)
}
}(ctx)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server listen and serve error %v", err)
}
}
func (r *ChatGPTWebServer) startTokenizer(ctx context.Context) {
// devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0755)
// if err != nil {
// klog.Error(err)
// os.Exit(1)
// }
args := strings.Split("nuxt --module tokenizer.py --workers 2", " ")
klog.Infof("Start Tokenizer with %v", args)
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
klog.Error(err)
os.Exit(1)
}
}
func (r *ChatGPTWebServer) updateAssetsFiles() error {
pairs := map[string]string{}
old := `{avatar:"https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg",name:"ChenZhaoYu",description:'Star on <a href="https://github.com/Chanzhaoyu/chatgpt-bot" class="text-blue-500" target="_blank" >Github</a>'}`
new := `{avatar:"https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg",name:"ChatGPT",description:'Star on <a href="https://github.com/Arvintian/chatgpt-web" class="text-blue-500" target="_blank" >Github</a>'}`
pairs[old] = new
old = `{}.VITE_GLOB_OPEN_LONG_REPLY`
new = `{VITE_GLOB_OPEN_LONG_REPLY:"true"}.VITE_GLOB_OPEN_LONG_REPLY`
pairs[old] = new
old = `<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script>`
new = ``
pairs[old] = new
return utils.ReplaceFiles(r.FrontendPath, pairs)
}
func (r *ChatGPTWebServer) ShowVersion() error {
fmt.Println(Version)
return nil
}
func main() {
root := cmdutil.Command(&ChatGPTWebServer{}, cobra.Command{
Long: "ChatGPT Web Server",
})
cmdutil.Main(root)
}

View File

@@ -0,0 +1,43 @@
module github.com/Arvintian/chatgpt-web
go 1.19
require (
github.com/Arvintian/go-utils v0.0.0-20221012040808-2e61c0c3eece
github.com/gin-gonic/gin v1.9.0
github.com/google/uuid v1.3.0
github.com/karlseguin/ccache/v3 v3.0.3
github.com/sashabaranov/go-openai v1.4.2
github.com/spf13/cobra v1.6.0
golang.org/x/time v0.3.0
k8s.io/klog/v2 v2.60.1
)
require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

105
chatgpt-web-backend/go.sum Normal file
View File

@@ -0,0 +1,105 @@
github.com/Arvintian/go-utils v0.0.0-20221012040808-2e61c0c3eece h1:2pV+ZmYaakmpP0DASolUiKVCykieF5AWzz2mMjhiaEY=
github.com/Arvintian/go-utils v0.0.0-20221012040808-2e61c0c3eece/go.mod h1:EM4VUepvcCPF4HFKf2Baq9nlNReRTJI4jKw2+rCvsx4=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/karlseguin/ccache/v3 v3.0.3 h1:cz+3tSdTrovp00xHPP3Y6ca/YuSl5kchhYG83wUPYN0=
github.com/karlseguin/ccache/v3 v3.0.3/go.mod h1:qxC372+Qn+IBj8Pe3KvGjHPj0sWwEF7AeZVhsNPZ6uY=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sashabaranov/go-openai v1.4.2 h1:IhacPY7O+ljlBoZRQe9VpsLNm0b4PHa6fOBGA9O4vfc=
github.com/sashabaranov/go-openai v1.4.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc=
k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

BIN
chatgpt-web-backend/main Executable file

Binary file not shown.

View File

@@ -0,0 +1,295 @@
package controllers
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/Arvintian/chatgpt-web/pkg/tokenizer"
"github.com/Arvintian/chatgpt-web/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
ccache "github.com/karlseguin/ccache/v3"
openai "github.com/sashabaranov/go-openai"
"k8s.io/klog/v2"
)
const (
ChatPrimedTokens = 2
KimiTopP = 0.95
)
type ChatService struct {
client *openai.Client
store *ccache.Cache[ChatMessage]
params ChatCompletionParams
}
type ChatCompletionParams struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float32 `json:"temperature,omitempty"`
PresencePenalty float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty float32 `json:"frequency_penalty,omitempty"`
ChatSessionTTL time.Duration `json:"chat_session_ttl"`
ChatMinResponseTokens int `json:"chat_min_response_tokens"`
}
type ChatMessageRequest struct {
Prompt string `json:"prompt"`
Options ChatMessageRequestOptions `json:"options"`
}
type ChatMessageRequestOptions struct {
Name string `json:"name"`
ParentMessageId string `json:"parentMessageId"`
}
type ChatMessage struct {
ID string `json:"id"`
Text string `json:"text"`
Role string `json:"role"`
Name string `json:"name"`
Delta string `json:"delta"`
Detail openai.ChatCompletionStreamResponse `json:"detail"`
TokenCount int `json:"tokenCount"`
ParentMessageId string `json:"parentMessageId"`
}
func NewChatService(apiKey string, baseURL string, socksProxy string, params ChatCompletionParams) (*ChatService, error) {
config := openai.DefaultConfig(apiKey)
if baseURL != "" {
config.BaseURL = baseURL
}
klog.Infof("use openai base url: %s", config.BaseURL)
if socksProxy != "" {
proxyUrl, err := url.Parse(socksProxy) //socks5://user:password@127.0.0.1:1080
if err != nil {
return nil, err
}
config.HTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
},
}
klog.Infof("use sock proxy: %s", proxyUrl)
}
chat := ChatService{
client: openai.NewClientWithConfig(config),
params: params,
store: ccache.New(ccache.Configure[ChatMessage]()),
}
return &chat, nil
}
func (chat *ChatService) ChatProcess(ctx *gin.Context) {
payload := ChatMessageRequest{}
if err := ctx.BindJSON(&payload); err != nil {
klog.Error(err)
ctx.JSON(200, gin.H{
"status": "Fail",
"message": fmt.Sprintf("%v", err),
"data": nil,
})
return
}
messageID := uuid.New().String()
message := ChatMessage{
ID: messageID,
Role: openai.ChatMessageRoleUser,
Text: payload.Prompt,
ParentMessageId: payload.Options.ParentMessageId,
}
result := ChatMessage{
ID: uuid.New().String(),
Role: openai.ChatMessageRoleAssistant,
Text: "",
ParentMessageId: messageID,
}
messages, numTokens, tokenCount, err := chat.buildMessage(payload)
if err != nil {
ctx.JSON(200, gin.H{
"status": "Fail",
"message": fmt.Sprintf("%v", err),
"data": nil,
})
return
}
message.TokenCount = tokenCount
chat.store.Set(messageID, message, chat.params.ChatSessionTTL)
//klog.Infof("send message %d tokens, set completion %d max tokens", numTokens, chat.params.MaxTokens-numTokens)
stream, err := chat.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: chat.params.Model,
Messages: messages,
MaxTokens: chat.params.MaxTokens - numTokens,
Temperature: chat.params.Temperature,
PresencePenalty: chat.params.PresencePenalty,
FrequencyPenalty: chat.params.FrequencyPenalty,
TopP: chat.topP(),
Stream: true,
})
if err != nil {
klog.Error(err)
ctx.JSON(200, gin.H{
"status": "Fail",
"message": fmt.Sprintf("%v", err),
"data": nil,
})
return
}
defer stream.Close()
resp := stream.GetResponse()
if resp.StatusCode != 200 {
bts, _ := io.ReadAll(resp.Body)
ctx.JSON(200, gin.H{
"status": "Fail",
"message": fmt.Sprintf("%v", string(bts)),
"data": nil,
})
return
}
firstChunk := true
ctx.Header("Content-type", "application/octet-stream")
for {
rsp, err := stream.Recv()
if errors.Is(err, io.EOF) {
go func() {
tokenCount, err := tokenizer.GetTokenCount(openai.ChatCompletionMessage{
Role: result.Role,
Content: result.Text,
Name: result.Name,
}, chat.params.Model)
if err != nil {
klog.Error(err)
}
result.TokenCount = tokenCount
chat.store.Set(result.ID, result, chat.params.ChatSessionTTL)
}()
return
}
if err != nil {
klog.Error(err)
ctx.JSON(200, gin.H{
"status": "Fail",
"message": fmt.Sprintf("OpenAI Event Error %v", err),
"data": nil,
})
return
}
if rsp.ID != "" {
result.ID = rsp.ID
}
if len(rsp.Choices) > 0 {
content := rsp.Choices[0].Delta.Content
result.Delta = content
if len(content) > 0 {
result.Text += content
}
result.Detail = rsp
}
bts, err := json.Marshal(result)
if err != nil {
klog.Error(err)
ctx.JSON(200, gin.H{
"status": "Fail",
"message": fmt.Sprintf("OpenAI Event Marshal Error %v", err),
"data": nil,
})
return
}
if !firstChunk {
ctx.Writer.Write([]byte("\n"))
} else {
firstChunk = false
}
if _, err := ctx.Writer.Write(bts); err != nil {
klog.Error(err)
return
}
ctx.Writer.Flush()
}
}
func (chat *ChatService) topP() float32 {
if strings.HasPrefix(chat.params.Model, "kimi-") || strings.HasPrefix(chat.params.Model, "moonshot-") {
return KimiTopP
}
return 1
}
func (chat *ChatService) buildMessage(payload ChatMessageRequest) ([]openai.ChatCompletionMessage, int, int, error) {
parentMessageId := payload.Options.ParentMessageId
messages := []openai.ChatCompletionMessage{}
tokenCount := 0
var err error
if len(payload.Prompt) > 0 {
chatMessage := openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser,
Content: payload.Prompt,
Name: payload.Options.Name,
}
messages = append(messages, chatMessage)
tokenCount, err = tokenizer.GetTokenCount(chatMessage, chat.params.Model)
if err != nil {
return nil, 0, 0, err
}
if tokenCount >= (chat.params.MaxTokens - chat.params.ChatMinResponseTokens) {
return nil, 0, 0, fmt.Errorf("this model's maximum context length is %d tokens. you requested %d tokens in the messages", chat.params.MaxTokens, tokenCount)
}
}
numTokens := tokenCount + ChatPrimedTokens
for {
if parentMessageId == "" {
break
}
parentMessage, ok := chat.getMessageByID(parentMessageId)
if !ok {
break
}
parentCompletioMessage := openai.ChatCompletionMessage{
Role: parentMessage.Role,
Content: parentMessage.Text,
Name: parentMessage.Name,
}
if (numTokens + parentMessage.TokenCount) >= (chat.params.MaxTokens - chat.params.ChatMinResponseTokens) {
break
}
numTokens += parentMessage.TokenCount
messages = append(messages, parentCompletioMessage)
parentMessageId = parentMessage.ParentMessageId
}
utils.Reverse(messages)
return messages, numTokens, tokenCount, nil
}
func (chat *ChatService) getMessageByID(id string) (ChatMessage, bool) {
item := chat.store.Get(id)
if item == nil {
return ChatMessage{}, false
}
if item.Expired() {
return ChatMessage{}, false
}
return item.Value(), true
}

View File

@@ -0,0 +1,23 @@
package middlewares
import (
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
func RateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc {
limiter := rate.NewLimiter(r, b)
return func(c *gin.Context) {
if !limiter.Allow() {
// 请求被限制,返回错误信息
c.JSON(429, gin.H{
"status": "Fail",
"message": "Too many requests, please try again later",
"data": nil,
})
c.Abort()
return
}
c.Next()
}
}

View File

@@ -0,0 +1,43 @@
package tokenizer
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/sashabaranov/go-openai"
)
type tokenInfo struct {
Code int `json:"code"`
Count int `json:"num_tokens"`
Msg string `json:"msg"`
}
func GetTokenCount(message openai.ChatCompletionMessage, model string) (int, error) {
url := fmt.Sprintf("http://127.0.0.1:5000/tokenizer/%s", model)
info := tokenInfo{}
if err := postJSON(url, &message, &info); err != nil {
return 0, err
}
if info.Code != 200 {
return 0, fmt.Errorf("%v", info.Msg)
}
return info.Count, nil
}
func postJSON(url string, requestData *openai.ChatCompletionMessage, responseData *tokenInfo) error {
requestBody, err := json.Marshal(requestData)
if err != nil {
return err
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
return err
}
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(responseData)
}

View File

@@ -0,0 +1,65 @@
package utils
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"k8s.io/klog/v2"
)
func ReplaceInFile(filePath string, targetStr string, replaceStr string) error {
// 读取文件内容
content, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
// 判断是否需要替换
if !strings.Contains(string(content), targetStr) {
return nil
}
// 替换字符串并写回文件,保持原有 filemode
info, err := os.Stat(filePath)
if err != nil {
return err
}
newContent := strings.ReplaceAll(string(content), targetStr, replaceStr)
err = ioutil.WriteFile(filePath, []byte(newContent), info.Mode())
if err != nil {
return err
}
klog.Infof("Replaced in file: %s\n", filePath)
return nil
}
func ReplaceFiles(rootDir string, replacePairs map[string]string) error {
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 如果当前路径是目录,则继续遍历
if info.IsDir() {
return nil
}
// 处理文件
for targetStr, replaceStr := range replacePairs {
err = ReplaceInFile(path, targetStr, replaceStr)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func Reverse[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

View File

@@ -0,0 +1,2 @@
nuxt>=0.2.0
tiktoken>=0.3.3

View File

@@ -0,0 +1 @@
{avatar:"https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg",name:"ChatGPT",description:'知之为知之'}

View File

@@ -0,0 +1,65 @@
from nuxt import route, logger, Request
from nuxt.repositorys.validation import fields, use_args
import traceback
import tiktoken
encoding_cache = {}
support_models = set(["kimi-k2.5",
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-32k"])
support_model_prefixes = (
"gpt-3.5-turbo-",
"gpt-4-",
"moonshot-",
"kimi-",
)
@route("/tokenizer/<str:model_name>", methods=["POST"])
@use_args({
"role": fields.Str(required=True),
"content": fields.Str(required=True),
"name": fields.Str(required=False)
}, location="json")
def get_num_tokens(req: Request, message: dict, model_name: str):
try:
return {
"code": 200,
"num_tokens": num_tokens_from_messages([message], model=model_name)
}
except Exception as e:
logger.error(traceback.format_exc())
return {
"code": 500,
"msg": "{}".format(e)
}
def num_tokens_from_messages(messages, model="gpt-3.5-turbo"):
"""Returns the number of tokens used by a list of messages."""
encoding = None
if model in encoding_cache:
encoding = encoding_cache.get(model)
else:
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
encoding_cache[model] = encoding
if model in support_models or model.startswith(support_model_prefixes):
num_tokens = 0
for message in messages:
num_tokens += 4 # every message follows <im_start>{role/name}\n{content}<im_end>\n
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name": # if there's a name, the role is omitted
num_tokens += -1 # role is always required and always 1 token
num_tokens += 3 # every reply is primed with <im_start>assistant
return num_tokens
else:
raise NotImplementedError(f"""num_tokens_from_messages() is not presently implemented for model {model}.
See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""")

View File

@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

View File

@@ -0,0 +1,7 @@
**/node_modules
*/node_modules
node_modules
Dockerfile
.*
*/.*
!.env

View File

@@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

10
chatgpt-web-frontend/.env Normal file
View File

@@ -0,0 +1,10 @@
# Glob API URL
VITE_GLOB_API_URL=/api
VITE_APP_API_BASE_URL=http://127.0.0.1:7080/
# Whether long replies are supported, which may result in higher API fees
VITE_GLOB_OPEN_LONG_REPLY=false
# When you want to use PWA
VITE_GLOB_APP_PWA=false

View File

@@ -0,0 +1,2 @@
docker-compose
kubernetes

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['@antfu'],
}

17
chatgpt-web-frontend/.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.cjs" eol=lf
"*.cts" eol=lf
"*.mjs" eol=lf
"*.mts" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.less" eol=lf
"*.scss" eol=lf
"*.sass" eol=lf
"*.styl" eol=lf
"*.md" eol=lf

32
chatgpt-web-frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables files
/service/.env

View File

@@ -0,0 +1 @@
strict-peer-dependencies=false

View File

@@ -0,0 +1,602 @@
## v2.11.0
`2023-04-26`
> [chatgpt-web-plus](https://github.com/Chanzhaoyu/chatgpt-web-plus) 新界面、完整用户管理
## Enhancement
- 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation` [[24min](https://github.com/Chanzhaoyu/chatgpt-web/pull/1567/files)]
- 添加自定义 `temperature``top_p` [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/1260)]
- 优化代码 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/1328)]
- 优化复制代码反馈效果
## BugFix
- 修复余额查询和文案 [[luckywangxi](https://github.com/Chanzhaoyu/chatgpt-web/pull/1174)][[zuoning777](https://github.com/Chanzhaoyu/chatgpt-web/pull/1296)]
- 修复默认语言错误 [[idawnwon](https://github.com/Chanzhaoyu/chatgpt-web/pull/1352)]
- 修复 `onRegenerate` 下问题 [[leafsummer](https://github.com/Chanzhaoyu/chatgpt-web/pull/1188)]
## Other
- 引导用户触发提示词 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/1183)]
- 添加韩语翻译 [[Kamilake](https://github.com/Chanzhaoyu/chatgpt-web/pull/1372)]
- 添加俄语翻译 [[aquaratixc](https://github.com/Chanzhaoyu/chatgpt-web/pull/1571)]
- 优化翻译和文本检查 [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/1460)]
- 移除无用文件
## v2.10.9
`2023-04-03`
> 更新默认 `accessToken` 反代地址为 [[pengzhile](https://github.com/pengzhile)] 的 `https://ai.fakeopen.com/api/conversation`
## Enhancement
- 添加 `socks5` 代理认证 [[yimiaoxiehou](https://github.com/Chanzhaoyu/chatgpt-web/pull/999)]
- 添加 `socks` 代理用户名密码的配置 [[hank-cp](https://github.com/Chanzhaoyu/chatgpt-web/pull/890)]
- 添加可选日志打印 [[zcong1993](https://github.com/Chanzhaoyu/chatgpt-web/pull/1041)]
- 更新侧边栏按钮本地化[[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/911)]
- 优化代码块滚动条高度 [[Fog3211](https://github.com/Chanzhaoyu/chatgpt-web/pull/1153)]
## BugFix
- 修复 `PWA` 问题 [[bingo235](https://github.com/Chanzhaoyu/chatgpt-web/pull/807)]
- 修复 `ESM` 错误 [[kidonng](https://github.com/Chanzhaoyu/chatgpt-web/pull/826)]
- 修复反向代理开启时限流失效的问题 [[gitgitgogogo](https://github.com/Chanzhaoyu/chatgpt-web/pull/863)]
- 修复 `docker` 构建时 `.env` 可能被忽略的问题 [[zaiMoe](https://github.com/Chanzhaoyu/chatgpt-web/pull/877)]
- 修复导出异常错误 [[KingTwinkle](https://github.com/Chanzhaoyu/chatgpt-web/pull/938)]
- 修复空值异常 [[vchenpeng](https://github.com/Chanzhaoyu/chatgpt-web/pull/1103)]
- 移动端上的体验问题
## Other
- `Docker` 容器名字名义 [[LOVECHEN](https://github.com/Chanzhaoyu/chatgpt-web/pull/1035)]
- `kubernetes` 部署配置 [[CaoYunzhou](https://github.com/Chanzhaoyu/chatgpt-web/pull/1001)]
- 感谢 [[assassinliujie](https://github.com/Chanzhaoyu/chatgpt-web/pull/962)] 和 [[puppywang](https://github.com/Chanzhaoyu/chatgpt-web/pull/1017)] 的某些贡献
- 更新 `kubernetes/deploy.yaml` [[idawnwon](https://github.com/Chanzhaoyu/chatgpt-web/pull/1085)]
- 文档更新 [[#yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/883)]
- 文档更新 [[weifeng12x](https://github.com/Chanzhaoyu/chatgpt-web/pull/880)]
- 依赖更新
## v2.10.8
`2023-03-23`
如遇问题,请删除 `node_modules` 重新安装依赖。
## Feature
- 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)]
- 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)]
- 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)]
- `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token``API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)]
- 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)]
## Enhancement
- 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)]
-`API``gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)]
- 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)]
- 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)]
- 渲染的链接新窗口打开
- 查询余额可选 `API_BASE_URL` 代理地址
- `config` 接口添加验证防止被无限制调用
- `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量
- 当网络连接时,刷新页面,`500` 错误页自动跳转到主页
## BugFix
- `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)]
- 重置异常的 `loading` 会话
## Common
- 创建 `start.cmd``windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)]
- 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)]
- 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)]
## Other
- 依赖更新
## v2.10.7
`2023-03-17`
## BugFix
- 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效
- 修复缺省状态的 `usingContext` 默认值
## v2.10.6
`2023-03-17`
## Feature
- 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)]
## Enhancement
- 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)]
- 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)]
- 上下文开关改为全局开关,现在记录在本地缓存中
- 配置信息按接口类型显示
## Perf
- 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)]
- 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)]
- 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)]
## BugFix
- 修复 `Prompt` 导入、导出兼容性错误
- 修复 `highlight.js` 控制台兼容性警告
## Other
- 依赖更新
## v2.10.5
`2023-03-13`
更新依赖,`access_token` 默认代理为 [pengzhile](https://github.com/pengzhile) 的 `https://bypass.duti.tech/api/conversation`
## Feature
- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521)
- 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308)
- `Prompt` 添加查询筛选
## Enhancement
- 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502)
- 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520)
- `Prompt` 添加翻译和优化布局
- 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542)
- 语言选择调整为下路框形式
- 权限输入框类型调整为密码形式
## BugFix
- `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523)
- 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499)
- 确定清空上下文时不应该重置会话标题
## Other
- 调整文档
- 更新依赖
## v2.10.4
`2023-03-11`
## Feature
- 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持
## Enhancement
- 设置添加关闭按钮[#495]
## Demo
![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966)
## v2.10.3
`2023-03-10`
> 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`如果你觉得本项目有监控后门或有问题导致你的账号、API被封那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。
## Feature
- 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)]
- 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)]
## Enhancement
- 调整移动端按钮和优化布局
- 调整 `iOS` 上安全距离
- 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)]
## BugFix
- 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)]
- 修复设置文字过长时导致的设置按钮消失的问题
## Other
- 更新依赖
## v2.10.2
`2023-03-09`
衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1)
## Enhancement
- 移动端下输入框获得焦点时左侧按钮隐藏
## BugFix
- 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉
- 回退 `2.10.1` 中前端变量影响 `Docker` 打包
## v2.10.1
`2023-03-09`
注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下:
- 在项目中添加 `.env` 从一开始就是个错误的示范
- 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰
- 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改
这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。
## Feature
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能
## Enhancement
- 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题
- 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL`
- 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题
## BugFix
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题
- 修复深色模式导出图片的样式问题
## v2.10.0
`2023-03-07`
- 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。
- 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。
- 演示图片请看最后
## Feature
- 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码
- 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译
- 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能
## Enhancement
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息
- 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性
- 优化部分代码
## BugFix
- 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充)
## Other
- 更新依赖到最新
## 演示
> 不是界面最新效果,有美化改动
权限
![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png)
聊天记录导出
![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif)
保存图片到本地
![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif)
## v2.9.3
`2023-03-06`
## Enhancement
- 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题
- 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档
- 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译
## BugFix
- 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题
- 修复复制代码的兼容性报错问题
- 修复部分优化小问题
## v2.9.2
`2023-03-04`
手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。
### Feature
- 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持
- 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持
- 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249]
## Enhancement
- 优化在高分屏上主题内容的最大宽度[#257]
- 现在文字按单词截断[#215][#225]
### BugFix
- 修复动态生成时代码块不能被复制的问题[#251][#260]
- 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256]
- 修复控制台渲染警告
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.1
`2023-03-02`
### Feature
- 代码块添加当前代码语言显示和复制功能[#197][#196]
- 完善多语言,现在可以切换中英文显示
## Enhancement
- 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档
### BugFix
- 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.0
`2023-03-02`
### Feature
- 现在能复制带格式的消息文本
- 新设计的设定页面,可以自定义姓名、描述、头像(链接方式)
- 新增`403``404`页面以便扩展
## Enhancement
- 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认)
- 取消了前端超时限制设定
## v2.8.3
`2023-03-01`
### Feature
- 消息已输出内容不会因为中断而消失[#167]
- 添加复制消息按钮[#133]
### Other
- `README` 添加声明内容
## v2.8.2
`2023-02-28`
### Enhancement
- 代码主题调整为 `One Dark - light|dark` 适配深色模式
### BugFix
- 修复普通文本代码渲染和深色模式下的问题[#139][#154]
## v2.8.1
`2023-02-27`
### BugFix
- 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146]
## v2.8.0
`2023-02-27`
- 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容)
- 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。
- 建议更新到此版本获得比较完整的体验
### Enhancement
- 优化了部份代码和类型提示
- 输入框添加换行提示
- 移动端输入框现在回车为换行,而不是直接提交
- 移动端双击标题返回顶部,箭头返回底部
### BugFix
- 流输出数据下的问题[#122]
- 修复了 `API Key` 下部份代码不换行的问题
- 修复移动端深色模式部份样式问题[#123][#126]
- 修复主题模式图标不一致的问题[#126]
## v2.7.3
`2023-02-25`
### Feature
- 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103)
### BugFix
- 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117)
## v2.7.2
`2023-02-24`
### Enhancement
- 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法
- 移除测试无用函数
## v2.7.1
`2023-02-23`
因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式
### Feature
- 现在可以中断请求过长没有答复的消息
- 现在可以删除单条消息
- 设置中显示当前版本信息
### BugFix
- 回退 `2.7.0` 的消息不稳定的问题
## v2.7.0
`2023-02-23`
### Feature
- 使用消息流返回信息,反应更迅速
### Enhancement
- 样式的一点小改动
## v2.6.2
`2023-02-22`
### BugFix
- 还原修改代理导致的异常问题
## v2.6.1
`2023-02-22`
### Feature
- 新增 `Railway` 部署模版
### BugFix
- 手动打包 `Proxy` 问题
## v2.6.0
`2023-02-21`
### Feature
- 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51)
- 前端页面设置按钮显示查看当前后端服务配置
### Enhancement
- 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62)
## v2.5.2
`2023-02-21`
### Feature
- 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77)
### BugFix
- 重载会话时滚动条保持
## v2.5.1
`2023-02-21`
### Enhancement
- 调整路由模式为 `hash`
- 调整新增会话添加到
- 调整移动端样式
## v2.5.0
`2023-02-20`
### Feature
- 会话 `loading` 现在显示为光标动画
- 会话现在可以再次生成回复
- 会话异常可以再次进行请求
- 所有删除选项添加确认操作
### Enhancement
- 调整 `chat` 为路由页面而不是组件形式
- 更新依赖至最新
- 调整移动端体验
### BugFix
- 修复移动端左侧菜单显示不完整的问题
## v2.4.1
`2023-02-18`
### Enhancement
- 调整部份移动端上的样式
- 输入框支持换行
## v2.4.0
`2023-02-17`
### Feature
- 响应式支持移动端
### Enhancement
- 修改部份描述错误
## v2.3.3
`2023-02-16`
### Feature
- 添加 `README` 部份说明和贡献列表
- 添加 `docker` 镜像
- 添加 `GitHub Action` 自动化构建
### BugFix
- 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896)
## v2.3.2
`2023-02-16`
### Enhancement
- 更新依赖至最新
- 优化部份内容
## v2.3.1
`2023-02-15`
### BugFix
- 修复多会话状态下一些意想不到的问题
## v2.3.0
`2023-02-15`
### Feature
- 代码类型信息高亮显示
- 支持 `node ^16` 版本
- 移动端响应式初步支持
- `vite``proxy` 代理
### Enhancement
- 调整超时处理范围
### BugFix
- 修复取消请求错误提示会添加到信息中
- 修复部份情况下提交请求不可用
- 修复侧边栏宽度变化闪烁的问题
## v2.2.0
`2023-02-14`
### Feature
- 会话和上下文本地储存
- 侧边栏本地储存
## v2.1.0
`2023-02-14`
### Enhancement
- 更新依赖至最新
- 联想功能移动至前端提交,后端只做转发
### BugFix
- 修复部份项目检测有关 `Bug`
- 修复清除上下文按钮失效
## v2.0.0
`2023-02-13`
### Refactor
重构并优化大部分内容
## v1.0.5
`2023-02-12`
### Enhancement
- 输入框焦点,连续提交
### BugFix
- 修复信息框样式问题
- 修复中文输入法提交问题
## v1.0.4
`2023-02-11`
### Feature
- 支持上下文联想
## v1.0.3
`2023-02-11`
### Enhancement
- 拆分 `service` 文件以便扩展
- 调整 `Eslint` 相关验证
### BugFix
- 修复部份控制台报错
## v1.0.2
`2023-02-10`
### BugFix
- 修复新增信息容器不会自动滚动到问题
- 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1)

View File

@@ -0,0 +1,49 @@
# Contribution Guide
Thank you for your valuable time. Your contributions will make this project better! Before submitting a contribution, please take some time to read the getting started guide below.
## Semantic Versioning
This project follows semantic versioning. We release patch versions for important bug fixes, minor versions for new features or non-important changes, and major versions for significant and incompatible changes.
Each major change will be recorded in the `changelog`.
## Submitting Pull Request
1. Fork [this repository](https://github.com/Chanzhaoyu/chatgpt-web) and create a branch from `main`. For new feature implementations, submit a pull request to the `feature` branch. For other changes, submit to the `main` branch.
2. Install the `pnpm` tool using `npm install pnpm -g`.
3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`.
4. Execute `pnpm bootstrap` in the root directory.
5. Execute `pnpm install` in the `/service/` directory.
6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done.
7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check.
8. Execute `pnpm type-check` in the root directory to perform a type check.
9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines).
10. Submit a `pull request`. If there is a corresponding `issue`, please link it using the [linking-a-pull-request-to-an-issue keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).
## Commit Guidelines
Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/):
```bash
<type>[optional scope]: <description>
[optional body]
[optional footer]
```
### Commit Types
The following is a list of commit types:
- feat: New feature or functionality
- fix: Bug fix
- docs: Documentation update
- style: Code style or component style update
- refactor: Code refactoring, no new features or bug fixes introduced
- perf: Performance optimization
- test: Unit test
- chore: Other commits that do not modify src or test files
## License
[MIT](./license)

View File

@@ -0,0 +1,49 @@
# 贡献指南
感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。
## 语义化版本
该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。
每个重大更改都将记录在 `changelog` 中。
## 提交 Pull Request
1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。
2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。
3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。
4. 根目录下执行 `pnpm bootstrap`
5. `/service/` 目录下执行 `pnpm install`
6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。
7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。
8. 请在根目录下执行 `pnpm type-check` 进行类型检查。
9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南)
10. 提交 `pull request` 如果有对应的 `issue`,请进行[关联](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)。
## Commit 指南
Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)
```bash
<类型>[可选 范围]: <描述>
[可选 正文]
[可选 脚注]
```
### Commit 类型
以下是 commit 类型列表:
- feat: 新特性或功能
- fix: 缺陷修复
- docs: 文档更新
- style: 代码风格或者组件样式更新
- refactor: 代码重构,不引入新功能和缺陷修复
- perf: 性能优化
- test: 单元测试
- chore: 其他不修改 src 或测试文件的提交
## License
[MIT](./license)

View File

@@ -0,0 +1,56 @@
# build front-end
FROM node:lts-alpine AS frontend
RUN npm install pnpm -g
WORKDIR /app
COPY ./package.json /app
COPY ./pnpm-lock.yaml /app
RUN pnpm install
COPY . /app
RUN pnpm run build
# build backend
FROM node:lts-alpine as backend
RUN npm install pnpm -g
WORKDIR /app
COPY /service/package.json /app
COPY /service/pnpm-lock.yaml /app
RUN pnpm install
COPY /service /app
RUN pnpm build
# service
FROM node:lts-alpine
RUN npm install pnpm -g
WORKDIR /app
COPY /service/package.json /app
COPY /service/pnpm-lock.yaml /app
RUN pnpm install --production && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*
COPY /service /app
COPY --from=frontend /app/dist /app/public
COPY --from=backend /app/build /app/build
EXPOSE 3002
CMD ["pnpm", "run", "prod"]

View File

@@ -0,0 +1,355 @@
# ChatGPT Web
> 声明:此项目只发布于 GitHub基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
更多功能:[chatgpt-web-plus](https://github.com/Chanzhaoyu/chatgpt-web-plus)
![cover](./docs/c1.png)
![cover2](./docs/c2.png)
- [ChatGPT Web](#chatgpt-web)
- [介绍](#介绍)
- [待实现路线](#待实现路线)
- [前置要求](#前置要求)
- [Node](#node)
- [PNPM](#pnpm)
- [填写密钥](#填写密钥)
- [安装依赖](#安装依赖)
- [后端](#后端)
- [前端](#前端)
- [测试环境运行](#测试环境运行)
- [后端服务](#后端服务)
- [前端网页](#前端网页)
- [环境变量](#环境变量)
- [打包](#打包)
- [使用 Docker](#使用-docker)
- [Docker 参数示例](#docker-参数示例)
- [Docker build \& Run](#docker-build--run)
- [Docker compose](#docker-compose)
- [防止爬虫抓取](#防止爬虫抓取)
- [使用 Railway 部署](#使用-railway-部署)
- [Railway 环境变量](#railway-环境变量)
- [手动打包](#手动打包)
- [后端服务](#后端服务-1)
- [前端网页](#前端网页-1)
- [常见问题](#常见问题)
- [参与贡献](#参与贡献)
- [赞助](#赞助)
- [License](#license)
## 介绍
支持双模型,提供了两种非官方 `ChatGPT API` 方法
| 方式 | 免费? | 可靠性 | 质量 |
| --------------------------------------------- | ------ | ---------- | ---- |
| `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 |
| `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 |
对比:
1. `ChatGPTAPI` 使用 `gpt-3.5-turbo` 通过 `OpenAI` 官方 `API` 调用 `ChatGPT`
2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(依赖于第三方服务器,并且有速率限制)
警告:
1. 你应该首先使用 `API` 方式
2. 使用 `API` 时,如果网络不通,那是国内被墙了,你需要自建代理,绝对不要使用别人的公开代理,那是危险的。
3. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌,这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险。
4. 使用 `accessToken` 时,不管你是国内还是国外的机器,都会使用代理。默认代理为 [pengzhile](https://github.com/pengzhile) 大佬的 `https://ai.fakeopen.com/api/conversation`,这不是后门也不是监听,除非你有能力自己翻过 `CF` 验证,用前请知悉。[社区代理](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别)
5. 把项目发布到公共网络时,你应该设置 `AUTH_SECRET_KEY` 变量添加你的密码访问权限,你也应该修改 `index.html` 中的 `title`,防止被关键词搜索到。
切换方式:
1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件
2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview)
3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session)
4. 同时存在时以 `OpenAI API Key` 优先
环境变量:
全部参数变量请查看或[这里](#环境变量)
```
/service/.env.example
```
## 待实现路线
[✓] 双模型
[✓] 多会话储存和上下文逻辑
[✓] 对代码等消息类型的格式化美化处理
[✓] 访问权限控制
[✓] 数据导入、导出
[✓] 保存消息到本地图片
[✓] 界面多语言
[✓] 界面主题
[✗] More...
## 前置要求
### Node
`node` 需要 `^16 || ^18 || ^19` 版本(`node >= 14` 需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本
```shell
node -v
```
### PNPM
如果你没有安装过 `pnpm`
```shell
npm install pnpm -g
```
### 填写密钥
获取 `Openai Api Key``accessToken` 并填写本地环境变量 [跳转](#介绍)
```
# service/.env 文件
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
OPENAI_ACCESS_TOKEN=
```
## 安装依赖
> 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。
### 后端
进入文件夹 `/service` 运行以下命令
```shell
pnpm install
```
### 前端
根目录下运行以下命令
```shell
pnpm bootstrap
```
## 测试环境运行
### 后端服务
进入文件夹 `/service` 运行以下命令
```shell
pnpm start
```
### 前端网页
根目录下运行以下命令
```shell
pnpm dev
```
## 环境变量
`API` 可用:
- `OPENAI_API_KEY``OPENAI_ACCESS_TOKEN` 二选一
- `OPENAI_API_MODEL` 设置模型,可选,默认:`gpt-3.5-turbo`
- `OPENAI_API_BASE_URL` 设置接口地址,可选,默认:`https://api.openai.com`
- `OPENAI_API_DISABLE_DEBUG` 设置接口关闭 debug 日志可选默认empty 不关闭
`ACCESS_TOKEN` 可用:
- `OPENAI_ACCESS_TOKEN``OPENAI_API_KEY` 二选一,同时存在时,`OPENAI_API_KEY` 优先
- `API_REVERSE_PROXY` 设置反向代理,可选,默认:`https://ai.fakeopen.com/api/conversation`[社区](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)(注意:只有这两个是推荐,其他第三方来源,请自行甄别)
通用:
- `AUTH_SECRET_KEY` 访问权限密钥,可选
- `MAX_REQUEST_PER_HOUR` 每小时最大请求次数,可选,默认无限
- `TIMEOUT_MS` 超时,单位毫秒,可选
- `SOCKS_PROXY_HOST``SOCKS_PROXY_PORT` 一起时生效,可选
- `SOCKS_PROXY_PORT``SOCKS_PROXY_HOST` 一起时生效,可选
- `HTTPS_PROXY` 支持 `http``https`, `socks5`,可选
- `ALL_PROXY` 支持 `http``https`, `socks5`,可选
## 打包
### 使用 Docker
#### Docker 参数示例
![docker](./docs/docker.png)
#### Docker build & Run
```bash
docker build -t chatgpt-web .
# 前台运行
docker run --name chatgpt-web --rm -it -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
# 后台运行
docker run --name chatgpt-web -d -p 127.0.0.1:3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
# 运行地址
http://localhost:3002/
```
#### Docker compose
[Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general)
```yml
version: '3'
services:
app:
image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可
ports:
- 127.0.0.1:3002:3002
environment:
# 二选一
OPENAI_API_KEY: sk-xxx
# 二选一
OPENAI_ACCESS_TOKEN: xxx
# API接口地址可选设置 OPENAI_API_KEY 时可用
OPENAI_API_BASE_URL: xxx
# API模型可选设置 OPENAI_API_KEY 时可用https://platform.openai.com/docs/models
# gpt-4, gpt-4-0314, gpt-4-32k, gpt-4-32k-0314, gpt-3.5-turbo, gpt-3.5-turbo-0301, text-davinci-003, text-davinci-002, code-davinci-002
OPENAI_API_MODEL: xxx
# 反向代理,可选
API_REVERSE_PROXY: xxx
# 访问权限密钥,可选
AUTH_SECRET_KEY: xxx
# 每小时最大请求次数,可选,默认无限
MAX_REQUEST_PER_HOUR: 0
# 超时,单位毫秒,可选
TIMEOUT_MS: 60000
# Socks代理可选和 SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_HOST: xxx
# Socks代理端口可选和 SOCKS_PROXY_HOST 一起时生效
SOCKS_PROXY_PORT: xxx
# HTTPS 代理,可选,支持 httphttpssocks5
HTTPS_PROXY: http://xxx:7890
```
- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用
- `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用
#### 防止爬虫抓取
**nginx**
将下面配置填入nginx配置文件中可以参考 `docker-compose/nginx/nginx.conf` 文件中添加反爬虫的方法
```
# 防止爬虫抓取
if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
{
return 403;
}
```
### 使用 Railway 部署
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc)
#### Railway 环境变量
| 环境变量名称 | 必填 | 备注 |
| --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- |
| `PORT` | 必填 | 默认 `3002`
| `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 |
| `MAX_REQUEST_PER_HOUR` | 可选 | 每小时最大请求次数,可选,默认无限 |
| `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 |
| `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) |
| `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) |
| `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 |
| `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 |
| `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) |
| `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 |
| `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 |
| `SOCKS_PROXY_USERNAME` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理用户名 |
| `SOCKS_PROXY_PASSWORD` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理密码 |
| `HTTPS_PROXY` | 可选 | HTTPS 代理,支持 httphttps, socks5 |
| `ALL_PROXY` | 可选 | 所有代理 代理,支持 httphttps, socks5 |
> 注意: `Railway` 修改环境变量会重新 `Deploy`
### 手动打包
#### 后端服务
> 如果你不需要本项目的 `node` 接口,可以省略如下操作
复制 `service` 文件夹到你有 `node` 服务环境的服务器上。
```shell
# 安装
pnpm install
# 打包
pnpm build
# 运行
pnpm prod
```
PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可
#### 前端网页
1、修改根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 为你的实际后端接口地址
2、根目录下运行以下命令然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下
[参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app)
```shell
pnpm build
```
## 常见问题
Q: 为什么 `Git` 提交总是报错?
A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md)
Q: 如果只使用前端页面,在哪里改请求接口?
A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。
Q: 文件保存时全部爆红?
A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。
Q: 前端没有打字机效果?
A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。
## 参与贡献
贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md)
感谢所有做过贡献的人!
<a href="https://github.com/Chanzhaoyu/chatgpt-web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Chanzhaoyu/chatgpt-web" />
</a>
## 赞助
如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~
<div style="display: flex; gap: 20px;">
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/wechat.png" alt="微信" />
<p>WeChat Pay</p>
</div>
<div style="text-align: center">
<img style="max-width: 100%" src="./docs/alipay.png" alt="支付宝" />
<p>Alipay</p>
</div>
</div>
## License
MIT © [ChenZhaoYu](./license)

View File

@@ -0,0 +1,47 @@
version: '3'
services:
app:
container_name: chatgpt-web
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
ports:
- 3002:3002
environment:
# 二选一
OPENAI_API_KEY:
# 二选一
OPENAI_ACCESS_TOKEN:
# API接口地址可选设置 OPENAI_API_KEY 时可用
OPENAI_API_BASE_URL:
# API模型可选设置 OPENAI_API_KEY 时可用
OPENAI_API_MODEL:
# 反向代理,可选
API_REVERSE_PROXY:
# 访问权限密钥,可选
AUTH_SECRET_KEY:
# 每小时最大请求次数,可选,默认无限
MAX_REQUEST_PER_HOUR: 0
# 超时,单位毫秒,可选
TIMEOUT_MS: 60000
# Socks代理可选和 SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_HOST:
# Socks代理端口可选和 SOCKS_PROXY_HOST 一起时生效
SOCKS_PROXY_PORT:
# Socks代理用户名可选和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_USERNAME:
# Socks代理密码可选和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_PASSWORD:
# HTTPS_PROXY 代理,可选
HTTPS_PROXY:
nginx:
container_name: nginx
image: nginx:alpine
ports:
- '80:80'
expose:
- '80'
volumes:
- ./nginx/html:/usr/share/nginx/html
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
links:
- app

View File

@@ -0,0 +1,27 @@
server {
listen 80;
server_name localhost;
charset utf-8;
error_page 500 502 503 504 /50x.html;
# 防止爬虫抓取
if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
{
return 403;
}
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
location /api {
proxy_set_header X-Real-IP $remote_addr; #转发用户IP
proxy_pass http://app:3002;
}
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

View File

@@ -0,0 +1,14 @@
### docker-compose 部署教程
- 将打包好的前端文件放到 `nginx/html` 目录下
- ```shell
# 启动
docker-compose up -d
```
- ```shell
# 查看运行状态
docker ps
```
- ```shell
# 结束运行
docker-compose down
```

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta content="yes" name="apple-mobile-web-app-capable"/>
<link rel="apple-touch-icon" href="/favicon.ico">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<title>ChatGPT Web</title>
</head>
<body class="dark:bg-black">
<div id="app">
<style>
.loading-wrap {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.balls {
width: 4em;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
}
.balls div {
width: 0.8em;
height: 0.8em;
border-radius: 50%;
background-color: #4b9e5f;
}
.balls div:nth-of-type(1) {
transform: translateX(-100%);
animation: left-swing 0.5s ease-in alternate infinite;
}
.balls div:nth-of-type(3) {
transform: translateX(-95%);
animation: right-swing 0.5s ease-out alternate infinite;
}
@keyframes left-swing {
50%,
100% {
transform: translateX(95%);
}
}
@keyframes right-swing {
50% {
transform: translateX(-95%);
}
100% {
transform: translateX(100%);
}
}
@media (prefers-color-scheme: dark) {
body {
background: #121212;
}
}
</style>
<div class="loading-wrap">
<div class="balls">
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
## 增加一个Kubernetes的部署方式
```
kubectl apply -f deploy.yaml
```
### 如果需要Ingress域名接入
```
kubectl apply -f ingress.yaml
```

View File

@@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: chatgpt-web
labels:
app: chatgpt-web
spec:
replicas: 1
selector:
matchLabels:
app: chatgpt-web
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: chatgpt-web
spec:
containers:
- image: chenzhaoyu94/chatgpt-web
name: chatgpt-web
imagePullPolicy: Always
ports:
- containerPort: 3002
env:
- name: OPENAI_API_KEY
value: sk-xxx
- name: OPENAI_API_BASE_URL
value: 'https://api.openai.com'
- name: OPENAI_API_MODEL
value: gpt-3.5-turbo
- name: API_REVERSE_PROXY
value: https://ai.fakeopen.com/api/conversation
- name: AUTH_SECRET_KEY
value: '123456'
- name: TIMEOUT_MS
value: '60000'
- name: SOCKS_PROXY_HOST
value: ''
- name: SOCKS_PROXY_PORT
value: ''
- name: HTTPS_PROXY
value: ''
resources:
limits:
cpu: 500m
memory: 500Mi
requests:
cpu: 300m
memory: 300Mi
---
apiVersion: v1
kind: Service
metadata:
labels:
app: chatgpt-web
name: chatgpt-web
spec:
ports:
- name: chatgpt-web
port: 3002
protocol: TCP
targetPort: 3002
selector:
app: chatgpt-web
type: ClusterIP

View File

@@ -0,0 +1,21 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-connect-timeout: '5'
name: chatgpt-web
spec:
rules:
- host: chatgpt.example.com
http:
paths:
- backend:
service:
name: chatgpt-web
port:
number: 3002
path: /
pathType: ImplementationSpecific
tls:
- secretName: chatgpt-web-tls

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 ChenZhaoYu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19631
chatgpt-web-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
{
"name": "chatgpt-web",
"version": "2.11.0",
"private": false,
"description": "ChatGPT Web",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"vue"
],
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bootstrap": "pnpm install && pnpm run common:prepare",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
"common:prepare": "husky install"
},
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",
"@vueuse/core": "^9.13.0",
"highlight.js": "^11.7.0",
"html2canvas": "^1.4.1",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"crypto-js": "^4.1.1",
"eslint": "^8.35.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.2.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
}
}

8371
chatgpt-web-frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1 @@
<svg id="openai-symbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,44 @@
# OpenAI API Key - https://platform.openai.com/overview
OPENAI_API_KEY=
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
OPENAI_ACCESS_TOKEN=
# OpenAI API Base URL - https://api.openai.com
OPENAI_API_BASE_URL=
# OpenAI API Model - https://platform.openai.com/docs/models
OPENAI_API_MODEL=
# set `true` to disable OpenAI API debug log
OPENAI_API_DISABLE_DEBUG=
# Reverse Proxy - Available on accessToken
# Default: https://ai.fakeopen.com/api/conversation
# More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy
API_REVERSE_PROXY=
# timeout
TIMEOUT_MS=100000
# Rate Limit
MAX_REQUEST_PER_HOUR=
# Secret key
AUTH_SECRET_KEY=
# Socks Proxy Host
SOCKS_PROXY_HOST=
# Socks Proxy Port
SOCKS_PROXY_PORT=
# Socks Proxy Username
SOCKS_PROXY_USERNAME=
# Socks Proxy Password
SOCKS_PROXY_PASSWORD=
# HTTPS PROXY
HTTPS_PROXY=

View File

@@ -0,0 +1,5 @@
{
"root": true,
"ignorePatterns": ["build"],
"extends": ["@antfu"]
}

31
chatgpt-web-frontend/service/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
build

View File

@@ -0,0 +1 @@
enable-pre-post-scripts=true

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@@ -0,0 +1,22 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"typescript",
"json",
"jsonc",
"json5",
"yaml"
],
"cSpell.words": [
"antfu",
"chatgpt",
"esno",
"GPTAPI",
"OPENAI"
]
}

View File

@@ -0,0 +1,47 @@
{
"name": "chatgpt-web-service",
"version": "1.0.0",
"private": false,
"description": "ChatGPT Web Service",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"express"
],
"engines": {
"node": "^16 || ^18 || ^19"
},
"scripts": {
"start": "esno ./src/index.ts",
"dev": "esno watch ./src/index.ts",
"prod": "node ./build/index.mjs",
"build": "pnpm clean && tsup",
"clean": "rimraf build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
},
"dependencies": {
"axios": "^1.3.4",
"chatgpt": "^5.1.2",
"dotenv": "^16.0.3",
"esno": "^0.16.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"https-proxy-agent": "^5.0.1",
"isomorphic-fetch": "^3.0.0",
"node-fetch": "^3.3.0",
"socks-proxy-agent": "^7.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@types/express": "^4.17.17",
"@types/node": "^18.14.6",
"eslint": "^8.35.0",
"rimraf": "^4.3.0",
"tsup": "^6.6.3",
"typescript": "^4.9.5"
}
}

3742
chatgpt-web-frontend/service/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
import * as dotenv from 'dotenv'
import 'isomorphic-fetch'
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
import { SocksProxyAgent } from 'socks-proxy-agent'
import httpsProxyAgent from 'https-proxy-agent'
import fetch from 'node-fetch'
import { sendResponse } from '../utils'
import { isNotEmptyString } from '../utils/is'
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
import type { RequestOptions, SetProxyOptions, UsageResponse } from './types'
const { HttpsProxyAgent } = httpsProxyAgent
dotenv.config()
const ErrorCodeMessage: Record<string, string> = {
401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided',
403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
502: '[OpenAI] 错误的网关 | Bad Gateway',
503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later',
504: '[OpenAI] 网关超时 | Gateway Time-out',
500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error',
}
const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 100 * 1000
const disableDebug: boolean = process.env.OPENAI_API_DISABLE_DEBUG === 'true'
let apiModel: ApiModel
const model = isNotEmptyString(process.env.OPENAI_API_MODEL) ? process.env.OPENAI_API_MODEL : 'gpt-3.5-turbo'
if (!isNotEmptyString(process.env.OPENAI_API_KEY) && !isNotEmptyString(process.env.OPENAI_ACCESS_TOKEN))
throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
(async () => {
// More Info: https://github.com/transitive-bullshit/chatgpt-api
if (isNotEmptyString(process.env.OPENAI_API_KEY)) {
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
const options: ChatGPTAPIOptions = {
apiKey: process.env.OPENAI_API_KEY,
completionParams: { model },
debug: !disableDebug,
}
// increase max token limit if use gpt-4
if (model.toLowerCase().includes('gpt-4')) {
// if use 32k model
if (model.toLowerCase().includes('32k')) {
options.maxModelTokens = 32768
options.maxResponseTokens = 8192
}
else {
options.maxModelTokens = 8192
options.maxResponseTokens = 2048
}
}
if (isNotEmptyString(OPENAI_API_BASE_URL))
options.apiBaseUrl = `${OPENAI_API_BASE_URL}/v1`
setupProxy(options)
api = new ChatGPTAPI({ ...options })
apiModel = 'ChatGPTAPI'
}
else {
const options: ChatGPTUnofficialProxyAPIOptions = {
accessToken: process.env.OPENAI_ACCESS_TOKEN,
apiReverseProxyUrl: isNotEmptyString(process.env.API_REVERSE_PROXY) ? process.env.API_REVERSE_PROXY : 'https://ai.fakeopen.com/api/conversation',
model,
debug: !disableDebug,
}
setupProxy(options)
api = new ChatGPTUnofficialProxyAPI({ ...options })
apiModel = 'ChatGPTUnofficialProxyAPI'
}
})()
async function chatReplyProcess(options: RequestOptions) {
const { message, lastContext, process, systemMessage, temperature, top_p } = options
try {
let options: SendMessageOptions = { timeoutMs }
if (apiModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage))
options.systemMessage = systemMessage
options.completionParams = { model, temperature, top_p }
}
if (lastContext != null) {
if (apiModel === 'ChatGPTAPI')
options.parentMessageId = lastContext.parentMessageId
else
options = { ...lastContext }
}
const response = await api.sendMessage(message, {
...options,
onProgress: (partialResponse) => {
process?.(partialResponse)
},
})
return sendResponse({ type: 'Success', data: response })
}
catch (error: any) {
const code = error.statusCode
global.console.log(error)
if (Reflect.has(ErrorCodeMessage, code))
return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] })
return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' })
}
}
async function fetchUsage() {
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_API_BASE_URL = process.env.OPENAI_API_BASE_URL
if (!isNotEmptyString(OPENAI_API_KEY))
return Promise.resolve('-')
const API_BASE_URL = isNotEmptyString(OPENAI_API_BASE_URL)
? OPENAI_API_BASE_URL
: 'https://api.openai.com'
const [startDate, endDate] = formatDate()
// 每月使用量
const urlUsage = `${API_BASE_URL}/v1/dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
const headers = {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
}
const options = {} as SetProxyOptions
setupProxy(options)
try {
// 获取已使用量
const useResponse = await options.fetch(urlUsage, { headers })
if (!useResponse.ok)
throw new Error('获取使用量失败')
const usageData = await useResponse.json() as UsageResponse
const usage = Math.round(usageData.total_usage) / 100
return Promise.resolve(usage ? `$${usage}` : '-')
}
catch (error) {
global.console.log(error)
return Promise.resolve('-')
}
}
function formatDate(): string[] {
const today = new Date()
const year = today.getFullYear()
const month = today.getMonth() + 1
const lastDay = new Date(year, month, 0)
const formattedFirstDay = `${year}-${month.toString().padStart(2, '0')}-01`
const formattedLastDay = `${year}-${month.toString().padStart(2, '0')}-${lastDay.getDate().toString().padStart(2, '0')}`
return [formattedFirstDay, formattedLastDay]
}
async function chatConfig() {
const usage = await fetchUsage()
const reverseProxy = process.env.API_REVERSE_PROXY ?? '-'
const httpsProxy = (process.env.HTTPS_PROXY || process.env.ALL_PROXY) ?? '-'
const socksProxy = (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT)
? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`)
: '-'
return sendResponse<ModelConfig>({
type: 'Success',
data: { apiModel, reverseProxy, timeoutMs, socksProxy, httpsProxy, usage },
})
}
function setupProxy(options: SetProxyOptions) {
if (isNotEmptyString(process.env.SOCKS_PROXY_HOST) && isNotEmptyString(process.env.SOCKS_PROXY_PORT)) {
const agent = new SocksProxyAgent({
hostname: process.env.SOCKS_PROXY_HOST,
port: process.env.SOCKS_PROXY_PORT,
userId: isNotEmptyString(process.env.SOCKS_PROXY_USERNAME) ? process.env.SOCKS_PROXY_USERNAME : undefined,
password: isNotEmptyString(process.env.SOCKS_PROXY_PASSWORD) ? process.env.SOCKS_PROXY_PASSWORD : undefined,
})
options.fetch = (url, options) => {
return fetch(url, { agent, ...options })
}
}
else if (isNotEmptyString(process.env.HTTPS_PROXY) || isNotEmptyString(process.env.ALL_PROXY)) {
const httpsProxy = process.env.HTTPS_PROXY || process.env.ALL_PROXY
if (httpsProxy) {
const agent = new HttpsProxyAgent(httpsProxy)
options.fetch = (url, options) => {
return fetch(url, { agent, ...options })
}
}
}
else {
options.fetch = (url, options) => {
return fetch(url, { ...options })
}
}
}
function currentModel(): ApiModel {
return apiModel
}
export type { ChatContext, ChatMessage }
export { chatReplyProcess, chatConfig, currentModel }

View File

@@ -0,0 +1,19 @@
import type { ChatMessage } from 'chatgpt'
import type fetch from 'node-fetch'
export interface RequestOptions {
message: string
lastContext?: { conversationId?: string; parentMessageId?: string }
process?: (chat: ChatMessage) => void
systemMessage?: string
temperature?: number
top_p?: number
}
export interface SetProxyOptions {
fetch?: typeof fetch
}
export interface UsageResponse {
total_usage: number
}

View File

@@ -0,0 +1,89 @@
import express from 'express'
import type { RequestProps } from './types'
import type { ChatMessage } from './chatgpt'
import { chatConfig, chatReplyProcess, currentModel } from './chatgpt'
import { auth } from './middleware/auth'
import { limiter } from './middleware/limiter'
import { isNotEmptyString } from './utils/is'
const app = express()
const router = express.Router()
app.use(express.static('public'))
app.use(express.json())
app.all('*', (_, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'authorization, Content-Type')
res.header('Access-Control-Allow-Methods', '*')
next()
})
router.post('/chat-process', [auth, limiter], async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream')
try {
const { prompt, options = {}, systemMessage, temperature, top_p } = req.body as RequestProps
let firstChunk = true
await chatReplyProcess({
message: prompt,
lastContext: options,
process: (chat: ChatMessage) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`)
firstChunk = false
},
systemMessage,
temperature,
top_p,
})
}
catch (error) {
res.write(JSON.stringify(error))
}
finally {
res.end()
}
})
router.post('/config', auth, async (req, res) => {
try {
const response = await chatConfig()
res.send(response)
}
catch (error) {
res.send(error)
}
})
router.post('/session', async (req, res) => {
try {
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
const hasAuth = isNotEmptyString(AUTH_SECRET_KEY)
res.send({ status: 'Success', message: '', data: { auth: hasAuth, model: currentModel() } })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})
router.post('/verify', async (req, res) => {
try {
const { token } = req.body as { token: string }
if (!token)
throw new Error('Secret key is empty')
if (process.env.AUTH_SECRET_KEY !== token)
throw new Error('密钥无效 | Secret key is invalid')
res.send({ status: 'Success', message: 'Verify successfully', data: null })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})
app.use('', router)
app.use('/api', router)
app.set('trust proxy', 1)
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))

View File

@@ -0,0 +1,21 @@
import { isNotEmptyString } from '../utils/is'
const auth = async (req, res, next) => {
const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY
if (isNotEmptyString(AUTH_SECRET_KEY)) {
try {
const Authorization = req.header('Authorization')
if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim())
throw new Error('Error: 无访问权限 | No access rights')
next()
}
catch (error) {
res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
}
}
else {
next()
}
}
export { auth }

View File

@@ -0,0 +1,19 @@
import { rateLimit } from 'express-rate-limit'
import { isNotEmptyString } from '../utils/is'
const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR
const maxCount = (isNotEmptyString(MAX_REQUEST_PER_HOUR) && !isNaN(Number(MAX_REQUEST_PER_HOUR)))
? parseInt(MAX_REQUEST_PER_HOUR)
: 0 // 0 means unlimited
const limiter = rateLimit({
windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour
max: maxCount,
statusCode: 200, // 200 means successbut the message is 'Too many request from this IP in 1 hour'
message: async (req, res) => {
res.send({ status: 'Fail', message: 'Too many request from this IP in 1 hour', data: null })
},
})
export { limiter }

View File

@@ -0,0 +1,34 @@
import type { FetchFn } from 'chatgpt'
export interface RequestProps {
prompt: string
options?: ChatContext
systemMessage: string
temperature?: number
top_p?: number
}
export interface ChatContext {
conversationId?: string
parentMessageId?: string
}
export interface ChatGPTUnofficialProxyAPIOptions {
accessToken: string
apiReverseProxyUrl?: string
model?: string
debug?: boolean
headers?: Record<string, string>
fetch?: FetchFn
}
export interface ModelConfig {
apiModel?: ApiModel
reverseProxy?: string
timeoutMs?: number
socksProxy?: string
httpsProxy?: string
usage?: string
}
export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined

View File

@@ -0,0 +1,22 @@
interface SendResponseOptions<T = any> {
type: 'Success' | 'Fail'
message?: string
data?: T
}
export function sendResponse<T>(options: SendResponseOptions<T>) {
if (options.type === 'Success') {
return Promise.resolve({
message: options.message ?? null,
data: options.data ?? null,
status: options.type,
})
}
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({
message: options.message ?? 'Failed',
data: options.data ?? null,
status: options.type,
})
}

View File

@@ -0,0 +1,19 @@
export function isNumber<T extends number>(value: T | unknown): value is number {
return Object.prototype.toString.call(value) === '[object Number]'
}
export function isString<T extends string>(value: T | unknown): value is string {
return Object.prototype.toString.call(value) === '[object String]'
}
export function isNotEmptyString(value: any): boolean {
return typeof value === 'string' && value.length > 0
}
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
return Object.prototype.toString.call(value) === '[object Boolean]'
}
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Function]'
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es2020",
"lib": [
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": ".",
"outDir": "build",
"noEmit": true
},
"exclude": [
"node_modules",
"build"
],
"include": [
"**/*.ts"
]
}

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'build',
target: 'es2020',
format: ['esm'],
splitting: false,
sourcemap: true,
minify: false,
shims: true,
dts: false,
})

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import { NConfigProvider } from 'naive-ui'
import { NaiveProvider } from '@/components/common'
import { useTheme } from '@/hooks/useTheme'
import { useLanguage } from '@/hooks/useLanguage'
const { theme, themeOverrides } = useTheme()
const { language } = useLanguage()
</script>
<template>
<NConfigProvider
class="h-full"
:theme="theme"
:theme-overrides="themeOverrides"
:locale="language"
>
<NaiveProvider>
<RouterView />
</NaiveProvider>
</NConfigProvider>
</template>

View File

@@ -0,0 +1,66 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { post } from '@/utils/request'
import { useAuthStore, useSettingStore } from '@/store'
export function fetchChatAPI<T = any>(
prompt: string,
options?: { conversationId?: string; parentMessageId?: string },
signal?: GenericAbortSignal,
) {
return post<T>({
url: '/chat',
data: { prompt, options },
signal,
})
}
export function fetchChatConfig<T = any>() {
return post<T>({
url: '/config',
})
}
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
const settingStore = useSettingStore()
const authStore = useAuthStore()
let data: Record<string, any> = {
prompt: params.prompt,
options: params.options,
}
if (authStore.isChatGPTAPI) {
data = {
...data,
systemMessage: settingStore.systemMessage,
temperature: settingStore.temperature,
top_p: settingStore.top_p,
}
}
return post<T>({
url: '/chat-process',
data,
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
export function fetchSession<T>() {
return post<T>({
url: '/session',
})
}
export function fetchVerify<T>(token: string) {
return post<T>({
url: '/verify',
data: { token },
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,14 @@
[
{
"key": "awesome-chatgpt-prompts-zh",
"desc": "ChatGPT 中文调教指南",
"downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json",
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
},
{
"key": "awesome-chatgpt-prompts-zh-TW",
"desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)",
"downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json",
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
}
]

View File

@@ -0,0 +1,20 @@
<script setup lang='ts'>
interface Emit {
(e: 'click'): void
}
const emit = defineEmits<Emit>()
function handleClick() {
emit('click')
}
</script>
<template>
<button
class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]"
@click="handleClick"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang='ts'>
import { computed } from 'vue'
import type { PopoverPlacement } from 'naive-ui'
import { NTooltip } from 'naive-ui'
import Button from './Button.vue'
interface Props {
tooltip?: string
placement?: PopoverPlacement
}
interface Emit {
(e: 'click'): void
}
const props = withDefaults(defineProps<Props>(), {
tooltip: '',
placement: 'bottom',
})
const emit = defineEmits<Emit>()
const showTooltip = computed(() => Boolean(props.tooltip))
function handleClick() {
emit('click')
}
</script>
<template>
<div v-if="showTooltip">
<NTooltip :placement="placement" trigger="hover">
<template #trigger>
<Button @click="handleClick">
<slot />
</Button>
</template>
{{ tooltip }}
</NTooltip>
</div>
<div v-else>
<Button @click="handleClick">
<slot />
</Button>
</div>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import {
NDialogProvider,
NLoadingBarProvider,
NMessageProvider,
NNotificationProvider,
useDialog,
useLoadingBar,
useMessage,
useNotification,
} from 'naive-ui'
function registerNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$dialog = useDialog()
window.$message = useMessage()
window.$notification = useNotification()
}
const NaiveProviderContent = defineComponent({
name: 'NaiveProviderContent',
setup() {
registerNaiveTools()
},
render() {
return h('div')
},
})
</script>
<template>
<NLoadingBarProvider>
<NDialogProvider>
<NNotificationProvider>
<NMessageProvider>
<slot />
<NaiveProviderContent />
</NMessageProvider>
</NNotificationProvider>
</NDialogProvider>
</NLoadingBarProvider>
</template>

View File

@@ -0,0 +1,480 @@
<script setup lang='ts'>
import type { DataTableColumns } from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui'
import PromptRecommend from '../../../assets/recommend.json'
import { SvgIcon } from '..'
import { usePromptStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
interface DataProps {
renderKey: string
renderValue: string
key: string
value: string
}
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const message = useMessage()
const show = computed({
get: () => props.visible,
set: (visible: boolean) => emit('update:visible', visible),
})
const showModal = ref(false)
const importLoading = ref(false)
const exportLoading = ref(false)
const searchValue = ref<string>('')
// 移动端自适应相关
const { isMobile } = useBasicLayout()
const promptStore = usePromptStore()
// Prompt在线导入推荐List,根据部署者喜好进行修改(assets/recommend.json)
const promptRecommendList = PromptRecommend
const promptList = ref<any>(promptStore.promptList)
// 用于添加修改的临时prompt参数
const tempPromptKey = ref('')
const tempPromptValue = ref('')
// Modal模式根据不同模式渲染不同的Modal内容
const modalMode = ref('')
// 这个是为了后期的修改Prompt内容考虑因为要针对无uuid的list进行修改且考虑到不能出现标题和内容的冲突所以就需要一个临时item来记录一下
const tempModifiedItem = ref<any>({})
// 添加修改导入都使用一个Modal, 临时修改内容占用tempPromptKey,切换状态前先将内容都清楚
const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => {
if (mode === 'add') {
tempPromptKey.value = ''
tempPromptValue.value = ''
}
else if (mode === 'modify') {
tempModifiedItem.value = { ...selected }
tempPromptKey.value = selected.key
tempPromptValue.value = selected.value
}
else if (mode === 'local_import') {
tempPromptKey.value = 'local_import'
tempPromptValue.value = ''
}
showModal.value = !showModal.value
modalMode.value = mode
}
// 在线导入相关
const downloadURL = ref('')
const downloadDisabled = computed(() => downloadURL.value.trim().length < 1)
const setDownloadURL = (url: string) => {
downloadURL.value = url
}
// 控制 input 按钮
const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1)
// Prompt模板相关操作
const addPromptTemplate = () => {
for (const i of promptList.value) {
if (i.key === tempPromptKey.value) {
message.error(t('store.addRepeatTitleTips'))
return
}
if (i.value === tempPromptValue.value) {
message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value }))
return
}
}
promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never)
message.success(t('common.addSuccess'))
changeShowModal('add')
}
const modifyPromptTemplate = () => {
let index = 0
// 通过临时索引把待修改项摘出来
for (const i of promptList.value) {
if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value)
break
index = index + 1
}
const tempList = promptList.value.filter((_: any, i: number) => i !== index)
// 搜索有冲突的部分
for (const i of tempList) {
if (i.key === tempPromptKey.value) {
message.error(t('store.editRepeatTitleTips'))
return
}
if (i.value === tempPromptValue.value) {
message.error(t('store.editRepeatContentTips', { msg: i.key }))
return
}
}
promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never
message.success(t('common.editSuccess'))
changeShowModal('modify')
}
const deletePromptTemplate = (row: { key: string; value: string }) => {
promptList.value = [
...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key),
] as never
message.success(t('common.deleteSuccess'))
}
const clearPromptTemplate = () => {
promptList.value = []
message.success(t('common.clearSuccess'))
}
const importPromptTemplate = (from = 'online') => {
try {
const jsonData = JSON.parse(tempPromptValue.value)
let key = ''
let value = ''
// 可以扩展加入更多模板字典的key
if ('key' in jsonData[0]) {
key = 'key'
value = 'value'
}
else if ('act' in jsonData[0]) {
key = 'act'
value = 'prompt'
}
else {
// 不支持的字典的key防止导入 以免破坏prompt商店打开
message.warning('prompt key not supported.')
throw new Error('prompt key not supported.')
}
for (const i of jsonData) {
if (!(key in i) || !(value in i))
throw new Error(t('store.importError'))
let safe = true
for (const j of promptList.value) {
if (j.key === i[key]) {
message.warning(t('store.importRepeatTitle', { msg: i[key] }))
safe = false
break
}
if (j.value === i[value]) {
message.warning(t('store.importRepeatContent', { msg: i[key] }))
safe = false
break
}
}
if (safe)
promptList.value.unshift({ key: i[key], value: i[value] } as never)
}
message.success(t('common.importSuccess'))
}
catch {
message.error('JSON 格式错误,请检查 JSON 格式')
}
if (from === 'local')
showModal.value = !showModal.value
}
// 模板导出
const exportPromptTemplate = () => {
exportLoading.value = true
const jsonDataStr = JSON.stringify(promptList.value)
const blob = new Blob([jsonDataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'ChatGPTPromptTemplate.json'
link.click()
URL.revokeObjectURL(url)
exportLoading.value = false
}
// 模板在线导入
const downloadPromptTemplate = async () => {
try {
importLoading.value = true
const response = await fetch(downloadURL.value)
const jsonData = await response.json()
if ('key' in jsonData[0] && 'value' in jsonData[0])
tempPromptValue.value = JSON.stringify(jsonData)
if ('act' in jsonData[0] && 'prompt' in jsonData[0]) {
const newJsonData = jsonData.map((item: { act: string; prompt: string }) => {
return {
key: item.act,
value: item.prompt,
}
})
tempPromptValue.value = JSON.stringify(newJsonData)
}
importPromptTemplate()
downloadURL.value = ''
}
catch {
message.error(t('store.downloadError'))
downloadURL.value = ''
}
finally {
importLoading.value = false
}
}
// 移动端自适应相关
const renderTemplate = () => {
const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50]
return promptList.value.map((item: { key: string; value: string }) => {
return {
renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`,
renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`,
key: item.key,
value: item.value,
}
})
}
const pagination = computed(() => {
const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15]
return {
pageSize, pageSlot,
}
})
// table相关
const createColumns = (): DataTableColumns<DataProps> => {
return [
{
title: t('store.title'),
key: 'renderKey',
},
{
title: t('store.description'),
key: 'renderValue',
},
{
title: t('common.action'),
key: 'actions',
width: 100,
align: 'center',
render(row) {
return h('div', { class: 'flex items-center flex-col gap-2' }, {
default: () => [h(
NButton,
{
tertiary: true,
size: 'small',
type: 'info',
onClick: () => changeShowModal('modify', row),
},
{ default: () => t('common.edit') },
),
h(
NButton,
{
tertiary: true,
size: 'small',
type: 'error',
onClick: () => deletePromptTemplate(row),
},
{ default: () => t('common.delete') },
),
],
})
},
},
]
}
const columns = createColumns()
watch(
() => promptList,
() => {
promptStore.updatePromptList(promptList.value)
},
{ deep: true },
)
const dataSource = computed(() => {
const data = renderTemplate()
const value = searchValue.value
if (value && value !== '') {
return data.filter((item: DataProps) => {
return item.renderKey.includes(value) || item.renderValue.includes(value)
})
}
return data
})
</script>
<template>
<NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card">
<div class="space-y-4">
<NTabs type="segment">
<NTabPane name="local" :tab="$t('store.local')">
<div
class="flex gap-3 mb-4"
:class="[isMobile ? 'flex-col' : 'flex-row justify-between']"
>
<div class="flex items-center space-x-4">
<NButton
type="primary"
size="small"
@click="changeShowModal('add')"
>
{{ $t('common.add') }}
</NButton>
<NButton
size="small"
@click="changeShowModal('local_import')"
>
{{ $t('common.import') }}
</NButton>
<NButton
size="small"
:loading="exportLoading"
@click="exportPromptTemplate()"
>
{{ $t('common.export') }}
</NButton>
<NPopconfirm @positive-click="clearPromptTemplate">
<template #trigger>
<NButton size="small">
{{ $t('common.clear') }}
</NButton>
</template>
{{ $t('store.clearStoreConfirm') }}
</NPopconfirm>
</div>
<div class="flex items-center">
<NInput v-model:value="searchValue" style="width: 100%" />
</div>
</div>
<NDataTable
v-if="!isMobile"
:max-height="400"
:columns="columns"
:data="dataSource"
:pagination="pagination"
:bordered="false"
/>
<NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;">
<NListItem v-for="(item, index) of dataSource" :key="index">
<NThing :title="item.renderKey" :description="item.renderValue" />
<template #suffix>
<div class="flex flex-col items-center gap-2">
<NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)">
{{ t('common.edit') }}
</NButton>
<NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)">
{{ t('common.delete') }}
</NButton>
</div>
</template>
</NListItem>
</NList>
</NTabPane>
<NTabPane name="download" :tab="$t('store.online')">
<p class="mb-4">
{{ $t('store.onlineImportWarning') }}
</p>
<div class="flex items-center gap-4">
<NInput v-model:value="downloadURL" placeholder="" />
<NButton
strong
secondary
:disabled="downloadDisabled"
:loading="importLoading"
@click="downloadPromptTemplate()"
>
{{ $t('common.download') }}
</NButton>
</div>
<NDivider />
<div class="max-h-[360px] overflow-y-auto space-y-4">
<NCard
v-for="info in promptRecommendList"
:key="info.key" :title="info.key"
:bordered="true"
embedded
>
<p
class="overflow-hidden text-ellipsis whitespace-nowrap"
:title="info.desc"
>
{{ info.desc }}
</p>
<template #footer>
<div class="flex items-center justify-end space-x-4">
<NButton text>
<a
:href="info.url"
target="_blank"
>
<SvgIcon class="text-xl" icon="ri:link" />
</a>
</NButton>
<NButton text @click="setDownloadURL(info.downloadUrl) ">
<SvgIcon class="text-xl" icon="ri:add-fill" />
</NButton>
</div>
</template>
</NCard>
</div>
</NTabPane>
</NTabs>
</div>
</NModal>
<NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card">
<NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical>
{{ t('store.title') }}
<NInput v-model:value="tempPromptKey" />
{{ t('store.description') }}
<NInput v-model:value="tempPromptValue" type="textarea" />
<NButton
block
type="primary"
:disabled="inputStatus"
@click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }"
>
{{ t('common.confirm') }}
</NButton>
</NSpace>
<NSpace v-if="modalMode === 'local_import'" vertical>
<NInput
v-model:value="tempPromptValue"
:placeholder="t('store.importPlaceholder')"
:autosize="{ minRows: 3, maxRows: 15 }"
type="textarea"
/>
<NButton
block
type="primary"
:disabled="inputStatus"
@click="() => { importPromptTemplate('local') }"
>
{{ t('common.import') }}
</NButton>
</NSpace>
</NModal>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang='ts'>
import { computed, onMounted, ref } from 'vue'
import { NSpin } from 'naive-ui'
import { fetchChatConfig } from '@/api'
import pkg from '@/../package.json'
import { useAuthStore } from '@/store'
interface ConfigState {
timeoutMs?: number
reverseProxy?: string
apiModel?: string
socksProxy?: string
httpsProxy?: string
usage?: string
}
const authStore = useAuthStore()
const loading = ref(false)
const config = ref<ConfigState>()
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
async function fetchConfig() {
try {
loading.value = true
const { data } = await fetchChatConfig<ConfigState>()
config.value = data
}
finally {
loading.value = false
}
}
onMounted(() => {
fetchConfig()
})
</script>
<template>
<NSpin :show="loading">
<div class="p-4 space-y-4">
<h2 class="text-xl font-bold">
Version - {{ pkg.version }}
</h2>
<div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
<p>
此项目开源于
<a
class="text-blue-600 dark:text-blue-500"
href="https://github.com/Chanzhaoyu/chatgpt-web"
target="_blank"
>
GitHub
</a>
免费且基于 MIT 协议没有任何形式的付费行为
</p>
<p>
如果你觉得此项目对你有帮助请在 GitHub 帮我点个 Star 或者给予一点赞助谢谢
</p>
</div>
<p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p v-if="isChatGPTAPI">
{{ $t("setting.monthlyUsage") }}{{ config?.usage ?? '-' }}
</p>
<p v-if="!isChatGPTAPI">
{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}
</p>
<p>{{ $t("setting.timeout") }}{{ config?.timeoutMs ?? '-' }}</p>
<p>{{ $t("setting.socks") }}{{ config?.socksProxy ?? '-' }}</p>
<p>{{ $t("setting.httpsProxy") }}{{ config?.httpsProxy ?? '-' }}</p>
</div>
</NSpin>
</template>

View File

@@ -0,0 +1,70 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { NButton, NInput, NSlider, useMessage } from 'naive-ui'
import { useSettingStore } from '@/store'
import type { SettingsState } from '@/store/modules/settings/helper'
import { t } from '@/locales'
const settingStore = useSettingStore()
const ms = useMessage()
const systemMessage = ref(settingStore.systemMessage ?? '')
const temperature = ref(settingStore.temperature ?? 0.5)
const top_p = ref(settingStore.top_p ?? 1)
function updateSettings(options: Partial<SettingsState>) {
settingStore.updateSetting(options)
ms.success(t('common.success'))
}
function handleReset() {
settingStore.resetSetting()
ms.success(t('common.success'))
window.location.reload()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.role') }}</span>
<div class="flex-1">
<NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" />
</div>
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.temperature') }} </span>
<div class="flex-1">
<NSlider v-model:value="temperature" :max="1" :min="0" :step="0.1" />
</div>
<span>{{ temperature }}</span>
<NButton size="tiny" text type="primary" @click="updateSettings({ temperature })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[120px]">{{ $t('setting.top_p') }} </span>
<div class="flex-1">
<NSlider v-model:value="top_p" :max="1" :min="0" :step="0.1" />
</div>
<span>{{ top_p }}</span>
<NButton size="tiny" text type="primary" @click="updateSettings({ top_p })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[120px]">&nbsp;</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,225 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
import type { Language, Theme } from '@/store/modules/app/helper'
import { SvgIcon } from '@/components/common'
import { useAppStore, useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper'
import { getCurrentDate } from '@/utils/functions'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
const appStore = useAppStore()
const userStore = useUserStore()
const { isMobile } = useBasicLayout()
const ms = useMessage()
const theme = computed(() => appStore.theme)
const userInfo = computed(() => userStore.userInfo)
const avatar = ref(userInfo.value.avatar ?? '')
const name = ref(userInfo.value.name ?? '')
const description = ref(userInfo.value.description ?? '')
const language = computed({
get() {
return appStore.language
},
set(value: Language) {
appStore.setLanguage(value)
},
})
const themeOptions: { label: string; key: Theme; icon: string }[] = [
{
label: 'Auto',
key: 'auto',
icon: 'ri:contrast-line',
},
{
label: 'Light',
key: 'light',
icon: 'ri:sun-foggy-line',
},
{
label: 'Dark',
key: 'dark',
icon: 'ri:moon-foggy-line',
},
]
const languageOptions: { label: string; key: Language; value: Language }[] = [
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
{ label: 'English', key: 'en-US', value: 'en-US' },
{ label: '한국어', key: 'ko-KR', value: 'ko-KR' },
{ label: 'Русский язык', key: 'ru-RU', value: 'ru-RU' },
]
function updateUserInfo(options: Partial<UserInfo>) {
userStore.updateUserInfo(options)
ms.success(t('common.success'))
}
function handleReset() {
userStore.resetUserInfo()
ms.success(t('common.success'))
window.location.reload()
}
function exportData(): void {
const date = getCurrentDate()
const data: string = localStorage.getItem('chatStorage') || '{}'
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
const url: string = URL.createObjectURL(blob)
const link: HTMLAnchorElement = document.createElement('a')
link.href = url
link.download = `chat-store_${date}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function importData(event: Event): void {
const target = event.target as HTMLInputElement
if (!target || !target.files)
return
const file: File = target.files[0]
if (!file)
return
const reader: FileReader = new FileReader()
reader.onload = () => {
try {
const data = JSON.parse(reader.result as string)
localStorage.setItem('chatStorage', JSON.stringify(data))
ms.success(t('common.success'))
location.reload()
}
catch (error) {
ms.error(t('common.invalidFileFormat'))
}
}
reader.readAsText(file)
}
function clearData(): void {
localStorage.removeItem('chatStorage')
location.reload()
}
function handleImportButtonClick(): void {
const fileInput = document.getElementById('fileInput') as HTMLElement
if (fileInput)
fileInput.click()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
<div class="flex-1">
<NInput v-model:value="avatar" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]">
<NInput v-model:value="name" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
<div class="flex-1">
<NInput v-model:value="description" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
{{ $t('common.save') }}
</NButton>
</div>
<div
class="flex items-center space-x-4"
:class="isMobile && 'items-start'"
>
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NButton size="small" @click="exportData">
<template #icon>
<SvgIcon icon="ri:download-2-fill" />
</template>
{{ $t('common.export') }}
</NButton>
<input id="fileInput" type="file" style="display:none" @change="importData">
<NButton size="small" @click="handleImportButtonClick">
<template #icon>
<SvgIcon icon="ri:upload-2-fill" />
</template>
{{ $t('common.import') }}
</NButton>
<NPopconfirm placement="bottom" @positive-click="clearData">
<template #trigger>
<NButton size="small">
<template #icon>
<SvgIcon icon="ri:close-circle-line" />
</template>
{{ $t('common.clear') }}
</NButton>
</template>
{{ $t('chat.clearHistoryConfirm') }}
</NPopconfirm>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
<div class="flex flex-wrap items-center gap-4">
<template v-for="item of themeOptions" :key="item.key">
<NButton
size="small"
:type="item.key === theme ? 'primary' : undefined"
@click="appStore.setTheme(item.key)"
>
<template #icon>
<SvgIcon :icon="item.icon" />
</template>
</NButton>
</template>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NSelect
style="width: 140px"
:value="language"
:options="languageOptions"
@update-value="value => appStore.setLanguage(value)"
/>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NModal, NTabPane, NTabs } from 'naive-ui'
import General from './General.vue'
import Advanced from './Advanced.vue'
import About from './About.vue'
import { useAuthStore } from '@/store'
import { SvgIcon } from '@/components/common'
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const authStore = useAuthStore()
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
const active = ref('General')
const show = computed({
get() {
return props.visible
},
set(visible: boolean) {
emit('update:visible', visible)
},
})
</script>
<template>
<NModal v-model:show="show" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
<div>
<NTabs v-model:value="active" type="line" animated>
<NTabPane name="General" tab="General">
<template #tab>
<SvgIcon class="text-lg" icon="ri:file-user-line" />
<span class="ml-2">{{ $t('setting.general') }}</span>
</template>
<div class="min-h-[100px]">
<General />
</div>
</NTabPane>
<NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
<template #tab>
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
<span class="ml-2">{{ $t('setting.advanced') }}</span>
</template>
<div class="min-h-[100px]">
<Advanced />
</div>
</NTabPane>
<NTabPane name="Config" tab="Config">
<template #tab>
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
<span class="ml-2">{{ $t('setting.config') }}</span>
</template>
<About />
</NTabPane>
</NTabs>
</div>
</NModal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang='ts'>
import { computed, useAttrs } from 'vue'
import { Icon } from '@iconify/vue'
interface Props {
icon?: string
}
defineProps<Props>()
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || '',
}))
</script>
<template>
<Icon :icon="icon" v-bind="bindAttrs" />
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang='ts'>
import { computed } from 'vue'
import { NAvatar } from 'naive-ui'
import { useUserStore } from '@/store'
import defaultAvatar from '@/assets/avatar.jpg'
import { isString } from '@/utils/is'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
</script>
<template>
<div class="flex items-center overflow-hidden">
<div class="w-10 h-10 overflow-hidden rounded-full shrink-0">
<template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
<NAvatar
size="large"
round
:src="userInfo.avatar"
:fallback-src="defaultAvatar"
/>
</template>
<template v-else>
<NAvatar size="large" round :src="defaultAvatar" />
</template>
</div>
<div class="flex-1 min-w-0 ml-2">
<h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap">
{{ userInfo.name ?? 'ChenZhaoYu' }}
</h2>
<p class="overflow-hidden text-xs text-gray-500 text-ellipsis whitespace-nowrap">
<span
v-if="isString(userInfo.description) && userInfo.description !== ''"
v-html="userInfo.description"
/>
</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,8 @@
import HoverButton from './HoverButton/index.vue'
import NaiveProvider from './NaiveProvider/index.vue'
import SvgIcon from './SvgIcon/index.vue'
import UserAvatar from './UserAvatar/index.vue'
import Setting from './Setting/index.vue'
import PromptStore from './PromptStore/index.vue'
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore }

View File

@@ -0,0 +1,8 @@
<template>
<div class="text-neutral-400">
<span>Star on</span>
<a href="https://github.com/Chanzhaoyu/chatgpt-bot" target="_blank" class="text-blue-500">
GitHub
</a>
</div>
</template>

View File

@@ -0,0 +1,3 @@
import GithubSite from './GithubSite.vue'
export { GithubSite }

View File

@@ -0,0 +1,8 @@
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
export function useBasicLayout() {
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('sm')
return { isMobile }
}

View File

@@ -0,0 +1,36 @@
import { h } from 'vue'
import { SvgIcon } from '@/components/common'
export const useIconRender = () => {
interface IconConfig {
icon?: string
color?: string
fontSize?: number
}
interface IconStyle {
color?: string
fontSize?: string
}
const iconRender = (config: IconConfig) => {
const { color, fontSize, icon } = config
const style: IconStyle = {}
if (color)
style.color = color
if (fontSize)
style.fontSize = `${fontSize}px`
if (!icon)
window.console.warn('iconRender: icon is required')
return () => h(SvgIcon, { icon, style })
}
return {
iconRender,
}
}

View File

@@ -0,0 +1,33 @@
import { computed } from 'vue'
import { enUS, koKR, zhCN, zhTW } from 'naive-ui'
import { useAppStore } from '@/store'
import { setLocale } from '@/locales'
export function useLanguage() {
const appStore = useAppStore()
const language = computed(() => {
switch (appStore.language) {
case 'en-US':
setLocale('en-US')
return enUS
case 'ru-RU':
setLocale('ru-RU')
return enUS
case 'ko-KR':
setLocale('ko-KR')
return koKR
case 'zh-CN':
setLocale('zh-CN')
return zhCN
case 'zh-TW':
setLocale('zh-TW')
return zhTW
default:
setLocale('zh-CN')
return zhCN
}
})
return { language }
}

View File

@@ -0,0 +1,43 @@
import type { GlobalThemeOverrides } from 'naive-ui'
import { computed, watch } from 'vue'
import { darkTheme, useOsTheme } from 'naive-ui'
import { useAppStore } from '@/store'
export function useTheme() {
const appStore = useAppStore()
const OsTheme = useOsTheme()
const isDark = computed(() => {
if (appStore.theme === 'auto')
return OsTheme.value === 'dark'
else
return appStore.theme === 'dark'
})
const theme = computed(() => {
return isDark.value ? darkTheme : undefined
})
const themeOverrides = computed<GlobalThemeOverrides>(() => {
if (isDark.value) {
return {
common: {},
}
}
return {}
})
watch(
() => isDark.value,
(dark) => {
if (dark)
document.documentElement.classList.add('dark')
else
document.documentElement.classList.remove('dark')
},
{ immediate: true },
)
return { theme, themeOverrides }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,94 @@
export default {
common: {
add: 'Add',
addSuccess: 'Add Success',
edit: 'Edit',
editSuccess: 'Edit Success',
delete: 'Delete',
deleteSuccess: 'Delete Success',
save: 'Save',
saveSuccess: 'Save Success',
reset: 'Reset',
action: 'Action',
export: 'Export',
exportSuccess: 'Export Success',
import: 'Import',
importSuccess: 'Import Success',
clear: 'Clear',
clearSuccess: 'Clear Success',
yes: 'Yes',
no: 'No',
confirm: 'Confirm',
download: 'Download',
noData: 'No Data',
wrong: 'Something went wrong, please try again later.',
success: 'Success',
failed: 'Failed',
verify: 'Verify',
unauthorizedTips: 'Unauthorized, please verify first.',
},
chat: {
newChatButton: 'New Chat',
placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)',
placeholderMobile: 'Ask me anything...',
copy: 'Copy',
copied: 'Copied',
copyCode: 'Copy Code',
clearChat: 'Clear Chat',
clearChatConfirm: 'Are you sure to clear this chat?',
exportImage: 'Export Image',
exportImageConfirm: 'Are you sure to export this chat to png?',
exportSuccess: 'Export Success',
exportFailed: 'Export Failed',
usingContext: 'Context Mode',
turnOnContext: 'In the current mode, sending messages will carry previous chat records.',
turnOffContext: 'In the current mode, sending messages will not carry previous chat records.',
deleteMessage: 'Delete Message',
deleteMessageConfirm: 'Are you sure to delete this message?',
deleteHistoryConfirm: 'Are you sure to clear this history?',
clearHistoryConfirm: 'Are you sure to clear chat history?',
preview: 'Preview',
showRawText: 'Show as raw text',
},
setting: {
setting: 'Setting',
general: 'General',
advanced: 'Advanced',
config: 'Config',
avatarLink: 'Avatar Link',
name: 'Name',
description: 'Description',
role: 'Role',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: 'Reset UserInfo',
chatHistory: 'ChatHistory',
theme: 'Theme',
language: 'Language',
api: 'API',
reverseProxy: 'Reverse Proxy',
timeout: 'Timeout',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API Balance',
monthlyUsage: 'Monthly Usage',
},
store: {
siderButton: 'Prompt Store',
local: 'Local',
online: 'Online',
title: 'Title',
description: 'Description',
clearStoreConfirm: 'Whether to clear the data?',
importPlaceholder: 'Please paste the JSON data here',
addRepeatTitleTips: 'Title duplicate, please re-enter',
addRepeatContentTips: 'Content duplicate: {msg}, please re-enter',
editRepeatTitleTips: 'Title conflict, please revise',
editRepeatContentTips: 'Content conflict {msg} , please re-modify',
importError: 'Key value mismatch',
importRepeatTitle: 'Title repeatedly skipped: {msg}',
importRepeatContent: 'Content is repeatedly skipped: {msg}',
onlineImportWarning: 'Note: Please check the JSON file source!',
downloadError: 'Please check the network status and JSON file validity',
},
}

View File

@@ -0,0 +1,38 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import enUS from './en-US'
import koKR from './ko-KR'
import zhCN from './zh-CN'
import zhTW from './zh-TW'
import ruRU from './ru-RU'
import { useAppStoreWithOut } from '@/store/modules/app'
import type { Language } from '@/store/modules/app/helper'
const appStore = useAppStoreWithOut()
const defaultLocale = appStore.language || 'zh-CN'
const i18n = createI18n({
locale: defaultLocale,
fallbackLocale: 'en-US',
allowComposition: true,
messages: {
'en-US': enUS,
'ko-KR': koKR,
'zh-CN': zhCN,
'zh-TW': zhTW,
'ru-RU': ruRU,
},
})
export const t = i18n.global.t
export function setLocale(locale: Language) {
i18n.global.locale = locale
}
export function setupI18n(app: App) {
app.use(i18n)
}
export default i18n

View File

@@ -0,0 +1,93 @@
export default {
common: {
add: '추가',
addSuccess: '추가 성공',
edit: '편집',
editSuccess: '편집 성공',
delete: '삭제',
deleteSuccess: '삭제 성공',
save: '저장',
saveSuccess: '저장 성공',
reset: '초기화',
action: '액션',
export: '내보내기',
exportSuccess: '내보내기 성공',
import: '가져오기',
importSuccess: '가져오기 성공',
clear: '비우기',
clearSuccess: '비우기 성공',
yes: '예',
no: '아니오',
confirm: '확인',
download: '다운로드',
noData: '데이터 없음',
wrong: '문제가 발생했습니다. 나중에 다시 시도하십시오.',
success: '성공',
failed: '실패',
verify: '검증',
unauthorizedTips: '인증되지 않았습니다. 먼저 확인하십시오.',
},
chat: {
newChatButton: '새로운 채팅',
placeholder: '무엇이든 물어보세요...(Shift + Enter = 줄바꿈, "/"를 눌러서 힌트를 보세요)',
placeholderMobile: '무엇이든 물어보세요...',
copy: '복사',
copied: '복사됨',
copyCode: '코드 복사',
clearChat: '채팅 비우기',
clearChatConfirm: '이 채팅을 비우시겠습니까?',
exportImage: '이미지 내보내기',
exportImageConfirm: '이 채팅을 png로 내보내시겠습니까?',
exportSuccess: '내보내기 성공',
exportFailed: '내보내기 실패',
usingContext: '컨텍스트 모드',
turnOnContext: '현재 모드에서는 이전 대화 기록을 포함하여 메시지를 보낼 수 있습니다.',
turnOffContext: '현재 모드에서는 이전 대화 기록을 포함하지 않고 메시지를 보낼 수 있습니다.',
deleteMessage: '메시지 삭제',
deleteMessageConfirm: '이 메시지를 삭제하시겠습니까?',
deleteHistoryConfirm: '이 기록을 삭제하시겠습니까?',
clearHistoryConfirm: '채팅 기록을 삭제하시겠습니까?',
preview: '미리보기',
showRawText: '원본 텍스트로 보기',
},
setting: {
setting: '설정',
general: '일반',
advanced: '고급',
config: '설정',
avatarLink: '아바타 링크',
name: '이름',
description: '설명',
role: '역할',
temperature: '온도',
top_p: 'Top_p',
resetUserInfo: '사용자 정보 초기화',
chatHistory: '채팅 기록',
theme: '테마',
language: '언어',
api: 'API',
reverseProxy: '리버스 프록시',
timeout: '타임아웃',
socks: 'Socks',
httpsProxy: 'HTTPS 프록시',
balance: 'API 잔액',
monthlyUsage: '월 사용량',
},
store: {
siderButton: '프롬프트 저장소',
local: '로컬',
online: '온라인',
title: '제목',
description: '설명',
clearStoreConfirm: '데이터를 삭제하시겠습니까?',
importPlaceholder: '여기에 JSON 데이터를 붙여넣으십시오',
addRepeatTitleTips: '제목 중복됨, 다시 입력하십시오',
addRepeatContentTips: '내용 중복됨: {msg}, 다시 입력하십시오',
editRepeatTitleTips: '제목 충돌, 수정하십시오',
editRepeatContentTips: '내용 충돌 {msg} , 수정하십시오',
importError: '키 값 불일치',
importRepeatTitle: '제목이 반복되어 건너뜀: {msg}',
importRepeatContent: '내용이 반복되어 건너뜀: {msg}',
onlineImportWarning: '참고: JSON 파일 소스를 확인하십시오!',
},
}

View File

@@ -0,0 +1,94 @@
export default {
common: {
add: 'Добавить',
addSuccess: 'Добавлено успешно',
edit: 'Редактировать',
editSuccess: 'Изменено успешно',
delete: 'Удалить',
deleteSuccess: 'Удалено успешно',
save: 'Сохранить',
saveSuccess: 'Сохранено успешно',
reset: 'Сбросить',
action: 'Действие',
export: 'Экспортировать',
exportSuccess: 'Экспорт выполнен успешно',
import: 'Импортировать',
importSuccess: 'Импорт выполнен успешно',
clear: 'Очистить',
clearSuccess: 'Очищено успешно',
yes: 'Да',
no: 'Нет',
confirm: 'Подтвердить',
download: 'Загрузить',
noData: 'Нет данных',
wrong: 'Что-то пошло не так, пожалуйста, повторите попытку позже.',
success: 'Успех',
failed: 'Не удалось',
verify: 'Проверить',
unauthorizedTips: 'Не авторизован, сначала подтвердите свою личность.',
},
chat: {
newChatButton: 'Новый чат',
placeholder: 'Спросите меня о чем-нибудь ... (Shift + Enter = перенос строки, "/" для вызова подсказок)',
placeholderMobile: 'Спросите меня о чем-нибудь ...',
copy: 'Копировать',
copied: 'Скопировано',
copyCode: 'Копировать код',
clearChat: 'Очистить чат',
clearChatConfirm: 'Вы уверены, что хотите очистить этот чат?',
exportImage: 'Экспорт в изображение',
exportImageConfirm: 'Вы уверены, что хотите экспортировать этот чат в формате PNG?',
exportSuccess: 'Экспортировано успешно',
exportFailed: 'Не удалось выполнить экспорт',
usingContext: 'Режим контекста',
turnOnContext: 'В текущем режиме отправка сообщений будет включать предыдущие записи чата.',
turnOffContext: 'В текущем режиме отправка сообщений не будет включать предыдущие записи чата.',
deleteMessage: 'Удалить сообщение',
deleteMessageConfirm: 'Вы уверены, что хотите удалить это сообщение?',
deleteHistoryConfirm: 'Вы уверены, что хотите очистить эту историю?',
clearHistoryConfirm: 'Вы уверены, что хотите очистить историю чата?',
preview: 'Предварительный просмотр',
showRawText: 'Показать как обычный текст',
},
setting: {
setting: 'Настройки',
general: 'Общее',
advanced: 'Дополнительно',
config: 'Конфигурация',
avatarLink: 'Ссылка на аватар',
name: 'Имя',
description: 'Описание',
role: 'Роль',
temperature: 'Температура',
top_p: 'Top_p',
resetUserInfo: 'Сбросить информацию о пользователе',
chatHistory: 'История чата',
theme: 'Тема',
language: 'Язык',
api: 'API',
reverseProxy: 'Обратный прокси-сервер',
timeout: 'Время ожидания',
socks: 'Socks',
httpsProxy: 'HTTPS-прокси',
balance: 'Баланс API',
monthlyUsage: 'Ежемесячное использование',
},
store: {
siderButton: 'Хранилище подсказок',
local: 'Локальное',
online: 'Онлайн',
title: 'Название',
description: 'Описание',
clearStoreConfirm: 'Вы действительно хотите очистить данные?',
importPlaceholder: 'Пожалуйста, вставьте здесь JSON-данные',
addRepeatTitleTips: 'Дубликат названия, пожалуйста, введите другое название',
addRepeatContentTips: 'Дубликат содержимого: {msg}, пожалуйста, введите другой текст',
editRepeatTitleTips: 'Конфликт названий, пожалуйста, измените название',
editRepeatContentTips: 'Конфликт содержимого {msg}, пожалуйста, измените текст',
importError: 'Не совпадает ключ-значение',
importRepeatTitle: 'Название повторяющееся, пропускается: {msg}',
importRepeatContent: 'Содержание повторяющееся, пропускается: {msg}',
onlineImportWarning: 'Внимание! Проверьте источник JSON-файла!',
downloadError: 'Проверьте состояние сети и правильность JSON-файла',
},
}

View File

@@ -0,0 +1,94 @@
export default {
common: {
add: '添加',
addSuccess: '添加成功',
edit: '编辑',
editSuccess: '编辑成功',
delete: '删除',
deleteSuccess: '删除成功',
save: '保存',
saveSuccess: '保存成功',
reset: '重置',
action: '操作',
export: '导出',
exportSuccess: '导出成功',
import: '导入',
importSuccess: '导入成功',
clear: '清空',
clearSuccess: '清空成功',
yes: '是',
no: '否',
confirm: '确定',
download: '下载',
noData: '暂无数据',
wrong: '好像出错了,请稍后再试。',
success: '操作成功',
failed: '操作失败',
verify: '验证',
unauthorizedTips: '未经授权,请先进行验证。',
},
chat: {
newChatButton: '新建聊天',
placeholder: '来说点什么吧...Shift + Enter = 换行,"/" 触发提示词)',
placeholderMobile: '来说点什么...',
copy: '复制',
copied: '复制成功',
copyCode: '复制代码',
clearChat: '清空会话',
clearChatConfirm: '是否清空会话?',
exportImage: '保存会话到图片',
exportImageConfirm: '是否将会话保存为图片?',
exportSuccess: '保存成功',
exportFailed: '保存失败',
usingContext: '上下文模式',
turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录',
turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录',
deleteMessage: '删除消息',
deleteMessageConfirm: '是否删除此消息?',
deleteHistoryConfirm: '确定删除此记录?',
clearHistoryConfirm: '确定清空聊天记录?',
preview: '预览',
showRawText: '显示原文',
},
setting: {
setting: '设置',
general: '总览',
advanced: '高级',
config: '配置',
avatarLink: '头像链接',
name: '名称',
description: '描述',
role: '角色设定',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: '重置用户信息',
chatHistory: '聊天记录',
theme: '主题',
language: '语言',
api: 'API',
reverseProxy: '反向代理',
timeout: '超时',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API余额',
monthlyUsage: '本月使用量',
},
store: {
siderButton: '提示词商店',
local: '本地',
online: '在线',
title: '标题',
description: '描述',
clearStoreConfirm: '是否清空数据?',
importPlaceholder: '请粘贴 JSON 数据到此处',
addRepeatTitleTips: '标题重复,请重新输入',
addRepeatContentTips: '内容重复:{msg},请重新输入',
editRepeatTitleTips: '标题冲突,请重新修改',
editRepeatContentTips: '内容冲突{msg} ,请重新修改',
importError: '键值不匹配',
importRepeatTitle: '标题重复跳过:{msg}',
importRepeatContent: '内容重复跳过:{msg}',
onlineImportWarning: '注意:请检查 JSON 文件来源!',
downloadError: '请检查网络状态与 JSON 文件有效性',
},
}

View File

@@ -0,0 +1,94 @@
export default {
common: {
add: '新增',
addSuccess: '新增成功',
edit: '編輯',
editSuccess: '編輯成功',
delete: '刪除',
deleteSuccess: '刪除成功',
save: '儲存',
saveSuccess: '儲存成功',
reset: '重設',
action: '操作',
export: '匯出',
exportSuccess: '匯出成功',
import: '匯入',
importSuccess: '匯入成功',
clear: '清除',
clearSuccess: '清除成功',
yes: '是',
no: '否',
confirm: '確認',
download: '下載',
noData: '目前無資料',
wrong: '發生錯誤,請稍後再試。',
success: '操作成功',
failed: '操作失敗',
verify: '驗證',
unauthorizedTips: '未經授權,請先進行驗證。',
},
chat: {
newChatButton: '新增對話',
placeholder: '來說點什麼...Shift + Enter = 換行,"/" 觸發提示詞)',
placeholderMobile: '來說點什麼...',
copy: '複製',
copied: '複製成功',
copyCode: '複製代碼',
clearChat: '清除對話',
clearChatConfirm: '是否清空對話?',
exportImage: '儲存對話為圖片',
exportImageConfirm: '是否將對話儲存為圖片?',
exportSuccess: '儲存成功',
exportFailed: '儲存失敗',
usingContext: '上下文模式',
turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。',
turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。',
deleteMessage: '刪除訊息',
deleteMessageConfirm: '是否刪除此訊息?',
deleteHistoryConfirm: '確定刪除此紀錄?',
clearHistoryConfirm: '確定清除紀錄?',
preview: '預覽',
showRawText: '顯示原文',
},
setting: {
setting: '設定',
general: '總覽',
advanced: '進階',
config: '設定',
avatarLink: '頭貼連結',
name: '名稱',
description: '描述',
role: '角色設定',
temperature: 'Temperature',
top_p: 'Top_p',
resetUserInfo: '重設使用者資訊',
chatHistory: '紀錄',
theme: '主題',
language: '語言',
api: 'API',
reverseProxy: '反向代理',
timeout: '逾時',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API Credit 餘額',
monthlyUsage: '本月使用量',
},
store: {
siderButton: '提示詞商店',
local: '本機',
online: '線上',
title: '標題',
description: '描述',
clearStoreConfirm: '是否清除資料?',
importPlaceholder: '請將 JSON 資料貼在此處',
addRepeatTitleTips: '標題重複,請重新輸入',
addRepeatContentTips: '內容重複:{msg},請重新輸入',
editRepeatTitleTips: '標題衝突,請重新修改',
editRepeatContentTips: '內容衝突{msg} ,請重新修改',
importError: '鍵值不符合',
importRepeatTitle: '因標題重複跳過:{msg}',
importRepeatContent: '因內容重複跳過:{msg}',
onlineImportWarning: '注意:請檢查 JSON 檔案來源!',
downloadError: '請檢查網路狀態與 JSON 檔案有效性',
},
}

View File

@@ -0,0 +1,23 @@
import { createApp } from 'vue'
import App from './App.vue'
import { setupI18n } from './locales'
import { setupAssets, setupScrollbarStyle } from './plugins'
import { setupStore } from './store'
import { setupRouter } from './router'
async function bootstrap() {
const app = createApp(App)
setupAssets()
setupScrollbarStyle()
setupStore(app)
setupI18n(app)
await setupRouter(app)
app.mount('#app')
}
bootstrap()

View File

@@ -0,0 +1,18 @@
import 'katex/dist/katex.min.css'
import '@/styles/lib/tailwind.css'
import '@/styles/lib/highlight.less'
import '@/styles/lib/github-markdown.less'
import '@/styles/global.less'
/** Tailwind's Preflight Style Override */
function naiveStyleOverride() {
const meta = document.createElement('meta')
meta.name = 'naive-ui-style'
document.head.appendChild(meta)
}
function setupAssets() {
naiveStyleOverride()
}
export default setupAssets

View File

@@ -0,0 +1,4 @@
import setupAssets from './assets'
import setupScrollbarStyle from './scrollbarStyle'
export { setupAssets, setupScrollbarStyle }

Some files were not shown because too many files have changed in this diff Show More