├── .github └── workflows │ ├── ci.yaml │ └── tag.yaml ├── .gitignore ├── License ├── README.md ├── assets └── logo.svg ├── dist ├── assets │ ├── index-9a4663b1.js │ ├── index-ef8ba4ac.css │ ├── logo-4312ea85.svg │ ├── team-6ddf162b.jpg │ └── usersdomain-73f886cd.jpg └── index.html ├── doc ├── API.md ├── ama_pandora_chat_test.png ├── ama_pandora_config_client.png ├── ama_pandora_copy_config.png ├── ama_pandora_provider.png ├── azure.md ├── azure_ama.png ├── azure_key&endpoint.png ├── azure_openai_for_team.png ├── deploy.md ├── gemini.md ├── gemini_key.jpg └── pandora.md ├── docker-compose.yml ├── docker ├── Dockerfile └── docker-compose.yml ├── go.mod ├── go.sum ├── makefile ├── opencat.go ├── pkg ├── azureopenai │ └── azureopenai.go ├── claude │ ├── chat.go │ └── claude.go ├── error │ └── errdata.go ├── google │ └── chat.go ├── openai │ ├── chat.go │ ├── dall-e.go │ ├── realtime.go │ ├── tts.go │ └── whisper.go ├── search │ └── bing.go ├── team │ ├── key.go │ ├── me.go │ ├── middleware.go │ ├── usage.go │ └── user.go ├── tokenizer │ └── tokenizer.go └── vertexai │ └── auth.go ├── router ├── chat.go └── router.go ├── store ├── cache.go ├── db.go ├── keydb.go ├── usage.go └── userdb.go └── web ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── App.vue ├── assets │ ├── chatgpt_client.jpg │ ├── logo.svg │ ├── team.jpg │ └── usersdomain.jpg ├── components │ └── HelloWorld.vue ├── main.js └── style.css ├── tailwind.config.js └── vite.config.js /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: 'true' 16 | - name: Get current date 17 | id: date 18 | run: echo "today=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v2 22 | 23 | - name: Set up Docker Buildx 24 | id: buildx 25 | uses: docker/setup-buildx-action@v2 26 | - name: Available platforms 27 | run: echo ${{ steps.buildx.outputs.platforms }} 28 | - name: Login to DockerHub 29 | uses: docker/login-action@v2 30 | with: 31 | username: ${{ secrets.DOCKERHUB_USERNAME }} 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | 34 | - name: Build and push 35 | uses: docker/build-push-action@v4 36 | with: 37 | context: . 38 | file: ./docker/Dockerfile 39 | push: true 40 | platforms: linux/amd64,linux/arm64 41 | tags: | 42 | ${{ github.repository }}:${{ steps.date.outputs.today }} 43 | ${{ github.repository }}:${{ contains(github.ref,'main') && 'latest' || github.ref_name }} 44 | - name: Docker Hub Description 45 | uses: peter-evans/dockerhub-description@v3 46 | with: 47 | username: ${{ secrets.DOCKERHUB_USERNAME }} 48 | password: ${{ secrets.DOCKERHUB_TOKEN }} 49 | repository: ${{ github.repository }} 50 | readme-filepath: ./README.md -------------------------------------------------------------------------------- /.github/workflows/tag.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI Tag 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | submodules: 'true' 14 | 15 | - name: Get version 16 | id: vars 17 | run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 18 | 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | 22 | - name: Set up Docker Buildx 23 | id: buildx 24 | uses: docker/setup-buildx-action@v2 25 | - name: Available platforms 26 | run: echo ${{ steps.buildx.outputs.platforms }} 27 | - name: Login to DockerHub 28 | uses: docker/login-action@v2 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Build and push 34 | uses: docker/build-push-action@v4 35 | with: 36 | context: . 37 | file: ./docker/Dockerfile 38 | push: true 39 | platforms: linux/amd64,linux/arm64 40 | tags: | 41 | ${{ github.repository }}:${{ steps.vars.outputs.tag }} 42 | ${{ github.repository }}:latest 43 | - name: Docker Hub Description 44 | uses: peter-evans/dockerhub-description@v3 45 | with: 46 | username: ${{ secrets.DOCKERHUB_USERNAME }} 47 | password: ${{ secrets.DOCKERHUB_TOKEN }} 48 | repository: ${{ github.repository }} 49 | readme-filepath: ./README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | test/ 3 | *.log 4 | *.db 5 | demo/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ~~opencatd-open~~ [OpenTeam](https://github.com/mirrors2/opencatd-open) 2 | 3 | 本项目即将更名,后续请关注 👉🏻 https://github.com/mirrors2/openteam 4 | 5 | 6 | GitHub Workflow Status 7 | 8 | 9 | [![Telegram group](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ftelegram%2FOpenTeamChat&query=count&color=2CA5E0&label=Telegram%20Group&logo=telegram&cacheSeconds=3600&style=flat-square)](https://t.me/OpenTeamChat) [![Telegram channel](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.swo.moe%2Fstats%2Ftelegram%2FOpenTeamLLM&query=count&color=2CA5E0&label=Telegram%20Channel&logo=telegram&cacheSeconds=3600&style=flat-square)](https://t.me/OpenTeamLLM) 10 | 11 | opencatd-open is an open-source, team-shared service for ChatGPT API that can be safely shared with others for API usage. 12 | 13 | --- 14 | OpenCat for Team的开源实现 15 | 16 | ~~基本~~实现了opencatd的全部功能 17 | 18 | (openai附属能力:whisper,tts,dall-e(text to image)...) 19 | 20 | ## Extra Support: 21 | 22 | | 🎯 | 🚧 |Extra Provider| 23 | | --- | --- | --- | 24 | |[OpenAI](./doc/azure.md) | ✅|Azure, Github Marketplace| 25 | |[Claude](./doc/azure.md) | ✅|VertexAI| 26 | |[Gemini](./doc/gemini.md) | ✅|| 27 | | ... | ... | 28 | 29 | 30 | 31 | ## 快速上手 32 | ``` 33 | docker run -d --name opencatd -p 80:80 -v /etc/opencatd:/app/db mirrors2/opencatd-open 34 | ``` 35 | ## docker-compose 36 | 37 | ``` 38 | version: '3.7' 39 | services: 40 | opencatd: 41 | image: mirrors2/opencatd-open 42 | container_name: opencatd-open 43 | restart: unless-stopped 44 | ports: 45 | - 80:80 46 | volumes: 47 | - /etc/opencatd:/app/db 48 | 49 | ``` 50 | or 51 | 52 | ``` 53 | wget https://github.com/mirrors2/opencatd-open/raw/main/docker/docker-compose.yml 54 | ``` 55 | ## 支持的命令 56 | >获取 root 的 token 57 | - `docker exec opencatd-open opencatd root_token` 58 | 59 | >重置 root 的 token 60 | - `docker exec opencatd-open opencatd reset_root` 61 | 62 | >导出 user info -> user.json (docker file path: /app/db/user.json) 63 | - `docker exec opencatd-open opencatd save` 64 | 65 | >导入 user.json -> db 66 | - `docker exec opencatd-open opencatd load` 67 | 68 | ## Q&A 69 | 关于证书? 70 | - docker部署会白白占用掉VPS的80,443很不河里,建议用Nginx/Caddy/Traefik等反代并自动管理HTTPS证书. 71 | 72 | 没有服务器? 73 | - 可以白嫖一些免费的容器托管服务:如: 74 | - [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/ppAoCV?referralCode=TW5RNa) 75 | - [Zeabur](https://zeabur.com/zh-CN) 76 | - [koyeb](https://koyeb.io/) 77 | - [Fly.io](https://fly.io/) 78 | - 或者其他 79 | 80 | 使用Nginx + Docker部署 81 | - [使用Nginx + Docker部署](./doc/deploy.md) 82 | 83 | pandora for team 84 | - [pandora for team](./doc/pandora.md) 85 | 86 | 如何自定义HOST地址? (仅OpenAI) 87 | - 需修改环境变量,优先级递增(全局配置谨慎修改) 88 | - Cloudflare AI Gateway地址 `AIGateWay_Endpoint=https://gateway.ai.cloudflare.com/v1/123456789/xxxx/openai/chat/completions` 89 | - 自定义的endpoint `OpenAI_Endpoint=https://your.domain/v1/chat/completions` 90 | 91 | 设置主页跳转地址? 92 | - 修改环境变量 `CUSTOM_REDIRECT=https://your.domain` 93 | 94 | ## 获取更多信息 95 | [![TG](https://telegram.org/img/favicon.ico)](https://t.me/OpenTeamLLM) 96 | 97 | ## 赞助 98 | [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD55?style=flat-square&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/littlecjun) 99 | 100 | # License 101 | 102 | [![GitHub License](https://img.shields.io/github/license/mirrors2/opencatd-open.svg?logo=github&style=flat-square)](https://github.com/mirrors2/opencatd-open/blob/main/License) 103 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/assets/index-ef8ba4ac.css: -------------------------------------------------------------------------------- 1 | *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.top-0{top:0px}.z-10{z-index:10}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-60{margin-left:15rem;margin-right:15rem}.my-0{margin-top:0;margin-bottom:0}.my-12{margin-top:3rem;margin-bottom:3rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-5{margin-top:1.25rem}.mt-8{margin-top:2rem}.flex{display:flex}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-auto{height:auto}.h-screen{height:100vh}.w-10\/12{width:83.333333%}.w-60{width:15rem}.w-full{width:100%}.w-screen{width:100vw}.flex-1{flex:1 1 0%}.flex-grow{flex-grow:1}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity))}.p-12{padding:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-rose-500{--tw-text-opacity: 1;color:rgb(244 63 94 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}@media (min-width: 640px){.sm\:max-w-full{max-width:100%}}@media (min-width: 768px){.md\:max-w-sm{max-width:24rem}}.bg-diy[data-v-b28a9e78]{background-color:#f0f0f0}.logo[data-v-b28a9e78]{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo[data-v-b28a9e78]:hover{filter:drop-shadow(0 0 3em #45f5e3aa)}p[data-v-b28a9e78]{margin-bottom:4px}blockquote[data-v-b28a9e78]{padding:0 1em;border-left:.25em solid #838989aa} 2 | -------------------------------------------------------------------------------- /dist/assets/logo-4312ea85.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/assets/team-6ddf162b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/dist/assets/team-6ddf162b.jpg -------------------------------------------------------------------------------- /dist/assets/usersdomain-73f886cd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/dist/assets/usersdomain-73f886cd.jpg -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | opencatd-open 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | ## 用户 4 | 5 | ### 初始化用户 6 | 7 | - URL: `/1/users/init` 8 | - Method: `POST` 9 | - Description: 初始化用户 10 | 11 | Resp: 12 | ``` 13 | { 14 | "id" : 1, 15 | "updatedAt" : "2023-05-28T18:47:19+08:00", 16 | "name" : "root", 17 | "token" : "df5982d6-d8fb-4eef-a513-24d0831f61ad", 18 | "createdAt" : "2023-05-28T18:47:19+08:00" 19 | } 20 | ``` 21 | 22 | ### 获取当前用户信息 23 | 24 | - URL: `/1/me` 25 | - Method: `GET` 26 | - Description: 获取当前用户信息 27 | - Requires authentication: Yes 28 | - Headers: 29 | - Authorization: Bearer {token} 30 | 31 | Resp: 32 | ``` 33 | { 34 | "id" : 1, 35 | "updatedAt" : "2023-05-28T18:47:19+08:00", 36 | "name" : "root", 37 | "token" : "df5982d6-d8fb-4eef-a513-24d0831f61ad", 38 | "createdAt" : "2023-05-28T18:47:19+08:00" 39 | } 40 | ``` 41 | 42 | ### 获取所有用户信息 43 | 44 | - URL: `/1/users` 45 | - Method: `GET` 46 | - Description: 获取所有用户信息 47 | - Requires authentication: Yes 48 | - Headers: 49 | - Authorization: Bearer {token} 50 | 51 | Resp: 52 | ``` 53 | [ 54 | { 55 | "createdAt" : "2023-05-28T18:47:19.711027498+08:00", 56 | "id" : 1, 57 | "updatedAt" : "2023-05-28T18:47:19.711027498+08:00", 58 | "IsDelete" : false, 59 | "name" : "root", 60 | "token" : "df5982d6-d8fb-4eef-a513-24d0831f61ad" 61 | }, 62 | { 63 | "createdAt" : "2023-05-28T18:48:29.018428441+08:00", 64 | "id" : 2, 65 | "updatedAt" : "2023-05-28T18:48:29.018428441+08:00", 66 | "IsDelete" : false, 67 | "name" : "u1", 68 | "token" : "6ac4bd1a-18a6-4c25-922f-db689a299e38" 69 | } 70 | ] 71 | ``` 72 | 73 | ### 添加用户 74 | 75 | - URL: `/1/users` 76 | - Method: `POST` 77 | - Description: 添加用户 78 | - Headers: 79 | - Authorization: Bearer {token} 80 | 81 | Req: 82 | ``` 83 | { 84 | "name" : "u1" 85 | } 86 | ``` 87 | 88 | Resp: 89 | ``` 90 | { 91 | "createdAt" : "2023-05-28T18:48:29.018428441+08:00", 92 | "id" : 2, 93 | "updatedAt" : "2023-05-28T18:48:29.018428441+08:00", 94 | "IsDelete" : false, 95 | "name" : "u1", 96 | "token" : "6ac4bd1a-18a6-4c25-922f-db689a299e38" 97 | } 98 | ``` 99 | 100 | ### 删除用户 101 | 102 | - URL: `/1/users/:id` 103 | - Method: `DELETE` 104 | - Description: 删除用户 105 | - Headers: 106 | - Authorization: Bearer {token} 107 | 108 | Resp: 109 | ``` 110 | { 111 | "message" : "ok" 112 | } 113 | ``` 114 | 115 | ### 重置用户 Token 116 | 117 | - URL: `/1/users/:id/reset` 118 | - URL: `/1/users/:id/reset?token={new user token}` 119 | - Method: `POST` 120 | - Description: 重置用户 Token 默认生成新 Token 也可以指定 121 | - Headers: 122 | - Authorization: Bearer {token} 123 | 124 | Resp: 125 | ``` 126 | { 127 | "createdAt" : "2023-05-28T19:33:54.83935763+08:00", 128 | "id" : 2, 129 | "updatedAt" : "2023-05-28T19:34:33.387843151+08:00", 130 | "IsDelete" : false, 131 | "name" : "u2", 132 | "token" : "881a30d2-2fc8-4758-a07e-7d9ad5f34266" 133 | } 134 | ``` 135 | ## Key 136 | 137 | ### 获取所有 Key 138 | 139 | - URL: `/1/keys` 140 | - Method: `GET` 141 | - Description: 获取所有 Key 142 | - Headers: 143 | - Authorization: Bearer {token} 144 | 145 | ``` 146 | [ 147 | { 148 | "id" : 1, 149 | "key" : "sk-zsbdzsbdzsbdzsbdzsbdzsbdzsbd", 150 | "createdAt" : "2023-05-28T18:47:49.936644953+08:00", 151 | "updatedAt" : "2023-05-28T18:47:49.936644953+08:00", 152 | "name" : "key", 153 | "ApiType" : "openai" 154 | }, 155 | { 156 | "id" : 2, 157 | "key" : "1234567890qwertyuiopasdfghjklzxcvbnm", 158 | "createdAt" : "2023-05-28T18:48:18.548627422+08:00", 159 | "updatedAt" : "2023-05-28T18:48:18.548627422+08:00", 160 | "name" : "key2", 161 | "ApiType" : "openai" 162 | } 163 | ] 164 | ``` 165 | 166 | ### 添加 Key 167 | 168 | - URL: `/1/keys` 169 | - Method: `POST` 170 | - Description: 添加 Key 171 | - Headers: 172 | - Authorization: Bearer {token} 173 | 174 | Req: 175 | ``` 176 | { 177 | "key" : "sk-zsbdzsbdzsbdzsbdzsbdzsbdzsbd", 178 | "name" : "key", 179 | "api_type": "openai", 180 | "endpoint": "" 181 | 182 | } 183 | ``` 184 | api_type:不传的话默认为“openai”;当前可选值[openai,azure,claude] 185 | endpoint: 当 api_type 为 azure_openai时传入(目前暂未使用) 186 | 187 | Resp: 188 | ``` 189 | { 190 | "id" : 1, 191 | "key" : "sk-zsbdzsbdzsbdzsbdzsbdzsbdzsbd", 192 | "createdAt" : "2023-05-28T18:47:49.936644953+08:00", 193 | "updatedAt" : "2023-05-28T18:47:49.936644953+08:00", 194 | "name" : "key", 195 | "ApiType" : "openai" 196 | } 197 | ``` 198 | 199 | ### 删除 Key 200 | 201 | - URL: `/1/keys/:id` 202 | - Method: `DELETE` 203 | - Description: 删除 Key 204 | - Headers: 205 | - Authorization: Bearer {token} 206 | 207 | Resp: 208 | 209 | ``` 210 | { 211 | "message" : "ok" 212 | } 213 | ``` 214 | 215 | ## Usages 216 | 217 | ### 获取用量信息 218 | 219 | - URL: `/1/usages?from=2023-03-18&to=2023-04-18` 220 | - Method: `GET` 221 | - Description: 获取用量信息 222 | - Headers: 223 | - Authorization: Bearer {token} 224 | 225 | Resp: 226 | ``` 227 | [ 228 | { 229 | "cost" : "0.000110", 230 | "userId" : 1, 231 | "totalUnit" : 55 232 | }, 233 | { 234 | "cost" : "0.000110", 235 | "userId" : 2, 236 | "totalUnit" : 55 237 | } 238 | ] 239 | ``` 240 | 241 | ## Whisper接口 242 | ### 与openai一致 243 | -------------------------------------------------------------------------------- /doc/ama_pandora_chat_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_chat_test.png -------------------------------------------------------------------------------- /doc/ama_pandora_config_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_config_client.png -------------------------------------------------------------------------------- /doc/ama_pandora_copy_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_copy_config.png -------------------------------------------------------------------------------- /doc/ama_pandora_provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_provider.png -------------------------------------------------------------------------------- /doc/azure.md: -------------------------------------------------------------------------------- 1 | # Azure OpenAI for team 2 | 3 | 1.需要获取 api-key和endpoint [https://[resource name].openai.azure.com/) 4 | ![](./azure_key%26endpoint.png) 5 | 6 | > 2.Pleause use model name as deployment name 7 | 8 | | model name | deployment name | 9 | | --- | --- | 10 | |gpt-35-turbo | gpt-35-turbo | 11 | |gpt-35-turbo-16k | gpt-35-turbo-16k | 12 | | gpt-4 | gpt-4 | 13 | 14 | ## How to use 15 | - opencat 使用方式 16 | - key name以 azure.[resource name]的方式添加 17 | - 密钥任取一个 18 | - azure_openai_for_team 19 | - [AMA(问天)](http://bytemyth.com/ama) 使用方式 20 | - ![](azure_ama.png) 21 | - 每个 team server 用户旁边有一个复制按钮,点击后,把复制的链接粘贴到浏览器,可以一键设置 22 | 23 | ## Claude 24 | 25 | - opencat 添加Claude api, key name以 "claude.key名称",即("Api类型.Key名称") 26 | -------------------------------------------------------------------------------- /doc/azure_ama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/azure_ama.png -------------------------------------------------------------------------------- /doc/azure_key&endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/azure_key&endpoint.png -------------------------------------------------------------------------------- /doc/azure_openai_for_team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/azure_openai_for_team.png -------------------------------------------------------------------------------- /doc/deploy.md: -------------------------------------------------------------------------------- 1 | 2 | # nginx + docker 3 | 自行安装相关环境,省事直接用了宝塔面板 4 | 5 | docker-compose.yml 6 | ``` 7 | version: '3.7' 8 | services: 9 | opencatd: 10 | image: mirrors2/opencatd-open 11 | container_name: opencatd-open 12 | restart: unless-stopped 13 | ports: 14 | - 8088:80 15 | volumes: 16 | - $PWD/db:/app/db 17 | ``` 18 | nginx配置 19 | ``` 20 | location / 21 | { 22 | proxy_pass http://localhost:8088; 23 | proxy_set_header Host localhost; 24 | proxy_set_header X-Real-IP $remote_addr; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | proxy_set_header REMOTE-HOST $remote_addr; 27 | } 28 | ``` -------------------------------------------------------------------------------- /doc/gemini.md: -------------------------------------------------------------------------------- 1 | ## 添加ApiKey 2 | gemini的"ApiType":"google" 3 | 4 | 或者使用 google.xxxx 的apikey名称 添加 5 | ![gemini key](./gemini_key.jpg) 6 | -------------------------------------------------------------------------------- /doc/gemini_key.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/gemini_key.jpg -------------------------------------------------------------------------------- /doc/pandora.md: -------------------------------------------------------------------------------- 1 | # pandora for team 2 | 3 | [pandora](https://github.com/pengzhile/pandora)是一个把ChatGPT(web/App)接口化的项目,可以看做是第三方 OpenAI API 提供方(接口和OpenAI一致) 4 | 5 | ## 准备 6 | - https://ai.fakeopen.com/auth1 获取accesstoken 7 | 8 | - https://ai.fakeopen.com/token 创建apikey 9 | 10 | ## 客户端设置 11 | 12 | 1.添加接口 13 | ![](ama_pandora_provider.png) 14 | 15 | 2.创建用户&Copy Config 16 | ![](ama_pandora_copy_config.png) 17 | 18 | Ex:`ama://set-api-key?server=http%3A%2F%2F123.456.7.89&key=8fc322fa-15d2-43d7-bc59-621554e82c2a` 19 | 20 | 3.Configure Client 21 | ![](ama_pandora_config_client.png) 22 | 23 | 3.测试聊天 24 | ![](ama_pandora_chat_test.png) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Email: admin@example.com 2 | # Password: changeme 3 | version: '3' 4 | 5 | services: 6 | npm: 7 | image: jc21/nginx-proxy-manager 8 | network_mode: host 9 | ports: 10 | - '80:80' 11 | - '81:81' 12 | - '443:443' 13 | volumes: 14 | - $PWD/data:/data 15 | - $PWD/www:/var/www 16 | - $PWD/letsencrypt:/etc/letsencrypt 17 | environment: 18 | - "TZ=Asia/Shanghai" # set timezone, default UTC 19 | - "PUID=1000" # set group id, default 0 (root) 20 | - "PGID=1000" 21 | 22 | # certbot: 23 | # image: certbot/certbot 24 | # volumes: 25 | # - $PWD/data/certbot/conf:/etc/letsencrypt 26 | # - $PWD/data/certbot/www:/var/www/certbot 27 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS frontend 2 | WORKDIR /frontend-build 3 | COPY ./web/ . 4 | RUN npm install && npm run build && rm -rf node_modules 5 | 6 | FROM golang:1.23-alpine as builder 7 | LABEL anther="github.com/Sakurasan" 8 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk --no-cache add make cmake upx 9 | WORKDIR /build 10 | COPY --from=frontend /frontend-build/dist /build/dist 11 | COPY . /build 12 | ENV GO111MODULE=on 13 | # ENV GOPROXY=https://goproxy.cn,direct 14 | CMD [ "go mod tidy","go mod download" ] 15 | RUN make build 16 | 17 | FROM alpine:latest AS runner 18 | # 设置alpine 时间为上海时间 19 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add tzdata ffmpeg && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 20 | && echo "Asia/Shanghai" > /etc/timezone 21 | # RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils 22 | WORKDIR /app 23 | COPY --from=builder /build/bin/opencatd /app/opencatd 24 | ENV GIN_MODE=release 25 | ENV PATH=$PATH:/app 26 | EXPOSE 80 27 | ENTRYPOINT ["/app/opencatd"] -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | opencatd: 4 | image: mirrors2/opencatd-open 5 | container_name: opencatd-open 6 | restart: unless-stopped 7 | #network_mode: host 8 | ports: 9 | - 80:80 10 | volumes: 11 | - $PWD/db:/app/db 12 | logging: 13 | # driver: "json-file" 14 | options: 15 | max-size: 10m 16 | max-file: 3 17 | # environment: 18 | # Vertex: | 19 | # { 20 | # "type": "service_account", 21 | # "universe_domain": "googleapis.com" 22 | # } 23 | 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module opencatd-open 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | cloud.google.com/go/vertexai v0.13.1 7 | github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d 8 | github.com/coder/websocket v1.8.12 9 | github.com/duke-git/lancet/v2 v2.3.3 10 | github.com/faiface/beep v1.1.0 11 | github.com/gin-gonic/gin v1.10.0 12 | github.com/glebarez/sqlite v1.11.0 13 | github.com/golang-jwt/jwt v3.2.2+incompatible 14 | github.com/google/generative-ai-go v0.18.0 15 | github.com/google/uuid v1.6.0 16 | github.com/gorilla/websocket v1.5.3 17 | github.com/joho/godotenv v1.5.1 18 | github.com/patrickmn/go-cache v2.1.0+incompatible 19 | github.com/pkoukk/tiktoken-go v0.1.7 20 | github.com/sashabaranov/go-openai v1.32.2 21 | github.com/tidwall/gjson v1.18.0 22 | golang.org/x/sync v0.8.0 23 | google.golang.org/api v0.201.0 24 | gopkg.in/vansante/go-ffprobe.v2 v2.2.0 25 | gorm.io/gorm v1.25.12 26 | ) 27 | 28 | require ( 29 | cloud.google.com/go v0.116.0 // indirect 30 | cloud.google.com/go/ai v0.8.2 // indirect 31 | cloud.google.com/go/aiplatform v1.68.0 // indirect 32 | cloud.google.com/go/auth v0.9.8 // indirect 33 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 34 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 35 | cloud.google.com/go/iam v1.2.1 // indirect 36 | cloud.google.com/go/longrunning v0.6.1 // indirect 37 | github.com/bytedance/sonic v1.12.3 // indirect 38 | github.com/bytedance/sonic/loader v0.2.1 // indirect 39 | github.com/cloudwego/base64x v0.1.4 // indirect 40 | github.com/cloudwego/iasm v0.2.0 // indirect 41 | github.com/dlclark/regexp2 v1.11.4 // indirect 42 | github.com/dustin/go-humanize v1.0.1 // indirect 43 | github.com/felixge/httpsnoop v1.0.4 // indirect 44 | github.com/gabriel-vasile/mimetype v1.4.6 // indirect 45 | github.com/gin-contrib/cors v1.7.2 // indirect 46 | github.com/gin-contrib/sse v0.1.0 // indirect 47 | github.com/glebarez/go-sqlite v1.22.0 // indirect 48 | github.com/go-logr/logr v1.4.2 // indirect 49 | github.com/go-logr/stdr v1.2.2 // indirect 50 | github.com/go-playground/locales v0.14.1 // indirect 51 | github.com/go-playground/universal-translator v0.18.1 // indirect 52 | github.com/go-playground/validator/v10 v10.22.1 // indirect 53 | github.com/goccy/go-json v0.10.3 // indirect 54 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 55 | github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect 56 | github.com/google/s2a-go v0.1.8 // indirect 57 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 58 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect 59 | github.com/hajimehoshi/go-mp3 v0.3.4 // indirect 60 | github.com/jinzhu/inflection v1.0.0 // indirect 61 | github.com/jinzhu/now v1.1.5 // indirect 62 | github.com/json-iterator/go v1.1.12 // indirect 63 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 64 | github.com/leodido/go-urn v1.4.0 // indirect 65 | github.com/mattn/go-isatty v0.0.20 // indirect 66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 67 | github.com/modern-go/reflect2 v1.0.2 // indirect 68 | github.com/ncruces/go-strftime v0.1.9 // indirect 69 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 70 | github.com/pkg/errors v0.9.1 // indirect 71 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 72 | github.com/tidwall/match v1.1.1 // indirect 73 | github.com/tidwall/pretty v1.2.0 // indirect 74 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 75 | github.com/ugorji/go/codec v1.2.12 // indirect 76 | go.opencensus.io v0.24.0 // indirect 77 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect 78 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect 79 | go.opentelemetry.io/otel v1.31.0 // indirect 80 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 81 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 82 | golang.org/x/arch v0.11.0 // indirect 83 | golang.org/x/crypto v0.28.0 // indirect 84 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 85 | golang.org/x/net v0.30.0 // indirect 86 | golang.org/x/oauth2 v0.23.0 // indirect 87 | golang.org/x/sys v0.26.0 // indirect 88 | golang.org/x/text v0.19.0 // indirect 89 | golang.org/x/time v0.7.0 // indirect 90 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 // indirect 91 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect 92 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect 93 | google.golang.org/grpc v1.67.1 // indirect 94 | google.golang.org/protobuf v1.35.1 // indirect 95 | gopkg.in/yaml.v3 v3.0.1 // indirect 96 | modernc.org/libc v1.61.0 // indirect 97 | modernc.org/mathutil v1.6.0 // indirect 98 | modernc.org/memory v1.8.0 // indirect 99 | modernc.org/sqlite v1.33.1 // indirect 100 | ) 101 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= 3 | cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= 4 | cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= 5 | cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= 6 | cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= 7 | cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= 8 | cloud.google.com/go/ai v0.8.2 h1:LEaQwqBv+k2ybrcdTtCTc9OPZXoEdcQaGrfvDYS6Bnk= 9 | cloud.google.com/go/ai v0.8.2/go.mod h1:Wb3EUUGWwB6yHBaUf/+oxUq/6XbCaU1yh0GrwUS8lr4= 10 | cloud.google.com/go/aiplatform v1.68.0 h1:EPPqgHDJpBZKRvv+OsB3cr0jYz3EL2pZ+802rBPcG8U= 11 | cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME= 12 | cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8= 13 | cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= 14 | cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= 15 | cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= 16 | cloud.google.com/go/compute v1.28.1 h1:XwPcZjgMCnU2tkwY10VleUjSAfpTj9RDn+kGrbYsi8o= 17 | cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= 18 | cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= 19 | cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= 20 | cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= 21 | cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= 22 | cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= 23 | cloud.google.com/go/vertexai v0.13.1 h1:E6I+eA6vNQxz7/rb0wdILdKg4hFmMNWZLp+dSy9DnEo= 24 | cloud.google.com/go/vertexai v0.13.1/go.mod h1:25DzKFzP9JByYxcNjJefu/px2dRjcRpCDSdULYL2avI= 25 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 26 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 27 | github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d h1:3v1QFdgk450QH+7C+lw1k+olbjK4fKGsrEfnEG/HLkY= 28 | github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d/go.mod h1:2sp0vsMyh5sqmKl5N+ps/cSspqLkoXUlesSzsufIGRU= 29 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 30 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 31 | github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= 32 | github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 33 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 34 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 35 | github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= 36 | github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 37 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 38 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 39 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 40 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 41 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 42 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 43 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 44 | github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= 45 | github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= 46 | github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= 47 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 49 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 51 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 52 | github.com/duke-git/lancet/v2 v2.3.2 h1:Cv+uNkx5yGqDSvGc5Vu9eiiZobsPIf0Ng7NGy5hEdow= 53 | github.com/duke-git/lancet/v2 v2.3.2/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= 54 | github.com/duke-git/lancet/v2 v2.3.3 h1:OhqzNzkbJBS9ZlWLo/C7g+WSAOAAyNj7p9CAiEHurUc= 55 | github.com/duke-git/lancet/v2 v2.3.3/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= 56 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 57 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 58 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 60 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 61 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 62 | github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= 63 | github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= 64 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 65 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 66 | github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= 67 | github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= 68 | github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= 69 | github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= 70 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 71 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 72 | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= 73 | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= 74 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 75 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 76 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 77 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 78 | github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 79 | github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 80 | github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= 81 | github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 82 | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= 83 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 84 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= 85 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= 86 | github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= 87 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 88 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 89 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 90 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 91 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 92 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 93 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 94 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 95 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 96 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 97 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 98 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 99 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 100 | github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= 101 | github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 102 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 103 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 104 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 105 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 106 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 107 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 109 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 110 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 111 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 112 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 114 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 115 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 116 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 117 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 118 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 119 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 120 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 121 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 122 | github.com/google/generative-ai-go v0.18.0 h1:6ybg9vOCLcI/UpBBYXOTVgvKmcUKFRNj+2Cj3GnebSo= 123 | github.com/google/generative-ai-go v0.18.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= 124 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 125 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 126 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 127 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 131 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 132 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 133 | github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= 134 | github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 135 | github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= 136 | github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= 137 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 138 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 139 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 140 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 141 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 142 | github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= 143 | github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= 144 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 145 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 146 | github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g= 147 | github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 148 | github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= 149 | github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= 150 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 151 | github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= 152 | github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= 153 | github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= 154 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= 155 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= 156 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 157 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 158 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 159 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 160 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 161 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 162 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 163 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 164 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 165 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 166 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 167 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 168 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 169 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 170 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 171 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 172 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 173 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 174 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 175 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 176 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 177 | github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= 178 | github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= 179 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 180 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 181 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 182 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 183 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 184 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 185 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 186 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 187 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 188 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 189 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 190 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 191 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 192 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 193 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 194 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 195 | github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= 196 | github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= 197 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 198 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 199 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 200 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 201 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 202 | github.com/sashabaranov/go-openai v1.31.0 h1:rGe77x7zUeCjtS2IS7NCY6Tp4bQviXNMhkQM6hz/UC4= 203 | github.com/sashabaranov/go-openai v1.31.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 204 | github.com/sashabaranov/go-openai v1.32.2 h1:8z9PfYaLPbRzmJIYpwcWu6z3XU8F+RwVMF1QRSeSF2M= 205 | github.com/sashabaranov/go-openai v1.32.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 206 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 207 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 208 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 209 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 210 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 211 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 212 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 213 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 214 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 215 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 216 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 217 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 218 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 219 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 220 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 221 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 222 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 223 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 224 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 225 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 226 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 227 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 228 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 229 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 230 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= 231 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= 232 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0= 233 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= 234 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 235 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 236 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= 237 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= 238 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 239 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 240 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= 241 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= 242 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 243 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 244 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= 245 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= 246 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 247 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 248 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= 249 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= 250 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 251 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 252 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 253 | golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= 254 | golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 255 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 256 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 257 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 258 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 259 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 260 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 261 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 262 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 263 | golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 264 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 265 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 266 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 267 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 268 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 269 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 270 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 271 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 272 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 273 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 274 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 275 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 276 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 277 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 278 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 279 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 280 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 281 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 282 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 283 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 284 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 285 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 286 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 287 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 288 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 289 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 295 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 296 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 297 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 298 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 299 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 300 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 301 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 302 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 303 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 304 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 305 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 306 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 307 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 308 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 309 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 310 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 311 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 312 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 313 | google.golang.org/api v0.200.0 h1:0ytfNWn101is6e9VBoct2wrGDjOi5vn7jw5KtaQgDrU= 314 | google.golang.org/api v0.200.0/go.mod h1:Tc5u9kcbjO7A8SwGlYj4IiVifJU01UqXtEgDMYmBmV8= 315 | google.golang.org/api v0.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0= 316 | google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4= 317 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 318 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 319 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 320 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 321 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 322 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 h1:nFS3IivktIU5Mk6KQa+v6RKkHUpdQpphqGNLxqNnbEk= 323 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:tEzYTYZxbmVNOu0OAFH9HzdJtLn6h4Aj89zzlBCdHms= 324 | google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA= 325 | google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE= 326 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= 327 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= 328 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= 329 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 330 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= 331 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 332 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 333 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 334 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 335 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 336 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 337 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 338 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 339 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 340 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 341 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 342 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 343 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 344 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 345 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 346 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 347 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 348 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 349 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 350 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 351 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 352 | gopkg.in/vansante/go-ffprobe.v2 v2.2.0 h1:iuOqTsbfYuqIz4tAU9NWh22CmBGxlGHdgj4iqP+NUmY= 353 | gopkg.in/vansante/go-ffprobe.v2 v2.2.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE= 354 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 355 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 356 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 357 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 358 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 359 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 360 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 361 | modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= 362 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 363 | modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= 364 | modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= 365 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 366 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 367 | modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= 368 | modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 369 | modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= 370 | modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= 371 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 372 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 373 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 374 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 375 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 376 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 377 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 378 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 379 | modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= 380 | modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= 381 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 382 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 383 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 384 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 385 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 386 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 387 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | VERSION=$(shell git describe --tags --always) 3 | # 获取源码最近一次 git commit log,包含 commit sha 值,以及 commit message 4 | GitCommitLog=$(shell git log) 5 | # 检查源码在最近一次 git commit 基础上,是否有本地修改,且未提交的文件 6 | GitStatus=$(shell git status -s) 7 | # 获取当前时间 8 | BuildTime=$(shell date +'%Y.%m.%d %H:%M:%S') 9 | # 获取Go的版本 10 | BuildGoVersion=$(shell go version) 11 | 12 | LDFlags=" \ 13 | -X 'main.Version=$(VERSION)' \ 14 | -X 'main.GitCommitLog=$(GitCommitLog)' \ 15 | -X 'main.BuildTime=$(BuildTime)' \ 16 | -X 'main.BuildGoVersion=$(BuildGoVersion)'" 17 | 18 | .PHONY: web 19 | # web 20 | web: 21 | cd web && npm install && npm run build && mv dist .. 22 | 23 | .PHONY: build 24 | # build 25 | build: 26 | # mkdir -p bin/ && go build -ldflags $(LDFlags) -o ./bin/ ./... 27 | rm -rf bin 28 | mkdir -p bin/ && go build -ldflags "-s -w" -o ./bin/opencatd . 29 | upx -9 bin/opencatd 30 | 31 | .PHONY:docker 32 | # build docker images 33 | docker: 34 | docker run --privileged --rm tonistiigi/binfmt --install all 35 | docker buildx create --use --name xbuilder --driver docker-container 36 | docker buildx inspect xbuilder --bootstrap 37 | docker buildx build --platform linux/amd64,linux/arm64 -t mirrors2/opencatd:latest -f docker/Dockerfile . --push 38 | 39 | .PHONY: clean 40 | # clean 41 | clean: 42 | rm -rf bin/ 43 | 44 | .PHONY: all 45 | # generate all 46 | all: 47 | make build; 48 | 49 | 50 | # show help 51 | help: 52 | @echo '' 53 | @echo 'Usage:' 54 | @echo ' make [target]' 55 | @echo '' 56 | @echo 'Targets:' 57 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 58 | helpMessage = match(lastLine, /^# (.*)/); \ 59 | if (helpMessage) { \ 60 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 61 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 62 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 63 | } \ 64 | } \ 65 | { lastLine = $$0 }' $(MAKEFILE_LIST) 66 | 67 | .DEFAULT_GOAL := help 68 | -------------------------------------------------------------------------------- /opencat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "encoding/json" 7 | "fmt" 8 | "io/fs" 9 | "log" 10 | "net/http" 11 | "opencatd-open/pkg/team" 12 | "opencatd-open/router" 13 | "opencatd-open/store" 14 | "os" 15 | 16 | "github.com/duke-git/lancet/v2/fileutil" 17 | "github.com/gin-gonic/gin" 18 | "github.com/google/uuid" 19 | "gorm.io/gorm" 20 | ) 21 | 22 | //go:embed dist/* 23 | var web embed.FS 24 | 25 | func getFileSystem(path string) http.FileSystem { 26 | fs, err := fs.Sub(web, path) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | return http.FS(fs) 32 | } 33 | 34 | func main() { 35 | args := os.Args[1:] 36 | if len(args) > 0 { 37 | type user struct { 38 | ID uint 39 | Name string 40 | Token string 41 | } 42 | var us []user 43 | switch args[0] { 44 | case "reset_root": 45 | log.Println("reset root token...") 46 | if _, err := store.GetUserByID(uint(1)); err != nil { 47 | if err == gorm.ErrRecordNotFound { 48 | log.Println("请在opencat(或其他APP)客户端完成team初始化") 49 | return 50 | } else { 51 | log.Fatalln(err) 52 | return 53 | } 54 | } 55 | ntoken := uuid.NewString() 56 | if err := store.UpdateUser(uint(1), ntoken); err != nil { 57 | log.Fatalln(err) 58 | return 59 | } 60 | log.Println("[success]new root token:", ntoken) 61 | return 62 | case "root_token": 63 | log.Println("query root token...") 64 | if user, err := store.GetUserByID(uint(1)); err != nil { 65 | log.Fatalln(err) 66 | return 67 | } else { 68 | log.Println("[success]root token:", user.Token) 69 | return 70 | } 71 | case "save": 72 | log.Println("backup user info -> user.json") 73 | if users, err := store.GetAllUsers(); err != nil { 74 | log.Fatalln(err) 75 | return 76 | } else { 77 | for _, u := range users { 78 | us = append(us, user{ID: u.ID, Name: u.Name, Token: u.Token}) 79 | } 80 | } 81 | if !fileutil.IsExist("./db/user.json") { 82 | file, err := os.Create("./db/user.json") 83 | if err != nil { 84 | log.Fatalln(err) 85 | return 86 | } 87 | defer file.Close() 88 | } else { 89 | // 文件存在,打开文件 90 | file, _ := os.OpenFile("./db/user.json", os.O_RDWR|os.O_TRUNC, 0666) 91 | defer file.Close() 92 | 93 | buff := bytes.NewBuffer(nil) 94 | json.NewEncoder(buff).Encode(us) 95 | 96 | file.WriteString(buff.String()) 97 | fmt.Println("------- END -------") 98 | return 99 | } 100 | case "load": 101 | fmt.Println("\nimport user.json -> db") 102 | if !fileutil.IsExist("./db/user.json") { 103 | log.Fatalln("404! user.json is not found.") 104 | return 105 | } 106 | users, err := store.GetAllUsers() 107 | if err != nil { 108 | log.Println(err) 109 | return 110 | } 111 | if len(users) != 0 { 112 | log.Println("user db 存在数据,取消导入") 113 | return 114 | } 115 | file, err := os.Open("./db/user.json") 116 | if err != nil { 117 | fmt.Println("Error opening file:", err) 118 | return 119 | } 120 | defer file.Close() 121 | 122 | decoder := json.NewDecoder(file) 123 | err = decoder.Decode(&us) 124 | if err != nil { 125 | fmt.Println("Error decoding JSON:", err) 126 | return 127 | } 128 | for _, u := range us { 129 | log.Println(u.ID, u.Name, u.Token) 130 | err := store.CreateUser(&store.User{ID: u.ID, Name: u.Name, Token: u.Token}) 131 | if err != nil { 132 | log.Println(err) 133 | } 134 | } 135 | fmt.Println("------- END -------") 136 | return 137 | 138 | default: 139 | return 140 | } 141 | 142 | } 143 | port := os.Getenv("PORT") 144 | r := gin.Default() 145 | r.Use(team.CORS()) 146 | group := r.Group("/1") 147 | { 148 | group.Use(team.AuthMiddleware()) 149 | 150 | // 获取当前用户信息 151 | group.GET("/me", team.HandleMe) 152 | group.GET("/me/usages", team.HandleMeUsage) 153 | 154 | group.GET("/keys", team.HandleKeys) // 获取所有Key 155 | group.POST("/keys", team.HandleAddKey) // 添加Key 156 | group.DELETE("/keys/:id", team.HandleDelKey) // 删除Key 157 | 158 | group.GET("/users", team.HandleUsers) // 获取所有用户信息 159 | group.POST("/users", team.HandleAddUser) // 添加用户 160 | group.DELETE("/users/:id", team.HandleDelUser) // 删除用户 161 | 162 | group.GET("/usages", team.HandleUsage) 163 | 164 | // 重置用户Token 165 | group.POST("/users/:id/reset", team.HandleResetUserToken) 166 | } 167 | // 初始化用户 168 | r.POST("/1/users/init", team.Handleinit) 169 | 170 | r.Any("/v1/*proxypath", router.HandleProxy) 171 | 172 | // r.POST("/v1/chat/completions", router.HandleProy) 173 | // r.GET("/v1/models", router.HandleProy) 174 | // r.GET("/v1/dashboard/billing/subscription", router.HandleProy) 175 | 176 | // r.Use(static.Serve("/", static.LocalFile("dist", false))) 177 | idxFS, err := fs.Sub(web, "dist") 178 | if err != nil { 179 | panic(err) 180 | } 181 | redirect := os.Getenv("CUSTOM_REDIRECT") 182 | if redirect != "" { 183 | r.GET("/", func(c *gin.Context) { 184 | c.Redirect(http.StatusMovedPermanently, redirect) 185 | }) 186 | 187 | } else { 188 | r.GET("/", gin.WrapH(http.FileServer(http.FS(idxFS)))) 189 | } 190 | assetsFS, err := fs.Sub(web, "dist/assets") 191 | if err != nil { 192 | panic(err) 193 | } 194 | r.GET("/assets/*filepath", gin.WrapH(http.StripPrefix("/assets/", http.FileServer(http.FS(assetsFS))))) 195 | if port == "" { 196 | port = "80" 197 | } 198 | r.Run(":" + port) 199 | } 200 | -------------------------------------------------------------------------------- /pkg/azureopenai/azureopenai.go: -------------------------------------------------------------------------------- 1 | /* 2 | https://learn.microsoft.com/zh-cn/azure/cognitive-services/openai/chatgpt-quickstart 3 | https://learn.microsoft.com/zh-cn/azure/ai-services/openai/reference#chat-completions 4 | 5 | curl $AZURE_OPENAI_ENDPOINT/openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-03-15-preview \ 6 | -H "Content-Type: application/json" \ 7 | -H "api-key: $AZURE_OPENAI_KEY" \ 8 | -d '{ 9 | "model": "gpt-3.5-turbo", 10 | "messages": [{"role": "user", "content": "你好"}] 11 | }' 12 | 13 | https://learn.microsoft.com/zh-cn/rest/api/cognitiveservices/azureopenaistable/models/list?tabs=HTTP 14 | 15 | curl $AZURE_OPENAI_ENDPOINT/openai/deployments?api-version=2022-12-01 \ 16 | -H "Content-Type: application/json" \ 17 | -H "api-key: $AZURE_OPENAI_KEY" \ 18 | 19 | > GPT-4 Turbo 20 | https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-service-launches-gpt-4-turbo-and-gpt-3-5-turbo-1106/ba-p/3985962 21 | 22 | */ 23 | 24 | package azureopenai 25 | 26 | import ( 27 | "encoding/json" 28 | "net/http" 29 | "regexp" 30 | "strings" 31 | ) 32 | 33 | var ( 34 | ENDPOINT string 35 | API_KEY string 36 | DEPLOYMENT_NAME string 37 | ) 38 | 39 | type ModelsList struct { 40 | Data []struct { 41 | ScaleSettings struct { 42 | ScaleType string `json:"scale_type"` 43 | } `json:"scale_settings"` 44 | Model string `json:"model"` 45 | Owner string `json:"owner"` 46 | ID string `json:"id"` 47 | Status string `json:"status"` 48 | CreatedAt int `json:"created_at"` 49 | UpdatedAt int `json:"updated_at"` 50 | Object string `json:"object"` 51 | } `json:"data"` 52 | Object string `json:"object"` 53 | } 54 | 55 | func Models(endpoint, apikey string) (*ModelsList, error) { 56 | endpoint = RemoveTrailingSlash(endpoint) 57 | var modelsl ModelsList 58 | req, _ := http.NewRequest(http.MethodGet, endpoint+"/openai/deployments?api-version=2022-12-01", nil) 59 | req.Header.Set("api-key", apikey) 60 | resp, err := http.DefaultClient.Do(req) 61 | if err != nil { 62 | return nil, err 63 | } 64 | defer resp.Body.Close() 65 | err = json.NewDecoder(resp.Body).Decode(&modelsl) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &modelsl, nil 70 | 71 | } 72 | 73 | func RemoveTrailingSlash(s string) string { 74 | const prefix = "openai.azure.com/" 75 | if strings.HasSuffix(strings.TrimSpace(s), prefix) && strings.HasSuffix(s, "/") { 76 | return s[:len(s)-1] 77 | } 78 | return s 79 | } 80 | 81 | func GetResourceName(url string) string { 82 | re := regexp.MustCompile(`https?://(.+)\.openai\.azure\.com/?`) 83 | match := re.FindStringSubmatch(url) 84 | if len(match) > 1 { 85 | return match[1] 86 | } 87 | return "" 88 | } 89 | -------------------------------------------------------------------------------- /pkg/claude/chat.go: -------------------------------------------------------------------------------- 1 | // https://docs.anthropic.com/claude/reference/messages_post 2 | 3 | package claude 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net/http" 13 | "opencatd-open/pkg/error" 14 | "opencatd-open/pkg/openai" 15 | "opencatd-open/pkg/tokenizer" 16 | "opencatd-open/pkg/vertexai" 17 | "opencatd-open/store" 18 | "strings" 19 | 20 | "github.com/gin-gonic/gin" 21 | ) 22 | 23 | func ChatProxy(c *gin.Context, chatReq *openai.ChatCompletionRequest) { 24 | ChatMessages(c, chatReq) 25 | } 26 | 27 | func ChatTextCompletions(c *gin.Context, chatReq *openai.ChatCompletionRequest) { 28 | 29 | } 30 | 31 | type ChatRequest struct { 32 | Model string `json:"model,omitempty"` 33 | Messages any `json:"messages,omitempty"` 34 | MaxTokens int `json:"max_tokens,omitempty"` 35 | Stream bool `json:"stream,omitempty"` 36 | System string `json:"system,omitempty"` 37 | TopK int `json:"top_k,omitempty"` 38 | TopP float64 `json:"top_p,omitempty"` 39 | Temperature float64 `json:"temperature,omitempty"` 40 | AnthropicVersion string `json:"anthropic_version,omitempty"` 41 | } 42 | 43 | func (c *ChatRequest) ByteJson() []byte { 44 | bytejson, _ := json.Marshal(c) 45 | return bytejson 46 | } 47 | 48 | type ChatMessage struct { 49 | Role string `json:"role,omitempty"` 50 | Content string `json:"content,omitempty"` 51 | } 52 | 53 | type VisionMessages struct { 54 | Role string `json:"role,omitempty"` 55 | Content []VisionContent `json:"content,omitempty"` 56 | } 57 | 58 | type VisionContent struct { 59 | Type string `json:"type,omitempty"` 60 | Source *VisionSource `json:"source,omitempty"` 61 | Text string `json:"text,omitempty"` 62 | } 63 | 64 | type VisionSource struct { 65 | Type string `json:"type,omitempty"` 66 | MediaType string `json:"media_type,omitempty"` 67 | Data string `json:"data,omitempty"` 68 | } 69 | 70 | type ChatResponse struct { 71 | ID string `json:"id"` 72 | Type string `json:"type"` 73 | Role string `json:"role"` 74 | Model string `json:"model"` 75 | StopSequence any `json:"stop_sequence"` 76 | Usage struct { 77 | InputTokens int `json:"input_tokens"` 78 | OutputTokens int `json:"output_tokens"` 79 | } `json:"usage"` 80 | Content []struct { 81 | Type string `json:"type"` 82 | Text string `json:"text"` 83 | } `json:"content"` 84 | StopReason string `json:"stop_reason"` 85 | } 86 | 87 | type ClaudeStreamResponse struct { 88 | Type string `json:"type"` 89 | Index int `json:"index"` 90 | ContentBlock struct { 91 | Type string `json:"type"` 92 | Text string `json:"text"` 93 | } `json:"content_block"` 94 | Delta struct { 95 | Type string `json:"type"` 96 | Text string `json:"text"` 97 | StopReason string `json:"stop_reason"` 98 | StopSequence any `json:"stop_sequence"` 99 | } `json:"delta"` 100 | Message struct { 101 | ID string `json:"id"` 102 | Type string `json:"type"` 103 | Role string `json:"role"` 104 | Content []any `json:"content"` 105 | Model string `json:"model"` 106 | StopReason string `json:"stop_reason"` 107 | StopSequence any `json:"stop_sequence"` 108 | Usage struct { 109 | InputTokens int `json:"input_tokens"` 110 | OutputTokens int `json:"output_tokens"` 111 | } `json:"usage"` 112 | } `json:"message"` 113 | Error struct { 114 | Type string `json:"type"` 115 | Message string `json:"message"` 116 | } `json:"error"` 117 | Usage struct { 118 | OutputTokens int `json:"output_tokens"` 119 | } `json:"usage"` 120 | } 121 | 122 | func ChatMessages(c *gin.Context, chatReq *openai.ChatCompletionRequest) { 123 | var ( 124 | req *http.Request 125 | targetURL = ClaudeMessageEndpoint 126 | ) 127 | 128 | apiKey, err := store.SelectKeyCache("claude") 129 | if err != nil { 130 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 131 | return 132 | } 133 | 134 | usagelog := store.Tokens{Model: chatReq.Model} 135 | var claudReq ChatRequest 136 | claudReq.Model = chatReq.Model 137 | claudReq.Stream = chatReq.Stream 138 | // claudReq.Temperature = chatReq.Temperature 139 | claudReq.TopP = chatReq.TopP 140 | claudReq.MaxTokens = 4096 141 | if apiKey.ApiType == "vertex" { 142 | claudReq.AnthropicVersion = "vertex-2023-10-16" 143 | claudReq.Model = "" 144 | } 145 | 146 | var claudecontent []VisionContent 147 | var prompt string 148 | for _, msg := range chatReq.Messages { 149 | switch ct := msg.Content.(type) { 150 | case string: 151 | prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n" 152 | if msg.Role == "system" { 153 | claudReq.System = msg.Content.(string) 154 | continue 155 | } 156 | claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + msg.Content.(string)}) 157 | case []any: 158 | for _, item := range ct { 159 | if m, ok := item.(map[string]interface{}); ok { 160 | if m["type"] == "text" { 161 | prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n" 162 | claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + m["text"].(string)}) 163 | } else if m["type"] == "image_url" { 164 | if url, ok := m["image_url"].(map[string]interface{}); ok { 165 | fmt.Printf(" URL: %v\n", url["url"]) 166 | if strings.HasPrefix(url["url"].(string), "http") { 167 | fmt.Println("网络图片:", url["url"].(string)) 168 | } else if strings.HasPrefix(url["url"].(string), "data:image") { 169 | fmt.Println("base64:", url["url"].(string)[:20]) 170 | var mediaType string 171 | if strings.HasPrefix(url["url"].(string), "data:image/jpeg") { 172 | mediaType = "image/jpeg" 173 | } 174 | if strings.HasPrefix(url["url"].(string), "data:image/png") { 175 | mediaType = "image/png" 176 | } 177 | claudecontent = append(claudecontent, VisionContent{Type: "image", Source: &VisionSource{Type: "base64", MediaType: mediaType, Data: strings.Split(url["url"].(string), ",")[1]}}) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | default: 184 | c.JSON(http.StatusInternalServerError, gin.H{ 185 | "error": gin.H{ 186 | "message": "Invalid content type", 187 | }, 188 | }) 189 | return 190 | } 191 | if len(chatReq.Tools) > 0 { 192 | tooljson, _ := json.Marshal(chatReq.Tools) 193 | prompt += ": " + string(tooljson) + "\n" 194 | } 195 | } 196 | 197 | claudReq.Messages = []VisionMessages{{Role: "user", Content: claudecontent}} 198 | 199 | usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model) 200 | 201 | if apiKey.ApiType == "vertex" { 202 | var vertexSecret vertexai.VertexSecretKey 203 | if err := json.Unmarshal([]byte(apiKey.ApiSecret), &vertexSecret); err != nil { 204 | c.JSON(http.StatusInternalServerError, error.ErrorData(err.Error())) 205 | return 206 | } 207 | 208 | vcmodel, ok := vertexai.VertexClaudeModelMap[chatReq.Model] 209 | if !ok { 210 | c.JSON(http.StatusInternalServerError, error.ErrorData("Model not found")) 211 | return 212 | } 213 | 214 | // 获取gcloud token,临时放置在apiKey.Key中 215 | gcloudToken, err := vertexai.GcloudAuth(vertexSecret.ClientEmail, vertexSecret.PrivateKey) 216 | if err != nil { 217 | c.JSON(http.StatusInternalServerError, error.ErrorData(err.Error())) 218 | return 219 | } 220 | 221 | // 拼接vertex的请求地址 222 | targetURL = fmt.Sprintf("https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:streamRawPredict", vcmodel.Region, vertexSecret.ProjectID, vcmodel.Region, vcmodel.VertexName) 223 | 224 | req, _ = http.NewRequest("POST", targetURL, bytes.NewReader(claudReq.ByteJson())) 225 | req.Header.Set("Authorization", "Bearer "+gcloudToken) 226 | req.Header.Set("Content-Type", "application/json") 227 | req.Header.Set("Accept", "text/event-stream") 228 | req.Header.Set("Accept-Encoding", "identity") 229 | } else { 230 | req, _ = http.NewRequest("POST", targetURL, bytes.NewReader(claudReq.ByteJson())) 231 | req.Header.Set("x-api-key", apiKey.Key) 232 | req.Header.Set("anthropic-version", "2023-06-01") 233 | req.Header.Set("Content-Type", "application/json") 234 | } 235 | 236 | client := http.DefaultClient 237 | rsp, err := client.Do(req) 238 | if err != nil { 239 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 240 | return 241 | } 242 | defer rsp.Body.Close() 243 | if rsp.StatusCode != http.StatusOK { 244 | io.Copy(c.Writer, rsp.Body) 245 | return 246 | } 247 | var buffer bytes.Buffer 248 | teeReader := io.TeeReader(rsp.Body, &buffer) 249 | 250 | dataChan := make(chan string) 251 | // stopChan := make(chan bool) 252 | 253 | var result string 254 | 255 | scanner := bufio.NewScanner(teeReader) 256 | 257 | go func() { 258 | for scanner.Scan() { 259 | line := scanner.Bytes() 260 | if len(line) > 0 && bytes.HasPrefix(line, []byte("data: ")) { 261 | if bytes.HasPrefix(line, []byte("data: [DONE]")) { 262 | dataChan <- string(line) + "\n" 263 | break 264 | } 265 | var claudeResp ClaudeStreamResponse 266 | line = bytes.Replace(line, []byte("data: "), []byte(""), -1) 267 | line = bytes.TrimSpace(line) 268 | if err := json.Unmarshal(line, &claudeResp); err != nil { 269 | continue 270 | } 271 | 272 | if claudeResp.Type == "message_start" { 273 | if claudeResp.Message.Role != "" { 274 | result += "<" + claudeResp.Message.Role + ">" 275 | } 276 | } else if claudeResp.Type == "message_stop" { 277 | break 278 | } 279 | 280 | if claudeResp.Delta.Text != "" { 281 | result += claudeResp.Delta.Text 282 | } 283 | var choice openai.Choice 284 | choice.Delta.Role = claudeResp.Message.Role 285 | choice.Delta.Content = claudeResp.Delta.Text 286 | choice.FinishReason = claudeResp.Delta.StopReason 287 | 288 | chatResp := openai.ChatCompletionStreamResponse{ 289 | Model: chatReq.Model, 290 | Choices: []openai.Choice{choice}, 291 | } 292 | dataChan <- "data: " + string(chatResp.ByteJson()) + "\n" 293 | if claudeResp.Delta.StopReason != "" { 294 | dataChan <- "\ndata: [DONE]\n" 295 | } 296 | } 297 | } 298 | defer close(dataChan) 299 | }() 300 | 301 | c.Writer.Header().Set("Content-Type", "text/event-stream") 302 | c.Writer.Header().Set("Cache-Control", "no-cache") 303 | c.Writer.Header().Set("Connection", "keep-alive") 304 | c.Writer.Header().Set("Transfer-Encoding", "chunked") 305 | c.Writer.Header().Set("X-Accel-Buffering", "no") 306 | 307 | c.Stream(func(w io.Writer) bool { 308 | if data, ok := <-dataChan; ok { 309 | if strings.HasPrefix(data, "data: ") { 310 | c.Writer.WriteString(data) 311 | // c.Writer.WriteString("\n\n") 312 | } else { 313 | c.Writer.WriteHeader(http.StatusBadGateway) 314 | c.Writer.WriteString(data) 315 | } 316 | c.Writer.Flush() 317 | return true 318 | } 319 | go func() { 320 | usagelog.CompletionCount = tokenizer.NumTokensFromStr(result, chatReq.Model) 321 | usagelog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(usagelog.Model, usagelog.PromptCount, usagelog.CompletionCount)) 322 | if err := store.Record(&usagelog); err != nil { 323 | log.Println(err) 324 | } 325 | if err := store.SumDaily(usagelog.UserID); err != nil { 326 | log.Println(err) 327 | } 328 | }() 329 | return false 330 | }) 331 | } 332 | -------------------------------------------------------------------------------- /pkg/claude/claude.go: -------------------------------------------------------------------------------- 1 | /* 2 | https://docs.anthropic.com/claude/reference/complete_post 3 | 4 | curl --request POST \ 5 | --url https://api.anthropic.com/v1/complete \ 6 | --header "anthropic-version: 2023-06-01" \ 7 | --header "content-type: application/json" \ 8 | --header "x-api-key: $ANTHROPIC_API_KEY" \ 9 | --data ' 10 | { 11 | "model": "claude-2", 12 | "prompt": "\n\nHuman: Hello, world!\n\nAssistant:", 13 | "max_tokens_to_sample": 256, 14 | "stream": true 15 | } 16 | ' 17 | 18 | {"completion":" Hello! Nice to meet you.","stop_reason":"stop_sequence","model":"claude-2.0","stop":"\n\nHuman:","log_id":"727bded01002627057967d02b3d557a01aa73266849b62f5aa0b97dec1247ed3"} 19 | 20 | event: completion 21 | data: {"completion":"","stop_reason":"stop_sequence","model":"claude-2.0","stop":"\n\nHuman:","log_id":"dfd42341ad08856ff01811885fb8640a1bf977551d8331f81fe9a6c8182c6c63"} 22 | 23 | # Model Pricing 24 | 25 | Claude Instant |100,000 tokens |Prompt $1.63/million tokens |Completion $5.51/million tokens 26 | 27 | Claude 2 |100,000 tokens |Prompt $11.02/million tokens |Completion $32.68/million tokens 28 | *Claude 1 is still accessible and offered at the same price as Claude 2. 29 | 30 | # AWS 31 | https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-service.html 32 | https://aws.amazon.com/cn/bedrock/pricing/ 33 | Anthropic models Price for 1000 input tokens Price for 1000 output tokens 34 | Claude Instant $0.00163 $0.00551 35 | 36 | Claude $0.01102 $0.03268 37 | 38 | https://docs.aws.amazon.com/bedrock/latest/userguide/endpointsTable.html 39 | 地区名称 地区 端点 协议 40 | 美国东部(弗吉尼亚北部) 美国东部1 bedrock-runtime.us-east-1.amazonaws.com HTTPS 41 | bedrock-runtime-fips.us-east-1.amazonaws.com HTTPS 42 | 美国西部(俄勒冈州) 美国西2号 bedrock-runtime.us-west-2.amazonaws.com HTTPS 43 | bedrock-runtime-fips.us-west-2.amazonaws.com HTTPS 44 | 亚太地区(新加坡) ap-东南-1 bedrock-runtime.ap-southeast-1.amazonaws.com HTTPS 45 | */ 46 | 47 | // package anthropic 48 | package claude 49 | 50 | import ( 51 | "bufio" 52 | "bytes" 53 | "encoding/json" 54 | "fmt" 55 | "io" 56 | "log" 57 | "net/http" 58 | "net/http/httputil" 59 | "net/url" 60 | "opencatd-open/pkg/tokenizer" 61 | "opencatd-open/store" 62 | "strings" 63 | "sync" 64 | "time" 65 | 66 | "github.com/gin-gonic/gin" 67 | "github.com/sashabaranov/go-openai" 68 | ) 69 | 70 | var ( 71 | ClaudeUrl = "https://api.anthropic.com/v1/complete" 72 | ClaudeMessageEndpoint = "https://api.anthropic.com/v1/messages" 73 | ) 74 | 75 | type MessageModule struct { 76 | Assistant string // returned data (do not modify) 77 | Human string // input content 78 | } 79 | 80 | type CompleteRequest struct { 81 | Model string `json:"model,omitempty"` //* 82 | Prompt string `json:"prompt,omitempty"` //* 83 | MaxTokensToSample int `json:"max_tokens_to_sample,omitempty"` //* 84 | StopSequences string `json:"stop_sequences,omitempty"` 85 | Temperature int `json:"temperature,omitempty"` 86 | TopP int `json:"top_p,omitempty"` 87 | TopK int `json:"top_k,omitempty"` 88 | Stream bool `json:"stream,omitempty"` 89 | Metadata struct { 90 | UserId string `json:"user_Id,omitempty"` 91 | } `json:"metadata,omitempty"` 92 | } 93 | 94 | type CompleteResponse struct { 95 | Completion string `json:"completion"` 96 | StopReason string `json:"stop_reason"` 97 | Model string `json:"model"` 98 | Stop string `json:"stop"` 99 | LogID string `json:"log_id"` 100 | } 101 | 102 | func Create() { 103 | complet := CompleteRequest{ 104 | Model: "claude-2", 105 | Prompt: "Human: Hello, world!\\n\\nAssistant:", 106 | Stream: true, 107 | } 108 | var payload *bytes.Buffer 109 | json.NewEncoder(payload).Encode(complet) 110 | 111 | // payload := strings.NewReader("{\"model\":\"claude-2\",\"prompt\":\"\\n\\nHuman: Hello, world!\\n\\nAssistant:\",\"max_tokens_to_sample\":256}") 112 | 113 | req, _ := http.NewRequest("POST", ClaudeUrl, payload) 114 | 115 | req.Header.Add("accept", "application/json") 116 | req.Header.Add("anthropic-version", "2023-06-01") 117 | req.Header.Add("x-api-key", "$ANTHROPIC_API_KEY") 118 | req.Header.Add("content-type", "application/json") 119 | 120 | res, _ := http.DefaultClient.Do(req) 121 | 122 | defer res.Body.Close() 123 | // body, _ := io.ReadAll(res.Body) 124 | 125 | // fmt.Println(string(body)) 126 | reader := bufio.NewReader(res.Body) 127 | for { 128 | line, err := reader.ReadString('\n') 129 | if err == nil { 130 | if strings.HasPrefix(line, "data:") { 131 | fmt.Println(line) 132 | // var result CompleteResponse 133 | // json.Unmarshal() 134 | } else { 135 | continue 136 | } 137 | } else { 138 | break 139 | } 140 | } 141 | } 142 | 143 | func ClaudeProxy(c *gin.Context) { 144 | var chatlog store.Tokens 145 | var complete CompleteRequest 146 | 147 | byteBody, _ := io.ReadAll(c.Request.Body) 148 | c.Request.Body = io.NopCloser(bytes.NewBuffer(byteBody)) 149 | 150 | if err := json.Unmarshal(byteBody, &complete); err != nil { 151 | c.AbortWithError(http.StatusBadRequest, err) 152 | return 153 | } 154 | 155 | key, err := store.SelectKeyCache("claude") //anthropic 156 | if err != nil { 157 | c.JSON(http.StatusInternalServerError, gin.H{ 158 | "error": gin.H{ 159 | "message": err.Error(), 160 | }, 161 | }) 162 | return 163 | } 164 | 165 | chatlog.Model = complete.Model 166 | 167 | token, _ := c.Get("localuser") 168 | 169 | lu, err := store.GetUserByToken(token.(string)) 170 | if err != nil { 171 | c.JSON(http.StatusInternalServerError, gin.H{ 172 | "error": gin.H{ 173 | "message": err.Error(), 174 | }, 175 | }) 176 | return 177 | } 178 | chatlog.UserID = int(lu.ID) 179 | 180 | chatlog.PromptCount = tokenizer.NumTokensFromStr(complete.Prompt, complete.Model) 181 | 182 | if key.EndPoint == "" { 183 | key.EndPoint = "https://api.anthropic.com" 184 | } 185 | targetUrl, _ := url.ParseRequestURI(key.EndPoint + c.Request.URL.String()) 186 | 187 | proxy := httputil.NewSingleHostReverseProxy(targetUrl) 188 | proxy.Director = func(req *http.Request) { 189 | req.Host = targetUrl.Host 190 | req.URL.Scheme = targetUrl.Scheme 191 | req.URL.Host = targetUrl.Host 192 | 193 | req.Header.Set("anthropic-version", "2023-06-01") 194 | req.Header.Set("content-type", "application/json") 195 | req.Header.Set("x-api-key", key.Key) 196 | } 197 | 198 | proxy.ModifyResponse = func(resp *http.Response) error { 199 | if resp.StatusCode != http.StatusOK { 200 | return nil 201 | } 202 | var byteResp []byte 203 | byteResp, _ = io.ReadAll(resp.Body) 204 | resp.Body = io.NopCloser(bytes.NewBuffer(byteResp)) 205 | if complete.Stream != true { 206 | var complete_resp CompleteResponse 207 | 208 | if err := json.Unmarshal(byteResp, &complete_resp); err != nil { 209 | log.Println(err) 210 | return nil 211 | } 212 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(complete_resp.Completion, chatlog.Model) 213 | } else { 214 | var completion string 215 | for { 216 | line, err := bufio.NewReader(bytes.NewBuffer(byteResp)).ReadString('\n') 217 | if err != nil { 218 | if strings.HasPrefix(line, "data:") { 219 | line = strings.TrimSpace(strings.TrimPrefix(line, "data:")) 220 | if strings.HasSuffix(line, "[DONE]") { 221 | break 222 | } 223 | line = strings.TrimSpace(line) 224 | var complete_resp CompleteResponse 225 | if err := json.Unmarshal([]byte(line), &complete_resp); err != nil { 226 | log.Println(err) 227 | break 228 | } 229 | completion += line 230 | } 231 | } 232 | } 233 | log.Println("completion:", completion) 234 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(completion, chatlog.Model) 235 | } 236 | 237 | // calc cost 238 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount 239 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)) 240 | 241 | if err := store.Record(&chatlog); err != nil { 242 | log.Println(err) 243 | } 244 | if err := store.SumDaily(chatlog.UserID); err != nil { 245 | log.Println(err) 246 | } 247 | return nil 248 | } 249 | proxy.ServeHTTP(c.Writer, c.Request) 250 | } 251 | 252 | func TransReq(chatreq *openai.ChatCompletionRequest) (*bytes.Buffer, error) { 253 | transReq := CompleteRequest{ 254 | Model: chatreq.Model, 255 | Temperature: int(chatreq.Temperature), 256 | TopP: int(chatreq.TopP), 257 | Stream: chatreq.Stream, 258 | MaxTokensToSample: chatreq.MaxTokens, 259 | } 260 | if transReq.MaxTokensToSample == 0 { 261 | transReq.MaxTokensToSample = 100000 262 | } 263 | var prompt string 264 | for _, msg := range chatreq.Messages { 265 | switch msg.Role { 266 | case "system": 267 | prompt += fmt.Sprintf("\n\nHuman:%s", msg.Content) 268 | case "user": 269 | prompt += fmt.Sprintf("\n\nHuman:%s", msg.Content) 270 | case "assistant": 271 | prompt += fmt.Sprintf("\n\nAssistant:%s", msg.Content) 272 | } 273 | } 274 | transReq.Prompt = prompt + "\n\nAssistant:" 275 | var payload = bytes.NewBuffer(nil) 276 | if err := json.NewEncoder(payload).Encode(transReq); err != nil { 277 | return nil, err 278 | } 279 | return payload, nil 280 | } 281 | 282 | func TransRsp(c *gin.Context, isStream bool, chatlog store.Tokens, reader *bufio.Reader) { 283 | if !isStream { 284 | var completersp CompleteResponse 285 | var chatrsp openai.ChatCompletionResponse 286 | json.NewDecoder(reader).Decode(&completersp) 287 | chatrsp.Model = completersp.Model 288 | chatrsp.ID = completersp.LogID 289 | chatrsp.Object = "chat.completion" 290 | chatrsp.Created = time.Now().Unix() 291 | choice := openai.ChatCompletionChoice{ 292 | Index: 0, 293 | FinishReason: "stop", 294 | Message: openai.ChatCompletionMessage{ 295 | Role: "assistant", 296 | Content: completersp.Completion, 297 | }, 298 | } 299 | chatrsp.Choices = append(chatrsp.Choices, choice) 300 | var payload *bytes.Buffer 301 | if err := json.NewEncoder(payload).Encode(chatrsp); err != nil { 302 | c.JSON(http.StatusInternalServerError, gin.H{ 303 | "error": gin.H{ 304 | "message": err.Error(), 305 | }, 306 | }) 307 | return 308 | } 309 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(completersp.Completion, chatlog.Model) 310 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount 311 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)) 312 | if err := store.Record(&chatlog); err != nil { 313 | log.Println(err) 314 | } 315 | if err := store.SumDaily(chatlog.UserID); err != nil { 316 | log.Println(err) 317 | } 318 | 319 | c.JSON(http.StatusOK, payload) 320 | return 321 | } else { 322 | var ( 323 | wg sync.WaitGroup 324 | dataChan = make(chan string) 325 | stopChan = make(chan bool) 326 | complete_resp string 327 | ) 328 | wg.Add(2) 329 | go func() { 330 | defer wg.Done() 331 | for { 332 | line, err := reader.ReadString('\n') 333 | if err == nil { 334 | if strings.HasPrefix(line, "data: ") { 335 | var result CompleteResponse 336 | json.NewDecoder(strings.NewReader(line[6:])).Decode(&result) 337 | if result.StopReason == "" { 338 | if result.Completion != "" { 339 | complete_resp += result.Completion 340 | chatrsp := openai.ChatCompletionStreamResponse{ 341 | ID: result.LogID, 342 | Model: result.Model, 343 | Object: "chat.completion", 344 | Created: time.Now().Unix(), 345 | } 346 | choice := openai.ChatCompletionStreamChoice{ 347 | Delta: openai.ChatCompletionStreamChoiceDelta{ 348 | Role: "assistant", 349 | Content: result.Completion, 350 | }, 351 | FinishReason: "", 352 | } 353 | chatrsp.Choices = append(chatrsp.Choices, choice) 354 | bytedate, _ := json.Marshal(chatrsp) 355 | dataChan <- string(bytedate) 356 | } 357 | } else { 358 | chatrsp := openai.ChatCompletionStreamResponse{ 359 | ID: result.LogID, 360 | Model: result.Model, 361 | Object: "chat.completion", 362 | Created: time.Now().Unix(), 363 | } 364 | choice := openai.ChatCompletionStreamChoice{ 365 | Delta: openai.ChatCompletionStreamChoiceDelta{ 366 | Role: "assistant", 367 | Content: result.Completion, 368 | }, 369 | } 370 | choice.FinishReason = openai.FinishReason(TranslatestopReason(result.StopReason)) 371 | chatrsp.Choices = append(chatrsp.Choices, choice) 372 | bytedate, _ := json.Marshal(chatrsp) 373 | dataChan <- string(bytedate) 374 | dataChan <- "[DONE]" 375 | break 376 | } 377 | } else { 378 | continue 379 | } 380 | } else { 381 | break 382 | } 383 | } 384 | 385 | close(dataChan) 386 | stopChan <- true 387 | close(stopChan) 388 | }() 389 | 390 | go func() { 391 | defer wg.Done() 392 | Loop: 393 | for { 394 | select { 395 | case data := <-dataChan: 396 | if data != "" { 397 | c.Writer.WriteString("data: " + data) 398 | c.Writer.WriteString("\n\n") 399 | c.Writer.Flush() 400 | } 401 | case <-stopChan: 402 | break Loop 403 | } 404 | } 405 | }() 406 | wg.Wait() 407 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(complete_resp, chatlog.Model) 408 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount 409 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)) 410 | if err := store.Record(&chatlog); err != nil { 411 | log.Println(err) 412 | } 413 | if err := store.SumDaily(chatlog.UserID); err != nil { 414 | log.Println(err) 415 | } 416 | } 417 | } 418 | 419 | // claude -> openai 420 | func TranslatestopReason(reason string) string { 421 | switch reason { 422 | case "stop_sequence": 423 | return "stop" 424 | case "max_tokens": 425 | return "length" 426 | default: 427 | return reason 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /pkg/error/errdata.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func ErrorData(message string) gin.H { 6 | return gin.H{ 7 | "error": gin.H{ 8 | "message": message, 9 | }, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pkg/google/chat.go: -------------------------------------------------------------------------------- 1 | // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/grounding-search-entry-points?authuser=2&hl=zh-cn 2 | // 3 | // https://cloud.google.com/vertex-ai/docs/generative-ai/quotas-genai 4 | package google 5 | 6 | import ( 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "log" 13 | "net/http" 14 | "opencatd-open/pkg/openai" 15 | "opencatd-open/pkg/tokenizer" 16 | "opencatd-open/store" 17 | "strings" 18 | 19 | "github.com/gin-gonic/gin" 20 | "github.com/google/generative-ai-go/genai" 21 | "google.golang.org/api/iterator" 22 | "google.golang.org/api/option" 23 | ) 24 | 25 | type GeminiChatRequest struct { 26 | Contents []GeminiContent `json:"contents,omitempty"` 27 | } 28 | 29 | func (g GeminiChatRequest) ByteJson() []byte { 30 | bytejson, _ := json.Marshal(g) 31 | return bytejson 32 | } 33 | 34 | type GeminiContent struct { 35 | Role string `json:"role,omitempty"` 36 | Parts []GeminiPart `json:"parts,omitempty"` 37 | } 38 | type GeminiPart struct { 39 | Text string `json:"text,omitempty"` 40 | // InlineData GeminiPartInlineData `json:"inlineData,omitempty"` 41 | } 42 | type GeminiPartInlineData struct { 43 | MimeType string `json:"mimeType,omitempty"` 44 | Data string `json:"data,omitempty"` // base64 45 | } 46 | 47 | type GeminiResponse struct { 48 | Candidates []struct { 49 | Content struct { 50 | Parts []struct { 51 | Text string `json:"text"` 52 | } `json:"parts"` 53 | Role string `json:"role"` 54 | } `json:"content"` 55 | FinishReason string `json:"finishReason"` 56 | Index int `json:"index"` 57 | SafetyRatings []struct { 58 | Category string `json:"category"` 59 | Probability string `json:"probability"` 60 | } `json:"safetyRatings"` 61 | } `json:"candidates"` 62 | PromptFeedback struct { 63 | SafetyRatings []struct { 64 | Category string `json:"category"` 65 | Probability string `json:"probability"` 66 | } `json:"safetyRatings"` 67 | } `json:"promptFeedback"` 68 | Error struct { 69 | Code int `json:"code"` 70 | Message string `json:"message"` 71 | Status string `json:"status"` 72 | Details []struct { 73 | Type string `json:"@type"` 74 | FieldViolations []struct { 75 | Field string `json:"field"` 76 | Description string `json:"description"` 77 | } `json:"fieldViolations"` 78 | } `json:"details"` 79 | } `json:"error"` 80 | } 81 | 82 | func ChatProxy(c *gin.Context, chatReq *openai.ChatCompletionRequest) { 83 | usagelog := store.Tokens{Model: chatReq.Model} 84 | 85 | token, _ := c.Get("localuser") 86 | 87 | lu, err := store.GetUserByToken(token.(string)) 88 | if err != nil { 89 | c.JSON(http.StatusInternalServerError, gin.H{ 90 | "error": gin.H{ 91 | "message": err.Error(), 92 | }, 93 | }) 94 | return 95 | } 96 | usagelog.UserID = int(lu.ID) 97 | var prompts []genai.Part 98 | var prompt string 99 | for _, msg := range chatReq.Messages { 100 | switch ct := msg.Content.(type) { 101 | case string: 102 | prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n" 103 | prompts = append(prompts, genai.Text("<"+msg.Role+">: "+msg.Content.(string))) 104 | case []any: 105 | for _, item := range ct { 106 | if m, ok := item.(map[string]interface{}); ok { 107 | if m["type"] == "text" { 108 | prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n" 109 | prompts = append(prompts, genai.Text("<"+msg.Role+">: "+m["text"].(string))) 110 | } else if m["type"] == "image_url" { 111 | if url, ok := m["image_url"].(map[string]interface{}); ok { 112 | if strings.HasPrefix(url["url"].(string), "http") { 113 | fmt.Println("网络图片:", url["url"].(string)) 114 | } else if strings.HasPrefix(url["url"].(string), "data:image") { 115 | fmt.Println("base64:", url["url"].(string)[:20]) 116 | var mime string 117 | // openai 会以 data:image 开头,则去掉 data:image/png;base64, 和 data:image/jpeg;base64, 118 | if strings.HasPrefix(url["url"].(string), "data:image/png") { 119 | mime = "image/png" 120 | } else if strings.HasPrefix(url["url"].(string), "data:image/jpeg") { 121 | mime = "image/jpeg" 122 | } else { 123 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Unsupported image format"}) 124 | return 125 | } 126 | imageString := strings.Split(url["url"].(string), ",")[1] 127 | imageBytes, err := base64.StdEncoding.DecodeString(imageString) 128 | if err != nil { 129 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 130 | return 131 | } 132 | prompts = append(prompts, genai.Blob{MIMEType: mime, Data: imageBytes}) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | default: 139 | c.JSON(http.StatusInternalServerError, gin.H{ 140 | "error": gin.H{ 141 | "message": "Invalid content type", 142 | }, 143 | }) 144 | return 145 | } 146 | if len(chatReq.Tools) > 0 { 147 | tooljson, _ := json.Marshal(chatReq.Tools) 148 | prompt += ": " + string(tooljson) + "\n" 149 | } 150 | } 151 | 152 | usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model) 153 | 154 | onekey, err := store.SelectKeyCache("google") 155 | if err != nil { 156 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 157 | return 158 | } 159 | 160 | ctx := context.Background() 161 | 162 | client, err := genai.NewClient(ctx, option.WithAPIKey(onekey.Key)) 163 | if err != nil { 164 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 165 | return 166 | } 167 | defer client.Close() 168 | 169 | model := client.GenerativeModel(chatReq.Model) 170 | model.Tools = []*genai.Tool{} 171 | 172 | iter := model.GenerateContentStream(ctx, prompts...) 173 | datachan := make(chan string) 174 | // closechan := make(chan error) 175 | var result string 176 | go func() { 177 | for { 178 | resp, err := iter.Next() 179 | if err == iterator.Done { 180 | 181 | var chatResp openai.ChatCompletionStreamResponse 182 | chatResp.Model = chatReq.Model 183 | choice := openai.Choice{} 184 | choice.FinishReason = "stop" 185 | chatResp.Choices = append(chatResp.Choices, choice) 186 | datachan <- "data: " + string(chatResp.ByteJson()) 187 | close(datachan) 188 | break 189 | } 190 | if err != nil { 191 | log.Println(err) 192 | var errResp openai.ErrResponse 193 | errResp.Error.Code = "500" 194 | errResp.Error.Message = err.Error() 195 | datachan <- string(errResp.ByteJson()) 196 | close(datachan) 197 | break 198 | } 199 | var content string 200 | if resp.Candidates != nil && len(resp.Candidates) > 0 && len(resp.Candidates[0].Content.Parts) > 0 { 201 | if s, ok := resp.Candidates[0].Content.Parts[0].(genai.Text); ok { 202 | content = string(s) 203 | result += content 204 | } 205 | } else { 206 | continue 207 | } 208 | 209 | var chatResp openai.ChatCompletionStreamResponse 210 | chatResp.Model = chatReq.Model 211 | choice := openai.Choice{} 212 | choice.Delta.Role = resp.Candidates[0].Content.Role 213 | choice.Delta.Content = content 214 | chatResp.Choices = append(chatResp.Choices, choice) 215 | 216 | chunk := "data: " + string(chatResp.ByteJson()) + "\n\n" 217 | datachan <- chunk 218 | } 219 | }() 220 | 221 | c.Writer.Header().Set("Content-Type", "text/event-stream") 222 | c.Writer.Header().Set("Cache-Control", "no-cache") 223 | c.Writer.Header().Set("Connection", "keep-alive") 224 | c.Writer.Header().Set("Transfer-Encoding", "chunked") 225 | c.Writer.Header().Set("X-Accel-Buffering", "no") 226 | 227 | c.Stream(func(w io.Writer) bool { 228 | if data, ok := <-datachan; ok { 229 | if strings.HasPrefix(data, "data: ") { 230 | c.Writer.WriteString(data) 231 | // c.Writer.WriteString("\n\n") 232 | } else { 233 | c.Writer.WriteHeader(http.StatusBadGateway) 234 | c.Writer.WriteString(data) 235 | } 236 | c.Writer.Flush() 237 | return true 238 | } 239 | go func() { 240 | 241 | }() 242 | return false 243 | }) 244 | } 245 | -------------------------------------------------------------------------------- /pkg/openai/chat.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "opencatd-open/pkg/tokenizer" 12 | "opencatd-open/store" 13 | "os" 14 | "strings" 15 | 16 | "github.com/gin-gonic/gin" 17 | ) 18 | 19 | const ( 20 | // https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation#latest-preview-api-releases 21 | AzureApiVersion = "2024-06-01" 22 | BaseHost = "api.openai.com" 23 | OpenAI_Endpoint = "https://api.openai.com/v1/chat/completions" 24 | Github_Marketplace = "https://models.inference.ai.azure.com/chat/completions" 25 | ) 26 | 27 | var ( 28 | Custom_Endpoint string 29 | AIGateWay_Endpoint string // "https://gateway.ai.cloudflare.com/v1/431ba10f11200d544922fbca177aaa7f/openai/openai/chat/completions" 30 | ) 31 | 32 | func init() { 33 | if os.Getenv("OpenAI_Endpoint") != "" { 34 | Custom_Endpoint = os.Getenv("OpenAI_Endpoint") 35 | } 36 | if os.Getenv("AIGateWay_Endpoint") != "" { 37 | AIGateWay_Endpoint = os.Getenv("AIGateWay_Endpoint") 38 | } 39 | } 40 | 41 | // Vision Content 42 | type VisionContent struct { 43 | Type string `json:"type,omitempty"` 44 | Text string `json:"text,omitempty"` 45 | ImageURL *VisionImageURL `json:"image_url,omitempty"` 46 | } 47 | type VisionImageURL struct { 48 | URL string `json:"url,omitempty"` 49 | Detail string `json:"detail,omitempty"` 50 | } 51 | 52 | type ChatCompletionMessage struct { 53 | Role string `json:"role"` 54 | Content any `json:"content"` 55 | Name string `json:"name,omitempty"` 56 | // MultiContent []VisionContent 57 | } 58 | 59 | type FunctionDefinition struct { 60 | Name string `json:"name"` 61 | Description string `json:"description,omitempty"` 62 | Parameters any `json:"parameters"` 63 | } 64 | 65 | type Tool struct { 66 | Type string `json:"type"` 67 | Function *FunctionDefinition `json:"function,omitempty"` 68 | } 69 | 70 | type StreamOption struct { 71 | IncludeUsage bool `json:"include_usage,omitempty"` 72 | } 73 | 74 | type ChatCompletionRequest struct { 75 | Model string `json:"model"` 76 | Messages []ChatCompletionMessage `json:"messages"` 77 | MaxTokens int `json:"max_tokens,omitempty"` 78 | Temperature float64 `json:"temperature,omitempty"` 79 | TopP float64 `json:"top_p,omitempty"` 80 | N int `json:"n,omitempty"` 81 | Stream bool `json:"stream"` 82 | Stop []string `json:"stop,omitempty"` 83 | PresencePenalty float64 `json:"presence_penalty,omitempty"` 84 | FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` 85 | LogitBias map[string]int `json:"logit_bias,omitempty"` 86 | User string `json:"user,omitempty"` 87 | // Functions []FunctionDefinition `json:"functions,omitempty"` 88 | // FunctionCall any `json:"function_call,omitempty"` 89 | Tools []Tool `json:"tools,omitempty"` 90 | ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"` 91 | // ToolChoice any `json:"tool_choice,omitempty"` 92 | StreamOptions *StreamOption `json:"stream_options,omitempty"` 93 | } 94 | 95 | func (c ChatCompletionRequest) ToByteJson() []byte { 96 | bytejson, _ := json.Marshal(c) 97 | return bytejson 98 | } 99 | 100 | type ToolCall struct { 101 | ID string `json:"id"` 102 | Type string `json:"type"` 103 | Function struct { 104 | Name string `json:"name"` 105 | Arguments string `json:"arguments"` 106 | } `json:"function"` 107 | } 108 | 109 | type ChatCompletionResponse struct { 110 | ID string `json:"id,omitempty"` 111 | Object string `json:"object,omitempty"` 112 | Created int `json:"created,omitempty"` 113 | Model string `json:"model,omitempty"` 114 | Choices []struct { 115 | Index int `json:"index,omitempty"` 116 | Message struct { 117 | Role string `json:"role,omitempty"` 118 | Content string `json:"content,omitempty"` 119 | ToolCalls []ToolCall `json:"tool_calls,omitempty"` 120 | } `json:"message,omitempty"` 121 | Logprobs string `json:"logprobs,omitempty"` 122 | FinishReason string `json:"finish_reason,omitempty"` 123 | } `json:"choices,omitempty"` 124 | Usage struct { 125 | PromptTokens int `json:"prompt_tokens,omitempty"` 126 | CompletionTokens int `json:"completion_tokens,omitempty"` 127 | TotalTokens int `json:"total_tokens,omitempty"` 128 | PromptTokensDetails struct { 129 | CachedTokens int `json:"cached_tokens,omitempty"` 130 | AudioTokens int `json:"audio_tokens,omitempty"` 131 | } `json:"prompt_tokens_details,omitempty"` 132 | CompletionTokensDetails struct { 133 | ReasoningTokens int `json:"reasoning_tokens,omitempty"` 134 | AudioTokens int `json:"audio_tokens,omitempty"` 135 | AcceptedPredictionTokens int `json:"accepted_prediction_tokens,omitempty"` 136 | RejectedPredictionTokens int `json:"rejected_prediction_tokens,omitempty"` 137 | } `json:"completion_tokens_details,omitempty"` 138 | } `json:"usage,omitempty"` 139 | SystemFingerprint string `json:"system_fingerprint,omitempty"` 140 | } 141 | 142 | type Choice struct { 143 | Index int `json:"index"` 144 | Delta struct { 145 | Role string `json:"role"` 146 | Content string `json:"content"` 147 | ToolCalls []ToolCall `json:"tool_calls"` 148 | } `json:"delta"` 149 | FinishReason string `json:"finish_reason"` 150 | Usage struct { 151 | PromptTokens int `json:"prompt_tokens"` 152 | CompletionTokens int `json:"completion_tokens"` 153 | TotalTokens int `json:"total_tokens"` 154 | } `json:"usage"` 155 | } 156 | 157 | type ChatCompletionStreamResponse struct { 158 | ID string `json:"id"` 159 | Object string `json:"object"` 160 | Created int `json:"created"` 161 | Model string `json:"model"` 162 | Choices []Choice `json:"choices"` 163 | } 164 | 165 | func (c *ChatCompletionStreamResponse) ByteJson() []byte { 166 | bytejson, _ := json.Marshal(c) 167 | return bytejson 168 | } 169 | 170 | func ChatProxy(c *gin.Context, chatReq *ChatCompletionRequest) { 171 | usagelog := store.Tokens{Model: chatReq.Model} 172 | 173 | token, _ := c.Get("localuser") 174 | 175 | lu, err := store.GetUserByToken(token.(string)) 176 | if err != nil { 177 | c.JSON(http.StatusInternalServerError, gin.H{ 178 | "error": gin.H{ 179 | "message": err.Error(), 180 | }, 181 | }) 182 | return 183 | } 184 | usagelog.UserID = int(lu.ID) 185 | 186 | var prompt string 187 | for _, msg := range chatReq.Messages { 188 | switch ct := msg.Content.(type) { 189 | case string: 190 | prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n" 191 | case []any: 192 | for _, item := range ct { 193 | if m, ok := item.(map[string]interface{}); ok { 194 | if m["type"] == "text" { 195 | prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n" 196 | } else if m["type"] == "image_url" { 197 | if url, ok := m["image_url"].(map[string]interface{}); ok { 198 | fmt.Printf(" URL: %v\n", url["url"]) 199 | if strings.HasPrefix(url["url"].(string), "http") { 200 | fmt.Println("网络图片:", url["url"].(string)) 201 | } 202 | } 203 | } 204 | } 205 | } 206 | default: 207 | c.JSON(http.StatusInternalServerError, gin.H{ 208 | "error": gin.H{ 209 | "message": "Invalid content type", 210 | }, 211 | }) 212 | return 213 | } 214 | if len(chatReq.Tools) > 0 { 215 | tooljson, _ := json.Marshal(chatReq.Tools) 216 | prompt += ": " + string(tooljson) + "\n" 217 | } 218 | } 219 | switch chatReq.Model { 220 | case "gpt-4o", "gpt-4o-mini", "chatgpt-4o-latest": 221 | chatReq.MaxTokens = 16384 222 | } 223 | if chatReq.Stream { 224 | chatReq.StreamOptions = &StreamOption{IncludeUsage: true} 225 | } 226 | 227 | usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model) 228 | 229 | // onekey, err := store.SelectKeyCache("openai") 230 | onekey, err := store.SelectKeyCacheByModel(chatReq.Model) 231 | if err != nil { 232 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 233 | return 234 | } 235 | 236 | var req *http.Request 237 | 238 | switch onekey.ApiType { 239 | case "github": 240 | req, err = http.NewRequest(c.Request.Method, Github_Marketplace, bytes.NewReader(chatReq.ToByteJson())) 241 | req.Header = c.Request.Header 242 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", onekey.Key)) 243 | case "azure": 244 | var buildurl string 245 | if onekey.EndPoint != "" { 246 | buildurl = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", onekey.EndPoint, modelmap(chatReq.Model), AzureApiVersion) 247 | } else { 248 | buildurl = fmt.Sprintf("https://%s.openai.azure.com/openai/deployments/%s/chat/completions?api-version=%s", onekey.ResourceNmae, modelmap(chatReq.Model), AzureApiVersion) 249 | } 250 | req, err = http.NewRequest(c.Request.Method, buildurl, bytes.NewReader(chatReq.ToByteJson())) 251 | req.Header = c.Request.Header 252 | req.Header.Set("api-key", onekey.Key) 253 | default: 254 | req, err = http.NewRequest(c.Request.Method, OpenAI_Endpoint, bytes.NewReader(chatReq.ToByteJson())) // default endpoint 255 | 256 | if AIGateWay_Endpoint != "" { // cloudflare gateway的endpoint 257 | req, err = http.NewRequest(c.Request.Method, AIGateWay_Endpoint, bytes.NewReader(chatReq.ToByteJson())) 258 | } 259 | if Custom_Endpoint != "" { // 自定义endpoint 260 | req, err = http.NewRequest(c.Request.Method, Custom_Endpoint, bytes.NewReader(chatReq.ToByteJson())) 261 | } 262 | if onekey.EndPoint != "" { // 优先key的endpoint 263 | req, err = http.NewRequest(c.Request.Method, onekey.EndPoint+c.Request.RequestURI, bytes.NewReader(chatReq.ToByteJson())) 264 | } 265 | 266 | req.Header = c.Request.Header 267 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", onekey.Key)) 268 | } 269 | if err != nil { 270 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 271 | return 272 | } 273 | 274 | resp, err := http.DefaultClient.Do(req) 275 | if err != nil { 276 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 277 | return 278 | } 279 | defer resp.Body.Close() 280 | 281 | var result string 282 | if chatReq.Stream { 283 | for key, value := range resp.Header { 284 | for _, v := range value { 285 | c.Writer.Header().Add(key, v) 286 | } 287 | } 288 | c.Writer.WriteHeader(resp.StatusCode) 289 | teeReader := io.TeeReader(resp.Body, c.Writer) 290 | // 流式响应 291 | scanner := bufio.NewScanner(teeReader) 292 | 293 | for scanner.Scan() { 294 | line := scanner.Bytes() 295 | if len(line) > 0 && bytes.HasPrefix(line, []byte("data: ")) { 296 | if bytes.HasPrefix(line, []byte("data: [DONE]")) { 297 | break 298 | } 299 | var opiResp ChatCompletionStreamResponse 300 | line = bytes.Replace(line, []byte("data: "), []byte(""), -1) 301 | line = bytes.TrimSpace(line) 302 | if err := json.Unmarshal(line, &opiResp); err != nil { 303 | continue 304 | } 305 | 306 | if opiResp.Choices != nil && len(opiResp.Choices) > 0 { 307 | if opiResp.Choices[0].Delta.Role != "" { 308 | result += "<" + opiResp.Choices[0].Delta.Role + "> " 309 | } 310 | result += opiResp.Choices[0].Delta.Content // 计算Content Token 311 | 312 | if len(opiResp.Choices[0].Delta.ToolCalls) > 0 { // 计算ToolCalls token 313 | if opiResp.Choices[0].Delta.ToolCalls[0].Function.Name != "" { 314 | result += "name:" + opiResp.Choices[0].Delta.ToolCalls[0].Function.Name + " arguments:" 315 | } 316 | result += opiResp.Choices[0].Delta.ToolCalls[0].Function.Arguments 317 | } 318 | } else { 319 | continue 320 | } 321 | } 322 | 323 | } 324 | } else { 325 | 326 | // 处理非流式响应 327 | body, err := io.ReadAll(resp.Body) 328 | if err != nil { 329 | fmt.Println("Error reading response body:", err) 330 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 331 | return 332 | } 333 | var opiResp ChatCompletionResponse 334 | if err := json.Unmarshal(body, &opiResp); err != nil { 335 | log.Println("Error parsing JSON:", err) 336 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 337 | return 338 | } 339 | if opiResp.Choices != nil && len(opiResp.Choices) > 0 { 340 | if opiResp.Choices[0].Message.Role != "" { 341 | result += "<" + opiResp.Choices[0].Message.Role + "> " 342 | } 343 | result += opiResp.Choices[0].Message.Content 344 | 345 | if len(opiResp.Choices[0].Message.ToolCalls) > 0 { 346 | if opiResp.Choices[0].Message.ToolCalls[0].Function.Name != "" { 347 | result += "name:" + opiResp.Choices[0].Message.ToolCalls[0].Function.Name + " arguments:" 348 | } 349 | result += opiResp.Choices[0].Message.ToolCalls[0].Function.Arguments 350 | } 351 | 352 | } 353 | for k, v := range resp.Header { 354 | c.Writer.Header().Set(k, v[0]) 355 | } 356 | 357 | c.JSON(http.StatusOK, opiResp) 358 | } 359 | usagelog.CompletionCount = tokenizer.NumTokensFromStr(result, chatReq.Model) 360 | usagelog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(usagelog.Model, usagelog.PromptCount, usagelog.CompletionCount)) 361 | if err := store.Record(&usagelog); err != nil { 362 | log.Println(err) 363 | } 364 | if err := store.SumDaily(usagelog.UserID); err != nil { 365 | log.Println(err) 366 | } 367 | } 368 | 369 | func modelmap(in string) string { 370 | // gpt-3.5-turbo -> gpt-35-turbo 371 | if strings.Contains(in, ".") { 372 | return strings.ReplaceAll(in, ".", "") 373 | } 374 | return in 375 | } 376 | 377 | type ErrResponse struct { 378 | Error struct { 379 | Message string `json:"message"` 380 | Code string `json:"code"` 381 | } `json:"error"` 382 | } 383 | 384 | func (e *ErrResponse) ByteJson() []byte { 385 | bytejson, _ := json.Marshal(e) 386 | return bytejson 387 | } 388 | -------------------------------------------------------------------------------- /pkg/openai/dall-e.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "opencatd-open/pkg/tokenizer" 13 | "opencatd-open/store" 14 | "strconv" 15 | 16 | "github.com/duke-git/lancet/v2/slice" 17 | "github.com/gin-gonic/gin" 18 | ) 19 | 20 | const ( 21 | DalleEndpoint = "https://api.openai.com/v1/images/generations" 22 | DalleEditEndpoint = "https://api.openai.com/v1/images/edits" 23 | DalleVariationEndpoint = "https://api.openai.com/v1/images/variations" 24 | ) 25 | 26 | type DallERequest struct { 27 | Model string `json:"model"` 28 | Prompt string `json:"prompt"` 29 | N int `form:"n" json:"n,omitempty"` 30 | Size string `form:"size" json:"size,omitempty"` 31 | Quality string `json:"quality,omitempty"` // standard,hd 32 | Style string `json:"style,omitempty"` // vivid,natural 33 | ResponseFormat string `json:"response_format,omitempty"` // url or b64_json 34 | } 35 | 36 | func DallEProxy(c *gin.Context) { 37 | 38 | var dalleRequest DallERequest 39 | if err := c.ShouldBind(&dalleRequest); err != nil { 40 | c.JSON(400, gin.H{"error": err.Error()}) 41 | return 42 | } 43 | if dalleRequest.N == 0 { 44 | dalleRequest.N = 1 45 | } 46 | 47 | if dalleRequest.Size == "" { 48 | dalleRequest.Size = "512x512" 49 | } 50 | 51 | model := dalleRequest.Model 52 | 53 | var chatlog store.Tokens 54 | chatlog.CompletionCount = dalleRequest.N 55 | 56 | if model == "dall-e" { 57 | model = "dall-e-2" 58 | } 59 | model = model + "." + dalleRequest.Size 60 | 61 | if dalleRequest.Model == "dall-e-2" || dalleRequest.Model == "dall-e" { 62 | if !slice.Contain([]string{"256x256", "512x512", "1024x1024"}, dalleRequest.Size) { 63 | c.JSON(http.StatusBadRequest, gin.H{ 64 | "error": gin.H{ 65 | "message": fmt.Sprintf("Invalid size: %s for %s", dalleRequest.Size, dalleRequest.Model), 66 | }, 67 | }) 68 | return 69 | } 70 | } else if dalleRequest.Model == "dall-e-3" { 71 | if !slice.Contain([]string{"256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"}, dalleRequest.Size) { 72 | c.JSON(http.StatusBadRequest, gin.H{ 73 | "error": gin.H{ 74 | "message": fmt.Sprintf("Invalid size: %s for %s", dalleRequest.Size, dalleRequest.Model), 75 | }, 76 | }) 77 | return 78 | } 79 | if dalleRequest.Quality == "hd" { 80 | model = model + ".hd" 81 | } 82 | } else { 83 | c.JSON(http.StatusBadRequest, gin.H{ 84 | "error": gin.H{ 85 | "message": fmt.Sprintf("Invalid model: %s", dalleRequest.Model), 86 | }, 87 | }) 88 | return 89 | } 90 | chatlog.Model = model 91 | 92 | token, _ := c.Get("localuser") 93 | 94 | lu, err := store.GetUserByToken(token.(string)) 95 | if err != nil { 96 | c.JSON(http.StatusInternalServerError, gin.H{ 97 | "error": gin.H{ 98 | "message": err.Error(), 99 | }, 100 | }) 101 | return 102 | } 103 | chatlog.UserID = int(lu.ID) 104 | 105 | key, err := store.SelectKeyCache("openai") 106 | if err != nil { 107 | c.JSON(http.StatusInternalServerError, gin.H{ 108 | "error": gin.H{ 109 | "message": err.Error(), 110 | }, 111 | }) 112 | return 113 | } 114 | 115 | targetURL, _ := url.Parse(DalleEndpoint) 116 | proxy := httputil.NewSingleHostReverseProxy(targetURL) 117 | proxy.Director = func(req *http.Request) { 118 | req.Header.Set("Authorization", "Bearer "+key.Key) 119 | req.Header.Set("Content-Type", "application/json") 120 | 121 | req.Host = targetURL.Host 122 | req.URL.Scheme = targetURL.Scheme 123 | req.URL.Host = targetURL.Host 124 | req.URL.Path = targetURL.Path 125 | req.URL.RawPath = targetURL.RawPath 126 | req.URL.RawQuery = targetURL.RawQuery 127 | 128 | bytebody, _ := json.Marshal(dalleRequest) 129 | req.Body = io.NopCloser(bytes.NewBuffer(bytebody)) 130 | req.ContentLength = int64(len(bytebody)) 131 | req.Header.Set("Content-Length", strconv.Itoa(len(bytebody))) 132 | } 133 | 134 | proxy.ModifyResponse = func(resp *http.Response) error { 135 | if resp.StatusCode == http.StatusOK { 136 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount 137 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)) 138 | if err := store.Record(&chatlog); err != nil { 139 | log.Println(err) 140 | } 141 | if err := store.SumDaily(chatlog.UserID); err != nil { 142 | log.Println(err) 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | proxy.ServeHTTP(c.Writer, c.Request) 149 | } 150 | -------------------------------------------------------------------------------- /pkg/openai/realtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | https://platform.openai.com/docs/guides/realtime 3 | https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/audio-real-time 4 | 5 | wss://my-eastus2-openai-resource.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview-1001 6 | */ 7 | package openai 8 | 9 | import ( 10 | "context" 11 | "encoding/json" 12 | "fmt" 13 | "log" 14 | "net/http" 15 | "net/url" 16 | "opencatd-open/pkg/tokenizer" 17 | "opencatd-open/store" 18 | "os" 19 | 20 | "github.com/gin-gonic/gin" 21 | "github.com/gorilla/websocket" 22 | "golang.org/x/sync/errgroup" 23 | ) 24 | 25 | // "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01" 26 | const realtimeURL = "wss://api.openai.com/v1/realtime" 27 | const azureRealtimeURL = "wss://%s.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview" 28 | 29 | var upgrader = websocket.Upgrader{ 30 | CheckOrigin: func(r *http.Request) bool { 31 | return true 32 | }, 33 | } 34 | 35 | type Message struct { 36 | Type string `json:"type"` 37 | Response Response `json:"response"` 38 | } 39 | 40 | type Response struct { 41 | Modalities []string `json:"modalities"` 42 | Instructions string `json:"instructions"` 43 | } 44 | 45 | type RealTimeResponse struct { 46 | Type string `json:"type"` 47 | EventID string `json:"event_id"` 48 | Response struct { 49 | Object string `json:"object"` 50 | ID string `json:"id"` 51 | Status string `json:"status"` 52 | StatusDetails any `json:"status_details"` 53 | Output []struct { 54 | ID string `json:"id"` 55 | Object string `json:"object"` 56 | Type string `json:"type"` 57 | Status string `json:"status"` 58 | Role string `json:"role"` 59 | Content []struct { 60 | Type string `json:"type"` 61 | Transcript string `json:"transcript"` 62 | } `json:"content"` 63 | } `json:"output"` 64 | Usage Usage `json:"usage"` 65 | } `json:"response"` 66 | } 67 | 68 | type Usage struct { 69 | TotalTokens int `json:"total_tokens"` 70 | InputTokens int `json:"input_tokens"` 71 | OutputTokens int `json:"output_tokens"` 72 | InputTokenDetails struct { 73 | CachedTokens int `json:"cached_tokens"` 74 | TextTokens int `json:"text_tokens"` 75 | AudioTokens int `json:"audio_tokens"` 76 | } `json:"input_token_details"` 77 | OutputTokenDetails struct { 78 | TextTokens int `json:"text_tokens"` 79 | AudioTokens int `json:"audio_tokens"` 80 | } `json:"output_token_details"` 81 | } 82 | 83 | func RealTimeProxy(c *gin.Context) { 84 | log.Println(c.Request.URL.String()) 85 | var model string = c.Query("model") 86 | value := url.Values{} 87 | value.Add("model", model) 88 | realtimeURL := realtimeURL + "?" + value.Encode() 89 | 90 | // 升级 HTTP 连接为 WebSocket 91 | clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 92 | if err != nil { 93 | log.Println("Upgrade error:", err) 94 | return 95 | } 96 | defer clientConn.Close() 97 | 98 | apikey, err := store.SelectKeyCacheByModel(model) 99 | if err != nil { 100 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 101 | return 102 | } 103 | // 连接到 OpenAI WebSocket 104 | headers := http.Header{"OpenAI-Beta": []string{"realtime=v1"}} 105 | 106 | if apikey.ApiType == "azure" { 107 | headers.Set("api-key", apikey.Key) 108 | if apikey.EndPoint != "" { 109 | realtimeURL = fmt.Sprintf("%s/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview", apikey.EndPoint) 110 | } else { 111 | realtimeURL = fmt.Sprintf(azureRealtimeURL, apikey.ResourceNmae) 112 | } 113 | } else { 114 | headers.Set("Authorization", "Bearer "+apikey.Key) 115 | } 116 | 117 | conn := websocket.DefaultDialer 118 | if os.Getenv("LOCAL_PROXY") != "" { 119 | proxyUrl, _ := url.Parse(os.Getenv("LOCAL_PROXY")) 120 | conn.Proxy = http.ProxyURL(proxyUrl) 121 | } 122 | 123 | openAIConn, _, err := conn.Dial(realtimeURL, headers) 124 | if err != nil { 125 | log.Println("OpenAI dial error:", err) 126 | return 127 | } 128 | defer openAIConn.Close() 129 | 130 | ctx, cancel := context.WithCancel(c.Request.Context()) 131 | defer cancel() 132 | 133 | g, ctx := errgroup.WithContext(ctx) 134 | 135 | g.Go(func() error { 136 | return forwardMessages(ctx, c, clientConn, openAIConn) 137 | }) 138 | 139 | g.Go(func() error { 140 | return forwardMessages(ctx, c, openAIConn, clientConn) 141 | }) 142 | 143 | if err := g.Wait(); err != nil { 144 | log.Println("Error in message forwarding:", err) 145 | return 146 | } 147 | 148 | } 149 | 150 | func forwardMessages(ctx context.Context, c *gin.Context, src, dst *websocket.Conn) error { 151 | usagelog := store.Tokens{Model: "gpt-4o-realtime-preview"} 152 | 153 | token, _ := c.Get("localuser") 154 | 155 | lu, err := store.GetUserByToken(token.(string)) 156 | if err != nil { 157 | return err 158 | } 159 | usagelog.UserID = int(lu.ID) 160 | for { 161 | select { 162 | case <-ctx.Done(): 163 | return ctx.Err() 164 | default: 165 | messageType, message, err := src.ReadMessage() 166 | if err != nil { 167 | if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { 168 | return nil // 正常关闭,不报错 169 | } 170 | return err 171 | } 172 | if messageType == websocket.TextMessage { 173 | var usage Usage 174 | err := json.Unmarshal(message, &usage) 175 | if err == nil { 176 | usagelog.PromptCount += usage.InputTokens 177 | usagelog.CompletionCount += usage.OutputTokens 178 | } 179 | 180 | } 181 | err = dst.WriteMessage(messageType, message) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | } 187 | defer func() { 188 | usagelog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(usagelog.Model, usagelog.PromptCount, usagelog.CompletionCount)) 189 | if err := store.Record(&usagelog); err != nil { 190 | log.Println(err) 191 | } 192 | if err := store.SumDaily(usagelog.UserID); err != nil { 193 | log.Println(err) 194 | } 195 | }() 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /pkg/openai/tts.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "opencatd-open/pkg/tokenizer" 13 | "opencatd-open/store" 14 | 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | const ( 19 | SpeechEndpoint = "https://api.openai.com/v1/audio/speech" 20 | ) 21 | 22 | type SpeechRequest struct { 23 | Model string `json:"model"` 24 | Input string `json:"input"` 25 | Voice string `json:"voice"` 26 | } 27 | 28 | func SpeechProxy(c *gin.Context) { 29 | var chatreq SpeechRequest 30 | if err := c.ShouldBindJSON(&chatreq); err != nil { 31 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | var chatlog store.Tokens 36 | chatlog.Model = chatreq.Model 37 | chatlog.CompletionCount = len(chatreq.Input) 38 | 39 | token, _ := c.Get("localuser") 40 | 41 | lu, err := store.GetUserByToken(token.(string)) 42 | if err != nil { 43 | c.JSON(http.StatusInternalServerError, gin.H{ 44 | "error": gin.H{ 45 | "message": err.Error(), 46 | }, 47 | }) 48 | return 49 | } 50 | chatlog.UserID = int(lu.ID) 51 | 52 | key, err := store.SelectKeyCache("openai") 53 | if err != nil { 54 | c.JSON(http.StatusInternalServerError, gin.H{ 55 | "error": gin.H{ 56 | "message": err.Error(), 57 | }, 58 | }) 59 | return 60 | } 61 | 62 | targetURL, _ := url.Parse(SpeechEndpoint) 63 | proxy := httputil.NewSingleHostReverseProxy(targetURL) 64 | 65 | proxy.Director = func(req *http.Request) { 66 | req.Header = c.Request.Header 67 | req.Header["Authorization"] = []string{"Bearer " + key.Key} 68 | req.Host = targetURL.Host 69 | req.URL.Scheme = targetURL.Scheme 70 | req.URL.Host = targetURL.Host 71 | req.URL.Path = targetURL.Path 72 | req.URL.RawPath = targetURL.RawPath 73 | 74 | reqBytes, _ := json.Marshal(chatreq) 75 | req.Body = io.NopCloser(bytes.NewReader(reqBytes)) 76 | req.ContentLength = int64(len(reqBytes)) 77 | 78 | } 79 | proxy.ModifyResponse = func(resp *http.Response) error { 80 | if resp.StatusCode == http.StatusOK { 81 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount 82 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)) 83 | if err := store.Record(&chatlog); err != nil { 84 | log.Println(err) 85 | } 86 | if err := store.SumDaily(chatlog.UserID); err != nil { 87 | log.Println(err) 88 | } 89 | } 90 | return nil 91 | } 92 | proxy.ServeHTTP(c.Writer, c.Request) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/openai/whisper.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "mime/multipart" 11 | "net/http" 12 | "net/http/httputil" 13 | "net/url" 14 | "opencatd-open/pkg/tokenizer" 15 | "opencatd-open/store" 16 | "path/filepath" 17 | "time" 18 | 19 | "github.com/faiface/beep" 20 | "github.com/faiface/beep/mp3" 21 | "github.com/faiface/beep/wav" 22 | "github.com/gin-gonic/gin" 23 | "gopkg.in/vansante/go-ffprobe.v2" 24 | ) 25 | 26 | func WhisperProxy(c *gin.Context) { 27 | var chatlog store.Tokens 28 | 29 | byteBody, _ := io.ReadAll(c.Request.Body) 30 | c.Request.Body = io.NopCloser(bytes.NewBuffer(byteBody)) 31 | 32 | model, _ := c.GetPostForm("model") 33 | 34 | key, err := store.SelectKeyCache("openai") 35 | if err != nil { 36 | c.JSON(http.StatusInternalServerError, gin.H{ 37 | "error": gin.H{ 38 | "message": err.Error(), 39 | }, 40 | }) 41 | return 42 | } 43 | 44 | chatlog.Model = model 45 | 46 | token, _ := c.Get("localuser") 47 | 48 | lu, err := store.GetUserByToken(token.(string)) 49 | if err != nil { 50 | c.JSON(http.StatusInternalServerError, gin.H{ 51 | "error": gin.H{ 52 | "message": err.Error(), 53 | }, 54 | }) 55 | return 56 | } 57 | chatlog.UserID = int(lu.ID) 58 | 59 | if err := ParseWhisperRequestTokens(c, &chatlog, byteBody); err != nil { 60 | c.JSON(http.StatusInternalServerError, gin.H{ 61 | "error": gin.H{ 62 | "message": err.Error(), 63 | }, 64 | }) 65 | return 66 | } 67 | if key.EndPoint == "" { 68 | key.EndPoint = "https://api.openai.com" 69 | } 70 | targetUrl, _ := url.ParseRequestURI(key.EndPoint + c.Request.URL.String()) 71 | log.Println(targetUrl) 72 | proxy := httputil.NewSingleHostReverseProxy(targetUrl) 73 | proxy.Director = func(req *http.Request) { 74 | req.Host = targetUrl.Host 75 | req.URL.Scheme = targetUrl.Scheme 76 | req.URL.Host = targetUrl.Host 77 | 78 | req.Header.Set("Authorization", "Bearer "+key.Key) 79 | } 80 | 81 | proxy.ModifyResponse = func(resp *http.Response) error { 82 | if resp.StatusCode != http.StatusOK { 83 | return nil 84 | } 85 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount 86 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)) 87 | if err := store.Record(&chatlog); err != nil { 88 | log.Println(err) 89 | } 90 | if err := store.SumDaily(chatlog.UserID); err != nil { 91 | log.Println(err) 92 | } 93 | return nil 94 | } 95 | proxy.ServeHTTP(c.Writer, c.Request) 96 | } 97 | 98 | func probe(fileReader io.Reader) (time.Duration, error) { 99 | ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) 100 | defer cancelFn() 101 | 102 | data, err := ffprobe.ProbeReader(ctx, fileReader) 103 | if err != nil { 104 | return 0, err 105 | } 106 | 107 | duration := data.Format.DurationSeconds 108 | pduration, err := time.ParseDuration(fmt.Sprintf("%fs", duration)) 109 | if err != nil { 110 | return 0, fmt.Errorf("Error parsing duration: %s", err) 111 | } 112 | return pduration, nil 113 | } 114 | 115 | func getAudioDuration(file *multipart.FileHeader) (time.Duration, error) { 116 | var ( 117 | streamer beep.StreamSeekCloser 118 | format beep.Format 119 | err error 120 | ) 121 | 122 | f, err := file.Open() 123 | defer f.Close() 124 | 125 | // Get the file extension to determine the audio file type 126 | fileType := filepath.Ext(file.Filename) 127 | 128 | switch fileType { 129 | case ".mp3": 130 | streamer, format, err = mp3.Decode(f) 131 | case ".wav": 132 | streamer, format, err = wav.Decode(f) 133 | case ".m4a": 134 | duration, err := probe(f) 135 | if err != nil { 136 | return 0, err 137 | } 138 | return duration, nil 139 | default: 140 | return 0, errors.New("unsupported audio file format") 141 | } 142 | 143 | if err != nil { 144 | return 0, err 145 | } 146 | defer streamer.Close() 147 | 148 | // Calculate the audio file's duration. 149 | numSamples := streamer.Len() 150 | sampleRate := format.SampleRate 151 | duration := time.Duration(numSamples) * time.Second / time.Duration(sampleRate) 152 | 153 | return duration, nil 154 | } 155 | 156 | func ParseWhisperRequestTokens(c *gin.Context, usage *store.Tokens, byteBody []byte) error { 157 | file, _ := c.FormFile("file") 158 | model, _ := c.GetPostForm("model") 159 | usage.Model = model 160 | 161 | if file != nil { 162 | duration, err := getAudioDuration(file) 163 | if err != nil { 164 | return fmt.Errorf("Error getting audio duration:%s", err) 165 | } 166 | 167 | if duration > 5*time.Minute { 168 | return fmt.Errorf("Audio duration exceeds 5 minutes") 169 | } 170 | // 计算时长,四舍五入到最接近的秒数 171 | usage.PromptCount = int(duration.Round(time.Second).Seconds()) 172 | } 173 | 174 | c.Request.Body = io.NopCloser(bytes.NewBuffer(byteBody)) 175 | 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /pkg/search/bing.go: -------------------------------------------------------------------------------- 1 | /* 2 | 文档 https://www.microsoft.com/en-us/bing/apis/bing-web-search-api 3 | 价格 https://www.microsoft.com/en-us/bing/apis/pricing 4 | 5 | curl -H "Ocp-Apim-Subscription-Key: " https://api.bing.microsoft.com/v7.0/search?q=今天上海天气怎么样 6 | curl -H "Ocp-Apim-Subscription-Key: 6fc7c97ebed54f75a5e383ee2272c917" https://api.bing.microsoft.com/v7.0/search?q=今天上海天气怎么样 7 | */ 8 | 9 | package search 10 | 11 | import ( 12 | "fmt" 13 | "io" 14 | "log" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | 19 | "github.com/tidwall/gjson" 20 | ) 21 | 22 | const ( 23 | bingEndpoint = "https://api.bing.microsoft.com/v7.0/search" 24 | ) 25 | 26 | var subscriptionKey string 27 | 28 | func init() { 29 | if os.Getenv("bing") != "" { 30 | subscriptionKey = os.Getenv("bing") 31 | } else { 32 | log.Println("bing key not found") 33 | } 34 | } 35 | 36 | func BingSearch(searchParams SearchParams) (any, error) { 37 | params := url.Values{} 38 | params.Set("q", searchParams.Query) 39 | params.Set("count", "5") 40 | if searchParams.Num > 0 { 41 | params.Set("count", fmt.Sprintf("%d", searchParams.Num)) 42 | } 43 | 44 | reqURL, _ := url.Parse(bingEndpoint) 45 | reqURL.RawQuery = params.Encode() 46 | 47 | req, _ := http.NewRequest("GET", reqURL.String(), nil) 48 | req.Header.Set("Ocp-Apim-Subscription-Key", subscriptionKey) 49 | 50 | client := &http.Client{} 51 | resp, err := client.Do(req) 52 | defer resp.Body.Close() 53 | if err != nil { 54 | fmt.Println("Error sending request:", err) 55 | return nil, err 56 | } 57 | 58 | body, err := io.ReadAll(resp.Body) 59 | if err != nil { 60 | fmt.Println("Error reading response:", err) 61 | return nil, err 62 | } 63 | result := gjson.ParseBytes(body).Get("webPages.value") 64 | 65 | return result.Raw, nil 66 | 67 | } 68 | 69 | type SearchParams struct { 70 | Query string `form:"q"` 71 | Num int `form:"num,default=5"` 72 | } 73 | -------------------------------------------------------------------------------- /pkg/team/key.go: -------------------------------------------------------------------------------- 1 | package team 2 | 3 | import ( 4 | "net/http" 5 | "opencatd-open/pkg/azureopenai" 6 | "opencatd-open/store" 7 | "strings" 8 | 9 | "github.com/Sakurasan/to" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type Key struct { 14 | ID int `json:"id,omitempty"` 15 | Key string `json:"key,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | ApiType string `json:"api_type,omitempty"` 18 | Endpoint string `json:"endpoint,omitempty"` 19 | UpdatedAt string `json:"updatedAt,omitempty"` 20 | CreatedAt string `json:"createdAt,omitempty"` 21 | } 22 | 23 | func HandleKeys(c *gin.Context) { 24 | keys, err := store.GetAllKeys() 25 | if err != nil { 26 | c.JSON(http.StatusOK, gin.H{ 27 | "error": err.Error(), 28 | }) 29 | } 30 | 31 | c.JSON(http.StatusOK, keys) 32 | } 33 | 34 | func HandleAddKey(c *gin.Context) { 35 | var body Key 36 | if err := c.BindJSON(&body); err != nil { 37 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{ 38 | "message": err.Error(), 39 | }}) 40 | return 41 | } 42 | body.Name = strings.ToLower(strings.TrimSpace(body.Name)) 43 | body.Key = strings.TrimSpace(body.Key) 44 | if strings.HasPrefix(body.Name, "azure.") { 45 | keynames := strings.Split(body.Name, ".") 46 | if len(keynames) < 2 { 47 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{ 48 | "message": "Invalid Key Name", 49 | }}) 50 | return 51 | } 52 | k := &store.Key{ 53 | ApiType: "azure", 54 | Name: body.Name, 55 | Key: body.Key, 56 | ResourceNmae: keynames[1], 57 | EndPoint: body.Endpoint, 58 | } 59 | if err := store.CreateKey(k); err != nil { 60 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 61 | "message": err.Error(), 62 | }}) 63 | return 64 | } 65 | } else if strings.HasPrefix(body.Name, "claude.") { 66 | keynames := strings.Split(body.Name, ".") 67 | if len(keynames) < 2 { 68 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{ 69 | "message": "Invalid Key Name", 70 | }}) 71 | return 72 | } 73 | if body.Endpoint == "" { 74 | body.Endpoint = "https://api.anthropic.com" 75 | } 76 | k := &store.Key{ 77 | // ApiType: "anthropic", 78 | ApiType: "claude", 79 | Name: body.Name, 80 | Key: body.Key, 81 | ResourceNmae: keynames[1], 82 | EndPoint: body.Endpoint, 83 | } 84 | if err := store.CreateKey(k); err != nil { 85 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 86 | "message": err.Error(), 87 | }}) 88 | return 89 | } 90 | } else if strings.HasPrefix(body.Name, "google.") { 91 | keynames := strings.Split(body.Name, ".") 92 | if len(keynames) < 2 { 93 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{ 94 | "message": "Invalid Key Name", 95 | }}) 96 | return 97 | } 98 | 99 | k := &store.Key{ 100 | // ApiType: "anthropic", 101 | ApiType: "google", 102 | Name: body.Name, 103 | Key: body.Key, 104 | ResourceNmae: keynames[1], 105 | EndPoint: body.Endpoint, 106 | } 107 | if err := store.CreateKey(k); err != nil { 108 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 109 | "message": err.Error(), 110 | }}) 111 | return 112 | } 113 | } else if strings.HasPrefix(body.Name, "github.") { 114 | keynames := strings.Split(body.Name, ".") 115 | if len(keynames) < 2 { 116 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{ 117 | "message": "Invalid Key Name", 118 | }}) 119 | return 120 | } 121 | 122 | k := &store.Key{ 123 | ApiType: "github", 124 | Name: body.Name, 125 | Key: body.Key, 126 | ResourceNmae: keynames[1], 127 | EndPoint: body.Endpoint, 128 | } 129 | if err := store.CreateKey(k); err != nil { 130 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 131 | "message": err.Error(), 132 | }}) 133 | return 134 | } 135 | } else { 136 | if body.ApiType == "" { 137 | if err := store.AddKey("openai", body.Key, body.Name); err != nil { 138 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 139 | "message": err.Error(), 140 | }}) 141 | return 142 | } 143 | } else { 144 | k := &store.Key{ 145 | ApiType: body.ApiType, 146 | Name: body.Name, 147 | Key: body.Key, 148 | ResourceNmae: azureopenai.GetResourceName(body.Endpoint), 149 | EndPoint: body.Endpoint, 150 | } 151 | if err := store.CreateKey(k); err != nil { 152 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 153 | "message": err.Error(), 154 | }}) 155 | return 156 | } 157 | } 158 | 159 | } 160 | 161 | k, err := store.GetKeyrByName(body.Name) 162 | if err != nil { 163 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{ 164 | "message": err.Error(), 165 | }}) 166 | return 167 | } 168 | c.JSON(http.StatusOK, k) 169 | } 170 | 171 | func HandleDelKey(c *gin.Context) { 172 | id := to.Int(c.Param("id")) 173 | if id < 1 { 174 | c.JSON(http.StatusOK, gin.H{"error": "invalid key id"}) 175 | return 176 | } 177 | if err := store.DeleteKey(uint(id)); err != nil { 178 | c.JSON(http.StatusOK, gin.H{"error": "invalid key id"}) 179 | return 180 | } 181 | c.JSON(http.StatusOK, gin.H{"message": "ok"}) 182 | } 183 | -------------------------------------------------------------------------------- /pkg/team/me.go: -------------------------------------------------------------------------------- 1 | package team 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "opencatd-open/store" 7 | "time" 8 | 9 | "github.com/Sakurasan/to" 10 | "github.com/gin-gonic/gin" 11 | "github.com/google/uuid" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func Handleinit(c *gin.Context) { 16 | user, err := store.GetUserByID(1) 17 | if err != nil { 18 | if errors.Is(err, gorm.ErrRecordNotFound) { 19 | u := store.User{Name: "root", Token: uuid.NewString()} 20 | u.ID = 1 21 | if err := store.CreateUser(&u); err != nil { 22 | c.JSON(http.StatusForbidden, gin.H{ 23 | "error": err.Error(), 24 | }) 25 | return 26 | } else { 27 | rootToken = u.Token 28 | resJSON := User{ 29 | false, 30 | int(u.ID), 31 | u.UpdatedAt.Format(time.RFC3339), 32 | u.Name, 33 | u.Token, 34 | u.CreatedAt.Format(time.RFC3339), 35 | } 36 | c.JSON(http.StatusOK, resJSON) 37 | return 38 | } 39 | } 40 | c.JSON(http.StatusOK, gin.H{ 41 | "error": err.Error(), 42 | }) 43 | return 44 | } 45 | if user.ID == uint(1) { 46 | c.JSON(http.StatusForbidden, gin.H{ 47 | "error": "super user already exists, use cli to reset password", 48 | }) 49 | } 50 | } 51 | 52 | func HandleMe(c *gin.Context) { 53 | token := c.GetHeader("Authorization") 54 | u, err := store.GetUserByToken(token[7:]) 55 | if err != nil { 56 | c.JSON(http.StatusOK, gin.H{ 57 | "error": err.Error(), 58 | }) 59 | } 60 | 61 | resJSON := User{ 62 | false, 63 | int(u.ID), 64 | u.UpdatedAt.Format(time.RFC3339), 65 | u.Name, 66 | u.Token, 67 | u.CreatedAt.Format(time.RFC3339), 68 | } 69 | c.JSON(http.StatusOK, resJSON) 70 | } 71 | 72 | func HandleMeUsage(c *gin.Context) { 73 | token := c.GetHeader("Authorization") 74 | fromStr := c.Query("from") 75 | toStr := c.Query("to") 76 | getMonthStartAndEnd := func() (start, end string) { 77 | loc, _ := time.LoadLocation("Local") 78 | now := time.Now().In(loc) 79 | 80 | year, month, _ := now.Date() 81 | 82 | startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc) 83 | endOfMonth := startOfMonth.AddDate(0, 1, 0) 84 | 85 | start = startOfMonth.Format("2006-01-02") 86 | end = endOfMonth.Format("2006-01-02") 87 | return 88 | } 89 | if fromStr == "" || toStr == "" { 90 | fromStr, toStr = getMonthStartAndEnd() 91 | } 92 | user, err := store.GetUserByToken(token) 93 | if err != nil { 94 | c.AbortWithError(http.StatusForbidden, err) 95 | return 96 | } 97 | usage, err := store.QueryUserUsage(to.String(user.ID), fromStr, toStr) 98 | if err != nil { 99 | c.AbortWithError(http.StatusForbidden, err) 100 | return 101 | } 102 | 103 | c.JSON(200, usage) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/team/middleware.go: -------------------------------------------------------------------------------- 1 | package team 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "opencatd-open/store" 7 | "strings" 8 | 9 | "github.com/gin-contrib/cors" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | var ( 14 | rootToken string 15 | ) 16 | 17 | func AuthMiddleware() gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | if rootToken == "" { 20 | u, err := store.GetUserByID(uint(1)) 21 | if err != nil { 22 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 23 | c.Abort() 24 | return 25 | } 26 | rootToken = u.Token 27 | } 28 | token := c.GetHeader("Authorization") 29 | if token == "" || token[:7] != "Bearer " { 30 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 31 | c.Abort() 32 | return 33 | } 34 | if store.IsExistAuthCache(token[7:]) { 35 | if strings.HasPrefix(c.Request.URL.Path, "/1/me") { 36 | c.Next() 37 | return 38 | } 39 | } 40 | if token[7:] != rootToken { 41 | u, err := store.GetUserByID(uint(1)) 42 | if err != nil { 43 | log.Println(err) 44 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 45 | c.Abort() 46 | return 47 | } 48 | if token[:7] != u.Token { 49 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 50 | c.Abort() 51 | return 52 | } 53 | rootToken = u.Token 54 | store.LoadAuthCache() 55 | } 56 | // 可以在这里对 token 进行验证并检查权限 57 | 58 | c.Next() 59 | } 60 | } 61 | 62 | func CORS() gin.HandlerFunc { 63 | config := cors.DefaultConfig() 64 | config.AllowAllOrigins = true 65 | config.AllowCredentials = true 66 | config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} 67 | config.AllowHeaders = []string{"*"} 68 | return cors.New(config) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/team/usage.go: -------------------------------------------------------------------------------- 1 | package team 2 | 3 | import ( 4 | "net/http" 5 | "opencatd-open/store" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func HandleUsage(c *gin.Context) { 12 | fromStr := c.Query("from") 13 | toStr := c.Query("to") 14 | getMonthStartAndEnd := func() (start, end string) { 15 | loc, _ := time.LoadLocation("Local") 16 | now := time.Now().In(loc) 17 | 18 | year, month, _ := now.Date() 19 | 20 | startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc) 21 | endOfMonth := startOfMonth.AddDate(0, 1, 0) 22 | 23 | start = startOfMonth.Format("2006-01-02") 24 | end = endOfMonth.Format("2006-01-02") 25 | return 26 | } 27 | if fromStr == "" || toStr == "" { 28 | fromStr, toStr = getMonthStartAndEnd() 29 | } 30 | 31 | usage, err := store.QueryUsage(fromStr, toStr) 32 | if err != nil { 33 | c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) 34 | return 35 | } 36 | 37 | c.JSON(200, usage) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/team/user.go: -------------------------------------------------------------------------------- 1 | package team 2 | 3 | import ( 4 | "net/http" 5 | "opencatd-open/store" 6 | 7 | "github.com/Sakurasan/to" 8 | "github.com/gin-gonic/gin" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type User struct { 13 | IsDelete bool `json:"IsDelete,omitempty"` 14 | ID int `json:"id,omitempty"` 15 | UpdatedAt string `json:"updatedAt,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | Token string `json:"token,omitempty"` 18 | CreatedAt string `json:"createdAt,omitempty"` 19 | } 20 | 21 | func HandleUsers(c *gin.Context) { 22 | users, err := store.GetAllUsers() 23 | if err != nil { 24 | c.JSON(http.StatusOK, gin.H{ 25 | "error": err.Error(), 26 | }) 27 | } 28 | 29 | c.JSON(http.StatusOK, users) 30 | } 31 | 32 | func HandleAddUser(c *gin.Context) { 33 | var body User 34 | if err := c.BindJSON(&body); err != nil { 35 | c.JSON(http.StatusOK, gin.H{"error": err.Error()}) 36 | return 37 | } 38 | if len(body.Name) == 0 { 39 | c.JSON(http.StatusOK, gin.H{"error": "invalid user name"}) 40 | return 41 | } 42 | 43 | if err := store.AddUser(body.Name, uuid.NewString()); err != nil { 44 | c.JSON(http.StatusOK, gin.H{"error": err.Error()}) 45 | return 46 | } 47 | u, err := store.GetUserByName(body.Name) 48 | if err != nil { 49 | c.JSON(http.StatusOK, gin.H{"error": err.Error()}) 50 | return 51 | } 52 | c.JSON(http.StatusOK, u) 53 | } 54 | 55 | func HandleDelUser(c *gin.Context) { 56 | id := to.Int(c.Param("id")) 57 | if id <= 1 { 58 | c.JSON(http.StatusOK, gin.H{"error": "invalid user id"}) 59 | return 60 | } 61 | if err := store.DeleteUser(uint(id)); err != nil { 62 | c.JSON(http.StatusOK, gin.H{"error": err.Error()}) 63 | return 64 | } 65 | 66 | c.JSON(http.StatusOK, gin.H{"message": "ok"}) 67 | } 68 | 69 | func HandleResetUserToken(c *gin.Context) { 70 | id := to.Int(c.Param("id")) 71 | newtoken := c.Query("token") 72 | if newtoken == "" { 73 | newtoken = uuid.NewString() 74 | } 75 | 76 | if err := store.UpdateUser(uint(id), newtoken); err != nil { 77 | c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) 78 | return 79 | } 80 | u, err := store.GetUserByID(uint(id)) 81 | if err != nil { 82 | c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) 83 | return 84 | } 85 | if u.ID == uint(1) { 86 | rootToken = u.Token 87 | } 88 | c.JSON(http.StatusOK, u) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/tokenizer/tokenizer.go: -------------------------------------------------------------------------------- 1 | package tokenizer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/pkoukk/tiktoken-go" 9 | "github.com/sashabaranov/go-openai" 10 | ) 11 | 12 | func NumTokensFromMessages(messages []openai.ChatCompletionMessage, model string) (numTokens int) { 13 | tkm, err := tiktoken.EncodingForModel(model) 14 | if err != nil { 15 | err = fmt.Errorf("EncodingForModel: %v", err) 16 | log.Println(err) 17 | return 18 | } 19 | 20 | var tokensPerMessage, tokensPerName int 21 | 22 | switch model { 23 | case "gpt-3.5-turbo", 24 | "gpt-3.5-turbo-0613", 25 | "gpt-3.5-turbo-16k", 26 | "gpt-3.5-turbo-16k-0613", 27 | "gpt-4", 28 | "gpt-4-0314", 29 | "gpt-4-0613", 30 | "gpt-4-32k", 31 | "gpt-4-32k-0314", 32 | "gpt-4-32k-0613": 33 | tokensPerMessage = 3 34 | tokensPerName = 1 35 | case "gpt-3.5-turbo-0301": 36 | tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n 37 | tokensPerName = -1 // if there's a name, the role is omitted 38 | default: 39 | if strings.Contains(model, "gpt-3.5-turbo") { 40 | log.Println("warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.") 41 | return NumTokensFromMessages(messages, "gpt-3.5-turbo-0613") 42 | } else if strings.Contains(model, "gpt-4") { 43 | log.Println("warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.") 44 | return NumTokensFromMessages(messages, "gpt-4-0613") 45 | } else { 46 | err = fmt.Errorf("warning: unknown model [%s]. Use default calculation method converted tokens.", model) 47 | log.Println(err) 48 | return NumTokensFromMessages(messages, "gpt-3.5-turbo-0613") 49 | } 50 | } 51 | 52 | for _, message := range messages { 53 | numTokens += tokensPerMessage 54 | numTokens += len(tkm.Encode(message.Content, nil, nil)) 55 | numTokens += len(tkm.Encode(message.Role, nil, nil)) 56 | numTokens += len(tkm.Encode(message.Name, nil, nil)) 57 | if message.Name != "" { 58 | numTokens += tokensPerName 59 | } 60 | } 61 | numTokens += 3 62 | return numTokens 63 | } 64 | 65 | func NumTokensFromStr(messages string, model string) (num_tokens int) { 66 | tkm, err := tiktoken.EncodingForModel(model) 67 | if err != nil { 68 | fmt.Println(err) 69 | fmt.Println("Unsupport Model,use cl100k_base Encode") 70 | tkm, _ = tiktoken.GetEncoding("cl100k_base") 71 | } 72 | 73 | num_tokens += len(tkm.Encode(messages, nil, nil)) 74 | return num_tokens 75 | } 76 | 77 | // https://openai.com/pricing 78 | func Cost(model string, promptCount, completionCount int) float64 { 79 | var cost, prompt, completion float64 80 | prompt = float64(promptCount) 81 | completion = float64(completionCount) 82 | 83 | switch model { 84 | case "gpt-3.5-turbo-0301": 85 | cost = 0.002 * float64((prompt+completion)/1000) 86 | case "gpt-3.5-turbo", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125": 87 | cost = 0.0015*float64((prompt)/1000) + 0.002*float64(completion/1000) 88 | case "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613": 89 | cost = 0.003*float64((prompt)/1000) + 0.004*float64(completion/1000) 90 | case "gpt-4", "gpt-4-0613", "gpt-4-0314": 91 | cost = 0.03*float64(prompt/1000) + 0.06*float64(completion/1000) 92 | case "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613": 93 | cost = 0.06*float64(prompt/1000) + 0.12*float64(completion/1000) 94 | case "gpt-4-1106-preview", "gpt-4-vision-preview", "gpt-4-0125-preview", "gpt-4-turbo-preview": 95 | cost = 0.01*float64(prompt/1000) + 0.03*float64(completion/1000) 96 | case "gpt-4-turbo", "gpt-4-turbo-2024-04-09": 97 | cost = 0.01*float64(prompt/1000) + 0.03*float64(completion/1000) 98 | // omni 99 | case "gpt-4o", "gpt-4o-2024-08-06": 100 | cost = 0.0025*float64(prompt/1000) + 0.01*float64(completion/1000) 101 | case "gpt-4o-2024-05-13": 102 | cost = 0.005*float64(prompt/1000) + 0.015*float64(completion/1000) 103 | case "gpt-4o-mini", "gpt-4o-mini-2024-07-18": 104 | cost = 0.00015*float64(prompt/1000) + 0.0006*float64(completion/1000) 105 | case "chatgpt-4o-latest": 106 | cost = 0.005*float64(prompt/1000) + 0.015*float64(completion/1000) 107 | // o1 108 | case "o1-preview", "o1-preview-2024-09-12": 109 | cost = 0.015*float64(prompt/1000) + 0.06*float64(completion/1000) 110 | case "o1-mini", "o1-mini-2024-09-12": 111 | cost = 0.003*float64(prompt/1000) + 0.012*float64(completion/1000) 112 | case "o3-mini", "o3-mini-2025-01-31": 113 | cost = 0.003*float64(prompt/1000) + 0.012*float64(completion/1000) 114 | // Realtime API 115 | // Audio* 116 | // $0.1 / 1K input tokens 117 | // $0.2 / 1K output tokens 118 | case "gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-12-17": 119 | cost = 0.0025*float64(prompt/1000) + 0.01*float64(completion/1000) 120 | case "gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01": 121 | cost = 0.005*float64(prompt/1000) + 0.020*float64(completion/1000) 122 | case "gpt-4o-realtime-preview.audio", "gpt-4o-realtime-preview-2024-10-01.audio": 123 | cost = 0.1*float64(prompt/1000) + 0.2*float64(completion/1000) 124 | 125 | case "gpt-4o-mini-audio-preview", "gpt-4o-mini-audio-preview-2024-12-17": 126 | cost = 0.00015*float64(prompt/1000) + 0.0006*float64(completion/1000) 127 | case "gpt-4o-mini-realtime-preview", "gpt-4o-mini-realtime-preview-2024-12-17": 128 | cost = 0.0006*float64(prompt/1000) + 0.0024*float64(completion/1000) 129 | 130 | case "whisper-1": 131 | // 0.006$/min 132 | cost = 0.006 * float64(prompt+completion) / 60 133 | case "tts-1": 134 | cost = 0.015 * float64(prompt+completion) 135 | case "tts-1-hd": 136 | cost = 0.03 * float64(prompt+completion) 137 | case "dall-e-2.256x256": 138 | cost = float64(0.016 * completion) 139 | case "dall-e-2.512x512": 140 | cost = float64(0.018 * completion) 141 | case "dall-e-2.1024x1024": 142 | cost = float64(0.02 * completion) 143 | case "dall-e-3.256x256": 144 | cost = float64(0.04 * completion) 145 | case "dall-e-3.512x512": 146 | cost = float64(0.04 * completion) 147 | case "dall-e-3.1024x1024": 148 | cost = float64(0.04 * completion) 149 | case "dall-e-3.1024x1792", "dall-e-3.1792x1024": 150 | cost = float64(0.08 * completion) 151 | case "dall-e-3.256x256.hd": 152 | cost = float64(0.08 * completion) 153 | case "dall-e-3.512x512.hd": 154 | cost = float64(0.08 * completion) 155 | case "dall-e-3.1024x1024.hd": 156 | cost = float64(0.08 * completion) 157 | case "dall-e-3.1024x1792.hd", "dall-e-3.1792x1024.hd": 158 | cost = float64(0.12 * completion) 159 | 160 | // claude /million tokens 161 | // https://aws.amazon.com/cn/bedrock/pricing/ 162 | case "claude-v1", "claude-v1-100k": 163 | cost = 11.02/1000000*float64(prompt) + (32.68/1000000)*float64(completion) 164 | case "claude-instant-v1", "claude-instant-v1-100k": 165 | cost = (1.63/1000000)*float64(prompt) + (5.51/1000000)*float64(completion) 166 | case "claude-2", "claude-2.1": 167 | cost = (8.0/1000000)*float64(prompt) + (24.0/1000000)*float64(completion) 168 | case "claude-3-haiku": 169 | cost = (0.00025/1000)*float64(prompt) + (0.00125/1000)*float64(completion) 170 | case "claude-3-sonnet": 171 | cost = (0.003/1000)*float64(prompt) + (0.015/1000)*float64(completion) 172 | case "claude-3-opus": 173 | cost = (0.015/1000)*float64(prompt) + (0.075/1000)*float64(completion) 174 | case "claude-3-haiku-20240307": 175 | cost = (0.00025/1000)*float64(prompt) + (0.00125/1000)*float64(completion) 176 | case "claude-3-5-haiku-latest", "claude-3-5-haiku-20241022": 177 | cost = (0.001/1000)*float64(prompt) + (0.005/1000)*float64(completion) 178 | case "claude-3-sonnet-20240229": 179 | cost = (0.003/1000)*float64(prompt) + (0.015/1000)*float64(completion) 180 | case "claude-3-opus-20240229": 181 | cost = (0.015/1000)*float64(prompt) + (0.075/1000)*float64(completion) 182 | case "claude-3-5-sonnet", "claude-3-5-sonnet-latest", "claude-3-5-sonnet-20240620", "claude-3-5-sonnet-20241022": 183 | cost = (0.003/1000)*float64(prompt) + (0.015/1000)*float64(completion) 184 | // google 185 | // https://ai.google.dev/pricing?hl=zh-cn 186 | case "gemini-pro": 187 | cost = (0.0005/1000)*float64(prompt) + (0.0015/1000)*float64(completion) 188 | case "gemini-pro-vision": 189 | cost = (0.0005/1000)*float64(prompt) + (0.0015/1000)*float64(completion) 190 | case "gemini-1.5-pro-latest": 191 | cost = (0.0035/1000)*float64(prompt) + (0.0105/1000)*float64(completion) 192 | case "gemini-1.5-flash-latest": 193 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) 194 | case "gemini-2.0-flash-exp": 195 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) 196 | case "gemini-2.0-flash-thinking-exp-1219", "gemini-2.0-flash-thinking-exp-01-21": 197 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) 198 | case "learnlm-1.5-pro-experimental", " gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206": 199 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion) 200 | 201 | // Mistral AI 202 | // https://docs.mistral.ai/platform/pricing/ 203 | case "mistral-small-latest": 204 | cost = (0.002/1000)*float64(prompt) + (0.006/1000)*float64(completion) 205 | case "mistral-medium-latest": 206 | cost = (0.0027/1000)*float64(prompt) + (0.0081/1000)*float64(completion) 207 | case "mistral-large-latest": 208 | cost = (0.008/1000)*float64(prompt) + (0.024/1000)*float64(completion) 209 | 210 | default: 211 | if strings.Contains(model, "gpt-3.5-turbo") { 212 | cost = 0.003 * float64((prompt+completion)/1000) 213 | } else if strings.Contains(model, "gpt-4") { 214 | cost = 0.06 * float64((prompt+completion)/1000) 215 | } else { 216 | cost = 0.002 * float64((prompt+completion)/1000) 217 | } 218 | } 219 | return cost 220 | } 221 | -------------------------------------------------------------------------------- /pkg/vertexai/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | https://docs.anthropic.com/zh-CN/api/claude-on-vertex-ai 3 | 4 | MODEL_ID=claude-3-5-sonnet@20240620 5 | REGION=us-east5 6 | PROJECT_ID=MY_PROJECT_ID 7 | 8 | curl \ 9 | -X POST \ 10 | -H "Authorization: Bearer $(gcloud auth print-access-token)" \ 11 | -H "Content-Type: application/json" \ 12 | https://$LOCATION-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/publishers/anthropic/models/${MODEL_ID}:streamRawPredict \ 13 | -d '{ 14 | "anthropic_version": "vertex-2023-10-16", 15 | "messages": [{ 16 | "role": "user", 17 | "content": "介绍一下你自己" 18 | }], 19 | "stream": true, 20 | "max_tokens": 4096 21 | }' 22 | 23 | quota: 24 | https://console.cloud.google.com/iam-admin/quotas?hl=zh-cn 25 | */ 26 | 27 | package vertexai 28 | 29 | import ( 30 | "crypto/rsa" 31 | "crypto/x509" 32 | "encoding/json" 33 | "encoding/pem" 34 | "fmt" 35 | "net/http" 36 | "net/url" 37 | "os" 38 | "time" 39 | 40 | "github.com/golang-jwt/jwt" 41 | ) 42 | 43 | // json文件存储在ApiKey.ApiSecret中 44 | type VertexSecretKey struct { 45 | Type string `json:"type"` 46 | ProjectID string `json:"project_id"` 47 | PrivateKeyID string `json:"private_key_id"` 48 | PrivateKey string `json:"private_key"` 49 | ClientEmail string `json:"client_email"` 50 | ClientID string `json:"client_id"` 51 | AuthURI string `json:"auth_uri"` 52 | TokenURI string `json:"token_uri"` 53 | AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"` 54 | ClientX509CertURL string `json:"client_x509_cert_url"` 55 | UniverseDomain string `json:"universe_domain"` 56 | } 57 | 58 | type VertexClaudeModel struct { 59 | VertexName string 60 | Region string 61 | } 62 | 63 | var VertexClaudeModelMap = map[string]VertexClaudeModel{ 64 | "claude-3-opus": { 65 | VertexName: "claude-3-opus@20240229", 66 | Region: "us-east5", 67 | }, 68 | "claude-3-sonnet": { 69 | VertexName: "claude-3-sonnet@20240229", 70 | Region: "us-central1", 71 | // Region: "asia-southeast1", 72 | }, 73 | "claude-3-haiku": { 74 | VertexName: "claude-3-haiku@20240307", 75 | Region: "us-central1", 76 | // Region: "europe-west4", 77 | }, 78 | "claude-3-opus-20240229": { 79 | VertexName: "claude-3-opus@20240229", 80 | Region: "us-east5", 81 | }, 82 | "claude-3-sonnet-20240229": { 83 | VertexName: "claude-3-sonnet@20240229", 84 | Region: "us-central1", 85 | // Region: "asia-southeast1", 86 | }, 87 | "claude-3-haiku-20240307": { 88 | VertexName: "claude-3-haiku@20240307", 89 | Region: "us-central1", 90 | // Region: "europe-west4", 91 | }, 92 | "claude-3-5-sonnet": { 93 | VertexName: "claude-3-5-sonnet@20240620", 94 | Region: "us-east5", 95 | // Region: "europe-west1", 96 | }, 97 | "claude-3-5-sonnet-20240620": { 98 | VertexName: "claude-3-5-sonnet@20240620", 99 | Region: "us-east5", 100 | // Region: "europe-west1", 101 | }, 102 | "claude-3-5-sonnet-20241022": { 103 | VertexName: "claude-3-5-sonnet-v2@20241022", 104 | Region: "us-east5", 105 | }, 106 | "claude-3-5-sonnet-latest": { //可能没有容量,指向老模型 107 | VertexName: "claude-3-5-sonnet@20240620", 108 | Region: "us-east5", 109 | }, 110 | } 111 | 112 | func createSignedJWT(email, privateKeyPEM string) (string, error) { 113 | block, _ := pem.Decode([]byte(privateKeyPEM)) 114 | if block == nil { 115 | return "", fmt.Errorf("failed to parse PEM block containing the private key") 116 | } 117 | 118 | privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) 119 | if err != nil { 120 | return "", err 121 | } 122 | 123 | rsaKey, ok := privateKey.(*rsa.PrivateKey) 124 | if !ok { 125 | return "", fmt.Errorf("not an RSA private key") 126 | } 127 | 128 | now := time.Now() 129 | claims := jwt.MapClaims{ 130 | "iss": email, 131 | "aud": "https://www.googleapis.com/oauth2/v4/token", 132 | "iat": now.Unix(), 133 | "exp": now.Add(10 * time.Minute).Unix(), 134 | "scope": "https://www.googleapis.com/auth/cloud-platform", 135 | } 136 | 137 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 138 | return token.SignedString(rsaKey) 139 | } 140 | 141 | func exchangeJwtForAccessToken(signedJWT string) (string, error) { 142 | authURL := "https://www.googleapis.com/oauth2/v4/token" 143 | data := url.Values{} 144 | data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") 145 | data.Set("assertion", signedJWT) 146 | 147 | client := http.DefaultClient 148 | if os.Getenv("LOCAL_PROXY") != "" { 149 | if proxyUrl, err := url.Parse(os.Getenv("LOCAL_PROXY")); err == nil { 150 | client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} 151 | } 152 | } 153 | 154 | resp, err := client.PostForm(authURL, data) 155 | if err != nil { 156 | return "", err 157 | } 158 | defer resp.Body.Close() 159 | 160 | var result map[string]interface{} 161 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 162 | return "", err 163 | } 164 | 165 | accessToken, ok := result["access_token"].(string) 166 | if !ok { 167 | return "", fmt.Errorf("access token not found in response") 168 | } 169 | 170 | return accessToken, nil 171 | } 172 | 173 | // 获取gcloud auth token 174 | func GcloudAuth(ClientEmail, PrivateKey string) (string, error) { 175 | signedJWT, err := createSignedJWT(ClientEmail, PrivateKey) 176 | if err != nil { 177 | return "", err 178 | } 179 | 180 | token, err := exchangeJwtForAccessToken(signedJWT) 181 | if err != nil { 182 | return "", fmt.Errorf("Invalid jwt token: %v\n", err) 183 | } 184 | 185 | return token, nil 186 | } 187 | -------------------------------------------------------------------------------- /router/chat.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "opencatd-open/pkg/claude" 8 | "opencatd-open/pkg/google" 9 | "opencatd-open/pkg/openai" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func ChatHandler(c *gin.Context) { 15 | var chatreq openai.ChatCompletionRequest 16 | if err := c.ShouldBindJSON(&chatreq); err != nil { 17 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 18 | return 19 | } 20 | 21 | if strings.Contains(chatreq.Model, "gpt") || strings.HasPrefix(chatreq.Model, "o1") || strings.HasPrefix(chatreq.Model, "o3") { 22 | openai.ChatProxy(c, &chatreq) 23 | return 24 | } 25 | 26 | if strings.HasPrefix(chatreq.Model, "claude") { 27 | claude.ChatProxy(c, &chatreq) 28 | return 29 | } 30 | 31 | if strings.HasPrefix(chatreq.Model, "gemini") || strings.HasPrefix(chatreq.Model, "learnlm") { 32 | google.ChatProxy(c, &chatreq) 33 | return 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "net/http/httputil" 8 | "opencatd-open/pkg/claude" 9 | oai "opencatd-open/pkg/openai" 10 | "opencatd-open/store" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | var ( 17 | GPT3Dot5Turbo = "gpt-3.5-turbo" 18 | GPT4 = "gpt-4" 19 | ) 20 | 21 | // type ChatCompletionMessage struct { 22 | // Role string `json:"role"` 23 | // Content string `json:"content"` 24 | // Name string `json:"name,omitempty"` 25 | // } 26 | 27 | // type ChatCompletionRequest struct { 28 | // Model string `json:"model"` 29 | // Messages []ChatCompletionMessage `json:"messages"` 30 | // MaxTokens int `json:"max_tokens,omitempty"` 31 | // Temperature float32 `json:"temperature,omitempty"` 32 | // TopP float32 `json:"top_p,omitempty"` 33 | // N int `json:"n,omitempty"` 34 | // Stream bool `json:"stream,omitempty"` 35 | // Stop []string `json:"stop,omitempty"` 36 | // PresencePenalty float32 `json:"presence_penalty,omitempty"` 37 | // FrequencyPenalty float32 `json:"frequency_penalty,omitempty"` 38 | // LogitBias map[string]int `json:"logit_bias,omitempty"` 39 | // User string `json:"user,omitempty"` 40 | // } 41 | 42 | // type ChatCompletionChoice struct { 43 | // Index int `json:"index"` 44 | // Message ChatCompletionMessage `json:"message"` 45 | // FinishReason string `json:"finish_reason"` 46 | // } 47 | 48 | // type ChatCompletionResponse struct { 49 | // ID string `json:"id"` 50 | // Object string `json:"object"` 51 | // Created int64 `json:"created"` 52 | // Model string `json:"model"` 53 | // Choices []ChatCompletionChoice `json:"choices"` 54 | // Usage struct { 55 | // PromptTokens int `json:"prompt_tokens"` 56 | // CompletionTokens int `json:"completion_tokens"` 57 | // TotalTokens int `json:"total_tokens"` 58 | // } `json:"usage"` 59 | // } 60 | 61 | func HandleProxy(c *gin.Context) { 62 | var ( 63 | localuser bool 64 | ) 65 | auth := c.Request.Header.Get("Authorization") 66 | if len(auth) > 7 && auth[:7] == "Bearer " { 67 | localuser = store.IsExistAuthCache(auth[7:]) 68 | c.Set("localuser", auth[7:]) 69 | } 70 | if c.Request.URL.Path == "/v1/complete" { 71 | if localuser { 72 | claude.ClaudeProxy(c) 73 | return 74 | } else { 75 | HandleReverseProxy(c, "api.anthropic.com") 76 | return 77 | } 78 | 79 | } 80 | if c.Request.URL.Path == "/v1/audio/transcriptions" { 81 | oai.WhisperProxy(c) 82 | return 83 | } 84 | if c.Request.URL.Path == "/v1/audio/speech" { 85 | oai.SpeechProxy(c) 86 | return 87 | } 88 | 89 | if c.Request.URL.Path == "/v1/images/generations" { 90 | oai.DallEProxy(c) 91 | return 92 | } 93 | 94 | if c.Request.URL.Path == "/v1/realtime" { 95 | oai.RealTimeProxy(c) 96 | return 97 | } 98 | 99 | if c.Request.URL.Path == "/v1/chat/completions" { 100 | if localuser { 101 | if store.KeysCache.ItemCount() == 0 { 102 | c.JSON(http.StatusBadGateway, gin.H{"error": gin.H{ 103 | "message": "No Api-Key Available", 104 | }}) 105 | return 106 | } 107 | 108 | ChatHandler(c) 109 | return 110 | } 111 | } else { 112 | HandleReverseProxy(c, "api.openai.com") 113 | return 114 | } 115 | 116 | } 117 | 118 | func HandleReverseProxy(c *gin.Context, targetHost string) { 119 | proxy := &httputil.ReverseProxy{ 120 | Director: func(req *http.Request) { 121 | req.URL.Scheme = "https" 122 | req.URL.Host = targetHost 123 | }, 124 | Transport: &http.Transport{ 125 | Proxy: http.ProxyFromEnvironment, 126 | DialContext: (&net.Dialer{ 127 | Timeout: 30 * time.Second, 128 | KeepAlive: 30 * time.Second, 129 | }).DialContext, 130 | ForceAttemptHTTP2: true, 131 | MaxIdleConns: 100, 132 | IdleConnTimeout: 90 * time.Second, 133 | TLSHandshakeTimeout: 10 * time.Second, 134 | ExpectContinueTimeout: 1 * time.Second, 135 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 136 | }, 137 | } 138 | 139 | req, err := http.NewRequest(c.Request.Method, c.Request.URL.Path, c.Request.Body) 140 | if err != nil { 141 | c.JSON(http.StatusOK, gin.H{"error": err.Error()}) 142 | return 143 | } 144 | req.Header = c.Request.Header 145 | 146 | proxy.ServeHTTP(c.Writer, req) 147 | return 148 | } 149 | -------------------------------------------------------------------------------- /store/cache.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "math/rand" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/Sakurasan/to" 12 | "github.com/patrickmn/go-cache" 13 | ) 14 | 15 | var ( 16 | KeysCache *cache.Cache 17 | AuthCache *cache.Cache 18 | ) 19 | 20 | func init() { 21 | KeysCache = cache.New(cache.NoExpiration, cache.NoExpiration) 22 | AuthCache = cache.New(cache.NoExpiration, cache.NoExpiration) 23 | } 24 | 25 | func LoadKeysCache() { 26 | KeysCache = cache.New(cache.NoExpiration, cache.NoExpiration) 27 | keys, err := GetAllKeys() 28 | if err != nil { 29 | log.Println(err) 30 | return 31 | } 32 | for idx, key := range keys { 33 | KeysCache.Set(to.String(idx), key, cache.NoExpiration) 34 | } 35 | } 36 | 37 | func FromKeyCacheRandomItemKey() Key { 38 | items := KeysCache.Items() 39 | if len(items) == 1 { 40 | return items[to.String(0)].Object.(Key) 41 | } 42 | idx := rand.Intn(len(items)) 43 | item := items[to.String(idx)] 44 | return item.Object.(Key) 45 | } 46 | 47 | func SelectKeyCache(apitype string) (Key, error) { 48 | var keys []Key 49 | items := KeysCache.Items() 50 | for _, item := range items { 51 | if item.Object.(Key).ApiType == apitype { 52 | keys = append(keys, item.Object.(Key)) 53 | } 54 | if apitype == "openai" { 55 | if item.Object.(Key).ApiType == "azure" { 56 | keys = append(keys, item.Object.(Key)) 57 | } 58 | if item.Object.(Key).ApiType == "github" { 59 | keys = append(keys, item.Object.(Key)) 60 | } 61 | } 62 | if apitype == "claude" { 63 | if item.Object.(Key).ApiType == "vertex" { 64 | keys = append(keys, item.Object.(Key)) 65 | } 66 | } 67 | } 68 | if len(keys) == 0 { 69 | return Key{}, errors.New("No key found") 70 | } else if len(keys) == 1 { 71 | return keys[0], nil 72 | } 73 | rand.Seed(time.Now().UnixNano()) 74 | idx := rand.Intn(len(keys)) 75 | return keys[idx], nil 76 | } 77 | 78 | func SelectKeyCacheByModel(model string) (Key, error) { 79 | var keys []Key 80 | if os.Getenv("OPENAI_API_KEY") != "" { 81 | keys = append(keys, Key{ApiType: "openai", Key: os.Getenv("OPENAI_API_KEY")}) 82 | } 83 | items := KeysCache.Items() 84 | for _, item := range items { 85 | if strings.Contains(model, "realtime") || strings.HasPrefix(model, "o1-") { 86 | if item.Object.(Key).ApiType == "openai" { 87 | keys = append(keys, item.Object.(Key)) 88 | } 89 | if item.Object.(Key).ApiType == "azure" { 90 | keys = append(keys, item.Object.(Key)) 91 | } 92 | } 93 | if strings.HasPrefix(model, "gpt-") { 94 | if item.Object.(Key).ApiType == "openai" { 95 | keys = append(keys, item.Object.(Key)) 96 | } 97 | if item.Object.(Key).ApiType == "azure" { 98 | keys = append(keys, item.Object.(Key)) 99 | } 100 | if item.Object.(Key).ApiType == "github" { 101 | keys = append(keys, item.Object.(Key)) 102 | } 103 | } 104 | if strings.HasPrefix(model, "chatgpt-") { 105 | if item.Object.(Key).ApiType == "openai" { 106 | keys = append(keys, item.Object.(Key)) 107 | } 108 | } 109 | } 110 | if len(keys) == 0 { 111 | return Key{}, errors.New("No key found") 112 | } else if len(keys) == 1 { 113 | return keys[0], nil 114 | } 115 | rand.Seed(time.Now().UnixNano()) 116 | idx := rand.Intn(len(keys)) 117 | return keys[idx], nil 118 | } 119 | 120 | func LoadAuthCache() { 121 | AuthCache = cache.New(cache.NoExpiration, cache.NoExpiration) 122 | users, err := GetAllUsers() 123 | if err != nil { 124 | log.Println(err) 125 | return 126 | } 127 | for _, user := range users { 128 | AuthCache.Set(user.Token, true, cache.NoExpiration) 129 | } 130 | } 131 | 132 | func IsExistAuthCache(auth string) bool { 133 | items := AuthCache.Items() 134 | _, ok := items[auth] 135 | return ok 136 | } 137 | -------------------------------------------------------------------------------- /store/db.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | // "gorm.io/driver/sqlite" 8 | "github.com/glebarez/sqlite" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | var db *gorm.DB 13 | 14 | var usage *gorm.DB 15 | 16 | func init() { 17 | if _, err := os.Stat("db"); os.IsNotExist(err) { 18 | errDir := os.MkdirAll("db", 0755) 19 | if errDir != nil { 20 | log.Fatalln("Error creating directory:", err) 21 | } 22 | } 23 | var err error 24 | db, err = gorm.Open(sqlite.Open("./db/cat.db"), &gorm.Config{}) 25 | if err != nil { 26 | panic("failed to connect database") 27 | } 28 | 29 | // 自动迁移 User 结构体 30 | err = db.AutoMigrate(&User{}, &Key{}) 31 | if err != nil { 32 | panic(err) 33 | } 34 | LoadKeysCache() 35 | LoadAuthCache() 36 | 37 | usage, err = gorm.Open(sqlite.Open("./db/usage.db"), &gorm.Config{}) 38 | if err != nil { 39 | panic(err) 40 | } 41 | err = usage.AutoMigrate(&DailyUsage{}, &Usage{}) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /store/keydb.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "opencatd-open/pkg/vertexai" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func init() { 13 | // check vertex 14 | if os.Getenv("Vertex") != "" { 15 | vertex_auth := os.Getenv("Vertex") 16 | var Vertex vertexai.VertexSecretKey 17 | if err := json.Unmarshal([]byte(vertex_auth), &Vertex); err != nil { 18 | log.Fatalln(fmt.Errorf("import vertex_auth json error: %w", err)) 19 | return 20 | } 21 | key := Key{ 22 | ApiType: "vertex", 23 | Name: Vertex.ProjectID, 24 | Key: vertex_auth, 25 | ApiSecret: vertex_auth, 26 | } 27 | if err := db.Where("name = ?", Vertex.ProjectID).FirstOrCreate(&key).Error; err != nil { 28 | log.Fatalln(fmt.Errorf("import vertex_auth json error: %w", err)) 29 | return 30 | } 31 | } 32 | LoadKeysCache() 33 | } 34 | 35 | type Key struct { 36 | ID uint `gorm:"primarykey" json:"id,omitempty"` 37 | Key string `gorm:"unique;not null" json:"key,omitempty"` 38 | Name string `gorm:"unique;not null" json:"name,omitempty"` 39 | UserId string `json:"-,omitempty"` 40 | ApiType string `gorm:"column:api_type"` 41 | EndPoint string `gorm:"column:endpoint"` 42 | ResourceNmae string `gorm:"column:resource_name"` 43 | DeploymentName string `gorm:"column:deployment_name"` 44 | ApiSecret string `gorm:"column:api_secret"` 45 | CreatedAt time.Time `json:"createdAt,omitempty"` 46 | UpdatedAt time.Time `json:"updatedAt,omitempty"` 47 | } 48 | 49 | func (k Key) ToString() string { 50 | bdate, _ := json.Marshal(k) 51 | return string(bdate) 52 | } 53 | 54 | func GetKeyrByName(name string) (*Key, error) { 55 | var key Key 56 | result := db.First(&key, "name = ?", name) 57 | if result.Error != nil { 58 | return nil, result.Error 59 | } 60 | return &key, nil 61 | } 62 | 63 | func GetAllKeys() ([]Key, error) { 64 | var keys []Key 65 | if err := db.Find(&keys).Error; err != nil { 66 | return nil, err 67 | } 68 | return keys, nil 69 | } 70 | 71 | // 添加记录 72 | func AddKey(apitype, apikey, name string) error { 73 | key := Key{ 74 | ApiType: apitype, 75 | Key: apikey, 76 | Name: name, 77 | } 78 | if err := db.Create(&key).Error; err != nil { 79 | return err 80 | } 81 | LoadKeysCache() 82 | return nil 83 | } 84 | 85 | func CreateKey(k *Key) error { 86 | if err := db.Create(&k).Error; err != nil { 87 | return err 88 | } 89 | LoadKeysCache() 90 | return nil 91 | } 92 | 93 | // 删除记录 94 | func DeleteKey(id uint) error { 95 | if err := db.Delete(&Key{}, id).Error; err != nil { 96 | return err 97 | } 98 | LoadKeysCache() 99 | return nil 100 | } 101 | 102 | // 更新记录 103 | func UpdateKey(id uint, apikey string, userId string) error { 104 | key := Key{ 105 | Key: apikey, 106 | UserId: userId, 107 | } 108 | if err := db.Model(&Key{}).Where("id = ?", id).Updates(key).Error; err != nil { 109 | return err 110 | } 111 | LoadKeysCache() 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /store/usage.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/Sakurasan/to" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type DailyUsage struct { 12 | ID int `gorm:"column:id"` 13 | UserID int `gorm:"column:user_id";primaryKey` 14 | Date time.Time `gorm:"column:date"` 15 | SKU string `gorm:"column:sku"` 16 | PromptUnits int `gorm:"column:prompt_units"` 17 | CompletionUnits int `gorm:"column:completion_units"` 18 | TotalUnit int `gorm:"column:total_unit"` 19 | Cost string `gorm:"column:cost"` 20 | } 21 | 22 | func (DailyUsage) TableName() string { 23 | return "daily_usages" 24 | } 25 | 26 | type Usage struct { 27 | ID int `gorm:"column:id"` 28 | PromptHash string `gorm:"column:prompt_hash"` 29 | UserID int `gorm:"column:user_id"` 30 | SKU string `gorm:"column:sku"` 31 | PromptUnits int `gorm:"column:prompt_units"` 32 | CompletionUnits int `gorm:"column:completion_units"` 33 | TotalUnit int `gorm:"column:total_unit"` 34 | Cost string `gorm:"column:cost"` 35 | Date time.Time `gorm:"column:date"` 36 | } 37 | 38 | func (Usage) TableName() string { 39 | return "usages" 40 | } 41 | 42 | type Summary struct { 43 | UserId int `gorm:"column:user_id"` 44 | SumPromptUnits int `gorm:"column:sum_prompt_units"` 45 | SumCompletionUnits int `gorm:"column:sum_completion_units"` 46 | SumTotalUnit int `gorm:"column:sum_total_unit"` 47 | SumCost float64 `gorm:"column:sum_cost"` 48 | } 49 | type CalcUsage struct { 50 | UserID int `json:"userId,omitempty"` 51 | TotalUnit int `json:"totalUnit,omitempty"` 52 | Cost string `json:"cost,omitempty"` 53 | } 54 | 55 | func QueryUsage(from, to string) ([]CalcUsage, error) { 56 | var results = []CalcUsage{} 57 | err := usage.Model(&DailyUsage{}).Select(`user_id, 58 | --SUM(prompt_units) AS prompt_units, 59 | -- SUM(completion_units) AS completion_units, 60 | SUM(total_unit) AS total_unit, 61 | printf('%.6f', SUM(cost)) AS cost`). 62 | Group("user_id"). 63 | Where("date >= ? AND date < ?", from, to). 64 | Find(&results).Error 65 | if err != nil { 66 | return nil, err 67 | } 68 | return results, nil 69 | } 70 | 71 | func QueryUserUsage(userid, from, to string) (*CalcUsage, error) { 72 | var results = new(CalcUsage) 73 | err := usage.Model(&DailyUsage{}).Select(`user_id, 74 | --SUM(prompt_units) AS prompt_units, 75 | -- SUM(completion_units) AS completion_units, 76 | SUM(total_unit) AS total_unit, 77 | printf('%.6f', SUM(cost)) AS cost`). 78 | Where("user_id = ? AND date >= ? AND date < ?", userid, from, to). 79 | Find(&results).Error 80 | if err != nil { 81 | return nil, err 82 | } 83 | return results, nil 84 | } 85 | 86 | type Tokens struct { 87 | UserID int 88 | PromptCount int 89 | CompletionCount int 90 | TotalTokens int 91 | Cost string 92 | Model string 93 | PromptHash string 94 | } 95 | 96 | func Record(chatlog *Tokens) (err error) { 97 | u := &Usage{ 98 | UserID: chatlog.UserID, 99 | SKU: chatlog.Model, 100 | PromptHash: chatlog.PromptHash, 101 | PromptUnits: chatlog.PromptCount, 102 | CompletionUnits: chatlog.CompletionCount, 103 | TotalUnit: chatlog.TotalTokens, 104 | Cost: to.String(chatlog.Cost), 105 | Date: time.Now(), 106 | } 107 | err = usage.Create(u).Error 108 | return 109 | 110 | } 111 | 112 | func SumDaily(userid int) error { 113 | var count int64 114 | err := usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)).Count(&count).Error 115 | if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 116 | return err 117 | } 118 | if count == 0 { 119 | if err := insertSumDaily(userid); err != nil { 120 | return err 121 | } 122 | } else { 123 | if err := updateSumDaily(userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)); err != nil { 124 | return err 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | func insertSumDaily(uid int) error { 131 | nowstr := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) 132 | err := usage.Exec(`INSERT INTO daily_usages 133 | (user_id, date, sku, prompt_units, completion_units, total_unit, cost) 134 | SELECT 135 | user_id, 136 | ?, 137 | sku, 138 | SUM(prompt_units) AS sum_prompt_units, 139 | SUM(completion_units) AS sum_completion_units, 140 | SUM(total_unit) AS sum_total_unit, 141 | SUM(cost) AS sum_cost 142 | FROM usages 143 | WHERE date >= ? 144 | AND user_id = ?`, nowstr, nowstr, uid).Error 145 | if err != nil { 146 | return err 147 | } 148 | return nil 149 | } 150 | 151 | func updateSumDaily(uid int, date time.Time) error { 152 | // var u = Summary{} 153 | err := usage.Model(&Usage{}).Exec(`UPDATE daily_usages 154 | SET 155 | prompt_units = (SELECT SUM(prompt_units) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date), 156 | completion_units = (SELECT SUM(completion_units) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date), 157 | total_unit = (SELECT SUM(total_unit) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date), 158 | cost = (SELECT SUM(cost) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date) 159 | WHERE user_id = ? AND date >= ?`, uid, date).Error 160 | if err != nil { 161 | return err 162 | } 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /store/userdb.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type User struct { 10 | gorm.Model 11 | IsDelete bool `gorm:"default:false" json:"IsDelete"` 12 | ID uint `gorm:"primarykey autoIncrement;" json:"id,omitempty"` 13 | Name string `gorm:"unique;not null" json:"name,omitempty"` 14 | Token string `gorm:"unique;not null" json:"token,omitempty"` 15 | CreatedAt time.Time `json:"createdAt,omitempty"` 16 | UpdatedAt time.Time `json:"updatedAt,omitempty"` 17 | // DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` 18 | } 19 | 20 | func CreateUser(u *User) error { 21 | result := db.Create(u) 22 | if result.Error != nil { 23 | return result.Error 24 | } 25 | LoadAuthCache() 26 | return nil 27 | } 28 | 29 | // 添加用户 30 | func AddUser(name, token string) error { 31 | user := &User{Name: name, Token: token} 32 | result := db.Create(&user) 33 | if result.Error != nil { 34 | return result.Error 35 | } 36 | LoadAuthCache() 37 | return nil 38 | } 39 | 40 | // 删除用户 41 | func DeleteUser(id uint) error { 42 | result := db.Delete(&User{}, id) 43 | if result.Error != nil { 44 | return result.Error 45 | } 46 | LoadAuthCache() 47 | return nil 48 | } 49 | 50 | // 修改用户 51 | func UpdateUser(id uint, token string) error { 52 | user := &User{Token: token} 53 | result := db.Model(&User{}).Where("id = ?", id).Updates(user) 54 | if result.Error != nil { 55 | return result.Error 56 | } 57 | LoadAuthCache() 58 | return nil 59 | } 60 | 61 | func GetUserByID(id uint) (*User, error) { 62 | var user User 63 | result := db.Where("id = ?", id).First(&user) 64 | if result.Error != nil { 65 | return nil, result.Error 66 | } 67 | return &user, nil 68 | } 69 | 70 | func GetUserByName(name string) (*User, error) { 71 | var user User 72 | result := db.Where(&User{Name: name}).First(&user) 73 | if result.Error != nil { 74 | return nil, result.Error 75 | } 76 | return &user, nil 77 | } 78 | 79 | func GetUserByToken(token string) (*User, error) { 80 | var user User 81 | result := db.Where("token = ?", token).First(&user) 82 | if result.Error != nil { 83 | return nil, result.Error 84 | } 85 | return &user, nil 86 | } 87 | 88 | func GetUserID(authkey string) (int, error) { 89 | var user User 90 | result := db.Where(&User{Token: authkey}).First(&user) 91 | if result.Error != nil { 92 | return 0, result.Error 93 | } 94 | return int(user.ID), nil 95 | } 96 | 97 | func GetAllUsers() ([]*User, error) { 98 | var users []*User 99 | result := db.Find(&users) 100 | if result.Error != nil { 101 | return nil, result.Error 102 | } 103 | return users, nil 104 | } 105 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.2.47" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^4.1.0", 16 | "autoprefixer": "^10.4.14", 17 | "postcss": "^8.4.23", 18 | "tailwindcss": "^3.3.2", 19 | "vite": "^4.3.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 135 | 136 | -------------------------------------------------------------------------------- /web/src/assets/chatgpt_client.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/web/src/assets/chatgpt_client.jpg -------------------------------------------------------------------------------- /web/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/team.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/web/src/assets/team.jpg -------------------------------------------------------------------------------- /web/src/assets/usersdomain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/web/src/assets/usersdomain.jpg -------------------------------------------------------------------------------- /web/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /web/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | line-height: 1.5; 8 | font-weight: 400; 9 | 10 | color-scheme: light dark; 11 | color: rgba(255, 255, 255, 0.87); 12 | background-color: #242424; 13 | 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | 21 | a { 22 | font-weight: 500; 23 | color: #646cff; 24 | text-decoration: inherit; 25 | } 26 | a:hover { 27 | color: #535bf2; 28 | } 29 | 30 | a { 31 | font-weight: 500; 32 | color: #646cff; 33 | text-decoration: inherit; 34 | } 35 | a:hover { 36 | color: #535bf2; 37 | } 38 | 39 | body { 40 | margin: 0; 41 | display: flex; 42 | place-items: center; 43 | min-width: 320px; 44 | min-height: 100vh; 45 | } 46 | 47 | h1 { 48 | font-size: 3.2em; 49 | line-height: 1.1; 50 | } 51 | 52 | button { 53 | border-radius: 8px; 54 | border: 1px solid transparent; 55 | padding: 0.6em 1.2em; 56 | font-size: 1em; 57 | font-weight: 500; 58 | font-family: inherit; 59 | background-color: #1a1a1a; 60 | cursor: pointer; 61 | transition: border-color 0.25s; 62 | } 63 | button:hover { 64 | border-color: #646cff; 65 | } 66 | button:focus, 67 | button:focus-visible { 68 | outline: 4px auto -webkit-focus-ring-color; 69 | } 70 | 71 | .card { 72 | padding: 2em; 73 | } 74 | 75 | #app { 76 | max-width: 1280px; 77 | margin: 0 auto; 78 | padding: 2rem; 79 | text-align: center; 80 | } 81 | 82 | @media (prefers-color-scheme: light) { 83 | :root { 84 | color: #213547; 85 | background-color: #ffffff; 86 | } 87 | a:hover { 88 | color: #747bff; 89 | } 90 | button { 91 | background-color: #f9f9f9; 92 | } 93 | } */ 94 | -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /web/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | --------------------------------------------------------------------------------