This commit is contained in:
Sakurasan
2023-03-30 02:27:54 +08:00
commit 6bf21b4bd7
13 changed files with 891 additions and 0 deletions

60
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Docker Image CI
#触发器设置
on:
push:
branches:
- main
- dev
tags:
- '*'
pull_request:
branches: [ "main","dev" ]
#项目任务,任务之间可以并行调度
jobs:
build:
#选择云端运行的环境
runs-on: ubuntu-latest
steps:
#uses代表使用一个模块此处使用的是checkout模块将github项目文件导入到当前环境中
- uses: actions/checkout@v3
#使用with跟在后面来为前面的模块输入参数
with:
submodules: 'true'
- name: Get current date
id: date
run: echo "::set-output name=today::$(date +'%Y%m%d')"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
#这里用到了github的secrets功能避免账户和密码随仓库泄露
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
#导入这个模块来完成自动编译和推送
uses: docker/build-push-action@v3
with:
context: .
file: ./docker/Dockerfile
push: true
#在这里通过加入需要编译的平台和前面配好的QEMUbuildx来达到多平台编译
platforms: linux/amd64,linux/arm64
#指定用户/仓库名
tags: |
${{ github.repository }}:${{ github.ref_slug }}
${{ github.repository }}:latest
${{ github.repository }}:${{ steps.date.outputs.today }}
- name: Docker Hub Description
#这里是通过md文件自动生成dockerhub描述的模块也可以不需要
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ github.repository }}
readme-filepath: ./README.md

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/
*.log

10
README.md Normal file
View File

@@ -0,0 +1,10 @@
# opencatd-open
OpenCat for Team的开源实现
基本实现了opencatd的全部功能
# License
See the [LICENSE](License) file for the full license text.

20
docker/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:1.19.7-alpine as builder
LABEL anther="github.com/Sakurasan"
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add openssl make cmake upx
WORKDIR /build
COPY . /build
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
CMD [ "go mod download" ]
RUN make build
FROM alpine:latest AS runner
# 设置alpine 时间为上海时间
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apk del tzdata && export GIN_MODE=release
# RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils
WORKDIR /app
COPY --from=builder /build/bin/opencatd /app/opencatd
EXPOSE 80
ENTRYPOINT ["/app/opencatd"]

41
go.mod Normal file
View File

@@ -0,0 +1,41 @@
module opencatd-open
go 1.19
require (
github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d
github.com/gin-gonic/gin v1.9.0
github.com/google/uuid v1.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.6
)
require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

99
go.sum Normal file
View File

@@ -0,0 +1,99 @@
github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d h1:3v1QFdgk450QH+7C+lw1k+olbjK4fKGsrEfnEG/HLkY=
github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d/go.mod h1:2sp0vsMyh5sqmKl5N+ps/cSspqLkoXUlesSzsufIGRU=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

61
makefile Normal file
View File

@@ -0,0 +1,61 @@
GOPATH:=$(shell go env GOPATH)
VERSION=$(shell git describe --tags --always)
# 获取源码最近一次 git commit log包含 commit sha 值,以及 commit message
GitCommitLog=$(shell git log)
# 检查源码在最近一次 git commit 基础上,是否有本地修改,且未提交的文件
GitStatus=$(shell git status -s)
# 获取当前时间
BuildTime=$(shell date +'%Y.%m.%d %H:%M:%S')
# 获取Go的版本
BuildGoVersion=$(shell go version)
LDFlags=" \
-X 'main.Version=$(VERSION)' \
-X 'main.GitCommitLog=$(GitCommitLog)' \
-X 'main.BuildTime=$(BuildTime)' \
-X 'main.BuildGoVersion=$(BuildGoVersion)'"
.PHONY: build
# build
build:
# mkdir -p bin/ && go build -ldflags $(LDFlags) -o ./bin/ ./...
rm -rf qq.tgz /bin/qq
mkdir -p bin/ && go build -ldflags "-s -w" -o ./bin/opencatd .
upx -9 bin/opencatd
.PHONY:docker
docker:
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --use --name xbuilder
docker buildx inspect xbuilder --bootstrap
docker buildx build --platform linux/amd64,linux/arm64 -t mirrors2/opencatd:latest . --push
.PHONY: clean
# clean
clean:
rm -rf bin/
.PHONY: all
# generate all
all:
make build;
# show help
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\_0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := help

47
opencat.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"opencatd-open/router"
_ "opencatd-open/store"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
group := r.Group("/1")
{
group.Use(router.AuthMiddleware())
// 获取当前用户信息
group.GET("/me", router.HandleMe)
// 获取所有Key
group.GET("/keys", router.HandleKeys)
// 获取所有用户信息
group.GET("/users", router.HandleUsers)
// 添加Key
group.POST("/keys", router.HandleAddKey)
// 删除Key
group.DELETE("/keys/:id", router.HandleDelKey)
// 添加用户
group.POST("/users", router.HandleAddUser)
// 删除用户
group.DELETE("/users/:id", router.HandleDelUser)
// 重置用户Token
group.POST("/users/:id/reset", router.HandleResetUserToken)
}
// 初始化用户
r.POST("/1/users/init", router.Handleinit)
r.POST("/v1/chat/completions", router.HandleProy)
r.Run(":80")
}

