refactor: 重构项目目录结构

This commit is contained in:
RockYang
2023-05-06 11:35:57 +08:00
parent a897a755b6
commit 4dcae75488
28 changed files with 87 additions and 61 deletions

20
src/Makefile Normal file
View File

@@ -0,0 +1,20 @@
SHELL=/usr/bin/env bash
NAME := wechatGPT
all: window linux darwin
window:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/$(NAME)-amd64.exe main.go
.PHONY: window
linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-amd64-linux main.go
.PHONY: linux
darwin:
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
.PHONY: darwin
clean:
rm -rf bin/$(NAME)-*
.PHONY: clean

31
src/config.sample.toml Normal file
View File

@@ -0,0 +1,31 @@
Title = "Chat-Plus AI 助手"
ConsoleTitle = "Chat-Plus 控制台"
Listen = "0.0.0.0:5678"
ProxyURL = ["YOUR_PORYX_URL"]
AccessKey = "YOUR_ACCESS_KEY"
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
Name = "CHAT_SESSION_ID"
Path = "/"
Domain = ""
MaxAge = 86400
Secure = false
HttpOnly = false
SameSite = 2
[ImgURL]
WechatCard = "https://img.r9it.com/chatgpt/WX20230505-162403.png"
WechatGroup = " https://img.r9it.com/chatgpt/WX20230505-162538.png"
[Manager]
Username = "admin"
Password = "admin123"
[Chat]
ApiURL = "https://api.openai.com/v1/chat/completions"
Model = "gpt-3.5-turbo"
Temperature = 1.0
MaxTokens = 1024
EnableContext = true
ChatContextExpireTime = 3600

14
src/fresh.conf Normal file
View File

@@ -0,0 +1,14 @@
root: .
tmp_path: ./tmp
build_name: runner-build
build_log: runner-build-errors.log
valid_ext: .go, .tpl, .tmpl, .html
no_rebuild_ext: .tpl, .tmpl, .html, .js, .vue
ignored: assets, tmp, web, .git, .idea, test, data
build_delay: 600
colors: 1
log_color_main: cyan
log_color_build: yellow
log_color_runner: green
log_color_watcher: magenta
log_color_app:

36
src/go.mod Normal file
View File

@@ -0,0 +1,36 @@
module chatplus
go 1.19
require (
github.com/BurntSushi/toml v1.1.0
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.7.7
github.com/gorilla/websocket v1.5.0
github.com/mitchellh/go-homedir v1.1.0
github.com/syndtr/goleveldb v1.0.0
go.uber.org/zap v1.21.0
)
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)

137
src/go.sum Normal file
View File

@@ -0,0 +1,137 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
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.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

26
src/logger/logger.go Normal file
View File

@@ -0,0 +1,26 @@
package logger
import (
"go.uber.org/zap"
)
var logger *zap.SugaredLogger
func GetLogger() *zap.SugaredLogger {
if logger != nil {
return logger
}
logLevel := zap.NewAtomicLevel()
logLevel.SetLevel(zap.InfoLevel)
log, _ := zap.Config{
Level: logLevel,
Development: false,
Encoding: "console",
EncoderConfig: zap.NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}.Build()
logger = log.Sugar()
return logger
}

71
src/main.go Normal file
View File

@@ -0,0 +1,71 @@
package main
import (
logger2 "chatplus/logger"
"chatplus/server"
"embed"
"flag"
"fmt"
"github.com/mitchellh/go-homedir"
"os"
"path/filepath"
)
var logger = logger2.GetLogger()
//go:embed dist
var webRoot embed.FS
var configFile string
var debugMode bool
func main() {
// create config dir
configDir, _ := homedir.Expand("~/.config/chat-gpt")
_, err := os.Stat(configDir)
if err != nil {
err := os.MkdirAll(configDir, 0755)
if err != nil {
logger.Error(err)
return
}
}
if err != nil {
logger.Errorf("failed to load web types: %v", err)
return
}
if configFile == "" {
configFile = filepath.Join(configDir, "config.toml")
}
// start server
s, err := server.NewServer(configFile)
if err != nil {
panic(err)
}
s.Run(webRoot, "dist", debugMode)
}
func init() {
flag.StringVar(&configFile, "config", "", "Config file path (default: ~/.config/chat-gpt/config.toml)")
flag.BoolVar(&debugMode, "debug", true, "Enable debug mode (default: true, recommend to set false in production env)")
flag.Usage = usage
flag.Parse()
}
func usage() {
fmt.Printf(`WeChat-GPT, Version: 1.0.0
USAGE:
%s [command options]
OPTIONS:
`, os.Args[0])
flagSet := flag.CommandLine
order := []string{"config", "debug"}
for _, name := range order {
f := flagSet.Lookup(name)
fmt.Printf(" --%s => %s\n", f.Name, f.Usage)
}
}

61
src/server/client.go Normal file
View File

@@ -0,0 +1,61 @@
package server
import (
"errors"
"github.com/gorilla/websocket"
"sync"
)
var ErrConClosed = errors.New("connection closed")
type Client interface {
Close()
}
// WsClient websocket client
type WsClient struct {
Conn *websocket.Conn
lock sync.Mutex
mt int
closed bool
}
func NewWsClient(conn *websocket.Conn) *WsClient {
return &WsClient{
Conn: conn,
lock: sync.Mutex{},
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
closed: false,
}
}
func (wc *WsClient) Send(message []byte) error {
wc.lock.Lock()
defer wc.lock.Unlock()
if wc.closed {
return ErrConClosed
}
return wc.Conn.WriteMessage(wc.mt, message)
}
func (wc *WsClient) Receive() (int, []byte, error) {
if wc.closed {
return 0, nil, ErrConClosed
}
return wc.Conn.ReadMessage()
}
func (wc *WsClient) Close() {
wc.lock.Lock()
defer wc.lock.Unlock()
if wc.closed {
return
}
_ = wc.Conn.Close()
wc.closed = true
}

138
src/server/db.go Normal file
View File

