mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-13 20:53:47 +08:00
refactor: 重构项目目录结构
This commit is contained in:
20
src/Makefile
Normal file
20
src/Makefile
Normal 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
31
src/config.sample.toml
Normal 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
14
src/fresh.conf
Normal 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
36
src/go.mod
Normal 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
137
src/go.sum
Normal 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
26
src/logger/logger.go
Normal 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
71
src/main.go
Normal 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
61
src/server/client.go
Normal 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
138
src/server/db.go
Normal 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)
|
||||
}
|
||||
75
src/server/handler_admin_apikey.go
Normal file
75
src/server/handler_admin_apikey.go
Normal 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})
|
||||
}
|
||||
163
src/server/handler_admin_config.go
Normal file
163
src/server/handler_admin_config.go
Normal 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})
|
||||
}
|
||||
157
src/server/handler_admin_role.go
Normal file
157
src/server/handler_admin_role.go
Normal 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})
|
||||
}
|
||||
272
src/server/handler_admin_user.go
Normal file
272
src/server/handler_admin_user.go
Normal 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
530
src/server/handler_chat.go
Normal 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, "", 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, "", 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, "", false)
|
||||
return errors.New("当前 TOKEN " + user.Name + "已过期")
|
||||
}
|
||||
|
||||
if user.MaxCalls > 0 && user.RemainingCalls <= 0 {
|
||||
replyMessage(ws, "当前 TOKEN 点数已经用尽,加入我们的知识星球可以免费领取点卡!", false)
|
||||
replyMessage(ws, "", 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, "", 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, "", 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
118
src/server/handler_login.go
Normal 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
276
src/server/server.go
Normal 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
73
src/test/test.go
Normal 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
258
src/types/chat.go
Normal 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
69
src/types/config.go
Normal 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
40
src/types/web.go
Normal 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
76
src/utils/config.go
Normal 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
104
src/utils/leveldb.go
Normal 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
54
src/utils/utils.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user