317
router/router.go Normal file
View File

@@ -0,0 +1,317 @@
package router
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"opencatd-open/store"
"time"
"github.com/Sakurasan/to"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
)
var (
rootToken string
baseUrl = "https://api.openai.com"
)
type User struct {
IsDelete bool `json:"IsDelete,omitempty"`
ID int `json:"id,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
}
type Key struct {
ID int `json:"id,omitempty"`
Key string `json:"key,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if rootToken == "" {
u, err := store.GetUserByID(uint(1))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
rootToken = u.Token
}
token := c.GetHeader("Authorization")
if token == "" || token[:7] != "Bearer " || token[7:] != rootToken {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// 可以在这里对 token 进行验证并检查权限
c.Next()
}
}
func Handleinit(c *gin.Context) {
user, err := store.GetUserByID(1)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
u := store.User{Name: "root", Token: uuid.NewString()}
u.ID = 1
if err := store.CreateUser(&u); err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
return
} else {
rootToken = u.Token
resJSON := User{
false,
int(u.ID),
u.UpdatedAt.Format(time.RFC3339),
u.Name,
u.Token,
u.CreatedAt.Format(time.RFC3339),
}
c.JSON(http.StatusOK, resJSON)
return
}
}
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
return
}
if user.ID == uint(1) {
c.JSON(http.StatusOK, gin.H{
"error": "super user already exists, use cli to reset password",
})
}
}
func HandleMe(c *gin.Context) {
u, err := store.GetUserByID(1)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
}
resJSON := User{
false,
int(u.ID),
u.UpdatedAt.Format(time.RFC3339),
u.Name,
u.Token,
u.CreatedAt.Format(time.RFC3339),
}
c.JSON(http.StatusOK, resJSON)
}
func HandleKeys(c *gin.Context) {
keys, err := store.GetAllKeys()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK, keys)
}
func HandleUsers(c *gin.Context) {
users, err := store.GetAllUsers()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK, users)
}
func HandleAddKey(c *gin.Context) {
var body Key
if err := c.BindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
if err := store.AddKey(body.Key, body.Name); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
k, err := store.GetKeyrByName(body.Name)
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, k)
}
func HandleDelKey(c *gin.Context) {
id := to.Int(c.Param("id"))
if id < 1 {
c.JSON(http.StatusOK, gin.H{"error": "invalid key id"})
return
}
if err := store.DeleteKey(uint(id)); err != nil {
c.JSON(http.StatusOK, gin.H{"error": "invalid key id"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
func HandleAddUser(c *gin.Context) {
var body User
if err := c.BindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
// if len(body.Name) == 0 {
// c.JSON(http.StatusOK, gin.H{"error": "invalid user name"})
// return
// }
if err := store.AddUser(body.Name, uuid.NewString()); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
u, err := store.GetUserByName(body.Name)
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, u)
}
func HandleDelUser(c *gin.Context) {
id := to.Int(c.Param("id"))
if id <= 1 {
c.JSON(http.StatusOK, gin.H{"error": "invalid user id"})
return
}
if err := store.DeleteUser(uint(id)); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
func HandleResetUserToken(c *gin.Context) {
id := to.Int(c.Param("id"))
if err := store.UpdateUser(uint(id), uuid.NewString()); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
u, err := store.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
func GenerateToken() string {
token := uuid.New()
return token.String()
}
func HandleProy(c *gin.Context) {
auth := c.Request.Header.Get("Authorization")
if auth[:7] == "Bearer " {
if len(auth[7:]) < 1 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
if !store.IsExistAuthCache(auth[7:]) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
client := http.DefaultClient
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client.Transport = tr
// 创建 API 请求
req, err := http.NewRequest(c.Request.Method, baseUrl+c.Request.URL.Path, c.Request.Body)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
req.Header = c.Request.Header
if store.KeysCache.ItemCount() == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "No Api-Key Available"})
return
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", store.FromKeyCacheRandomItem()))
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
defer resp.Body.Close()
// 复制 API 响应头部
for name, values := range resp.Header {
for _, value := range values {
c.Writer.Header().Add(name, value)
}
}
head := map[string]string{
"Cache-Control": "no-store",
"access-control-allow-origin": "*",
"access-control-allow-credentials": "true",
}
for k, v := range head {
if _, ok := resp.Header[k]; !ok {
c.Writer.Header().Set(k, v)
}
}
resp.Header.Del("content-security-policy")
resp.Header.Del("content-security-policy-report-only")
resp.Header.Del("clear-site-data")
bodyRes, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
if resp.StatusCode == 200 {
// todo
}
resbody := io.NopCloser(bytes.NewReader(bodyRes))
// 返回 API 响应主体
c.Writer.WriteHeader(resp.StatusCode)
if _, err := io.Copy(c.Writer, resbody); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
}

54
store/cache.go Normal file
View File

@@ -0,0 +1,54 @@
package store
import (
"log"
"math/rand"
"github.com/Sakurasan/to"
"github.com/patrickmn/go-cache"
)
var (
KeysCache *cache.Cache
AuthCache *cache.Cache
)
func init() {
KeysCache = cache.New(cache.NoExpiration, cache.NoExpiration)
AuthCache = cache.New(cache.NoExpiration, cache.NoExpiration)
}
func LoadKeysCache() {
keys, err := GetAllKeys()
if err != nil {
log.Println(err)
return
}
for _, key := range keys {
KeysCache.Set(key.Key, true, cache.NoExpiration)
}
}
func FromKeyCacheRandomItem() string {
items := KeysCache.Items()
idx := rand.Intn(len(items))
item := items[to.String(idx)]
return item.Object.(string)
}
func LoadAuthCache() {
users, err := GetAllUsers()
if err != nil {
log.Println(err)
return
}
for _, user := range users {
AuthCache.Set(user.Token, true, cache.NoExpiration)
}
}
func IsExistAuthCache(auth string) bool {
items := AuthCache.Items()
_, ok := items[auth]
return ok
}

33
store/db.go Normal file
View File

@@ -0,0 +1,33 @@
package store
import (
"log"
"os"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var db *gorm.DB
func init() {
if _, err := os.Stat("db"); os.IsNotExist(err) {
errDir := os.MkdirAll("db", 0755)
if errDir != nil {
log.Fatalln("Error creating directory:", err)
}
}
var err error
db, err = gorm.Open(sqlite.Open("./db/cat.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 自动迁移 User 结构体
err = db.AutoMigrate(&User{}, &Key{})
if err != nil {
panic(err)
}
LoadKeysCache()
LoadAuthCache()
}

64
store/keydb.go Normal file
View File

@@ -0,0 +1,64 @@
package store
import "time"
type Key struct {
ID uint `gorm:"primarykey" json:"id,omitempty"`
Key string `gorm:"unique;not null" json:"key,omitempty"`
Name string `gorm:"unique;not null" json:"name,omitempty"`
UserId string `json:"-,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
}
func GetKeyrByName(name string) (*Key, error) {
var key Key
result := db.First(&key, "name = ?", name)
if result.Error != nil {
return nil, result.Error
}
return &key, nil
}
func GetAllKeys() ([]Key, error) {
var keys []Key
if err := db.Find(&keys).Error; err != nil {
return nil, err
}
return keys, nil
}
// 添加记录
func AddKey(apikey, name string) error {
key := Key{
Key: apikey,
Name: name,
}
if err := db.Create(&key).Error; err != nil {
return err
}
LoadKeysCache()
return nil
}
// 删除记录
func DeleteKey(id uint) error {
if err := db.Delete(&Key{}, id).Error; err != nil {
return err
}
LoadKeysCache()
return nil
}
// 更新记录
func UpdateKey(id uint, apikey string, userId string) error {
key := Key{
Key: apikey,
UserId: userId,
}
if err := db.Model(&Key{}).Where("id = ?", id).Updates(key).Error; err != nil {
return err
}
LoadKeysCache()
return nil
}

83
store/userdb.go Normal file
View File

@@ -0,0 +1,83 @@
package store
import (
"time"
)
type User struct {
IsDelete bool `gorm:"default:false" json:"IsDelete"`
ID uint `gorm:"primarykey" json:"id,omitempty"`
Name string `gorm:"unique;not null" json:"name,omitempty"`
Token string `gorm:"unique;not null" json:"token,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
// DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func CreateUser(u *User) error {
result := db.Create(u)
if result.Error != nil {
return result.Error
}
LoadAuthCache()
return nil
}
// 添加用户
func AddUser(name, token string) error {
user := &User{Name: name, Token: token}
result := db.Create(&user)
if result.Error != nil {
return result.Error
}
LoadAuthCache()
return nil
}
// 删除用户
func DeleteUser(id uint) error {
result := db.Delete(&User{}, id)
if result.Error != nil {
return result.Error
}
LoadAuthCache()
return nil
}
// 修改用户
func UpdateUser(id uint, token string) error {
user := &User{Token: token}
result := db.Model(&User{}).Where("id = ?", id).Updates(user)
if result.Error != nil {
return result.Error
}
LoadAuthCache()
return nil
}
func GetUserByID(id uint) (*User, error) {
var user User
result := db.Where("id = ?", id).First(&user)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
func GetUserByName(name string) (*User, error) {
var user User
result := db.Where(&User{Name: name}).First(&user)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
func GetAllUsers() ([]*User, error) {
var users []*User
result := db.Find(&users)
if result.Error != nil {
return nil, result.Error
}
return users, nil
}