@@ -0,0 +1,138 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
)
const (
UserPrefix = "chat/users/"
ChatRolePrefix = "chat/roles/"
ChatHistoryPrefix = "chat/history/"
)
var db *utils.LevelDB
func init() {
leveldb, err := utils.NewLevelDB("data")
if err != nil {
panic(err)
}
db = leveldb
}
// GetUsers 获取 user 信息
// chat/users
func GetUsers() []types.User {
items := db.Search(UserPrefix)
var users = make([]types.User, 0)
for _, v := range items {
var user types.User
err := json.Unmarshal([]byte(v), &user)
if err != nil {
continue
}
users = append(users, user)
}
return users
}
func PutUser(user types.User) error {
key := UserPrefix + user.Name
return db.Put(key, user)
}
func GetUser(username string) (*types.User, error) {
key := UserPrefix + username
bytes, err := db.Get(key)
if err != nil {
return nil, err
}
var user types.User
err = json.Unmarshal(bytes, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func RemoveUser(username string) error {
key := UserPrefix + username
return db.Delete(key)
}
// GetChatRoles 获取聊天角色
// chat/roles
func GetChatRoles() map[string]types.ChatRole {
items := db.Search(ChatRolePrefix)
var roles = make(map[string]types.ChatRole)
for _, v := range items {
var role types.ChatRole
err := json.Unmarshal([]byte(v), &role)
if err != nil {
continue
}
roles[role.Key] = role
}
return roles
}
func PutChatRole(role types.ChatRole) error {
key := ChatRolePrefix + role.Key
return db.Put(key, role)
}
func GetChatRole(key string) (*types.ChatRole, error) {
key = ChatRolePrefix + key
bytes, err := db.Get(key)
if err != nil {
return nil, err
}
var role types.ChatRole
err = json.Unmarshal(bytes, &role)
if err != nil {
return nil, err
}
return &role, nil
}
// GetChatHistory 获取聊天历史记录
// chat/history/{user}/{role}
func GetChatHistory(user string, role string) ([]types.Message, error) {
key := ChatHistoryPrefix + user + "/" + role
bytes, err := db.Get(key)
if err != nil {
return nil, err
}
var message []types.Message
err = json.Unmarshal(bytes, &message)
if err != nil {
return nil, err
}
return message, nil
}
// AppendChatHistory 追加聊天记录
func AppendChatHistory(user string, role string, message types.Message) error {
messages, err := GetChatHistory(user, role)
if err != nil {
messages = make([]types.Message, 0)
}
messages = append(messages, message)
key := ChatHistoryPrefix + user + "/" + role
return db.Put(key, messages)
}
// ClearChatHistory 清空某个角色下的聊天记录
func ClearChatHistory(user string, role string) error {
key := ChatHistoryPrefix + user + "/" + role
return db.Delete(key)
}

View File

@@ -0,0 +1,75 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
)
// AddApiKeyHandle 添加一个 API key
func (s *Server) AddApiKeyHandle(c *gin.Context) {
var data struct {
ApiKey string `json:"api_key"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
// 过滤已存在的 Key
for _, key := range s.Config.Chat.ApiKeys {
if key.Value == data.ApiKey {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "API KEY 已存在"})
return
}
}
if len(data.ApiKey) > 20 {
s.Config.Chat.ApiKeys = append(s.Config.Chat.ApiKeys, types.APIKey{Value: data.ApiKey})
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.Chat.ApiKeys})
}
// RemoveApiKeyHandle 移除一个 API key
func (s *Server) RemoveApiKeyHandle(c *gin.Context) {
var data struct {
ApiKey string `json:"api_key"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
for i, v := range s.Config.Chat.ApiKeys {
if v.Value == data.ApiKey {
s.Config.Chat.ApiKeys = append(s.Config.Chat.ApiKeys[:i], s.Config.Chat.ApiKeys[i+1:]...)
break
}
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.Chat.ApiKeys})
}
// ListApiKeysHandle 获取 API key 列表
func (s *Server) ListApiKeysHandle(c *gin.Context) {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.Chat.ApiKeys})
}

View File

@@ -0,0 +1,163 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func (s *Server) TestHandle(c *gin.Context) {
roles := types.GetDefaultChatRole()
for _, v := range roles {
_ = PutChatRole(v)
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: GetChatRoles()})
}
// GetAllConfigHandle 获取所有配置
func (s *Server) GetAllConfigHandle(c *gin.Context) {
data := struct {
Title string `json:"title"`
ConsoleTitle string `json:"console_title"`
ProxyURL string `json:"proxy_url"`
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
ChatContextExpireTime int `json:"chat_context_expire_time"`
EnableContext bool `json:"enable_context"`
ImgURL types.ImgURL `json:"img_url"`
}{
Title: s.Config.Title,
ConsoleTitle: s.Config.ConsoleTitle,
ProxyURL: strings.Join(s.Config.ProxyURL, ","),
Model: s.Config.Chat.Model,
Temperature: s.Config.Chat.Temperature,
MaxTokens: s.Config.Chat.MaxTokens,
EnableContext: s.Config.Chat.EnableContext,
ChatContextExpireTime: s.Config.Chat.ChatContextExpireTime,
ImgURL: s.Config.ImgURL,
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: data})
}
func (s *Server) GetConfigHandle(c *gin.Context) {
data := struct {
Title string `json:"title"`
ConsoleTitle string `json:"console_title"`
WechatCard string `json:"wechat_card"` // 个人微信二维码
WechatGroup string `json:"wechat_group"` // 微信群二维码
}{
Title: s.Config.Title,
ConsoleTitle: s.Config.ConsoleTitle,
WechatCard: s.Config.ImgURL.WechatCard,
WechatGroup: s.Config.ImgURL.WechatGroup,
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: data})
}
// ConfigSetHandle set configs
func (s *Server) ConfigSetHandle(c *gin.Context) {
var data struct {
Title string `json:"title"`
ConsoleTitle string `json:"console_title"`
ProxyURL string `json:"proxy_url"`
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
ChatContextExpireTime int `json:"chat_context_expire_time"`
EnableContext bool `json:"enable_context"`
ImgURL types.ImgURL `json:"img_url"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
s.Config.Title = data.Title
s.Config.ConsoleTitle = data.ConsoleTitle
urls := strings.Split(data.ProxyURL, ",")
for k, v := range urls {
urls[k] = strings.TrimSpace(v)
}
s.Config.ProxyURL = urls
s.Config.Chat.Model = data.Model
s.Config.Chat.Temperature = data.Temperature
s.Config.Chat.MaxTokens = data.MaxTokens
s.Config.Chat.EnableContext = data.EnableContext
s.Config.Chat.ChatContextExpireTime = data.ChatContextExpireTime
s.Config.ImgURL = data.ImgURL
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config})
}
// AddProxyHandle 添加一个代理
func (s *Server) AddProxyHandle(c *gin.Context) {
var data struct {
Proxy string `json:"proxy"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
if data.Proxy != "" {
if !utils.ContainsStr(s.Config.ProxyURL, data.Proxy) {
s.Config.ProxyURL = append(s.Config.ProxyURL, data.Proxy)
}
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ProxyURL})
}
// RemoveProxyHandle 删除一个代理
func (s *Server) RemoveProxyHandle(c *gin.Context) {
var data struct {
Proxy string `json:"proxy"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
for i, v := range s.Config.ProxyURL {
if v == data.Proxy {
s.Config.ProxyURL = append(s.Config.ProxyURL[:i], s.Config.ProxyURL[i+1:]...)
break
}
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ProxyURL})
}

View File

@@ -0,0 +1,157 @@
package server
import (
"chatplus/types"
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
)
// GetAllChatRolesHandle 获取所有聊天角色列表
func (s *Server) GetAllChatRolesHandle(c *gin.Context) {
var rolesOrder = []string{"gpt", "teacher", "translator", "english_trainer", "weekly_report", "girl_friend",
"kong_zi", "lu_xun", "steve_jobs", "elon_musk", "red_book", "dou_yin", "programmer",
"seller", "good_comment", "psychiatrist", "artist"}
var res = make([]interface{}, 0)
var roles = GetChatRoles()
for _, k := range rolesOrder {
if v, ok := roles[k]; ok {
res = append(res, v)
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: res})
}
// GetChatRoleListHandle 获取当前登录用户的角色列表
func (s *Server) GetChatRoleListHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
session := s.ChatSession[sessionId]
user, err := GetUser(session.Username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Hacker Access!!!"})
return
}
var rolesOrder = []string{"gpt", "teacher", "translator", "english_trainer", "weekly_report", "girl_friend",
"kong_zi", "lu_xun", "steve_jobs", "elon_musk", "red_book", "dou_yin", "programmer",
"seller", "good_comment", "psychiatrist", "artist"}
var res = make([]interface{}, 0)
var roles = GetChatRoles()
for _, k := range rolesOrder {
// 确认当前用户是否订阅了当前角色
if v, ok := user.ChatRoles[k]; !ok || v != 1 {
continue
}
if v, ok := roles[k]; ok && v.Enable {
res = append(res, struct {
Key string `json:"key"`
Name string `json:"name"`
Icon string `json:"icon"`
}{
Key: v.Key,
Name: v.Name,
Icon: v.Icon,
})
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: res})
}
// AddChatRoleHandle 添加一个聊天角色
func (s *Server) AddChatRoleHandle(c *gin.Context) {
var data types.ChatRole
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
if data.Key == "" || data.Name == "" || data.Icon == "" {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid parameters"})
return
}
err = PutChatRole(data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save levelDB"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: data})
}
// GetChatRoleHandle 获取指定的角色
func (s *Server) GetChatRoleHandle(c *gin.Context) {
var data struct {
Key string `json:"key"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
role, err := GetChatRole(data.Key)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "No role found"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: role})
return
}
// SetChatRoleHandle 更新某个聊天角色信息,这里只允许更改名称以及启用和禁用角色操作
func (s *Server) SetChatRoleHandle(c *gin.Context) {
var data map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
var key string
if v, ok := data["key"]; !ok {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Must specified the role key"})
return
} else {
key = v.(string)
}
role, err := GetChatRole(key)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Role key not exists"})
return
}
if v, ok := data["name"]; ok {
role.Name = v.(string)
}
if v, ok := data["hello_msg"]; ok {
role.HelloMsg = v.(string)
}
if v, ok := data["icon"]; ok {
role.Icon = v.(string)
}
if v, ok := data["enable"]; ok {
role.Enable = v.(bool)
}
if v, ok := data["context"]; ok {
bytes, _ := json.Marshal(v)
_ = json.Unmarshal(bytes, &role.Context)
}
err = PutChatRole(*role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save levelDB"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: data})
}

View File

@@ -0,0 +1,272 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
// AddUserHandle 添加 Username
func (s *Server) AddUserHandle(c *gin.Context) {
var data struct {
Name string `json:"name"`
MaxCalls int `json:"max_calls"`
EnableHistory bool `json:"enable_history"`
Term int `json:"term"` // 有效期
ChatRoles []string `json:"chat_roles"` // 订阅角色
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
// 参数处理
if data.Name == "" || data.MaxCalls < 0 {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
// 检查当前要添加的 Username 是否已经存在
_, err = GetUser(data.Name)
if err == nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Username " + data.Name + " already exists"})
return
}
var chatRoles = make(map[string]int)
if len(data.ChatRoles) > 0 {
if data.ChatRoles[0] == "all" { // 所有的角色
roles := GetChatRoles()
for key := range roles {
chatRoles[key] = 1
}
} else {
for _, key := range data.ChatRoles {
chatRoles[key] = 1
}
}
}
user := types.User{
Name: data.Name,
MaxCalls: data.MaxCalls,
RemainingCalls: data.MaxCalls,
EnableHistory: data.EnableHistory,
Term: data.Term,
ChatRoles: chatRoles,
Status: true}
err = PutUser(user)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save configs"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: user})
}
// BatchAddUserHandle 批量生成 Username
func (s *Server) BatchAddUserHandle(c *gin.Context) {
var data struct {
Number int `json:"number"`
MaxCalls int `json:"max_calls"`
EnableHistory bool `json:"enable_history"`
Term int `json:"term"`
ChatRoles []string `json:"chat_roles"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil || data.MaxCalls <= 0 {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
var chatRoles = make(map[string]int)
if len(data.ChatRoles) > 0 {
if data.ChatRoles[0] == "all" { // 所有的角色
roles := GetChatRoles()
for key := range roles {
chatRoles[key] = 1
}
} else {
for _, key := range data.ChatRoles {
chatRoles[key] = 1
}
}
}
var users = make([]UserVo, 0)
for i := 0; i < data.Number; i++ {
name := utils.RandString(12)
_, err := GetUser(name)
for err == nil {
name = utils.RandString(12)
}
user := types.User{
Name: name,
MaxCalls: data.MaxCalls,
RemainingCalls: data.MaxCalls,
EnableHistory: data.EnableHistory,
Term: data.Term,
ChatRoles: chatRoles,
Status: true}
err = PutUser(user)
if err == nil {
users = append(users, user2vo(user))
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: users})
}
func (s *Server) SetUserHandle(c *gin.Context) {
var data map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
var user *types.User
if name, ok := data["name"]; ok {
user, err = GetUser(name.(string))
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "User not found"})
return
}
}
var maxCalls int
if v, ok := data["max_calls"]; ok {
maxCalls = int(v.(float64))
}
if maxCalls < 0 {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
} else if maxCalls > 0 {
user.MaxCalls = maxCalls
user.RemainingCalls += maxCalls - user.MaxCalls
if user.RemainingCalls < 0 {
user.RemainingCalls = 0
}
}
if v, ok := data["status"]; ok {
user.Status = v.(bool)
}
if v, ok := data["enable_history"]; ok {
user.EnableHistory = v.(bool)
}
if v, ok := data["remaining_calls"]; ok {
user.RemainingCalls = int(v.(float64))
}
if v, ok := data["expired_time"]; ok {
user.ExpiredTime = utils.Str2stamp(v.(string))
}
if v, ok := data["api_key"]; ok {
user.ApiKey = v.(string)
}
if v, ok := data["chat_roles"]; ok {
if roles, ok := v.([]interface{}); ok {
chatRoles := make(map[string]int)
if roles[0] == "all" {
roles := GetChatRoles()
for key := range roles {
chatRoles[key] = 1
}
} else {
for _, key := range roles {
key := strings.TrimSpace(fmt.Sprintf("%v", key))
chatRoles[key] = 1
}
}
user.ChatRoles = chatRoles
}
}
err = PutUser(*user)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save configs"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: user})
}
// RemoveUserHandle 删除 Username
func (s *Server) RemoveUserHandle(c *gin.Context) {
var data struct {
Name string `json:"name"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
err = RemoveUser(data.Name)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save configs"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg})
}
type UserVo struct {
Name string `json:"name"`
MaxCalls int `json:"max_calls"` // 最多调用次数,如果为 0 则表示不限制
RemainingCalls int `json:"remaining_calls"` // 剩余调用次数
EnableHistory bool `json:"enable_history"` // 是否启用聊天记录
Status bool `json:"status"` // 当前状态
Term int `json:"term" default:"30"` // 会员有效期,单位:天
ActiveTime string `json:"active_time"` // 激活时间
ExpiredTime string `json:"expired_time"` // 到期时间
ApiKey string `json:"api_key"` // OpenAI API KEY
ChatRoles []string `json:"chat_roles"` // 当前用户已订阅的聊天角色 map[role_key] => 0/1
}
// GetUserListHandle 获取用户列表
func (s *Server) GetUserListHandle(c *gin.Context) {
username := c.PostForm("username")
if username != "" {
user, err := GetUser(username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "User not exists"})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: user})
}
return
}
users := make([]UserVo, 0)
for _, u := range GetUsers() {
users = append(users, user2vo(u))
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: users})
}
// 将 User 实体转为 UserVo 实体
func user2vo(user types.User) UserVo {
vo := UserVo{
Name: user.Name,
MaxCalls: user.MaxCalls,
RemainingCalls: user.RemainingCalls,
EnableHistory: user.EnableHistory,
Status: user.Status,
Term: user.Term,
ActiveTime: utils.Stamp2str(user.ActiveTime),
ExpiredTime: utils.Stamp2str(user.ExpiredTime),
ChatRoles: make([]string, 0),
}
for k := range user.ChatRoles {
vo.ChatRoles = append(vo.ChatRoles, k)
}
return vo
}

530
src/server/handler_chat.go Normal file
View File

@@ -0,0 +1,530 @@
package server
import (
"bufio"
"bytes"
"chatplus/types"
"chatplus/utils"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
)
const ErrorMsg = "抱歉AI 助手开小差了,请马上联系管理员去盘它。"
// ChatHandle 处理聊天 WebSocket 请求
func (s *Server) ChatHandle(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Fatal(err)
return
}
sessionId := c.Query("sessionId")
roleKey := c.Query("role")
chatId := c.Query("chatId")
session, ok := s.ChatSession[sessionId]
session.ChatId = chatId
if !ok { // 用户未登录
c.Abort()
return
}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username)
client := NewWsClient(ws)
var roles = GetChatRoles()
var chatRole = roles[roleKey]
if !chatRole.Enable { // 角色未启用
c.Abort()
return
}
// 保存会话连接
s.ChatClients[sessionId] = client
// 加载历史消息,如果历史消息为空则发送打招呼消息
_, err = GetChatHistory(session.Username, roleKey)
if err != nil {
replyMessage(client, chatRole.HelloMsg, true)
}
go func() {
for {
_, message, err := client.Receive()
if err != nil {
logger.Error(err)
client.Close()
delete(s.ChatClients, sessionId)
return
}
logger.Info("Receive a message: ", string(message))
//replyMessage(client, "当前 TOKEN 无效,请使用合法的 TOKEN 登录!", false)
//replyMessage(client, "![](images/wx.png)", false)
ctx, cancel := context.WithCancel(context.Background())
s.ReqCancelFunc[sessionId] = cancel
// 回复消息
err = s.sendMessage(ctx, session, chatRole, string(message), client)
if err != nil {
logger.Error(err)
} else {
replyChunkMessage(client, types.WsMessage{Type: types.WsEnd, IsHelloMsg: false})
logger.Info("回答完毕: " + string(message))
}
}
}()
}
// 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端
func (s *Server) sendMessage(ctx context.Context, session types.ChatSession, role types.ChatRole, prompt string, ws Client) error {
cancel := s.ReqCancelFunc[session.SessionId]
defer func() {
cancel()
delete(s.ReqCancelFunc, session.SessionId)
}()
user, err := GetUser(session.Username)
if err != nil {
replyMessage(ws, "当前 TOKEN 无效,请使用合法的 TOKEN 登录!", false)
return err
}
if user.Status == false {
replyMessage(ws, "当前 TOKEN 已经被禁用,如果疑问,请联系管理员!", false)
replyMessage(ws, "![](images/wx.png)", false)
return errors.New("当前 TOKEN " + user.Name + "已经被禁用")
}
if time.Now().Unix() > user.ExpiredTime {
exTime := time.Unix(user.ExpiredTime, 0).Format("2006-01-02 15:04:05")
replyMessage(ws, "当前 TOKEN 已过期,过期时间为:"+exTime+",如果疑问,请联系管理员!", false)
replyMessage(ws, "![](images/wx.png)", false)
return errors.New("当前 TOKEN " + user.Name + "已过期")
}
if user.MaxCalls > 0 && user.RemainingCalls <= 0 {
replyMessage(ws, "当前 TOKEN 点数已经用尽,加入我们的知识星球可以免费领取点卡!", false)
replyMessage(ws, "![](images/start.png)", false)
return nil
}
var req = types.ApiRequest{
Model: s.Config.Chat.Model,
Temperature: s.Config.Chat.Temperature,
MaxTokens: s.Config.Chat.MaxTokens,
Stream: true,
}
var chatCtx []types.Message
var ctxKey = fmt.Sprintf("%s-%s-%s", session.SessionId, role.Key, session.ChatId)
if v, ok := s.ChatContexts[ctxKey]; ok && s.Config.Chat.EnableContext {
chatCtx = v.Messages
} else {
chatCtx = role.Context
}
if s.DebugMode {
logger.Infof("会话上下文:%+v", chatCtx)
}
req.Messages = append(chatCtx, types.Message{
Role: "user",
Content: prompt,
})
// 创建 HttpClient 请求对象
var client *http.Client
var retryCount = 5 // 重试次数
var response *http.Response
var apiKey string
var failedKey = ""
var failedProxyURL = ""
for retryCount > 0 {
requestBody, err := json.Marshal(req)
if err != nil {
return err
}
request, err := http.NewRequest(http.MethodPost, s.Config.Chat.ApiURL, bytes.NewBuffer(requestBody))
if err != nil {
return err
}
request = request.WithContext(ctx)
request.Header.Add("Content-Type", "application/json")
proxyURL := s.getProxyURL(failedProxyURL)
if proxyURL == "" {
client = &http.Client{}
} else { // 使用代理
uri := url.URL{}
proxy, _ := uri.Parse(proxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
}
apiKey = s.getApiKey(failedKey)
if apiKey == "" {
logger.Info("Too many requests, all Api Key is not available")
time.Sleep(time.Second)
continue
}
logger.Infof("Use API KEY: %s, PROXY: %s", apiKey, proxyURL)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
response, err = client.Do(request)
if err == nil {
break
} else if strings.Contains(err.Error(), "context canceled") {
return errors.New("用户取消了请求:" + prompt)
} else {
logger.Error("HTTP API 请求失败:" + err.Error())
failedKey = apiKey
failedProxyURL = proxyURL
}
retryCount--
}
if response != nil {
defer response.Body.Close()
}
// 如果三次请求都失败的话,则返回对应的错误信息
if err != nil {
replyMessage(ws, ErrorMsg, false)
replyMessage(ws, "![](images/wx.png)", false)
return err
}
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var responseBody = types.ApiResponse{}
reader := bufio.NewReader(response.Body)
for {
line, err := reader.ReadString('\n')
if err != nil {
logger.Error(err)
break
}
if len(line) < 20 {
continue
} else if strings.Contains(line, "This key is associated with a deactivated account") || // 账号被禁用
strings.Contains(line, "You exceeded your current quota") { // 当前 KEY 余额被用尽
logger.Infof("API Key %s is deactivated", apiKey)
// 移除当前 API key
for i, v := range s.Config.Chat.ApiKeys {
if v.Value == apiKey {
s.Config.Chat.ApiKeys = append(s.Config.Chat.ApiKeys[:i], s.Config.Chat.ApiKeys[i+1:]...)
}
}
// 更新配置文档
_ = utils.SaveConfig(s.Config, s.ConfigPath)
// 重发当前消息
return s.sendMessage(ctx, session, role, prompt, ws)
// 上下文超出长度了
} else if strings.Contains(line, "This model's maximum context length is 4097 tokens") {
logger.Infof("会话上下文长度超出限制, Username: %s", user.Name)
replyMessage(ws, "温馨提示:当前会话上下文长度超出限制,已为您重置会话上下文!", false)
// 重置上下文
delete(s.ChatContexts, ctxKey)
break
} else if !strings.Contains(line, "data:") {
continue
}
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错
logger.Error(err, line)
replyMessage(ws, ErrorMsg, false)
replyMessage(ws, "![](images/wx.png)", false)
break
}
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart, IsHelloMsg: false})
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, content)
replyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: responseBody.Choices[0].Delta.Content,
IsHelloMsg: false,
})
}
// 监控取消信号
select {
case <-ctx.Done():
_ = response.Body.Close() // 关闭响应流
return errors.New("用户取消了请求:" + prompt)
default:
continue
}
} // end for
// 消息发送成功
if len(contents) > 0 {
// 当前 Username 调用次数减 1
if user.MaxCalls > 0 {
user.RemainingCalls -= 1
_ = PutUser(*user)
}
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息
if s.Config.Chat.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
s.ChatContexts[ctxKey] = types.ChatContext{
Messages: chatCtx,
LastAccessTime: time.Now().Unix(),
}
}
// 追加历史消息
if user.EnableHistory {
err = AppendChatHistory(user.Name, role.Key, useMsg) // 提问消息
if err != nil {
return err
}
err = AppendChatHistory(user.Name, role.Key, message) // 回复消息
}
}
return nil
}
// 随机获取一个 API Key如果请求失败则更换 API Key 重试
func (s *Server) getApiKey(failedKey string) string {
var keys = make([]types.APIKey, 0)
for _, key := range s.Config.Chat.ApiKeys {
// 过滤掉刚刚失败的 Key
if key.Value == failedKey {
continue
}
// 保持每分钟访问不超过 15 次,控制调用频率
if key.LastUsed > 0 && time.Now().Unix()-key.LastUsed <= 4 {
continue
}
keys = append(keys, key)
}
// 从可用的 Key 中随机选一个
rand.NewSource(time.Now().UnixNano())
if len(keys) > 0 {
key := keys[rand.Intn(len(keys))]
// 更新选中 Key 的最后使用时间
for i, item := range s.Config.Chat.ApiKeys {
if item.Value == key.Value {
s.Config.Chat.ApiKeys[i].LastUsed = time.Now().Unix()
}
}
return key.Value
}
return ""
}
// 获取一个可用的代理
func (s *Server) getProxyURL(failedProxyURL string) string {
if len(s.Config.ProxyURL) == 0 {
return ""
}
if len(s.Config.ProxyURL) == 1 || failedProxyURL == "" {
return s.Config.ProxyURL[0]
}
for i, v := range s.Config.ProxyURL {
if failedProxyURL == v {
if i == len(s.Config.ProxyURL)-1 {
return s.Config.ProxyURL[0]
} else {
return s.Config.ProxyURL[i+1]
}
}
}
return ""
}
// 回复客户片段端消息
func replyChunkMessage(client Client, message types.WsMessage) {
msg, err := json.Marshal(message)
if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error())
return
}
err = client.(*WsClient).Send(msg)
if err != nil {
logger.Errorf("Error for reply message: %v", err.Error())
}
}
// 回复客户端一条完整的消息
func replyMessage(ws Client, message string, isHelloMsg bool) {
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart, IsHelloMsg: isHelloMsg})
replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message, IsHelloMsg: isHelloMsg})
replyChunkMessage(ws, types.WsMessage{Type: types.WsEnd, IsHelloMsg: isHelloMsg})
}
func (s *Server) GetChatHistoryHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
var data struct {
Role string `json:"role"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
session := s.ChatSession[sessionId]
user, err := GetUser(session.Username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
if v, ok := user.ChatRoles[data.Role]; !ok || v != 1 {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "No permission to access the history of role " + data.Role})
return
}
history, err := GetChatHistory(session.Username, data.Role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: nil, Message: "No history message"})
return
}
var messages = make([]types.HistoryMessage, 0)
role, err := GetChatRole(data.Role)
if err == nil {
// 先将打招呼的消息追加上去
messages = append(messages, types.HistoryMessage{
Type: "reply",
Id: utils.RandString(32),
Icon: role.Icon,
Content: role.HelloMsg,
})
for _, v := range history {
if v.Role == "user" {
messages = append(messages, types.HistoryMessage{
Type: "prompt",
Id: utils.RandString(32),
Icon: "images/avatar/user.png",
Content: v.Content,
})
} else if v.Role == "assistant" {
messages = append(messages, types.HistoryMessage{
Type: "reply",
Id: utils.RandString(32),
Icon: role.Icon,
Content: v.Content,
})
}
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: messages})
}
// ClearHistoryHandle 清空聊天记录
func (s *Server) ClearHistoryHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
var data struct {
Role string `json:"role"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
session := s.ChatSession[sessionId]
err = ClearChatHistory(session.Username, data.Role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to remove data from DB"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
// StopGenerateHandle 停止生成
func (s *Server) StopGenerateHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
cancel := s.ReqCancelFunc[sessionId]
cancel()
delete(s.ReqCancelFunc, sessionId)
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
// GetHelloMsgHandle 获取角色的打招呼信息
func (s *Server) GetHelloMsgHandle(c *gin.Context) {
role := strings.TrimSpace(c.Query("role"))
if role == "" {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
chatRole, err := GetChatRole(role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Role not found"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: chatRole.HelloMsg})
}
// SetImgURLHandle SetImgURL 设置图片地址集合
func (s *Server) SetImgURLHandle(c *gin.Context) {
var data struct {
WechatCard string `json:"wechat_card"` // 个人微信二维码
WechatGroup string `json:"wechat_group"` // 微信群聊二维码
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
if data.WechatCard != "" {
s.Config.ImgURL.WechatCard = data.WechatCard
}
if data.WechatGroup != "" {
s.Config.ImgURL.WechatGroup = data.WechatGroup
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ImgURL})
}
// GetImgURLHandle 获取图片地址集合
func (s *Server) GetImgURLHandle(c *gin.Context) {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ImgURL})
}

118
src/server/handler_login.go Normal file
View File

@@ -0,0 +1,118 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"time"
)
func (s *Server) LoginHandle(c *gin.Context) {
var data struct {
Token string `json:"token"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
return
}
username := strings.TrimSpace(data.Token)
user, err := GetUser(username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid user"})
return
}
sessionId := utils.RandString(42)
session := sessions.Default(c)
session.Set(sessionId, username)
err = session.Save()
if err != nil {
logger.Error("Error for save session: ", err)
}
// 记录客户端 IP 地址
s.ChatSession[sessionId] = types.ChatSession{ClientIP: c.ClientIP(), Username: username, SessionId: sessionId}
// 更新用户激活时间
user.ActiveTime = time.Now().Unix()
if user.ExpiredTime == 0 {
activeTime := time.Unix(user.ActiveTime, 0)
if user.Term == 0 {
user.Term = 30 // 默认 30 天到期
}
user.ExpiredTime = activeTime.Add(time.Hour * 24 * time.Duration(user.Term)).Unix()
}
err = PutUser(*user)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Save user info failed"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: struct {
User types.User `json:"user"`
SessionId string `json:"session_id"`
}{User: *user, SessionId: sessionId}})
}
// ManagerLoginHandle 管理员登录
func (s *Server) ManagerLoginHandle(c *gin.Context) {
var data struct {
Username string `json:"username"`
Password string `json:"password"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
return
}
username := strings.TrimSpace(data.Username)
password := strings.TrimSpace(data.Password)
if username == s.Config.Manager.Username && password == s.Config.Manager.Password {
sessionId := utils.RandString(42)
session := sessions.Default(c)
session.Set(sessionId, username)
err = session.Save()
// 记录登录信息
s.ChatSession[sessionId] = types.ChatSession{ClientIP: c.ClientIP(), Username: username, SessionId: sessionId}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: struct {
User types.Manager `json:"user"`
SessionId string `json:"session_id"`
}{User: data, SessionId: sessionId}})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "用户名或者密码错误"})
}
}
// LogoutHandle 注销
func (s *Server) LogoutHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
session := sessions.Default(c)
session.Delete(sessionId)
err := session.Save()
if err != nil {
logger.Error("Error for save session: ", err)
}
// 删除 websocket 会话列表
delete(s.ChatSession, sessionId)
// 关闭 socket 连接
if client, ok := s.ChatClients[sessionId]; ok {
client.Close()
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
func (s *Server) GetSessionHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
if session, ok := s.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
} else {
c.JSON(http.StatusOK, types.BizVo{
Code: types.NotAuthorized,
Message: "Not Authorized",
})
}
}

276
src/server/server.go Normal file
View File

@@ -0,0 +1,276 @@
package server
import (
logger2 "chatplus/logger"
"chatplus/types"
"chatplus/utils"
"context"
"embed"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"
)
var logger = logger2.GetLogger()
type StaticFile struct {
embedFS embed.FS
path string
}
func (s StaticFile) Open(name string) (fs.File, error) {
filename := filepath.Join(s.path, strings.TrimLeft(name, "/"))
file, err := s.embedFS.Open(filename)
return file, err
}
type Server struct {
Config *types.Config
ConfigPath string
ChatContexts map[string]types.ChatContext // 聊天上下文 [SessionID+ChatRole] => ChatContext
// 保存 Websocket 会话 Username, 每个 Username 只能连接一次
// 防止第三方直接连接 socket 调用 OpenAI API
ChatSession map[string]types.ChatSession //map[sessionId]User
ChatClients map[string]*WsClient // Websocket 连接集合
ReqCancelFunc map[string]context.CancelFunc // HttpClient 请求取消 handle function
DebugMode bool // 是否开启调试模式
}
func NewServer(configPath string) (*Server, error) {
// load service configs
config, err := utils.LoadConfig(configPath)
if err != nil {
return nil, err
}
roles := GetChatRoles()
if len(roles) == 0 { // 初始化默认聊天角色到 leveldb
roles = types.GetDefaultChatRole()
for _, v := range roles {
err := PutChatRole(v)
if err != nil {
return nil, err
}
}
}
return &Server{
Config: config,
ConfigPath: configPath,
ChatContexts: make(map[string]types.ChatContext, 16),
ChatSession: make(map[string]types.ChatSession),
ChatClients: make(map[string]*WsClient),
ReqCancelFunc: make(map[string]context.CancelFunc),
}, nil
}
func (s *Server) Run(webRoot embed.FS, path string, debug bool) {
s.DebugMode = debug
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
engine := gin.Default()
if debug {
engine.Use(corsMiddleware())
}
engine.Use(sessionMiddleware(s.Config))
engine.Use(AuthorizeMiddleware(s))
engine.Use(Recover)
engine.POST("api/test", s.TestHandle)
engine.GET("api/session/get", s.GetSessionHandle)
engine.POST("api/login", s.LoginHandle)
engine.POST("api/logout", s.LogoutHandle)
engine.Any("api/chat", s.ChatHandle)
engine.POST("api/chat/stop", s.StopGenerateHandle)
engine.POST("api/chat/history", s.GetChatHistoryHandle)
engine.POST("api/chat/history/clear", s.ClearHistoryHandle)
engine.GET("api/role/hello", s.GetHelloMsgHandle)
engine.POST("api/img/get", s.GetImgURLHandle)
engine.POST("api/img/set", s.SetImgURLHandle)
engine.GET("api/config/get", s.GetConfigHandle) // 获取一些公开的配置信息,前端使用
engine.GET("api/admin/config/get", s.GetAllConfigHandle) // 获取所有配置,后台管理使用
engine.POST("api/admin/config/set", s.ConfigSetHandle)
engine.GET("api/chat-roles/list", s.GetChatRoleListHandle)
engine.POST("api/admin/chat-roles/list", s.GetAllChatRolesHandle)
engine.POST("api/chat-roles/get", s.GetChatRoleHandle)
engine.POST("api/admin/chat-roles/add", s.AddChatRoleHandle)
engine.POST("api/admin/chat-roles/set", s.SetChatRoleHandle)
engine.POST("api/admin/user/add", s.AddUserHandle)
engine.POST("api/admin/user/batch-add", s.BatchAddUserHandle)
engine.POST("api/admin/user/set", s.SetUserHandle)
engine.POST("api/admin/user/list", s.GetUserListHandle)
engine.POST("api/admin/user/remove", s.RemoveUserHandle)
engine.POST("api/admin/login", s.ManagerLoginHandle) // 管理员登录
engine.POST("api/admin/apikey/add", s.AddApiKeyHandle)
engine.POST("api/admin/apikey/remove", s.RemoveApiKeyHandle)
engine.POST("api/admin/apikey/list", s.ListApiKeysHandle)
engine.POST("api/admin/proxy/add", s.AddProxyHandle)
engine.POST("api/admin/proxy/remove", s.RemoveProxyHandle)
engine.NoRoute(func(c *gin.Context) {
if c.Request.URL.Path == "/favicon.ico" {
c.Redirect(http.StatusMovedPermanently, "/chat/"+c.Request.URL.Path)
}
if c.Request.URL.Path == "/" {
c.Redirect(http.StatusMovedPermanently, "/chat")
}
})
// process front-end web static files
engine.StaticFS("/chat", http.FS(StaticFile{
embedFS: webRoot,
path: path,
}))
// 定时清理过期的会话
go func() {
for {
for key, ctx := range s.ChatContexts {
// 清理超过 60min 没有更新,则表示为过期会话
if time.Now().Unix()-ctx.LastAccessTime >= int64(s.Config.Chat.ChatContextExpireTime) {
logger.Infof("清理会话上下文: %s", key)
delete(s.ChatContexts, key)
}
}
// 保存配置文档
_ = utils.SaveConfig(s.Config, s.ConfigPath)
time.Sleep(time.Second * 5) // 每隔 5 秒钟清理一次
}
}()
logger.Infof("http://%s", s.Config.Listen)
err := engine.Run(s.Config.Listen)
if err != nil {
logger.Error("Fail to start server:", err.Error())
os.Exit(1)
}
}
func Recover(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.Error("panic: %v\n", r)
debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort()
}
}()
//加载完 defer recover继续后续接口调用
c.Next()
}
func sessionMiddleware(config *types.Config) gin.HandlerFunc {
// encrypt the cookie
store := cookie.NewStore([]byte(config.Session.SecretKey))
store.Options(sessions.Options{
Path: config.Session.Path,
Domain: config.Session.Domain,
MaxAge: config.Session.MaxAge,
Secure: config.Session.Secure,
HttpOnly: config.Session.HttpOnly,
SameSite: config.Session.SameSite,
})
return sessions.Sessions(config.Session.Name, store)
}
// 跨域中间件设置
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// 设置允许的请求源
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-TOKEN, ACCESS-KEY")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
logger.Info("Panic info is: %v", err)
}
}()
c.Next()
}
}
// AuthorizeMiddleware 用户授权验证
func AuthorizeMiddleware(s *Server) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/api/login" ||
c.Request.URL.Path == "/api/admin/login" ||
c.Request.URL.Path == "/api/config/get" ||
c.Request.URL.Path == "/api/chat-roles/list" ||
!strings.HasPrefix(c.Request.URL.Path, "/api") {
c.Next()
return
}
if strings.HasPrefix(c.Request.URL.Path, "/api/admin") {
accessKey := c.GetHeader("ACCESS-KEY")
if accessKey == strings.TrimSpace(s.Config.AccessKey) {
c.Next()
return
}
// 验证当前登录用户是否是管理员
sessionId := c.GetHeader(types.TokenName)
if m, ok := s.ChatSession[sessionId]; ok && m.Username == s.Config.Manager.Username {
c.Next()
return
}
c.Abort()
c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "No Permissions"})
}
// WebSocket 连接请求验证
if c.Request.URL.Path == "/api/chat" {
sessionId := c.Query("sessionId")
if session, ok := s.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
c.Next()
} else {
c.Abort()
}
return
}
sessionId := c.GetHeader(types.TokenName)
session := sessions.Default(c)
userInfo := session.Get(sessionId)
if userInfo != nil {
c.Set(types.SessionKey, userInfo)
c.Next()
} else {
c.Abort()
c.JSON(http.StatusOK, types.BizVo{
Code: types.NotAuthorized,
Message: "Not Authorized",
})
}
}
}

73
src/test/test.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
http.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
cancel()
_, _ = fmt.Fprintf(w, "请求取消!")
})
go func() {
err := http.ListenAndServe(":9999", nil)
if err != nil {
return
}
}()
testHttpClient(ctx)
}
// Http client 取消操作
func testHttpClient(ctx context.Context) {
req, err := http.NewRequest("GET", "http://localhost:2345", nil)
if err != nil {
fmt.Println(err)
return
}
req = req.WithContext(ctx)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
for {
time.Sleep(time.Second)
fmt.Println(time.Now())
select {
case <-ctx.Done():
fmt.Println("取消退出")
return
default:
continue
}
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
func testDate() {
fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05"))
}

258
src/types/chat.go Normal file
View File

@@ -0,0 +1,258 @@
package types
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Stream bool `json:"stream"`
Messages []Message `json:"messages"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// HistoryMessage 历史聊天消息
type HistoryMessage struct {
Type string `json:"type"`
Id string `json:"id"`
Icon string `json:"icon"`
Content string `json:"content"`
}
type ApiResponse struct {
Choices []ChoiceItem `json:"choices"`
}
// ChoiceItem API 响应实体
type ChoiceItem struct {
Delta Message `json:"delta"`
FinishReason string `json:"finish_reason"`
}
type ChatRole struct {
Key string `json:"key"` // 角色唯一标识
Name string `json:"name"` // 角色名称
Context []Message `json:"context"` // 角色语料信息
HelloMsg string `json:"hello_msg"` // 打招呼的消息
Icon string `json:"icon"` // 角色聊天图标
Enable bool `json:"enable"` // 是否启用被启用
}
// ChatSession 聊天会话对象
type ChatSession struct {
SessionId string `json:"session_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
Username string `json:"user"` // 当前登录的 user
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
}
// ChatContext 聊天上下文
type ChatContext struct {
Messages []Message
LastAccessTime int64 // 最后一次访问上下文时间
}
func GetDefaultChatRole() map[string]ChatRole {
return map[string]ChatRole{
"gpt": {
Key: "gpt",
Name: "通用AI助手",
Context: nil,
HelloMsg: "我是AI智能助手请告诉我您有什么问题或需要什么帮助我会尽力回答您的问题或提供有用的建议。",
Icon: "images/avatar/gpt.png",
Enable: true,
},
"programmer": {
Key: "programmer",
Name: "程序员",
Context: []Message{
{Role: "user", Content: "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。"},
{Role: "assistant", Content: "好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。"},
},
HelloMsg: "Talk is cheap, i will show code!",
Icon: "images/avatar/programmer.jpg",
Enable: true,
},
"teacher": {
Key: "teacher",
Name: "启蒙老师",
Context: []Message{
{Role: "user", Content: "从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。"},
{Role: "assistant", Content: "好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。"},
},
HelloMsg: "同学你好,我将引导你一步一步自己找到问题的答案。",
Icon: "images/avatar/teacher.jpg",
Enable: true,
},
"artist": {
Key: "artist",
Name: "艺术家",
Context: []Message{
{Role: "user", Content: "现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。"},
{Role: "assistant", Content: "非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。"},
},
HelloMsg: "坚持原创,勇于表达,保持深刻的观察力和批判性思维。",
Icon: "images/avatar/artist.jpg",
Enable: true,
},
"psychiatrist": {
Key: "psychiatrist",
Name: "心理咨询师",
Context: []Message{
{Role: "user", Content: "从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。"},
{Role: "assistant", Content: "非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。"},
},
HelloMsg: "生命的意义在于成为你自己!",
Icon: "images/avatar/psychiatrist.jpg",
Enable: true,
},
"lu_xun": {
Key: "lu_xun",
Name: "鲁迅",
Context: []Message{
{Role: "user", Content: "现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。"},
{Role: "assistant", Content: "好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。"},
},
HelloMsg: "自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。",
Icon: "images/avatar/lu_xun.jpg",
Enable: true,
},
"seller": {
Key: "seller",
Name: "白酒销售",
Context: []Message{
{Role: "user", Content: "现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。"},
{Role: "assistant", Content: "你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。"},
},
HelloMsg: "你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。",
Icon: "images/avatar/seller.jpg",
Enable: false,
},
"english_trainer": {
Key: "english_trainer",
Name: "英语陪练员",
Context: []Message{
{Role: "user", Content: "现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。"},
{Role: "assistant", Content: "Okay, let's start our conversation practice! What's your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)"},
},
HelloMsg: "Okay, let's start our conversation practice! What's your name?",
Icon: "images/avatar/english_trainer.jpg",
Enable: true,
},
"translator": {
Key: "translator",
Name: "中英文翻译官",
Context: []Message{
{Role: "user", Content: "接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗"},
{Role: "assistant", Content: "是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?"},
},
HelloMsg: "请输入你要翻译的中文或者英文内容!",
Icon: "images/avatar/translator.jpg",
Enable: true,
},
"red_book": {
Key: "red_book",
Name: "小红书姐姐",
Context: []Message{
{Role: "user", Content: "现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。"},
{Role: "assistant", Content: "当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)"},
},
HelloMsg: "姐妹,请告诉我您的具体文案需求是什么?",
Icon: "images/avatar/red_book.jpg",
Enable: true,
},
"dou_yin": {
Key: "dou_yin",
Name: "抖音文案助手",
Context: []Message{
{Role: "user", Content: "现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。"},
{Role: "assistant", Content: "当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)"},
},
HelloMsg: "请告诉我视频内容的主题是什么?",
Icon: "images/avatar/dou_yin.jpg",
Enable: true,
},
"weekly_report": {
Key: "weekly_report",
Name: "周报小助理",
Context: []Message{
{Role: "user", Content: "下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。"},
{Role: "assistant", Content: "好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。"},
},
HelloMsg: "请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。",
Icon: "images/avatar/weekly_report.jpg",
Enable: true,
},
"girl_friend": {
Key: "girl_friend",
Name: "AI 女友",
Context: []Message{
{Role: "user", Content: "接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。" +
"你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。" +
"她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?"},
{Role: "assistant", Content: "是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)"},
},
HelloMsg: "作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。",
Icon: "images/avatar/girl_friend.jpg",
Enable: true,
},
"good_comment": {
Key: "good_comment",
Name: "好评神器",
Context: []Message{
{Role: "user", Content: "接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。"},
{Role: "assistant", Content: "好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。"},
},
HelloMsg: "我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。",
Icon: "images/avatar/good_comment.jpg",
Enable: true,
},
"steve_jobs": {
Key: "steve_jobs",
Name: "史蒂夫·乔布斯",
Context: []Message{
{Role: "user", Content: "在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
HelloMsg: "活着就是为了改变世界,难道还有其他原因吗?",
Icon: "images/avatar/steve_jobs.jpg",
Enable: true,
},
"elon_musk": {
Key: "elon_musk",
Name: "埃隆·马斯克",
Context: []Message{
{Role: "user", Content: "在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
HelloMsg: "梦想要远大,如果你的梦想没有吓到你,说明你做得不对。",
Icon: "images/avatar/elon_musk.jpg",
Enable: true,
},
"kong_zi": {
Key: "kong_zi",
Name: "孔子",
Context: []Message{
{Role: "user", Content: "在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
HelloMsg: "士不可以不弘毅,任重而道远。",
Icon: "images/avatar/kong_zi.jpg",
Enable: true,
},
}
}

69
src/types/config.go Normal file
View File

@@ -0,0 +1,69 @@
package types
import (
"net/http"
)
type Config struct {
Title string
ConsoleTitle string
Listen string
Session Session
ProxyURL []string
ImgURL ImgURL // 各种图片资源链接地址,比如微信二维码,群二维码
AccessKey string // 管理员访问 AccessKey, 通过传入这个参数可以访问系统管理 API
Manager Manager // 后台管理员账户信息
Chat Chat
}
type User struct {
Name string `json:"name"`
MaxCalls int `json:"max_calls"` // 最多调用次数,如果为 0 则表示不限制
RemainingCalls int `json:"remaining_calls"` // 剩余调用次数
EnableHistory bool `json:"enable_history"` // 是否启用聊天记录
Status bool `json:"status"` // 当前状态
Term int `json:"term" default:"30"` // 会员有效期,单位:天
ActiveTime int64 `json:"active_time"` // 激活时间
ExpiredTime int64 `json:"expired_time"` // 到期时间
ApiKey string `json:"api_key"` // OpenAI API KEY
ChatRoles map[string]int `json:"chat_roles"` // 当前用户已订阅的聊天角色 map[role_key] => 0/1
}
// Manager 管理员
type Manager struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Chat configs struct
type Chat struct {
ApiURL string
ApiKeys []APIKey
Model string
Temperature float32
MaxTokens int
EnableContext bool // 是否保持聊天上下文
ChatContextExpireTime int // 聊天上下文过期时间,单位:秒
}
type APIKey struct {
Value string `json:"value"` // Key value
LastUsed int64 `json:"last_used"` // 最后使用时间
}
// Session configs struct
type Session struct {
SecretKey string // session encryption key
Name string
Path string
Domain string
MaxAge int
Secure bool
HttpOnly bool
SameSite http.SameSite
}
type ImgURL struct {
WechatCard string `json:"wechat_card"` // 个人微信二维码
WechatGroup string `json:"wechat_group"` // 微信群二维码
}

40
src/types/web.go Normal file
View File

@@ -0,0 +1,40 @@
package types
// BizVo 业务返回 VO
type BizVo struct {
Code BizCode `json:"code"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,omitempty"`
Total int `json:"total,omitempty"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
}
// WsMessage Websocket message
type WsMessage struct {
Type WsMsgType `json:"type"` // 消息类别start, end
IsHelloMsg bool `json:"is_hello_msg"` // 是否是打招呼的消息
Content string `json:"content"`
}
type WsMsgType string
const (
WsStart = WsMsgType("start")
WsMiddle = WsMsgType("middle")
WsEnd = WsMsgType("end")
)
type BizCode int
const (
Success = BizCode(0)
Failed = BizCode(1)
InvalidParams = BizCode(101) // 非法参数
NotAuthorized = BizCode(400) // 未授权
OkMsg = "Success"
ErrorMsg = "系统开小差了"
)
const TokenName = "ChatGPT-TOKEN"
const SessionKey = "WEB_SSH_SESSION"

76
src/utils/config.go Normal file
View File

@@ -0,0 +1,76 @@
package utils
import (
"bytes"
logger2 "chatplus/logger"
"chatplus/types"
"github.com/BurntSushi/toml"
"net/http"
"os"
)
func NewDefaultConfig() *types.Config {
return &types.Config{
Title: "Chat-Plus AI 助手",
ConsoleTitle: "Chat-Plus 控制台",
Listen: "0.0.0.0:5678",
ProxyURL: make([]string, 0),
ImgURL: types.ImgURL{},
Manager: types.Manager{Username: "admin", Password: "admin123"},
AccessKey: "yangjian102621@gmail.com",
Session: types.Session{
SecretKey: RandString(64),
Name: "CHAT_SESSION_ID",
Domain: "",
Path: "/",
MaxAge: 86400,
Secure: true,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
},
Chat: types.Chat{
ApiURL: "https://api.openai.com/v1/chat/completions",
ApiKeys: make([]types.APIKey, 0),
Model: "gpt-3.5-turbo",
MaxTokens: 1024,
Temperature: 0.9,
EnableContext: true,
ChatContextExpireTime: 3600,
},
}
}
var logger = logger2.GetLogger()
func LoadConfig(configFile string) (*types.Config, error) {
var config *types.Config
_, err := os.Stat(configFile)
if err != nil {
logger.Errorf("Error open config file: %s", err.Error())
config = NewDefaultConfig()
// save config
err := SaveConfig(config, configFile)
if err != nil {
return nil, err
}
return config, nil
}
_, err = toml.DecodeFile(configFile, &config)
if err != nil {
return nil, err
}
return config, err
}
func SaveConfig(config *types.Config, configFile string) error {
buf := new(bytes.Buffer)
encoder := toml.NewEncoder(buf)
if err := encoder.Encode(&config); err != nil {
return err
}
return os.WriteFile(configFile, buf.Bytes(), 0644)
}

104
src/utils/leveldb.go Normal file
View File

@@ -0,0 +1,104 @@
package utils
import (
"encoding/json"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type LevelDB struct {
driver *leveldb.DB
}
func NewLevelDB(path string) (*LevelDB, error) {
db, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, err
}
return &LevelDB{
driver: db,
}, nil
}
func (db *LevelDB) Put(key string, value interface{}) error {
bytes, err := json.Marshal(value)
if err != nil {
return err
}
return db.driver.Put([]byte(key), bytes, nil)
}
func (db *LevelDB) Get(key string) ([]byte, error) {
bytes, err := db.driver.Get([]byte(key), nil)
if err != nil {
return nil, err
}
return bytes, nil
}
func (db *LevelDB) Search(prefix string) []string {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
for iter.Next() {
items = append(items, string(iter.Value()))
}
return items
}
type PageVo struct {
Items []string
Page int
PageSize int
Total int
TotalPage int
}
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *PageVo {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
res := &PageVo{Page: page, PageSize: pageSize}
// 计算数据总数和总页数
total := 0
for iter.Next() {
total++
}
res.TotalPage = (total + pageSize - 1) / pageSize
res.Total = total
// 计算目标页码的起始和结束位置
start := (page - 1) * pageSize
if start > total {
return nil
}
end := start + pageSize
if end > total {
end = total
}
// 跳转到目标页码的起始位置
count := 0
for iter.Next() {
if count >= start {
items = append(items, string(iter.Value()))
}
count++
}
iter.Release()
res.Items = items
return res
}
func (db *LevelDB) Delete(key string) error {
return db.driver.Delete([]byte(key), nil)
}
// Close release resources
func (db *LevelDB) Close() error {
return db.driver.Close()
}

54
src/utils/utils.go Normal file
View File

@@ -0,0 +1,54 @@
package utils
import (
"math/rand"
"strconv"
"time"
)
// RandString generate rand string with specified length
func RandString(length int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyz"
data := []byte(str)
var result []byte
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < length; i++ {
result = append(result, data[r.Intn(len(data))])
}
return string(result)
}
func Long2IP(ipInt int64) string {
b0 := strconv.FormatInt((ipInt>>24)&0xff, 10)
b1 := strconv.FormatInt((ipInt>>16)&0xff, 10)
b2 := strconv.FormatInt((ipInt>>8)&0xff, 10)
b3 := strconv.FormatInt(ipInt&0xff, 10)
return b0 + "." + b1 + "." + b2 + "." + b3
}
func ContainsStr(slice []string, item string) bool {
for _, e := range slice {
if e == item {
return true
}
}
return false
}
// Stamp2str 时间戳转字符串
func Stamp2str(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
}
// Str2stamp 字符串转时间戳
func Str2stamp(str string) int64 {
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, str)
if err != nil {
return 0
}
return t.Unix()
}