├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── channel_update.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── app.yaml │ ├── build.yaml │ ├── docker-cd.yaml │ ├── docker-ci.yaml │ └── issue-translator.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── README_ja-JP.md ├── README_zh-CN.md ├── adapter ├── adapter.go ├── azure │ ├── chat.go │ ├── image.go │ ├── processor.go │ ├── struct.go │ └── types.go ├── baichuan │ ├── chat.go │ ├── processor.go │ ├── struct.go │ └── types.go ├── bing │ ├── chat.go │ ├── struct.go │ └── types.go ├── claude │ ├── chat.go │ ├── struct.go │ └── types.go ├── common │ ├── interface.go │ └── types.go ├── coze │ ├── chat.go │ ├── processor.go │ └── struct.go ├── dashscope │ ├── chat.go │ ├── struct.go │ └── types.go ├── deepseek │ ├── chat.go │ └── struct.go ├── dify │ ├── chat.go │ ├── processor.go │ └── struct.go ├── hunyuan │ ├── chat.go │ ├── sdk.go │ └── struct.go ├── midjourney │ ├── api.go │ ├── chat.go │ ├── expose.go │ ├── handler.go │ ├── storage.go │ ├── struct.go │ └── types.go ├── openai │ ├── chat.go │ ├── image.go │ ├── processor.go │ ├── struct.go │ └── types.go ├── palm2 │ ├── chat.go │ ├── formatter.go │ ├── image.go │ ├── struct.go │ └── types.go ├── request.go ├── router.go ├── skylark │ ├── chat.go │ ├── formatter.go │ └── struct.go ├── slack │ ├── chat.go │ └── struct.go ├── sparkdesk │ ├── chat.go │ ├── struct.go │ └── types.go ├── zhinao │ ├── chat.go │ ├── processor.go │ ├── struct.go │ └── types.go └── zhipuai │ ├── chat.go │ ├── processor.go │ ├── struct.go │ └── types.go ├── addition ├── article │ ├── api.go │ ├── data │ │ └── .gitkeep │ ├── generate.go │ ├── template.docx │ └── utils.go ├── card │ ├── .gitignore │ ├── card.go │ ├── card.php │ ├── error.php │ ├── favicon.ico │ └── utils.php ├── generation │ ├── api.go │ ├── build.go │ ├── data │ │ └── .gitkeep │ ├── generate.go │ └── prompt.go ├── router.go └── web │ ├── call.go │ └── search.go ├── admin ├── analysis.go ├── controller.go ├── format.go ├── instance.go ├── invitation.go ├── logger.go ├── market.go ├── redeem.go ├── router.go ├── statistic.go ├── types.go └── user.go ├── app ├── .env.deeptrain ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── components.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── icons │ │ ├── 360gpt.png │ │ ├── baichuan.png │ │ ├── chatglm.png │ │ ├── claude.png │ │ ├── claude100k.png │ │ ├── dalle.jpeg │ │ ├── gemini.jpeg │ │ ├── gpt35turbo.png │ │ ├── gpt35turbo16k.webp │ │ ├── gpt4.png │ │ ├── gpt432k.webp │ │ ├── gpt4dalle.png │ │ ├── gpt4v.png │ │ ├── hunyuan.png │ │ ├── llama2.webp │ │ ├── llamacode.webp │ │ ├── midjourney.jpg │ │ ├── newbing.jpg │ │ ├── palm2.webp │ │ ├── skylark.jpg │ │ ├── sparkdesk.jpg │ │ ├── stablediffusion.jpeg │ │ └── tongyi.png │ ├── logo.png │ ├── robots.txt │ ├── service.js │ ├── service │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── favicon-64x64.png │ ├── site.webmanifest │ ├── source │ │ └── qq.jpg │ └── workbox.js ├── qodana.yaml ├── src-tauri │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src │ │ └── main.rs │ └── tauri.conf.json ├── src │ ├── App.tsx │ ├── admin │ │ ├── api │ │ │ ├── channel.ts │ │ │ ├── charge.ts │ │ │ ├── chart.ts │ │ │ ├── info.ts │ │ │ ├── logger.ts │ │ │ ├── market.ts │ │ │ ├── plan.ts │ │ │ └── system.ts │ │ ├── channel.ts │ │ ├── charge.ts │ │ ├── colors.ts │ │ ├── datasets │ │ │ └── charge.ts │ │ ├── hook.tsx │ │ ├── market.ts │ │ └── types.ts │ ├── api │ │ ├── addition.ts │ │ ├── auth.ts │ │ ├── broadcast.ts │ │ ├── common.ts │ │ ├── connection.ts │ │ ├── file.ts │ │ ├── generation.ts │ │ ├── history.ts │ │ ├── invitation.ts │ │ ├── mask.ts │ │ ├── quota.ts │ │ ├── redeem.ts │ │ ├── sharing.ts │ │ ├── types.tsx │ │ └── v1.ts │ ├── assets │ │ ├── admin │ │ │ ├── all.less │ │ │ ├── broadcast.less │ │ │ ├── channel.less │ │ │ ├── charge.less │ │ │ ├── dashboard.less │ │ │ ├── logger.less │ │ │ ├── management.less │ │ │ ├── market.less │ │ │ ├── menu.less │ │ │ ├── subscription.less │ │ │ └── system.less │ │ ├── common │ │ │ ├── 404.less │ │ │ ├── editor.less │ │ │ ├── file.less │ │ │ └── loader.less │ │ ├── fonts │ │ │ ├── all.less │ │ │ ├── common.less │ │ │ └── katex.less │ │ ├── globals.less │ │ ├── main.less │ │ ├── markdown │ │ │ ├── all.less │ │ │ ├── highlight.less │ │ │ ├── style.less │ │ │ └── theme.less │ │ ├── pages │ │ │ ├── api.less │ │ │ ├── article.less │ │ │ ├── auth.less │ │ │ ├── chat.less │ │ │ ├── generation.less │ │ │ ├── home.less │ │ │ ├── mask.less │ │ │ ├── navbar.less │ │ │ ├── package.less │ │ │ ├── quota.less │ │ │ ├── settings.less │ │ │ ├── share-manager.less │ │ │ ├── sharing.less │ │ │ └── subscription.less │ │ └── ui.less │ ├── components │ │ ├── Avatar.tsx │ │ ├── Broadcast.tsx │ │ ├── EditorProvider.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── FileProvider.tsx │ │ ├── FileViewer.tsx │ │ ├── I18nProvider.tsx │ │ ├── Loader.tsx │ │ ├── Markdown.tsx │ │ ├── Message.tsx │ │ ├── OperationAction.tsx │ │ ├── Paragraph.tsx │ │ ├── PopupDialog.tsx │ │ ├── ProjectLink.tsx │ │ ├── ReloadService.tsx │ │ ├── Require.tsx │ │ ├── SelectGroup.tsx │ │ ├── ThemeProvider.tsx │ │ ├── ThinkContent.tsx │ │ ├── TickButton.tsx │ │ ├── Tips.tsx │ │ ├── admin │ │ │ ├── ChannelSettings.tsx │ │ │ ├── ChargeWidget.tsx │ │ │ ├── ChartBox.tsx │ │ │ ├── CommunityBanner.tsx │ │ │ ├── InfoBox.tsx │ │ │ ├── InvitationTable.tsx │ │ │ ├── MenuBar.tsx │ │ │ ├── RedeemTable.tsx │ │ │ ├── UserTable.tsx │ │ │ ├── assemblies │ │ │ │ ├── BillingChart.tsx │ │ │ │ ├── BroadcastTable.tsx │ │ │ │ ├── ChannelEditor.tsx │ │ │ │ ├── ChannelTable.tsx │ │ │ │ ├── ErrorChart.tsx │ │ │ │ ├── ModelChart.tsx │ │ │ │ ├── ModelUsageChart.tsx │ │ │ │ ├── RequestChart.tsx │ │ │ │ └── UserTypeChart.tsx │ │ │ └── common │ │ │ │ └── StateBadge.tsx │ │ ├── app │ │ │ ├── Announcement.tsx │ │ │ ├── AppProvider.tsx │ │ │ ├── MenuBar.tsx │ │ │ └── NavBar.tsx │ │ ├── home │ │ │ ├── ChatInterface.tsx │ │ │ ├── ChatSpace.tsx │ │ │ ├── ChatWrapper.tsx │ │ │ ├── ConversationSegment.tsx │ │ │ ├── ModelFinder.tsx │ │ │ ├── ModelMarket.tsx │ │ │ ├── SideBar.tsx │ │ │ ├── assemblies │ │ │ │ ├── ActionButton.tsx │ │ │ │ ├── ChatAction.tsx │ │ │ │ ├── ChatInput.tsx │ │ │ │ └── ScrollAction.tsx │ │ │ └── subscription │ │ │ │ ├── BuyDialog.tsx │ │ │ │ └── SubscriptionUsage.tsx │ │ ├── markdown │ │ │ ├── Code.tsx │ │ │ ├── Label.tsx │ │ │ ├── Link.tsx │ │ │ └── VirtualMessage.tsx │ │ ├── plugins │ │ │ ├── file.tsx │ │ │ ├── mermaid.tsx │ │ │ └── progress.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── combo-box.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── icons │ │ │ │ └── Github.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── lib │ │ │ │ └── utils.ts │ │ │ ├── multi-combobox.tsx │ │ │ ├── number-input.tsx │ │ │ ├── pagination.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ ├── tooltip.tsx │ │ │ └── use-toast.ts │ │ └── utils │ │ │ └── Icon.tsx │ ├── conf │ │ ├── api.ts │ │ ├── bootstrap.ts │ │ ├── deeptrain.tsx │ │ ├── env.ts │ │ ├── model.ts │ │ ├── storage.ts │ │ └── subscription.tsx │ ├── dialogs │ │ ├── ApikeyDialog.tsx │ │ ├── InvitationDialog.tsx │ │ ├── MaskDialog.tsx │ │ ├── PackageDialog.tsx │ │ ├── QuotaDialog.tsx │ │ ├── SettingsDialog.tsx │ │ ├── ShareManagementDialog.tsx │ │ ├── SubscriptionDialog.tsx │ │ └── index.tsx │ ├── events │ │ ├── announcement.ts │ │ ├── blob.ts │ │ ├── info.ts │ │ ├── model.ts │ │ ├── spinner.ts │ │ ├── struct.ts │ │ └── theme.ts │ ├── i18n.ts │ ├── main.tsx │ ├── masks │ │ ├── prompts.ts │ │ └── types.ts │ ├── resources │ │ └── i18n │ │ │ ├── cn.json │ │ │ ├── en.json │ │ │ ├── ja.json │ │ │ ├── ru.json │ │ │ └── tw.json │ ├── router.tsx │ ├── routes │ │ ├── Admin.tsx │ │ ├── Article.tsx │ │ ├── Auth.tsx │ │ ├── Forgot.tsx │ │ ├── Generation.tsx │ │ ├── Home.tsx │ │ ├── NotFound.tsx │ │ ├── Register.tsx │ │ ├── Sharing.tsx │ │ └── admin │ │ │ ├── Broadcast.tsx │ │ │ ├── Channel.tsx │ │ │ ├── Charge.tsx │ │ │ ├── DashBoard.tsx │ │ │ ├── Logger.tsx │ │ │ ├── Market.tsx │ │ │ ├── Subscription.tsx │ │ │ ├── System.tsx │ │ │ └── Users.tsx │ ├── spinner.tsx │ ├── store │ │ ├── api.ts │ │ ├── auth.ts │ │ ├── chat.ts │ │ ├── globals.ts │ │ ├── index.ts │ │ ├── info.ts │ │ ├── invitation.ts │ │ ├── menu.ts │ │ ├── package.ts │ │ ├── quota.ts │ │ ├── settings.ts │ │ ├── sharing.ts │ │ ├── subscription.ts │ │ └── utils.ts │ ├── translator │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── io.ts │ │ └── translator.ts │ ├── types │ │ ├── performance.d.ts │ │ ├── service.d.ts │ │ └── ui.d.ts │ ├── utils │ │ ├── app.ts │ │ ├── base.ts │ │ ├── desktop.ts │ │ ├── dev.ts │ │ ├── device.ts │ │ ├── dom.ts │ │ ├── form.ts │ │ ├── groups.ts │ │ ├── hook.ts │ │ ├── loader.tsx │ │ ├── memory.ts │ │ ├── path.ts │ │ └── processor.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── auth ├── analysis.go ├── apikey.go ├── auth.go ├── call.go ├── cert.go ├── controller.go ├── invitation.go ├── package.go ├── payment.go ├── quota.go ├── redeem.go ├── router.go ├── rule.go ├── struct.go ├── subscription.go ├── usage.go └── validators.go ├── channel ├── channel.go ├── charge.go ├── controller.go ├── manager.go ├── plan.go ├── router.go ├── sequence.go ├── system.go ├── ticker.go ├── types.go └── worker.go ├── cli ├── admin.go ├── exec.go ├── help.go ├── invite.go ├── parser.go └── token.go ├── config.example.yaml ├── connection ├── cache.go ├── database.go ├── db_migration.go └── worker.go ├── docker-compose.stable.yaml ├── docker-compose.watch.yaml ├── docker-compose.yaml ├── globals ├── constant.go ├── interface.go ├── logger.go ├── method.go ├── params.go ├── sql.go ├── tools.go ├── types.go ├── usage.go └── variables.go ├── go.mod ├── go.sum ├── main.go ├── manager ├── broadcast │ ├── controller.go │ ├── manage.go │ ├── router.go │ ├── types.go │ └── view.go ├── chat.go ├── chat_completions.go ├── completions.go ├── connection.go ├── conversation │ ├── api.go │ ├── conversation.go │ ├── mask.go │ ├── router.go │ ├── shared.go │ └── storage.go ├── images.go ├── manager.go ├── relay.go ├── router.go ├── types.go └── usage.go ├── middleware ├── auth.go ├── builtins.go ├── cors.go ├── middleware.go └── throttle.go ├── migration ├── 3.6.sql └── 3.8.sql ├── nginx.conf ├── screenshot ├── chatnio-pro.png └── chatnio.png ├── utils ├── base.go ├── buffer.go ├── cache.go ├── char.go ├── compress.go ├── config.go ├── ctx.go ├── encrypt.go ├── fs.go ├── image.go ├── net.go ├── scanner.go ├── smtp.go ├── sse.go ├── templates │ └── code.html ├── tokenizer.go └── websocket.go └── zeabur.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | app/node_modules 2 | app/src-tauri 3 | app/.idea 4 | app/.vscode 5 | app/dist 6 | app/dev-dist 7 | app/dist-ssr 8 | app/target 9 | app/tauri.conf.json 10 | app/tauri.js 11 | 12 | screenshot 13 | .vscode 14 | .idea 15 | config.yaml 16 | config.dev.yaml 17 | 18 | # current in ~/storage 19 | addition/generation/data/* 20 | !addition/generation/data/.gitkeep 21 | 22 | addition/article/data/* 23 | !addition/article/data/.gitkeep 24 | sdk 25 | logs 26 | 27 | chat 28 | chat.exe 29 | 30 | # for reverse engine 31 | reverse 32 | access.json 33 | access/*.json 34 | 35 | db 36 | cache 37 | config 38 | 39 | README.md 40 | .gitignore 41 | screenshot 42 | LICENSE 43 | 44 | .github 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告问题 | Bug Report 3 | about: 使用简练详细的语言描述你遇到的问题 | Describe the issue you encountered in detail 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | [//]: # (方框内删除已有的空格,填 x 号) 11 | + [ ] 我已确认目前没有类似 issue 12 | + [ ] 我已确认我已升级到最新版本 13 | + [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案 14 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 15 | + [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block) 16 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 17 | 18 | **问题描述** 19 | 20 | **复现步骤** 21 | 22 | **预期结果** 23 | 24 | **日志信息** 25 | 26 | **相关截图 (如果有)** 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/channel_update.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 渠道更新 | Channel Update 3 | about: 新大模型供应商格式增加、更新请求 | Request to add or update a new llm provider format 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | [//]: # (方框内删除已有的空格,填 x 号) 11 | + [ ] 我已确认目前没有类似 issue 12 | + [ ] 我已确认我已升级到最新版本 13 | + [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案 14 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 15 | + [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block) 16 | + [ ] 如果为新供应商格式,我已确认此供应商有一定的用户群体和知名度,借此以广告和推广类的名义的中转站点请求将被直接关闭 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 18 | 19 | **供应商名称** 20 | 21 | **描述** 22 | 23 | **供应商网址 / 截图 / 样例 (如果愿意提供)** -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.gg/rpzNSmqaF2 5 | about: Join Discord Community 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 | Feature Request 3 | about: 使用简练详细的语言描述希望加入的新功能 | Describe the feature you would like to request 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | [//]: # (方框内删除已有的空格,填 x 号) 11 | + [ ] 我已确认目前没有类似 issue 12 | + [ ] 我已确认我已升级到最新版本 13 | + [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案 14 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 15 | + [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block) 16 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** 17 | 18 | **功能描述** 19 | 20 | **相关截图 (如果有)** 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build Test 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - '*' 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [ 18.x ] 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Use Golang 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.20' 23 | 24 | - name: Build Backend 25 | run: | 26 | go build . 27 | 28 | - name: Use Node.js 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | - name: Build Frontend 34 | run: | 35 | cd app 36 | npm install -g pnpm 37 | pnpm install 38 | pnpm build 39 | - name: Upload a Build Artifact 40 | uses: actions/upload-artifact@v4.0.0 41 | with: 42 | name: Build result 43 | path: app/dist 44 | -------------------------------------------------------------------------------- /.github/workflows/docker-cd.yaml: -------------------------------------------------------------------------------- 1 | name: Docker CD 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | 9 | deploy: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Login to DockerHub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 20 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v1 24 | 25 | - name: Build and push Docker images 26 | uses: docker/build-push-action@v2 27 | with: 28 | context: . 29 | file: ./Dockerfile 30 | platforms: linux/amd64,linux/arm64 31 | push: true 32 | tags: programzmh/chatnio:stable 33 | -------------------------------------------------------------------------------- /.github/workflows/docker-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v2 17 | with: 18 | platforms: "arm64,amd64" 19 | 20 | - name: Login to DockerHub 21 | uses: docker/login-action@v2 22 | with: 23 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 24 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Build and push Docker images 30 | uses: docker/build-push-action@v3 31 | with: 32 | context: . 33 | file: ./Dockerfile 34 | platforms: linux/amd64,linux/arm64 35 | push: true 36 | tags: programzmh/chatnio:latest 37 | cache-from: type=registry,ref=programzmh/chatnio:buildcache 38 | cache-to: type=registry,ref=programzmh/chatnio:buildcache,mode=max 39 | -------------------------------------------------------------------------------- /.github/workflows/issue-translator.yaml: -------------------------------------------------------------------------------- 1 | name: Issue Translator 2 | on: 3 | issue_comment: 4 | types: [created] 5 | issues: 6 | types: [opened] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: usthe/issues-translate-action@v2.7 13 | with: 14 | IS_MODIFY_TITLE: false 15 | CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/node_modules 2 | .vscode 3 | .idea 4 | config.yaml 5 | config.dev.yaml 6 | storage 7 | 8 | addition/generation/data/* 9 | !addition/generation/data/.gitkeep 10 | 11 | addition/article/data/* 12 | !addition/article/data/.gitkeep 13 | sdk 14 | logs 15 | 16 | chat 17 | *.exe 18 | chat.exe 19 | 20 | # for reverse engine 21 | reverse 22 | access.json 23 | access/*.json 24 | 25 | db 26 | redis 27 | config 28 | 29 | # for opencommit 30 | .env -------------------------------------------------------------------------------- /adapter/azure/struct.go: -------------------------------------------------------------------------------- 1 | package azure 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | ) 7 | 8 | type ChatInstance struct { 9 | Endpoint string 10 | ApiKey string 11 | Resource string 12 | } 13 | 14 | func (c *ChatInstance) GetEndpoint() string { 15 | return c.Endpoint 16 | } 17 | 18 | func (c *ChatInstance) GetApiKey() string { 19 | return c.ApiKey 20 | } 21 | 22 | func (c *ChatInstance) GetResource() string { 23 | return c.Resource 24 | } 25 | 26 | func (c *ChatInstance) GetHeader() map[string]string { 27 | return map[string]string{ 28 | "Content-Type": "application/json", 29 | "api-key": c.GetApiKey(), 30 | } 31 | } 32 | 33 | func NewChatInstance(endpoint, apiKey string, resource string) *ChatInstance { 34 | return &ChatInstance{ 35 | Endpoint: endpoint, 36 | ApiKey: apiKey, 37 | Resource: resource, 38 | } 39 | } 40 | 41 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 42 | param := conf.SplitRandomSecret(2) 43 | return NewChatInstance( 44 | conf.GetEndpoint(), 45 | param[0], 46 | param[1], 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /adapter/baichuan/processor.go: -------------------------------------------------------------------------------- 1 | package baichuan 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | func processChatResponse(data string) *ChatStreamResponse { 11 | return utils.UnmarshalForm[ChatStreamResponse](data) 12 | } 13 | 14 | func processChatErrorResponse(data string) *ChatStreamErrorResponse { 15 | return utils.UnmarshalForm[ChatStreamErrorResponse](data) 16 | } 17 | 18 | func getChoices(form *ChatStreamResponse) *globals.Chunk { 19 | if len(form.Choices) == 0 { 20 | return &globals.Chunk{Content: ""} 21 | } 22 | 23 | choice := form.Choices[0].Delta 24 | 25 | return &globals.Chunk{ 26 | Content: choice.Content, 27 | } 28 | } 29 | 30 | func (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) { 31 | if form := processChatResponse(data); form != nil { 32 | return getChoices(form), nil 33 | } 34 | 35 | if form := processChatErrorResponse(data); form != nil { 36 | return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("baichuan error: %s (type: %s)", form.Error.Message, form.Error.Type)) 37 | } 38 | 39 | globals.Warn(fmt.Sprintf("baichuan error: cannot parse chat completion response: %s", data)) 40 | return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response") 41 | } 42 | -------------------------------------------------------------------------------- /adapter/baichuan/struct.go: -------------------------------------------------------------------------------- 1 | package baichuan 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | "fmt" 7 | ) 8 | 9 | type ChatInstance struct { 10 | Endpoint string 11 | ApiKey string 12 | } 13 | 14 | func (c *ChatInstance) GetEndpoint() string { 15 | return c.Endpoint 16 | } 17 | 18 | func (c *ChatInstance) GetApiKey() string { 19 | return c.ApiKey 20 | } 21 | 22 | func (c *ChatInstance) GetHeader() map[string]string { 23 | return map[string]string{ 24 | "Content-Type": "application/json", 25 | "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), 26 | } 27 | } 28 | 29 | func NewChatInstance(endpoint, apiKey string) *ChatInstance { 30 | return &ChatInstance{ 31 | Endpoint: endpoint, 32 | ApiKey: apiKey, 33 | } 34 | } 35 | 36 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 37 | return NewChatInstance( 38 | conf.GetEndpoint(), 39 | conf.GetRandomSecret(), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/baichuan/types.go: -------------------------------------------------------------------------------- 1 | package baichuan 2 | 3 | import "chat/globals" 4 | 5 | // Baichuan AI API is similar to OpenAI API 6 | 7 | type ChatRequest struct { 8 | Model string `json:"model"` 9 | Messages []globals.Message `json:"messages"` 10 | Stream bool `json:"stream"` 11 | TopP *float32 `json:"top_p,omitempty"` 12 | TopK *int `json:"top_k,omitempty"` 13 | Temperature *float32 `json:"temperature,omitempty"` 14 | WithSearchEnhance *bool `json:"with_search_enhance,omitempty"` 15 | } 16 | 17 | // ChatResponse is the native http request body for baichuan 18 | type ChatResponse struct { 19 | ID string `json:"id"` 20 | Object string `json:"object"` 21 | Created int64 `json:"created"` 22 | Model string `json:"model"` 23 | Choices []struct { 24 | Message struct { 25 | Content string `json:"content"` 26 | } 27 | } `json:"choices"` 28 | Error struct { 29 | Message string `json:"message"` 30 | } `json:"error"` 31 | } 32 | 33 | // ChatStreamResponse is the stream response body for baichuan 34 | type ChatStreamResponse struct { 35 | ID string `json:"id"` 36 | Object string `json:"object"` 37 | Created int64 `json:"created"` 38 | Model string `json:"model"` 39 | Choices []struct { 40 | Delta struct { 41 | Content string `json:"content"` 42 | } 43 | Index int `json:"index"` 44 | } `json:"choices"` 45 | } 46 | 47 | type ChatStreamErrorResponse struct { 48 | Error struct { 49 | Message string `json:"message"` 50 | Type string `json:"type"` 51 | } `json:"error"` 52 | } 53 | -------------------------------------------------------------------------------- /adapter/bing/chat.go: -------------------------------------------------------------------------------- 1 | package bing 2 | 3 | import ( 4 | adaptercommon "chat/adapter/common" 5 | "chat/globals" 6 | "chat/utils" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error { 12 | var conn *utils.WebSocket 13 | if conn = utils.NewWebsocketClient(c.GetEndpoint()); conn == nil { 14 | return fmt.Errorf("bing error: websocket connection failed") 15 | } 16 | defer conn.DeferClose() 17 | 18 | model := strings.TrimPrefix(props.Model, "bing-") 19 | prompt := props.Message[len(props.Message)-1].Content 20 | if err := conn.SendJSON(&ChatRequest{ 21 | Prompt: prompt, 22 | Hash: c.Secret, 23 | Model: model, 24 | }); err != nil { 25 | return err 26 | } 27 | 28 | for { 29 | form, err := utils.ReadForm[ChatResponse](conn) 30 | if err != nil { 31 | if strings.Contains(err.Error(), "websocket: close 1000") { 32 | return nil 33 | } 34 | globals.Debug(fmt.Sprintf("bing error: %s", err.Error())) 35 | return nil 36 | } 37 | 38 | if err := hook(&globals.Chunk{ 39 | Content: form.Response, 40 | }); err != nil { 41 | return err 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /adapter/bing/struct.go: -------------------------------------------------------------------------------- 1 | package bing 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | "fmt" 7 | ) 8 | 9 | type ChatInstance struct { 10 | Endpoint string 11 | Secret string 12 | } 13 | 14 | func (c *ChatInstance) GetEndpoint() string { 15 | return fmt.Sprintf("%s/chat", c.Endpoint) 16 | } 17 | 18 | func NewChatInstance(endpoint, secret string) *ChatInstance { 19 | return &ChatInstance{ 20 | Endpoint: endpoint, 21 | Secret: secret, 22 | } 23 | } 24 | 25 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 26 | return NewChatInstance( 27 | conf.GetEndpoint(), 28 | conf.GetRandomSecret(), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /adapter/bing/types.go: -------------------------------------------------------------------------------- 1 | package bing 2 | 3 | // see https://github.com/Deeptrain-Community/chatnio-bing-service 4 | 5 | type ChatRequest struct { 6 | Prompt string `json:"prompt"` 7 | Hash string `json:"hash"` 8 | Model string `json:"model"` 9 | } 10 | 11 | type ChatResponse struct { 12 | Response string `json:"response"` 13 | } 14 | -------------------------------------------------------------------------------- /adapter/claude/struct.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | ) 7 | 8 | type ChatInstance struct { 9 | Endpoint string 10 | ApiKey string 11 | } 12 | 13 | func NewChatInstance(endpoint, apiKey string) *ChatInstance { 14 | return &ChatInstance{ 15 | Endpoint: endpoint, 16 | ApiKey: apiKey, 17 | } 18 | } 19 | 20 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 21 | return NewChatInstance( 22 | conf.GetEndpoint(), 23 | conf.GetRandomSecret(), 24 | ) 25 | } 26 | 27 | func (c *ChatInstance) GetEndpoint() string { 28 | return c.Endpoint 29 | } 30 | 31 | func (c *ChatInstance) GetApiKey() string { 32 | return c.ApiKey 33 | } 34 | -------------------------------------------------------------------------------- /adapter/claude/types.go: -------------------------------------------------------------------------------- 1 | package claude 2 | 3 | // ChatBody is the request body for anthropic claude 4 | 5 | type Message struct { 6 | Role string `json:"role"` 7 | Content interface{} `json:"content"` 8 | } 9 | 10 | type MessageImage struct { 11 | Type string `json:"type"` 12 | MediaType interface{} `json:"media_type"` 13 | Data interface{} `json:"data"` 14 | } 15 | 16 | type MessageContent struct { 17 | Type string `json:"type"` 18 | Text *string `json:"text,omitempty"` 19 | Source *MessageImage `json:"source,omitempty"` 20 | } 21 | 22 | type ChatBody struct { 23 | Messages []Message `json:"messages"` 24 | MaxTokens int `json:"max_tokens"` 25 | Model string `json:"model"` 26 | System string `json:"system"` 27 | Stream bool `json:"stream"` 28 | Temperature *float32 `json:"temperature,omitempty"` 29 | TopP *float32 `json:"top_p,omitempty"` 30 | TopK *int `json:"top_k,omitempty"` 31 | } 32 | 33 | type ChatStreamResponse struct { 34 | Type string `json:"type"` 35 | Index int `json:"index"` 36 | Delta struct { 37 | Type string `json:"type"` 38 | Text string `json:"text"` 39 | } `json:"delta"` 40 | } 41 | 42 | type ChatErrorResponse struct { 43 | Error struct { 44 | Type string `json:"type" binding:"required"` 45 | Message string `json:"message"` 46 | } `json:"error"` 47 | } 48 | -------------------------------------------------------------------------------- /adapter/common/interface.go: -------------------------------------------------------------------------------- 1 | package adaptercommon 2 | 3 | import ( 4 | "chat/globals" 5 | ) 6 | 7 | type Factory interface { 8 | CreateStreamChatRequest(props *ChatProps, hook globals.Hook) error 9 | } 10 | 11 | type FactoryCreator func(globals.ChannelConfig) Factory 12 | -------------------------------------------------------------------------------- /adapter/common/types.go: -------------------------------------------------------------------------------- 1 | package adaptercommon 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | ) 7 | 8 | type RequestProps struct { 9 | MaxRetries *int `json:"-"` 10 | Current int `json:"-"` 11 | Group string `json:"-"` 12 | Proxy globals.ProxyConfig `json:"-"` 13 | } 14 | 15 | type ChatProps struct { 16 | RequestProps 17 | 18 | Model string `json:"model,omitempty"` 19 | OriginalModel string `json:"-"` 20 | 21 | Message []globals.Message `json:"messages,omitempty"` 22 | MaxTokens *int `json:"max_tokens,omitempty"` 23 | PresencePenalty *float32 `json:"presence_penalty,omitempty"` 24 | FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` 25 | RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` 26 | Temperature *float32 `json:"temperature,omitempty"` 27 | TopP *float32 `json:"top_p,omitempty"` 28 | TopK *int `json:"top_k,omitempty"` 29 | Tools *globals.FunctionTools `json:"tools,omitempty"` 30 | ToolChoice *interface{} `json:"tool_choice,omitempty"` 31 | Buffer *utils.Buffer `json:"-"` 32 | } 33 | 34 | func (c *ChatProps) SetupBuffer(buf *utils.Buffer) { 35 | buf.SetPrompts(c) 36 | c.Buffer = buf 37 | } 38 | 39 | func CreateChatProps(props *ChatProps, buffer *utils.Buffer) *ChatProps { 40 | props.SetupBuffer(buffer) 41 | return props 42 | } 43 | -------------------------------------------------------------------------------- /adapter/dashscope/struct.go: -------------------------------------------------------------------------------- 1 | package dashscope 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | ) 7 | 8 | type ChatInstance struct { 9 | Endpoint string 10 | ApiKey string 11 | } 12 | 13 | func (c *ChatInstance) GetApiKey() string { 14 | return c.ApiKey 15 | } 16 | 17 | func (c *ChatInstance) GetEndpoint() string { 18 | return c.Endpoint 19 | } 20 | 21 | func NewChatInstance(endpoint string, apiKey string) *ChatInstance { 22 | return &ChatInstance{ 23 | Endpoint: endpoint, 24 | ApiKey: apiKey, 25 | } 26 | } 27 | 28 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 29 | return NewChatInstance( 30 | conf.GetEndpoint(), 31 | conf.GetRandomSecret(), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /adapter/dashscope/types.go: -------------------------------------------------------------------------------- 1 | package dashscope 2 | 3 | // ChatRequest is the request body for dashscope 4 | type ChatRequest struct { 5 | Model string `json:"model"` 6 | Input ChatInput `json:"input"` 7 | Parameters ChatParam `json:"parameters"` 8 | } 9 | 10 | type Message struct { 11 | Role string `json:"role"` 12 | Content string `json:"content"` 13 | } 14 | 15 | type ChatInput struct { 16 | Messages []Message `json:"messages"` 17 | } 18 | 19 | type ChatParam struct { 20 | IncrementalOutput bool `json:"incremental_output"` 21 | EnableSearch *bool `json:"enable_search,omitempty"` 22 | MaxTokens int `json:"max_tokens"` 23 | Temperature *float32 `json:"temperature,omitempty"` 24 | TopP *float32 `json:"top_p,omitempty"` 25 | TopK *int `json:"top_k,omitempty"` 26 | RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` 27 | } 28 | 29 | // ChatResponse is the response body for dashscope 30 | type ChatResponse struct { 31 | Output struct { 32 | FinishReason string `json:"finish_reason"` 33 | Text string `json:"text"` 34 | } `json:"output"` 35 | RequestId string `json:"request_id"` 36 | Usage struct { 37 | InputTokens int `json:"input_tokens"` 38 | OutputTokens int `json:"output_tokens"` 39 | } `json:"usage"` 40 | Message string `json:"message"` 41 | } 42 | -------------------------------------------------------------------------------- /adapter/hunyuan/chat.go: -------------------------------------------------------------------------------- 1 | package hunyuan 2 | 3 | import ( 4 | adaptercommon "chat/adapter/common" 5 | "chat/globals" 6 | "context" 7 | "fmt" 8 | ) 9 | 10 | func (c *ChatInstance) FormatMessages(messages []globals.Message) []globals.Message { 11 | var result []globals.Message 12 | for _, message := range messages { 13 | switch message.Role { 14 | case globals.System: 15 | result = append(result, globals.Message{Role: globals.User, Content: message.Content}) 16 | case globals.Assistant, globals.User: 17 | bound := len(result) > 0 && result[len(result)-1].Role == message.Role 18 | if bound { 19 | result[len(result)-1].Content += message.Content 20 | } else { 21 | result = append(result, message) 22 | } 23 | case globals.Tool: 24 | continue 25 | default: 26 | result = append(result, message) 27 | } 28 | } 29 | 30 | return result 31 | } 32 | 33 | func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { 34 | credential := NewCredential(c.GetSecretId(), c.GetSecretKey()) 35 | client := NewInstance(c.GetAppId(), c.GetEndpoint(), credential) 36 | channel, err := client.Chat(context.Background(), NewRequest(Stream, c.FormatMessages(props.Message), props.Temperature, props.TopP)) 37 | if err != nil { 38 | return fmt.Errorf("tencent hunyuan error: %+v", err) 39 | } 40 | 41 | for chunk := range channel { 42 | if chunk.Error.Code != 0 { 43 | fmt.Printf("tencent hunyuan error: %+v\n", chunk.Error) 44 | break 45 | } 46 | 47 | if len(chunk.Choices) == 0 { 48 | continue 49 | } 50 | 51 | choice := chunk.Choices[0].Delta 52 | if err := callback(&globals.Chunk{Content: choice.Content}); err != nil { 53 | return err 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /adapter/hunyuan/struct.go: -------------------------------------------------------------------------------- 1 | package hunyuan 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | "chat/utils" 7 | ) 8 | 9 | type ChatInstance struct { 10 | Endpoint string 11 | AppId int64 12 | SecretId string 13 | SecretKey string 14 | } 15 | 16 | func (c *ChatInstance) GetAppId() int64 { 17 | return c.AppId 18 | } 19 | 20 | func (c *ChatInstance) GetEndpoint() string { 21 | return c.Endpoint 22 | } 23 | 24 | func (c *ChatInstance) GetSecretId() string { 25 | return c.SecretId 26 | } 27 | 28 | func (c *ChatInstance) GetSecretKey() string { 29 | return c.SecretKey 30 | } 31 | 32 | func NewChatInstance(endpoint, appId, secretId, secretKey string) *ChatInstance { 33 | return &ChatInstance{ 34 | Endpoint: endpoint, 35 | AppId: utils.ParseInt64(appId), 36 | SecretId: secretId, 37 | SecretKey: secretKey, 38 | } 39 | } 40 | 41 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 42 | params := conf.SplitRandomSecret(3) 43 | return NewChatInstance( 44 | conf.GetEndpoint(), 45 | params[0], params[1], params[2], 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /adapter/midjourney/expose.go: -------------------------------------------------------------------------------- 1 | package midjourney 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | var whiteList []string 13 | 14 | func SaveWhiteList(raw string) { 15 | arr := utils.Filter(strings.Split(raw, ","), func(s string) bool { 16 | return len(strings.TrimSpace(s)) > 0 17 | }) 18 | 19 | for _, ip := range arr { 20 | if !utils.Contains(ip, whiteList) { 21 | whiteList = append(whiteList, ip) 22 | } 23 | } 24 | } 25 | 26 | func InWhiteList(ip string) bool { 27 | if len(whiteList) == 0 { 28 | return true 29 | } 30 | return utils.Contains(ip, whiteList) 31 | } 32 | 33 | func NotifyAPI(c *gin.Context) { 34 | if !InWhiteList(c.ClientIP()) { 35 | globals.Info(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP())) 36 | c.AbortWithStatus(http.StatusForbidden) 37 | return 38 | } 39 | 40 | var form NotifyForm 41 | if err := c.ShouldBindJSON(&form); err != nil { 42 | c.AbortWithStatus(http.StatusBadRequest) 43 | return 44 | } 45 | globals.Debug(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP())) 46 | 47 | if !utils.Contains(form.Status, []string{InProgress, Success, Failure}) { 48 | // ignore 49 | return 50 | } 51 | 52 | reason, ok := form.FailReason.(string) 53 | if !ok { 54 | reason = "unknown" 55 | } 56 | 57 | err := setStorage(form.Id, StorageForm{ 58 | Task: form.Id, 59 | Action: form.Action, 60 | Url: form.ImageUrl, 61 | FailReason: reason, 62 | Progress: form.Progress, 63 | Status: form.Status, 64 | }) 65 | 66 | c.JSON(http.StatusOK, gin.H{ 67 | "status": err == nil, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /adapter/midjourney/storage.go: -------------------------------------------------------------------------------- 1 | package midjourney 2 | 3 | import ( 4 | "chat/connection" 5 | "chat/utils" 6 | "fmt" 7 | ) 8 | 9 | func getTaskName(task string) string { 10 | return fmt.Sprintf("nio:mj-task:%s", task) 11 | } 12 | 13 | func setStorage(task string, form StorageForm) error { 14 | return utils.SetJson(connection.Cache, getTaskName(task), form, 60*60) 15 | } 16 | 17 | func getNotifyStorage(task string) *StorageForm { 18 | return utils.GetCacheStore[StorageForm](connection.Cache, getTaskName(task)) 19 | } 20 | -------------------------------------------------------------------------------- /adapter/midjourney/struct.go: -------------------------------------------------------------------------------- 1 | package midjourney 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | "fmt" 7 | ) 8 | 9 | var midjourneyEmptySecret = "null" 10 | 11 | type ChatInstance struct { 12 | Endpoint string 13 | ApiSecret string 14 | } 15 | 16 | func (c *ChatInstance) GetApiSecret() string { 17 | return c.ApiSecret 18 | } 19 | 20 | func (c *ChatInstance) GetEndpoint() string { 21 | return c.Endpoint 22 | } 23 | 24 | func (c *ChatInstance) GetMidjourneyHeaders() map[string]string { 25 | secret := c.GetApiSecret() 26 | if secret == "" || secret == midjourneyEmptySecret { 27 | return map[string]string{ 28 | "Content-Type": "application/json", 29 | } 30 | } 31 | 32 | return map[string]string{ 33 | "Content-Type": "application/json", 34 | "mj-api-secret": secret, 35 | } 36 | } 37 | 38 | func (c *ChatInstance) GetNotifyEndpoint() string { 39 | return fmt.Sprintf("%s/mj/notify", globals.NotifyUrl) 40 | } 41 | 42 | func NewChatInstance(endpoint, apiSecret, whiteList string) *ChatInstance { 43 | SaveWhiteList(whiteList) 44 | 45 | return &ChatInstance{ 46 | Endpoint: endpoint, 47 | ApiSecret: apiSecret, 48 | } 49 | } 50 | 51 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 52 | params := conf.SplitRandomSecret(2) 53 | 54 | return NewChatInstance( 55 | conf.GetEndpoint(), 56 | params[0], params[1], 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /adapter/openai/struct.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | "fmt" 7 | ) 8 | 9 | type ChatInstance struct { 10 | Endpoint string 11 | ApiKey string 12 | } 13 | 14 | func (c *ChatInstance) GetEndpoint() string { 15 | return c.Endpoint 16 | } 17 | 18 | func (c *ChatInstance) GetApiKey() string { 19 | return c.ApiKey 20 | } 21 | 22 | func (c *ChatInstance) GetHeader() map[string]string { 23 | return map[string]string{ 24 | "Content-Type": "application/json", 25 | "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), 26 | } 27 | } 28 | 29 | func NewChatInstance(endpoint, apiKey string) *ChatInstance { 30 | return &ChatInstance{ 31 | Endpoint: endpoint, 32 | ApiKey: apiKey, 33 | } 34 | } 35 | 36 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 37 | return NewChatInstance( 38 | conf.GetEndpoint(), 39 | conf.GetRandomSecret(), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/palm2/struct.go: -------------------------------------------------------------------------------- 1 | package palm2 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | ) 7 | 8 | type ChatInstance struct { 9 | Endpoint string 10 | ApiKey string 11 | } 12 | 13 | func (c *ChatInstance) GetApiKey() string { 14 | return c.ApiKey 15 | } 16 | 17 | func (c *ChatInstance) GetEndpoint() string { 18 | return c.Endpoint 19 | } 20 | 21 | func NewChatInstance(endpoint string, apiKey string) *ChatInstance { 22 | return &ChatInstance{ 23 | Endpoint: endpoint, 24 | ApiKey: apiKey, 25 | } 26 | } 27 | 28 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 29 | return NewChatInstance( 30 | conf.GetEndpoint(), 31 | conf.GetRandomSecret(), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /adapter/router.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "chat/adapter/midjourney" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func Register(app *gin.RouterGroup) { 9 | app.POST("/mj/notify", midjourney.NotifyAPI) 10 | } 11 | -------------------------------------------------------------------------------- /adapter/skylark/struct.go: -------------------------------------------------------------------------------- 1 | package skylark 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | 7 | "github.com/volcengine/volcengine-go-sdk/service/arkruntime" 8 | ) 9 | 10 | type ChatInstance struct { 11 | Instance *arkruntime.Client 12 | isFirstReasoning bool 13 | isReasonOver bool 14 | } 15 | 16 | func NewChatInstance(endpoint, apiKey string) *ChatInstance { 17 | //https://ark.cn-beijing.volces.com/api/v3 18 | instance := arkruntime.NewClientWithApiKey(apiKey, arkruntime.WithBaseUrl(endpoint)) 19 | return &ChatInstance{ 20 | Instance: instance, 21 | isFirstReasoning: true, 22 | isReasonOver: false, 23 | } 24 | } 25 | 26 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 27 | params := conf.SplitRandomSecret(1) 28 | 29 | return NewChatInstance( 30 | conf.GetEndpoint(), 31 | params[0], 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /adapter/slack/chat.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | adaptercommon "chat/adapter/common" 5 | "chat/globals" 6 | "context" 7 | ) 8 | 9 | func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error { 10 | if err := c.Instance.NewChannel(c.GetChannel()); err != nil { 11 | return err 12 | } 13 | 14 | resp, err := c.Instance.Reply(context.Background(), c.FormatMessage(props.Message), nil) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | return c.ProcessPartialResponse(resp, hook) 20 | } 21 | -------------------------------------------------------------------------------- /adapter/zhinao/struct.go: -------------------------------------------------------------------------------- 1 | package zhinao 2 | 3 | import ( 4 | factory "chat/adapter/common" 5 | "chat/globals" 6 | "fmt" 7 | ) 8 | 9 | type ChatInstance struct { 10 | Endpoint string 11 | ApiKey string 12 | } 13 | 14 | func (c *ChatInstance) GetEndpoint() string { 15 | return c.Endpoint 16 | } 17 | 18 | func (c *ChatInstance) GetApiKey() string { 19 | return c.ApiKey 20 | } 21 | 22 | func (c *ChatInstance) GetHeader() map[string]string { 23 | return map[string]string{ 24 | "Content-Type": "application/json", 25 | "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), 26 | } 27 | } 28 | 29 | func NewChatInstance(endpoint, apiKey string) *ChatInstance { 30 | return &ChatInstance{ 31 | Endpoint: endpoint, 32 | ApiKey: apiKey, 33 | } 34 | } 35 | 36 | func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { 37 | return NewChatInstance( 38 | conf.GetEndpoint(), 39 | conf.GetRandomSecret(), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /addition/article/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/addition/article/data/.gitkeep -------------------------------------------------------------------------------- /addition/article/template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/addition/article/template.docx -------------------------------------------------------------------------------- /addition/article/utils.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "fmt" 7 | "github.com/lukasjarosch/go-docx" 8 | ) 9 | 10 | func GenerateDocxFile(target, title, content string) error { 11 | data := docx.PlaceholderMap{ 12 | "title": title, 13 | "content": content, 14 | } 15 | 16 | doc, err := docx.Open("addition/article/template.docx") 17 | if err != nil { 18 | return err 19 | } 20 | 21 | if err := doc.ReplaceAll(data); err != nil { 22 | return err 23 | } 24 | 25 | if err := doc.WriteToFile(target); err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func CreateArticleFile(hash, title, content string) string { 33 | target := fmt.Sprintf("storage/article/data/%s/%s.docx", hash, title) 34 | utils.FileDirSafe(target) 35 | if err := GenerateDocxFile(target, title, content); err != nil { 36 | globals.Debug(fmt.Sprintf("[article] error during generate article %s: %s", title, err.Error())) 37 | } 38 | 39 | return target 40 | } 41 | -------------------------------------------------------------------------------- /addition/card/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | -------------------------------------------------------------------------------- /addition/card/error.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | Error 13 | Error Card 14 | 37 | 38 | 39 | 40 | Sorry, there is something wrong... 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /addition/card/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/addition/card/favicon.ico -------------------------------------------------------------------------------- /addition/card/utils.php: -------------------------------------------------------------------------------- 1 | [^\S ]+/', '/[^\S ]+ ', '<', '\\1', '><', ':', '{', '}'); 9 | return preg_replace($search, $replace, $buffer); 10 | } 11 | 12 | function fetch($message, $web): array|string|null 13 | { 14 | $opts = array('http' => 15 | array( 16 | 'method' => 'POST', 17 | 'header' => 'Content-type: application/json', 18 | 'content' => json_encode(array('message' => $message, 'web' => $web)) 19 | ) 20 | ); 21 | 22 | $context = stream_context_create($opts); 23 | $response = @file_get_contents("http://localhost:8094/card", false, $context); 24 | $ok = $response !== false; 25 | return $ok ? json_decode($response, true) : null; 26 | } 27 | 28 | function get($param, $default = null) 29 | { 30 | return $_GET[$param] ?? $default; 31 | } 32 | -------------------------------------------------------------------------------- /addition/generation/build.go: -------------------------------------------------------------------------------- 1 | package generation 2 | 3 | import ( 4 | "chat/utils" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | func GetFolder(hash string) string { 10 | return fmt.Sprintf("storage/generation/data/%s", hash) 11 | } 12 | 13 | func GetFolderByHash(model string, prompt string) (string, string) { 14 | hash := utils.Sha2Encrypt(model + prompt + time.Now().Format("2006-01-02 15:04:05")) 15 | return hash, GetFolder(hash) 16 | } 17 | 18 | func GenerateProject(path string, instance ProjectResult) bool { 19 | for name, data := range instance.Result { 20 | current := fmt.Sprintf("%s/%s", path, name) 21 | if content, ok := data.(string); ok { 22 | if utils.WriteFile(current, content, true) != nil { 23 | return false 24 | } 25 | } else { 26 | GenerateProject(current, ProjectResult{ 27 | Result: data.(map[string]interface{}), 28 | }) 29 | } 30 | } 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /addition/generation/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/addition/generation/data/.gitkeep -------------------------------------------------------------------------------- /addition/generation/generate.go: -------------------------------------------------------------------------------- 1 | package generation 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "fmt" 7 | ) 8 | 9 | func CreateGenerationWithCache(group, model, prompt string, hook func(buffer *utils.Buffer, data string)) (string, error) { 10 | hash, path := GetFolderByHash(model, prompt) 11 | if !utils.Exists(path) { 12 | if err := CreateGeneration(group, model, prompt, path, hook); err != nil { 13 | globals.Info(fmt.Sprintf("[project] error during generation %s (model %s): %s", prompt, model, err.Error())) 14 | return "", fmt.Errorf("error during generate project: %s", err.Error()) 15 | } 16 | } 17 | 18 | if _, _, err := utils.GenerateCompressTask(hash, "storage/generation", path, path); err != nil { 19 | return "", fmt.Errorf("error during generate compress task: %s", err.Error()) 20 | } 21 | 22 | return hash, nil 23 | } 24 | -------------------------------------------------------------------------------- /addition/router.go: -------------------------------------------------------------------------------- 1 | package addition 2 | 3 | import ( 4 | "chat/addition/article" 5 | "chat/addition/card" 6 | "chat/addition/generation" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Register(app *gin.RouterGroup) { 11 | { 12 | app.POST("/card", card.HandlerAPI) 13 | 14 | app.GET("/generation/create", generation.GenerateAPI) 15 | app.GET("/generation/download/tar", generation.ProjectTarDownloadAPI) 16 | app.GET("/generation/download/zip", generation.ProjectZipDownloadAPI) 17 | 18 | app.GET("/article/create", article.GenerateAPI) 19 | app.GET("/article/download/tar", article.ProjectTarDownloadAPI) 20 | app.GET("/article/download/zip", article.ProjectZipDownloadAPI) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /addition/web/call.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/manager/conversation" 6 | "chat/utils" 7 | "fmt" 8 | "time" 9 | ) 10 | 11 | type Hook func(message []globals.Message, token int) (string, error) 12 | 13 | func toWebSearchingMessage(message []globals.Message) []globals.Message { 14 | data, _ := GenerateSearchResult(message[len(message)-1].Content) 15 | 16 | return utils.Insert(message, 0, globals.Message{ 17 | Role: globals.System, 18 | Content: fmt.Sprintf("You will play the role of an AI Q&A assistant, where your knowledge base is not offline, but can be networked in real time, and you can provide real-time networked information with links to networked search sources."+ 19 | "Current time: %s, Real-time internet search results: %s", 20 | time.Now().Format("2006-01-02 15:04:05"), data, 21 | ), 22 | }) 23 | } 24 | 25 | func ToChatSearched(instance *conversation.Conversation, restart bool) []globals.Message { 26 | segment := conversation.CopyMessage(instance.GetChatMessage(restart)) 27 | 28 | if instance.IsEnableWeb() { 29 | segment = toWebSearchingMessage(segment) 30 | } 31 | 32 | return segment 33 | } 34 | 35 | func ToSearched(enable bool, message []globals.Message) []globals.Message { 36 | if enable { 37 | return toWebSearchingMessage(message) 38 | } 39 | 40 | return message 41 | } 42 | -------------------------------------------------------------------------------- /admin/format.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func getMonth() string { 9 | date := time.Now() 10 | return date.Format("2006-01") 11 | } 12 | 13 | func getDay() string { 14 | date := time.Now() 15 | return date.Format("2006-01-02") 16 | } 17 | 18 | func getDays(n int) []time.Time { 19 | current := time.Now() 20 | var days []time.Time 21 | for i := n; i > 0; i-- { 22 | days = append(days, current.AddDate(0, 0, -i+1)) 23 | } 24 | 25 | return days 26 | } 27 | 28 | func getErrorFormat(t string) string { 29 | return fmt.Sprintf("nio:err-analysis-%s", t) 30 | } 31 | 32 | func getBillingFormat(t string) string { 33 | return fmt.Sprintf("nio:billing-analysis-%s", t) 34 | } 35 | 36 | func getMonthBillingFormat(t string) string { 37 | return fmt.Sprintf("nio:billing-analysis-%s", t) 38 | } 39 | 40 | func getRequestFormat(t string) string { 41 | return fmt.Sprintf("nio:request-analysis-%s", t) 42 | } 43 | 44 | func getModelFormat(t string, model string) string { 45 | return fmt.Sprintf("nio:model-analysis-%s-%s", model, t) 46 | } 47 | -------------------------------------------------------------------------------- /admin/instance.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | var MarketInstance *Market 4 | 5 | func InitInstance() { 6 | MarketInstance = NewMarket() 7 | } 8 | -------------------------------------------------------------------------------- /admin/logger.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "strings" 9 | ) 10 | 11 | type LogFile struct { 12 | Path string `json:"path"` 13 | Size int64 `json:"size"` 14 | } 15 | 16 | func ListLogs() []LogFile { 17 | return utils.Each(utils.Walk("logs"), func(path string) LogFile { 18 | return LogFile{ 19 | Path: strings.TrimLeft(path, "logs/"), 20 | Size: utils.GetFileSize(path), 21 | } 22 | }) 23 | } 24 | 25 | func getLogPath(path string) string { 26 | return fmt.Sprintf("logs/%s", path) 27 | } 28 | 29 | func getBlobFile(c *gin.Context, path string) { 30 | c.File(getLogPath(path)) 31 | } 32 | 33 | func deleteLogFile(path string) error { 34 | return utils.DeleteFile(getLogPath(path)) 35 | } 36 | 37 | func getLatestLogs(n int) string { 38 | if n <= 0 { 39 | n = 100 40 | } 41 | 42 | content, err := utils.ReadFileLatestLines(getLogPath(globals.DefaultLoggerFile), n) 43 | 44 | if err != nil { 45 | return fmt.Sprintf("read error: %s", err.Error()) 46 | } 47 | 48 | return content 49 | } 50 | -------------------------------------------------------------------------------- /admin/market.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "chat/globals" 5 | "fmt" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | type ModelTag []string 11 | type MarketModel struct { 12 | Id string `json:"id" mapstructure:"id" required:"true"` 13 | Name string `json:"name" mapstructure:"name" required:"true"` 14 | Description string `json:"description" mapstructure:"description"` 15 | Default bool `json:"default" mapstructure:"default"` 16 | HighContext bool `json:"high_context" mapstructure:"highcontext"` 17 | Avatar string `json:"avatar" mapstructure:"avatar"` 18 | Tag ModelTag `json:"tag" mapstructure:"tag"` 19 | } 20 | type MarketModelList []MarketModel 21 | 22 | type Market struct { 23 | Models MarketModelList `json:"models" mapstructure:"models"` 24 | } 25 | 26 | func NewMarket() *Market { 27 | var models MarketModelList 28 | if err := viper.UnmarshalKey("market", &models); err != nil { 29 | globals.Warn(fmt.Sprintf("[market] read config error: %s, use default config", err.Error())) 30 | models = MarketModelList{} 31 | } 32 | 33 | return &Market{ 34 | Models: models, 35 | } 36 | } 37 | 38 | func (m *Market) GetModels() MarketModelList { 39 | return m.Models 40 | } 41 | 42 | func (m *Market) GetModel(id string) *MarketModel { 43 | for _, model := range m.Models { 44 | if model.Id == id { 45 | return &model 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (m *Market) SaveConfig() error { 52 | viper.Set("market", m.Models) 53 | return viper.WriteConfig() 54 | } 55 | 56 | func (m *Market) SetModels(models MarketModelList) error { 57 | m.Models = models 58 | return m.SaveConfig() 59 | } 60 | -------------------------------------------------------------------------------- /admin/statistic.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "chat/adapter" 5 | "chat/connection" 6 | "chat/utils" 7 | "time" 8 | 9 | "github.com/go-redis/redis/v8" 10 | ) 11 | 12 | func IncrErrorRequest(cache *redis.Client) { 13 | utils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2) 14 | } 15 | 16 | func IncrBillingRequest(cache *redis.Client, amount int64) { 17 | utils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*30*2) 18 | utils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*30*2) 19 | } 20 | 21 | func IncrRequest(cache *redis.Client) { 22 | utils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2) 23 | } 24 | 25 | func IncrModelRequest(cache *redis.Client, model string, tokens int64) { 26 | utils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2) 27 | } 28 | 29 | func AnalyseRequest(model string, buffer *utils.Buffer, err error) { 30 | instance := connection.Cache 31 | 32 | if adapter.IsAvailableError(err) { 33 | IncrErrorRequest(instance) 34 | return 35 | } 36 | 37 | IncrRequest(instance) 38 | IncrModelRequest(instance, model, int64(buffer.CountToken())) 39 | } 40 | -------------------------------------------------------------------------------- /app/.env.deeptrain: -------------------------------------------------------------------------------- 1 | VITE_USE_DEEPTRAIN=true 2 | VITE_BACKEND_ENDPOINT=https://api.chatnio.net 3 | -------------------------------------------------------------------------------- /app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /app/.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 | dev-dist 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # Libre 28 | db 29 | -------------------------------------------------------------------------------- /app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "src/components", 14 | "utils": "@/components/ui/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chat Nio 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/icons/360gpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/360gpt.png -------------------------------------------------------------------------------- /app/public/icons/baichuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/baichuan.png -------------------------------------------------------------------------------- /app/public/icons/chatglm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/chatglm.png -------------------------------------------------------------------------------- /app/public/icons/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/claude.png -------------------------------------------------------------------------------- /app/public/icons/claude100k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/claude100k.png -------------------------------------------------------------------------------- /app/public/icons/dalle.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/dalle.jpeg -------------------------------------------------------------------------------- /app/public/icons/gemini.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gemini.jpeg -------------------------------------------------------------------------------- /app/public/icons/gpt35turbo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gpt35turbo.png -------------------------------------------------------------------------------- /app/public/icons/gpt35turbo16k.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gpt35turbo16k.webp -------------------------------------------------------------------------------- /app/public/icons/gpt4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gpt4.png -------------------------------------------------------------------------------- /app/public/icons/gpt432k.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gpt432k.webp -------------------------------------------------------------------------------- /app/public/icons/gpt4dalle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gpt4dalle.png -------------------------------------------------------------------------------- /app/public/icons/gpt4v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/gpt4v.png -------------------------------------------------------------------------------- /app/public/icons/hunyuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/hunyuan.png -------------------------------------------------------------------------------- /app/public/icons/llama2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/llama2.webp -------------------------------------------------------------------------------- /app/public/icons/llamacode.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/llamacode.webp -------------------------------------------------------------------------------- /app/public/icons/midjourney.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/midjourney.jpg -------------------------------------------------------------------------------- /app/public/icons/newbing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/newbing.jpg -------------------------------------------------------------------------------- /app/public/icons/palm2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/palm2.webp -------------------------------------------------------------------------------- /app/public/icons/skylark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/skylark.jpg -------------------------------------------------------------------------------- /app/public/icons/sparkdesk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/sparkdesk.jpg -------------------------------------------------------------------------------- /app/public/icons/stablediffusion.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/stablediffusion.jpeg -------------------------------------------------------------------------------- /app/public/icons/tongyi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/icons/tongyi.png -------------------------------------------------------------------------------- /app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/logo.png -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | Disallow: /admin/ 4 | -------------------------------------------------------------------------------- /app/public/service.js: -------------------------------------------------------------------------------- 1 | 2 | const SERVICE_NAME = "chatnio"; 3 | 4 | self.addEventListener('activate', function (event) { 5 | console.debug("[service] service worker activated"); 6 | }); 7 | 8 | self.addEventListener('install', function (event) { 9 | event.waitUntil( 10 | caches.open(SERVICE_NAME) 11 | .then(function (cache) { 12 | return cache.addAll([]); 13 | }) 14 | ); 15 | }); 16 | 17 | self.addEventListener('fetch', function (event) { 18 | event.respondWith( 19 | caches.match(event.request) 20 | .then(function (response) { 21 | return response || fetch(event.request); 22 | }) 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /app/public/service/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/service/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/public/service/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/service/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/public/service/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/service/favicon-64x64.png -------------------------------------------------------------------------------- /app/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chat Nio", 3 | "short_name": "Chat Nio", 4 | "icons": [ 5 | { 6 | "src": "/service/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/service/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "theme_color": "#000000", 18 | "background_color": "#0000000", 19 | "display": "standalone" 20 | } 21 | -------------------------------------------------------------------------------- /app/public/source/qq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/public/source/qq.jpg -------------------------------------------------------------------------------- /app/public/workbox.js: -------------------------------------------------------------------------------- 1 | 2 | if ('serviceWorker' in navigator) { 3 | window.addEventListener('load', function () { 4 | navigator.serviceWorker.register('/service.js').then(function (registration) { 5 | console.debug(`[service] service worker registered with scope: ${registration.scope}`); 6 | }, function (err) { 7 | console.debug(`[service] service worker registration failed: ${err}`); 8 | }); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /app/qodana.yaml: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------------------# 2 | # Qodana analysis is configured by qodana.yaml file # 3 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html # 4 | #-------------------------------------------------------------------------------# 5 | version: "1.0" 6 | 7 | #Specify inspection profile for code analysis 8 | profile: 9 | name: qodana.starter 10 | 11 | #Enable inspections 12 | #include: 13 | # - name: 14 | 15 | #Disable inspections 16 | #exclude: 17 | # - name: 18 | # paths: 19 | # - 20 | 21 | #Execute shell command before Qodana execution (Applied in CI/CD pipeline) 22 | #bootstrap: sh ./prepare-qodana.sh 23 | 24 | #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) 25 | #plugins: 26 | # - id: #(plugin id can be found at https://plugins.jetbrains.com) 27 | 28 | #Specify Qodana linter for analysis (Applied in CI/CD pipeline) 29 | linter: jetbrains/qodana-js:latest 30 | -------------------------------------------------------------------------------- /app/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /app/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "Your Next Powerful AI Chat Platform" 5 | authors = ["Deeptrain Team"] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/Deeptrain-Community/chatnio" 8 | default-run = "app" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.5.0", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.5.2", features = [ "updater", "system-tray"] } 21 | 22 | [features] 23 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 24 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 25 | # DO NOT REMOVE!! 26 | custom-protocol = [ "tauri/custom-protocol" ] 27 | -------------------------------------------------------------------------------- /app/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /app/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /app/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /app/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /app/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /app/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /app/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/app/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /app/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri::Builder::default() 6 | .run(tauri::generate_context!()) 7 | .expect("error while running tauri application"); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from "react-redux"; 2 | import store from "./store/index.ts"; 3 | import AppProvider from "./components/app/AppProvider.tsx"; 4 | import { AppRouter } from "./router.tsx"; 5 | import { Toaster } from "@/components/ui/sonner"; 6 | import Spinner from "@/spinner.tsx"; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /app/src/admin/api/charge.ts: -------------------------------------------------------------------------------- 1 | import { CommonResponse } from "@/api/common.ts"; 2 | import { ChargeProps } from "@/admin/charge.ts"; 3 | import { getErrorMessage } from "@/utils/base.ts"; 4 | import axios from "axios"; 5 | 6 | export type ChargeListResponse = CommonResponse & { 7 | data: ChargeProps[]; 8 | }; 9 | 10 | export type ChargeSyncRequest = { 11 | overwrite: boolean; 12 | data: ChargeProps[]; 13 | }; 14 | 15 | export async function listCharge(): Promise { 16 | try { 17 | const response = await axios.get("/admin/charge/list"); 18 | return response.data as ChargeListResponse; 19 | } catch (e) { 20 | return { status: false, error: getErrorMessage(e), data: [] }; 21 | } 22 | } 23 | 24 | export async function setCharge(charge: ChargeProps): Promise { 25 | try { 26 | const response = await axios.post(`/admin/charge/set`, charge); 27 | return response.data as CommonResponse; 28 | } catch (e) { 29 | return { status: false, error: getErrorMessage(e) }; 30 | } 31 | } 32 | 33 | export async function deleteCharge(id: number): Promise { 34 | try { 35 | const response = await axios.get(`/admin/charge/delete/${id}`); 36 | return response.data as CommonResponse; 37 | } catch (e) { 38 | return { status: false, error: getErrorMessage(e) }; 39 | } 40 | } 41 | 42 | export async function syncCharge( 43 | data: ChargeSyncRequest, 44 | ): Promise { 45 | try { 46 | const response = await axios.post(`/admin/charge/sync`, data); 47 | return response.data as CommonResponse; 48 | } catch (e) { 49 | return { status: false, error: getErrorMessage(e) }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/admin/api/info.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { 3 | setAnnouncement, 4 | setAppLogo, 5 | setAppName, 6 | setBlobEndpoint, 7 | setBuyLink, 8 | setDocsUrl, 9 | } from "@/conf/env.ts"; 10 | import { infoEvent, InfoForm } from "@/events/info.ts"; 11 | 12 | export type SiteInfo = { 13 | title: string; 14 | logo: string; 15 | docs: string; 16 | file: string; 17 | announcement: string; 18 | buy_link: string; 19 | mail: boolean; 20 | contact: string; 21 | footer: string; 22 | auth_footer: boolean; 23 | article: string[]; 24 | generation: string[]; 25 | relay_plan: boolean; 26 | }; 27 | 28 | export async function getSiteInfo(): Promise { 29 | try { 30 | const response = await axios.get("/info"); 31 | return response.data as SiteInfo; 32 | } catch (e) { 33 | console.warn(e); 34 | return { 35 | title: "", 36 | logo: "", 37 | docs: "", 38 | file: "", 39 | announcement: "", 40 | buy_link: "", 41 | contact: "", 42 | footer: "", 43 | auth_footer: false, 44 | mail: false, 45 | article: [], 46 | generation: [], 47 | relay_plan: false, 48 | }; 49 | } 50 | } 51 | 52 | export function syncSiteInfo() { 53 | setTimeout(async () => { 54 | const info = await getSiteInfo(); 55 | 56 | setAppName(info.title); 57 | setAppLogo(info.logo); 58 | setDocsUrl(info.docs); 59 | setBlobEndpoint(info.file); 60 | setAnnouncement(info.announcement); 61 | setBuyLink(info.buy_link); 62 | 63 | infoEvent.emit(info as InfoForm); 64 | }, 25); 65 | } 66 | -------------------------------------------------------------------------------- /app/src/admin/api/logger.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { CommonResponse } from "@/api/common.ts"; 3 | import { getErrorMessage } from "@/utils/base.ts"; 4 | 5 | export type Logger = { 6 | path: string; 7 | size: number; 8 | }; 9 | 10 | export async function listLoggers(): Promise { 11 | try { 12 | const response = await axios.get("/admin/logger/list"); 13 | return (response.data || []) as Logger[]; 14 | } catch (e) { 15 | console.warn(e); 16 | return []; 17 | } 18 | } 19 | 20 | export async function getLoggerConsole(n?: number): Promise { 21 | try { 22 | const response = await axios.get(`/admin/logger/console?n=${n ?? 100}`); 23 | return response.data.content as string; 24 | } catch (e) { 25 | console.warn(e); 26 | return `failed to get info from server: ${getErrorMessage(e)}`; 27 | } 28 | } 29 | 30 | export async function downloadLogger(path: string): Promise { 31 | try { 32 | const response = await axios.get("/admin/logger/download", { 33 | responseType: "blob", 34 | params: { path }, 35 | }); 36 | const url = window.URL.createObjectURL(new Blob([response.data])); 37 | const link = document.createElement("a"); 38 | link.href = url; 39 | link.setAttribute("download", path); 40 | document.body.appendChild(link); 41 | link.click(); 42 | } catch (e) { 43 | console.warn(e); 44 | } 45 | } 46 | 47 | export async function deleteLogger(path: string): Promise { 48 | try { 49 | const response = await axios.post(`/admin/logger/delete?path=${path}`); 50 | return response.data as CommonResponse; 51 | } catch (e) { 52 | console.warn(e); 53 | return { status: false, error: getErrorMessage(e) }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/admin/api/market.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@/api/types.tsx"; 2 | import { CommonResponse } from "@/api/common.ts"; 3 | import axios from "axios"; 4 | import { getErrorMessage } from "@/utils/base.ts"; 5 | 6 | export async function updateMarket(data: Model[]): Promise { 7 | try { 8 | const resp = await axios.post("/admin/market/update", data); 9 | return resp.data as CommonResponse; 10 | } catch (e) { 11 | console.warn(e); 12 | return { status: false, error: getErrorMessage(e) }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/admin/api/plan.ts: -------------------------------------------------------------------------------- 1 | import { Plan } from "@/api/types.tsx"; 2 | import axios from "axios"; 3 | import { CommonResponse } from "@/api/common.ts"; 4 | import { getErrorMessage } from "@/utils/base.ts"; 5 | import { getApiPlans } from "@/api/v1.ts"; 6 | 7 | export type PlanConfig = { 8 | enabled: boolean; 9 | plans: Plan[]; 10 | }; 11 | 12 | export async function getPlanConfig(): Promise { 13 | try { 14 | const response = await axios.get("/admin/plan/view"); 15 | const conf = response.data as PlanConfig; 16 | conf.plans = (conf.plans || []).filter((item) => item.level > 0); 17 | if (conf.plans.length === 0) 18 | conf.plans = [1, 2, 3].map( 19 | (level) => ({ level, price: 0, items: [] }) as Plan, 20 | ); 21 | return conf; 22 | } catch (e) { 23 | console.warn(e); 24 | return { enabled: false, plans: [] }; 25 | } 26 | } 27 | 28 | export async function getExternalPlanConfig( 29 | endpoint: string, 30 | ): Promise { 31 | const response = await getApiPlans({ endpoint }); 32 | return { enabled: response.length > 0, plans: response }; 33 | } 34 | 35 | export async function setPlanConfig( 36 | config: PlanConfig, 37 | ): Promise { 38 | try { 39 | const response = await axios.post(`/admin/plan/update`, config); 40 | return response.data as CommonResponse; 41 | } catch (e) { 42 | return { status: false, error: getErrorMessage(e) }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/admin/charge.ts: -------------------------------------------------------------------------------- 1 | export const tokenBilling = "token-billing"; 2 | export const timesBilling = "times-billing"; 3 | export const nonBilling = "non-billing"; 4 | 5 | export const defaultChargeType = tokenBilling; 6 | export const chargeTypes = [nonBilling, timesBilling, tokenBilling]; 7 | export type ChargeType = (typeof chargeTypes)[number]; 8 | 9 | export type ChargeBaseProps = { 10 | type: string; 11 | anonymous: boolean; 12 | input: number; 13 | output: number; 14 | }; 15 | 16 | export type ChargeProps = ChargeBaseProps & { 17 | id: number; 18 | models: string[]; 19 | }; 20 | -------------------------------------------------------------------------------- /app/src/admin/hook.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { getUniqueList } from "@/utils/base.ts"; 3 | import { defaultChannelModels } from "@/admin/channel.ts"; 4 | import { getApiMarket, getApiModels } from "@/api/v1.ts"; 5 | import { useEffectAsync } from "@/utils/hook.ts"; 6 | import { Model } from "@/api/types.tsx"; 7 | 8 | export type onStateChange = (state: boolean, data?: T) => void; 9 | 10 | export const useAllModels = (onStateChange?: onStateChange) => { 11 | const [allModels, setAllModels] = useState([]); 12 | 13 | const update = async () => { 14 | onStateChange?.(false, allModels); 15 | const models = await getApiModels(); 16 | onStateChange?.(true, models.data); 17 | 18 | setAllModels(models.data); 19 | }; 20 | 21 | useEffectAsync(update, []); 22 | 23 | return { 24 | allModels, 25 | update, 26 | }; 27 | }; 28 | 29 | export const useChannelModels = (onStateChange?: onStateChange) => { 30 | const { allModels, update } = useAllModels(onStateChange); 31 | 32 | const channelModels = useMemo( 33 | () => getUniqueList([...allModels, ...defaultChannelModels]), 34 | [allModels], 35 | ); 36 | 37 | return { 38 | channelModels, 39 | allModels, 40 | update, 41 | }; 42 | }; 43 | 44 | export const useSupportModels = (onStateChange?: onStateChange) => { 45 | const [supportModels, setSupportModels] = useState([]); 46 | 47 | const update = async () => { 48 | onStateChange?.(false, supportModels); 49 | const market = await getApiMarket(); 50 | onStateChange?.(true, market); 51 | 52 | setSupportModels(market); 53 | }; 54 | 55 | useEffectAsync(update, []); 56 | 57 | return { 58 | supportModels, 59 | update, 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /app/src/admin/market.ts: -------------------------------------------------------------------------------- 1 | export const marketEditableTags = [ 2 | "official", 3 | "unstable", 4 | "web", 5 | "high-quality", 6 | "high-price", 7 | "open-source", 8 | "image-generation", 9 | "multi-modal", 10 | "fast", 11 | "english-model", 12 | ]; 13 | 14 | export const modelImages = [ 15 | "gpt35turbo.png", 16 | "gpt35turbo16k.webp", 17 | "gpt4.png", 18 | "gpt432k.webp", 19 | "gpt4v.png", 20 | "gpt4dalle.png", 21 | "claude.png", 22 | "claude100k.png", 23 | "stablediffusion.jpeg", 24 | "llama2.webp", 25 | "llamacode.webp", 26 | "dalle.jpeg", 27 | "midjourney.jpg", 28 | "newbing.jpg", 29 | "palm2.webp", 30 | "gemini.jpeg", 31 | "chatglm.png", 32 | "tongyi.png", 33 | "sparkdesk.jpg", 34 | "hunyuan.png", 35 | "360gpt.png", 36 | "baichuan.png", 37 | "skylark.jpg", 38 | ]; 39 | 40 | export const marketTags = [...marketEditableTags, "free", "high-context"]; 41 | -------------------------------------------------------------------------------- /app/src/api/common.ts: -------------------------------------------------------------------------------- 1 | export type CommonResponse = { 2 | status: boolean; 3 | error?: string; 4 | reason?: string; 5 | message?: string; 6 | data?: any; 7 | }; 8 | 9 | export function toastState( 10 | toast: any, 11 | t: any, 12 | state: CommonResponse, 13 | toastSuccess?: boolean, 14 | ) { 15 | if (state.status) 16 | toastSuccess && 17 | toast({ title: t("success"), description: t("request-success") }); 18 | else 19 | toast({ 20 | title: t("error"), 21 | description: 22 | state.error ?? state.reason ?? state.message ?? "error occurred", 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/api/file.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { blobEndpoint } from "@/conf/env.ts"; 3 | import { trimSuffixes } from "@/utils/base.ts"; 4 | 5 | export type BlobParserResponse = { 6 | status: boolean; 7 | content: string; 8 | error?: string; 9 | }; 10 | 11 | export type FileObject = { 12 | name: string; 13 | content: string; 14 | size?: number; 15 | }; 16 | 17 | export type FileArray = FileObject[]; 18 | 19 | export async function blobParser( 20 | file: File, 21 | model: string, 22 | ): Promise { 23 | const endpoint = trimSuffixes(blobEndpoint, ["/upload", "/"]); 24 | 25 | try { 26 | const resp = await axios.post( 27 | `${endpoint}/upload`, 28 | { file, model }, 29 | { 30 | headers: { "Content-Type": "multipart/form-data" }, 31 | }, 32 | ); 33 | 34 | return resp.data as BlobParserResponse; 35 | } catch (e) { 36 | return { status: false, content: "", error: (e as Error).message }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/api/invitation.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export type InvitationResponse = { 4 | status: boolean; 5 | error: string; 6 | quota: number; 7 | }; 8 | 9 | export async function getInvitation(code: string): Promise { 10 | try { 11 | const resp = await axios.get(`/invite?code=${code}`); 12 | return resp.data as InvitationResponse; 13 | } catch (e) { 14 | console.debug(e); 15 | return { status: false, error: "network error", quota: 0 }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/api/mask.ts: -------------------------------------------------------------------------------- 1 | import { CustomMask } from "@/masks/types.ts"; 2 | import axios from "axios"; 3 | import { CommonResponse } from "@/api/common.ts"; 4 | import { getErrorMessage } from "@/utils/base.ts"; 5 | 6 | type ListMaskResponse = CommonResponse & { 7 | data: CustomMask[]; 8 | }; 9 | 10 | export async function listMasks(): Promise { 11 | try { 12 | const resp = await axios.get("/conversation/mask/view"); 13 | return ( 14 | resp.data ?? { 15 | status: true, 16 | data: [], 17 | } 18 | ); 19 | } catch (e) { 20 | return { 21 | status: false, 22 | data: [], 23 | error: getErrorMessage(e), 24 | }; 25 | } 26 | } 27 | 28 | export async function saveMask(mask: CustomMask): Promise { 29 | try { 30 | const resp = await axios.post("/conversation/mask/save", mask); 31 | return resp.data; 32 | } catch (e) { 33 | return { 34 | status: false, 35 | error: getErrorMessage(e), 36 | }; 37 | } 38 | } 39 | 40 | export async function deleteMask(id: number): Promise { 41 | try { 42 | const resp = await axios.post("/conversation/mask/delete", { id }); 43 | return resp.data; 44 | } catch (e) { 45 | return { 46 | status: false, 47 | error: getErrorMessage(e), 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/api/quota.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export async function getQuota(): Promise { 4 | try { 5 | const response = await axios.get("/quota"); 6 | if (response.data.status) { 7 | return response.data.quota as number; 8 | } 9 | } catch (e) { 10 | console.debug(e); 11 | } 12 | 13 | return NaN; 14 | } 15 | -------------------------------------------------------------------------------- /app/src/api/redeem.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getErrorMessage } from "@/utils/base.ts"; 3 | 4 | export type RedeemResponse = { 5 | status: boolean; 6 | error: string; 7 | quota: number; 8 | }; 9 | 10 | export async function useRedeem(code: string): Promise { 11 | try { 12 | const resp = await axios.get(`/redeem?code=${code}`); 13 | return resp.data as RedeemResponse; 14 | } catch (e) { 15 | console.debug(e); 16 | return { 17 | status: false, 18 | error: `network error: ${getErrorMessage(e)}`, 19 | quota: 0, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/assets/admin/all.less: -------------------------------------------------------------------------------- 1 | @import "menu"; 2 | @import "dashboard"; 3 | @import "market"; 4 | @import "management"; 5 | @import "broadcast"; 6 | @import "channel"; 7 | @import "charge"; 8 | @import "system"; 9 | @import "subscription"; 10 | @import "logger"; 11 | 12 | 13 | .admin-page { 14 | position: relative; 15 | display: flex; 16 | flex-direction: row; 17 | width: 100%; 18 | height: calc(100% - 56px); 19 | } 20 | 21 | .admin-card { 22 | border: 0 !important; 23 | } 24 | 25 | 26 | @media (max-width: 768px) { 27 | .admin-card { 28 | border-radius: 0 !important; 29 | } 30 | 31 | .user-interface, 32 | .market, 33 | .broadcast, 34 | .channel, 35 | .charge, 36 | .system, 37 | .logger, 38 | .admin-subscription 39 | { 40 | padding: 0 !important; 41 | 42 | & > * { 43 | margin-bottom: 0 !important; 44 | border-bottom: 1px solid hsl(var(--border)) !important; 45 | border-radius: 0 !important; 46 | } 47 | } 48 | } 49 | 50 | .object-id { 51 | display: flex; 52 | flex-direction: row; 53 | align-items: center; 54 | justify-items: center; 55 | border-radius: var(--radius); 56 | border: 1px solid hsl(var(--border)); 57 | color: hsl(var(--text-secondary)); 58 | user-select: none; 59 | font-size: 0.75rem; 60 | height: 2.5rem; 61 | padding: 0.5rem 1.25rem; 62 | cursor: pointer; 63 | transition: 0.25s; 64 | flex-shrink: 0; 65 | 66 | &:hover { 67 | color: hsl(var(--text)); 68 | border-color: hsl(var(--border-hover)); 69 | } 70 | 71 | svg { 72 | transform: translateY(1px); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/assets/admin/broadcast.less: -------------------------------------------------------------------------------- 1 | .broadcast { 2 | width: 100%; 3 | height: max-content; 4 | padding: 2rem; 5 | display: flex; 6 | flex-direction: column; 7 | 8 | .broadcast-card { 9 | width: 100%; 10 | height: 100%; 11 | min-height: 20vh; 12 | } 13 | 14 | .empty { 15 | color: hsl(var(--text-secondary)) !important; 16 | font-size: 14px; 17 | margin: auto; 18 | user-select: none; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/assets/admin/charge.less: -------------------------------------------------------------------------------- 1 | .charge { 2 | width: 100%; 3 | height: max-content; 4 | padding: 2rem; 5 | display: flex; 6 | flex-direction: column; 7 | 8 | .charge-card { 9 | width: 100%; 10 | height: 100%; 11 | min-height: 20vh; 12 | } 13 | } 14 | 15 | .charge-widget { 16 | height: max-content; 17 | width: 100%; 18 | display: flex; 19 | flex-direction: column; 20 | 21 | & > * { 22 | margin-bottom: 1rem; 23 | 24 | &:last-child { 25 | margin-bottom: 0; 26 | } 27 | } 28 | } 29 | 30 | .charge-alert { 31 | .model-list { 32 | display: flex; 33 | flex-direction: row; 34 | flex-wrap: wrap; 35 | gap: 0.5rem; 36 | margin-top: 1rem; 37 | 38 | .model { 39 | padding: 0.5rem 0.75rem; 40 | border-radius: var(--radius); 41 | border: 1px solid hsl(var(--border)); 42 | } 43 | } 44 | } 45 | 46 | .charge-editor { 47 | padding: 1.5rem; 48 | border: 1px solid hsl(var(--border)); 49 | border-radius: var(--radius); 50 | 51 | .token { 52 | color: hsl(var(--text-secondary)); 53 | user-select: none; 54 | } 55 | } 56 | 57 | 58 | .charge-table { 59 | border: 1px solid hsl(var(--border)); 60 | border-radius: var(--radius); 61 | overflow-x: auto; 62 | scrollbar-width: thin; 63 | 64 | &::-webkit-scrollbar { 65 | width: 0.5rem; 66 | } 67 | 68 | .table { 69 | scrollbar-width: thin; 70 | } 71 | 72 | .charge-id { 73 | color: hsl(var(--text-secondary)); 74 | user-select: none; 75 | 76 | &:before { 77 | content: '#'; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/assets/admin/management.less: -------------------------------------------------------------------------------- 1 | .user-interface { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: calc(100% - 56px); 6 | padding: 2rem; 7 | 8 | & > * { 9 | margin-bottom: 2rem; 10 | 11 | &:last-child { 12 | margin-bottom: 0; 13 | } 14 | } 15 | 16 | &.mobile { 17 | padding: 1rem; 18 | } 19 | 20 | .pagination { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | margin-top: 1rem; 25 | 26 | & > * { 27 | scale: 0.8; 28 | margin: 0 0.5rem; 29 | } 30 | } 31 | 32 | .empty { 33 | user-select: none; 34 | text-align: center; 35 | font-size: 14px; 36 | margin: 4rem 0 2rem; 37 | color: hsl(var(--text-secondary)); 38 | } 39 | 40 | .action { 41 | display: flex; 42 | flex-direction: row; 43 | align-items: center; 44 | margin-top: 1rem; 45 | } 46 | } 47 | 48 | .user-row, 49 | .redeem-row, 50 | .invitation-row { 51 | display: flex; 52 | flex-direction: row; 53 | align-items: center; 54 | justify-content: center; 55 | white-space: nowrap; 56 | user-select: none; 57 | color: hsl(var(--text)); 58 | margin: 1rem 0; 59 | } 60 | 61 | .user-action, 62 | .redeem-action, 63 | .invitation-action { 64 | display: flex; 65 | margin-top: 1rem; 66 | flex-direction: row; 67 | align-items: center; 68 | 69 | & > * { 70 | margin-right: 0.5rem; 71 | 72 | &:last-child { 73 | margin-right: 0; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/assets/admin/system.less: -------------------------------------------------------------------------------- 1 | .system { 2 | width: 100%; 3 | height: max-content; 4 | padding: 2rem; 5 | display: flex; 6 | flex-direction: column; 7 | 8 | .system-card { 9 | width: 100%; 10 | height: 100%; 11 | min-height: 20vh; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/assets/common/404.less: -------------------------------------------------------------------------------- 1 | .error-page { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | height: calc(100% - 56px); 7 | font-size: 30px; 8 | color: hsl(var(--tw-content)); 9 | gap: 12px; 10 | user-select: none; 11 | 12 | .icon { 13 | width: 58px; 14 | height: 58px; 15 | transform: translateY(-62px); 16 | } 17 | 18 | h1 { 19 | font-size: 48px; 20 | transform: translateY(-50px); 21 | } 22 | 23 | p { 24 | font-size: 24px; 25 | transform: translateY(-32px); 26 | } 27 | 28 | button { 29 | transform: translateY(-4px); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/assets/fonts/all.less: -------------------------------------------------------------------------------- 1 | @import "common"; 2 | @import "katex"; -------------------------------------------------------------------------------- /app/src/assets/fonts/common.less: -------------------------------------------------------------------------------- 1 | /* Copyright Jetbrains Mono https://www.jetbrains.com/lp/mono/ */ 2 | @font-face { 3 | font-family: "JetBrains Mono"; 4 | src: url(https://open.lightxi.com/gstatic/JetBrainsMono-Regular.woff2) format("woff2"); 5 | font-weight: 400; 6 | font-style: normal; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Andika'; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: url(https://open.lightxi.com/gstatic/s/andika/v25/mem_Ya6iyW-LwqgwarYV.ttf) format('truetype'); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/assets/pages/api.less: -------------------------------------------------------------------------------- 1 | .api-dialog { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | width: 100%; 6 | height: max-content; 7 | padding: 24px 0 12px; 8 | gap: 24px; 9 | } 10 | 11 | .api-wrapper { 12 | display: flex; 13 | flex-direction: row; 14 | gap: 6px; 15 | width: 100%; 16 | 17 | input { 18 | text-align: center; 19 | font-size: 16px; 20 | cursor: pointer; 21 | flex-grow: 1; 22 | } 23 | 24 | button { 25 | flex-shrink: 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/assets/pages/article.less: -------------------------------------------------------------------------------- 1 | .article-page { 2 | position: relative; 3 | display: flex; 4 | width: 100%; 5 | min-height: calc(100% - 56px); 6 | height: max-content; 7 | } 8 | 9 | 10 | .article-container { 11 | display: flex; 12 | flex-direction: column; 13 | padding: 12px 16px; 14 | gap: 6px; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | .article-wrapper { 20 | width: calc(96vw - 32px); 21 | height: 100%; 22 | margin: 1rem auto; 23 | padding: 1rem; 24 | max-width: 840px; 25 | 26 | .article-title { 27 | display: flex; 28 | flex-direction: row; 29 | user-select: none; 30 | align-items: center; 31 | } 32 | 33 | .article-action { 34 | @media (max-width: 768px) { 35 | flex-direction: column; 36 | } 37 | } 38 | 39 | .article-content { 40 | display: flex; 41 | flex-direction: column; 42 | margin: 1rem 0; 43 | 44 | & > * { 45 | margin-bottom: 1rem; 46 | 47 | &:last-child { 48 | margin-bottom: 0; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/assets/pages/package.less: -------------------------------------------------------------------------------- 1 | .package-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 24px 4px !important; 5 | gap: 18px; 6 | 7 | .package { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 8px; 11 | 12 | .package-title { 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | gap: 4px; 17 | font-size: 16px; 18 | color: hsl(var(--text)); 19 | user-select: none; 20 | 21 | svg { 22 | transform: translateY(1px); 23 | } 24 | } 25 | 26 | .package-content { 27 | font-size: 14px; 28 | color: hsl(var(--text-secondary)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/assets/pages/share-manager.less: -------------------------------------------------------------------------------- 1 | .share-table { 2 | width: 100%; 3 | height: max-content; 4 | } 5 | -------------------------------------------------------------------------------- /app/src/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { deeptrainApiEndpoint, useDeeptrain } from "@/conf/env.ts"; 2 | import { ImgHTMLAttributes, useMemo } from "react"; 3 | import { cn } from "@/components/ui/lib/utils.ts"; 4 | 5 | export interface AvatarProps extends ImgHTMLAttributes { 6 | username: string; 7 | } 8 | 9 | function Avatar({ username, ...props }: AvatarProps) { 10 | const code = useMemo( 11 | () => (username.length > 0 ? username[0].toUpperCase() : "A"), 12 | [username], 13 | ); 14 | 15 | const background = useMemo(() => { 16 | const colors = [ 17 | "bg-red-500", 18 | "bg-yellow-500", 19 | "bg-green-500", 20 | "bg-indigo-500", 21 | "bg-purple-500", 22 | "bg-sky-500", 23 | "bg-pink-500", 24 | ]; 25 | const index = code.charCodeAt(0) % colors.length; 26 | return colors[index]; 27 | }, [username]); 28 | 29 | return useDeeptrain ? ( 30 | 31 | ) : ( 32 |
33 |

{code}

34 |
35 | ); 36 | } 37 | 38 | export default Avatar; 39 | -------------------------------------------------------------------------------- /app/src/components/Broadcast.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { selectInit } from "@/store/auth.ts"; 3 | import { useToast } from "@/components/ui/use-toast.ts"; 4 | import { useTranslation } from "react-i18next"; 5 | import { useEffectAsync } from "@/utils/hook.ts"; 6 | import { getBroadcast } from "@/api/broadcast.ts"; 7 | import Markdown from "@/components/Markdown.tsx"; 8 | 9 | function Broadcast() { 10 | const { t } = useTranslation(); 11 | const init = useSelector(selectInit); 12 | const { toast } = useToast(); 13 | useEffectAsync(async () => { 14 | if (!init) return; 15 | 16 | const content = await getBroadcast(); 17 | if (content.length === 0) return; 18 | 19 | toast( 20 | { 21 | title: t("broadcast"), 22 | description: ( 23 | 24 | {content} 25 | 26 | ), 27 | }, 28 | 30000, 29 | ); 30 | }, [init]); 31 | 32 | return
; 33 | } 34 | 35 | export default Broadcast; 36 | -------------------------------------------------------------------------------- /app/src/components/I18nProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./ui/button.tsx"; 2 | import { Languages } from "lucide-react"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuCheckboxItem, 6 | DropdownMenuContent, 7 | DropdownMenuTrigger, 8 | } from "./ui/dropdown-menu.tsx"; 9 | import { langsProps, setLanguage } from "@/i18n.ts"; 10 | import { useTranslation } from "react-i18next"; 11 | 12 | function I18nProvider() { 13 | const { i18n } = useTranslation(); 14 | 15 | return ( 16 | 17 | 18 | 21 | 22 | 23 | {Object.entries(langsProps).map(([key, value]) => ( 24 | setLanguage(i18n, key)} 28 | > 29 | {value} 30 | 31 | ))} 32 | 33 | 34 | ); 35 | } 36 | 37 | export default I18nProvider; 38 | -------------------------------------------------------------------------------- /app/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import "@/assets/common/loader.less"; 2 | 3 | type LoaderProps = { 4 | className?: string; 5 | prompt?: string; 6 | }; 7 | 8 | function Loader({ className, prompt }: LoaderProps) { 9 | return ( 10 |
11 |
12 |

{prompt}

13 |
14 | ); 15 | } 16 | 17 | export default Loader; 18 | -------------------------------------------------------------------------------- /app/src/components/ProjectLink.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./ui/button.tsx"; 2 | import { useConversationActions, useMessages } from "@/store/chat.ts"; 3 | import { MessageSquarePlus } from "lucide-react"; 4 | import Github from "@/components/ui/icons/Github.tsx"; 5 | import { openWindow } from "@/utils/device.ts"; 6 | 7 | function ProjectLink() { 8 | const messages = useMessages(); 9 | 10 | const { toggle } = useConversationActions(); 11 | 12 | return messages.length > 0 ? ( 13 | 20 | ) : ( 21 | 30 | ); 31 | } 32 | 33 | export default ProjectLink; 34 | -------------------------------------------------------------------------------- /app/src/components/ReloadService.tsx: -------------------------------------------------------------------------------- 1 | import { version } from "@/conf/bootstrap.ts"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useToast } from "./ui/use-toast.ts"; 4 | import { getMemory, setMemory } from "@/utils/memory.ts"; 5 | 6 | function ReloadPrompt() { 7 | const { t } = useTranslation(); 8 | const { toast } = useToast(); 9 | 10 | const before = getMemory("version"); 11 | if (before.length > 0 && before !== version) { 12 | setMemory("version", version); 13 | toast({ 14 | title: t("service.update-success"), 15 | description: t("service.update-success-prompt"), 16 | }); 17 | console.debug( 18 | `[service] service worker updated (from ${before} to ${version})`, 19 | ); 20 | } 21 | setMemory("version", version); 22 | 23 | return <>; 24 | } 25 | 26 | export default ReloadPrompt; 27 | -------------------------------------------------------------------------------- /app/src/components/TickButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from "@/components/ui/button.tsx"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import { isAsyncFunc } from "@/utils/base.ts"; 4 | 5 | export interface TickButtonProps extends ButtonProps { 6 | tick: number; 7 | onTickChange?: (tick: number) => void; 8 | onClick?: ( 9 | e: React.MouseEvent, 10 | ) => boolean | Promise; 11 | } 12 | 13 | function TickButton({ 14 | tick, 15 | onTickChange, 16 | onClick, 17 | children, 18 | ...props 19 | }: TickButtonProps) { 20 | const stamp = useRef(0); 21 | const [timer, setTimer] = useState(0); 22 | 23 | useEffect(() => { 24 | setInterval(() => { 25 | const offset = Math.floor((Number(Date.now()) - stamp.current) / 1000); 26 | let value = tick - offset; 27 | if (value <= 0) value = 0; 28 | setTimer(value); 29 | onTickChange && onTickChange(value); 30 | }, 250); 31 | }, []); 32 | 33 | const onReset = () => (stamp.current = Number(Date.now())); 34 | 35 | // if is async function, use this: 36 | const onTrigger = isAsyncFunc(onClick) 37 | ? async (e: React.MouseEvent) => { 38 | if (timer !== 0 || !onClick) return; 39 | if (await onClick(e)) onReset(); 40 | } 41 | : (e: React.MouseEvent) => { 42 | if (timer !== 0 || !onClick) return; 43 | if (onClick(e)) onReset(); 44 | }; 45 | 46 | return ( 47 | 50 | ); 51 | } 52 | 53 | export default TickButton; 54 | -------------------------------------------------------------------------------- /app/src/components/admin/assemblies/BillingChart.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useMemo } from "react"; 3 | import { Loader2 } from "lucide-react"; 4 | import { AreaChart } from "@tremor/react"; 5 | 6 | type BillingChartProps = { 7 | labels: string[]; 8 | datasets: number[]; 9 | }; 10 | function BillingChart({ labels, datasets }: BillingChartProps) { 11 | const { t } = useTranslation(); 12 | 13 | const data = useMemo(() => { 14 | return datasets.map((data, index) => ({ 15 | date: labels[index], 16 | [t("admin.billing")]: data, 17 | })); 18 | }, [labels, datasets, t("admin.billing")]); 19 | 20 | return ( 21 |
22 |
23 |

{t("admin.billing-chart")}

24 | {labels.length === 0 && ( 25 | 26 | )} 27 |
28 | `¥${value.toFixed(2)}`} 36 | /> 37 |
38 | ); 39 | } 40 | 41 | export default BillingChart; 42 | -------------------------------------------------------------------------------- /app/src/components/admin/assemblies/ErrorChart.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useMemo } from "react"; 3 | import { Loader2 } from "lucide-react"; 4 | import { AreaChart } from "@tremor/react"; 5 | import { getReadableNumber } from "@/utils/processor.ts"; 6 | 7 | type ErrorChartProps = { 8 | labels: string[]; 9 | datasets: number[]; 10 | }; 11 | function ErrorChart({ labels, datasets }: ErrorChartProps) { 12 | const { t } = useTranslation(); 13 | const data = useMemo(() => { 14 | return datasets.map((data, index) => ({ 15 | date: labels[index], 16 | [t("admin.times")]: data, 17 | })); 18 | }, [labels, datasets, t("admin.times")]); 19 | 20 | return ( 21 |
22 |
23 |

{t("admin.error-chart")}

24 | {labels.length === 0 && ( 25 | 26 | )} 27 |
28 | getReadableNumber(value, 1)} 36 | /> 37 |
38 | ); 39 | } 40 | 41 | export default ErrorChart; 42 | -------------------------------------------------------------------------------- /app/src/components/admin/assemblies/RequestChart.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useMemo } from "react"; 3 | import { Loader2 } from "lucide-react"; 4 | import { AreaChart } from "@tremor/react"; 5 | import { getReadableNumber } from "@/utils/processor.ts"; 6 | 7 | type RequestChartProps = { 8 | labels: string[]; 9 | datasets: number[]; 10 | }; 11 | 12 | function RequestChart({ labels, datasets }: RequestChartProps) { 13 | const { t } = useTranslation(); 14 | const data = useMemo(() => { 15 | return datasets.map((data, index) => ({ 16 | date: labels[index], 17 | [t("admin.requests")]: data, 18 | })); 19 | }, [labels, datasets, t("admin.requests")]); 20 | 21 | return ( 22 |
23 |
24 |

{t("admin.request-chart")}

25 | {labels.length === 0 && ( 26 | 27 | )} 28 |
29 | getReadableNumber(value, 1)} 37 | /> 38 |
39 | ); 40 | } 41 | 42 | export default RequestChart; 43 | -------------------------------------------------------------------------------- /app/src/components/admin/common/StateBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge.tsx"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | export type StateBadgeProps = { 5 | state: boolean; 6 | }; 7 | 8 | export default function StateBadge({ state }: StateBadgeProps) { 9 | const { t } = useTranslation(); 10 | 11 | return {t(`admin.used-${state}`)}; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/components/app/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from "./NavBar.tsx"; 2 | import { ThemeProvider } from "@/components/ThemeProvider.tsx"; 3 | import DialogManager from "@/dialogs"; 4 | import Broadcast from "@/components/Broadcast.tsx"; 5 | import { useEffectAsync } from "@/utils/hook.ts"; 6 | import { bindMarket, getApiPlans } from "@/api/v1.ts"; 7 | import { useDispatch } from "react-redux"; 8 | import { 9 | stack, 10 | updateMasks, 11 | updateSupportModels, 12 | useMessageActions, 13 | } from "@/store/chat.ts"; 14 | import { dispatchSubscriptionData, setTheme } from "@/store/globals.ts"; 15 | import { infoEvent } from "@/events/info.ts"; 16 | import { setForm } from "@/store/info.ts"; 17 | import { themeEvent } from "@/events/theme.ts"; 18 | import { useEffect } from "react"; 19 | 20 | function AppProvider() { 21 | const dispatch = useDispatch(); 22 | const { receive } = useMessageActions(); 23 | 24 | useEffect(() => { 25 | infoEvent.bind((data) => dispatch(setForm(data))); 26 | themeEvent.bind((theme) => dispatch(setTheme(theme))); 27 | 28 | stack.setCallback(async (id, message) => { 29 | await receive(id, message); 30 | }); 31 | }, []); 32 | 33 | useEffectAsync(async () => { 34 | updateSupportModels(dispatch, await bindMarket()); 35 | dispatchSubscriptionData(dispatch, await getApiPlans()); 36 | await updateMasks(dispatch); 37 | }, []); 38 | 39 | return ( 40 | <> 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | export default AppProvider; 50 | -------------------------------------------------------------------------------- /app/src/components/home/assemblies/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button.tsx"; 2 | import { PauseCircle } from "lucide-react"; 3 | 4 | type SendButtonProps = { 5 | working: boolean; 6 | onClick: () => any; 7 | }; 8 | 9 | function ActionButton({ onClick, working }: SendButtonProps) { 10 | return ( 11 | 31 | ); 32 | } 33 | 34 | export default ActionButton; 35 | -------------------------------------------------------------------------------- /app/src/components/home/assemblies/ScrollAction.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronsDown } from "lucide-react"; 2 | import { useEffect } from "react"; 3 | import { addEventListeners, scrollDown } from "@/utils/dom.ts"; 4 | import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx"; 5 | import { useTranslation } from "react-i18next"; 6 | import { Message } from "@/api/types.tsx"; 7 | import { useMessages } from "@/store/chat.ts"; 8 | 9 | type ScrollActionProps = { 10 | visible: boolean; 11 | setVisibility: (visible: boolean) => void; 12 | target: HTMLElement | null; 13 | }; 14 | 15 | function ScrollAction( 16 | this: any, 17 | { target, visible, setVisibility }: ScrollActionProps, 18 | ) { 19 | const { t } = useTranslation(); 20 | const messages: Message[] = useMessages(); 21 | 22 | const scrollableHandler = () => { 23 | if (!target) return; 24 | 25 | const position = target.scrollTop + target.clientHeight; 26 | const height = target.scrollHeight; 27 | const diff = Math.abs(position - height); 28 | setVisibility(diff > 50); 29 | }; 30 | 31 | useEffect(() => { 32 | if (!target) return; 33 | return addEventListeners( 34 | target, 35 | ["scroll", "touchmove"], 36 | scrollableHandler, 37 | ); 38 | }, [target]); 39 | 40 | useEffect(() => { 41 | if (!target) return; 42 | 43 | if (target.scrollHeight <= target.clientHeight) { 44 | setVisibility(false); 45 | } 46 | }, [messages]); 47 | 48 | return ( 49 | visible && ( 50 | scrollDown(target)}> 51 | 52 | 53 | ) 54 | ); 55 | } 56 | 57 | export default ScrollAction; 58 | -------------------------------------------------------------------------------- /app/src/components/home/subscription/SubscriptionUsage.tsx: -------------------------------------------------------------------------------- 1 | import { SubscriptionIcon } from "@/conf/subscription.tsx"; 2 | import React from "react"; 3 | import Icon from "@/components/utils/Icon.tsx"; 4 | 5 | type UsageProps = { 6 | icon: string | React.ReactElement; 7 | name: string; 8 | usage: 9 | | { 10 | used: number; 11 | total: number; 12 | } 13 | | number 14 | | undefined; 15 | }; 16 | 17 | function SubscriptionUsage({ icon, name, usage }: UsageProps) { 18 | return ( 19 | usage && ( 20 |
21 | {typeof icon === "string" ? ( 22 | 23 | ) : ( 24 | 25 | )} 26 | {name} 27 |
28 | {typeof usage === "number" ? ( 29 |
30 |

{usage}

31 |
32 | ) : ( 33 |
34 |

{usage.used}

/{" "} 35 |

{usage.total === -1 ? "∞" : usage.total}

36 |
37 | )} 38 |
39 | ) 40 | ); 41 | } 42 | 43 | export default SubscriptionUsage; 44 | -------------------------------------------------------------------------------- /app/src/components/markdown/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Codepen, Codesandbox, Github, Twitter, Youtube } from "lucide-react"; 3 | import { VirtualMessage } from "./VirtualMessage"; 4 | 5 | function getSocialIcon(url: string) { 6 | try { 7 | const { hostname } = new URL(url); 8 | 9 | if (hostname.includes("github.com")) 10 | return ; 11 | if (hostname.includes("twitter.com")) 12 | return ; 13 | if (hostname.includes("youtube.com")) 14 | return ; 15 | if (hostname.includes("codepen.io")) 16 | return ; 17 | if (hostname.includes("codesandbox.io")) 18 | return ; 19 | } catch (e) { 20 | return; 21 | } 22 | } 23 | 24 | type LinkProps = { 25 | href?: string; 26 | children: React.ReactNode; 27 | }; 28 | 29 | export default function ({ href, children }: LinkProps) { 30 | const url: string = href?.toString() || ""; 31 | 32 | if (url.startsWith("https://chatnio.virtual")) { 33 | const message = url.slice(23); 34 | const prefix = message.split("-")[0]; 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | return ( 44 | 45 | {getSocialIcon(url)} 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "./lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/90", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/90", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /app/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 3 | import { Check } from "lucide-react"; 4 | 5 | import { cn } from "@/components/ui/lib/utils"; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )); 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 | 28 | export { Checkbox }; 29 | -------------------------------------------------------------------------------- /app/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "./lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /app/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "./lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /app/src/components/ui/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 3 | 4 | import { cn } from "@/components/ui/lib/utils"; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )); 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 28 | 29 | export { Popover, PopoverTrigger, PopoverContent }; 30 | -------------------------------------------------------------------------------- /app/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "@/components/ui/lib/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )); 24 | Progress.displayName = ProgressPrimitive.Root.displayName; 25 | 26 | export { Progress }; 27 | -------------------------------------------------------------------------------- /app/src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; 3 | import { Circle } from "lucide-react"; 4 | 5 | import { cn } from "@/components/ui/lib/utils"; 6 | 7 | const RadioGroup = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => { 11 | return ( 12 | 17 | ); 18 | }); 19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 20 | 21 | const RadioGroupItem = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => { 25 | return ( 26 | 34 | 35 | 36 | 37 | 38 | ); 39 | }); 40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 41 | 42 | export { RadioGroup, RadioGroupItem }; 43 | -------------------------------------------------------------------------------- /app/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "./lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref, 13 | ) => ( 14 | 25 | ), 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /app/src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SliderPrimitive from "@radix-ui/react-slider"; 3 | 4 | import { cn } from "@/components/ui/lib/utils"; 5 | 6 | type SliderProps = { 7 | classNameThumb?: string; 8 | }; 9 | 10 | const Slider = React.forwardRef< 11 | React.ElementRef & SliderProps, 12 | React.ComponentPropsWithoutRef & SliderProps 13 | >(({ className, classNameThumb, ...props }, ref) => ( 14 | 22 | 23 | 24 | 25 | 31 | 32 | )); 33 | Slider.displayName = SliderPrimitive.Root.displayName; 34 | 35 | export { Slider }; 36 | -------------------------------------------------------------------------------- /app/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { Toaster as Sonner } from "sonner"; 3 | import React from "react"; 4 | 5 | type ToasterProps = React.ComponentProps; 6 | 7 | const Toaster = ({ ...props }: ToasterProps) => { 8 | const { theme = "system" } = useTheme(); 9 | 10 | return ( 11 | 27 | ); 28 | }; 29 | 30 | export { Toaster }; 31 | -------------------------------------------------------------------------------- /app/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | 4 | import { cn } from "./lib/utils"; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )); 25 | Switch.displayName = SwitchPrimitives.Root.displayName; 26 | 27 | export { Switch }; 28 | -------------------------------------------------------------------------------- /app/src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Toast, 3 | ToastClose, 4 | ToastDescription, 5 | ToastProvider, 6 | ToastTitle, 7 | ToastViewport, 8 | } from "./toast"; 9 | import { useToast } from "./use-toast"; 10 | 11 | export function Toaster() { 12 | const { toasts } = useToast(); 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ); 29 | })} 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/src/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TogglePrimitive from "@radix-ui/react-toggle"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "./lib/utils"; 6 | 7 | const toggleVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-transparent", 13 | outline: 14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", 15 | }, 16 | size: { 17 | default: "h-10 px-3", 18 | sm: "h-9 px-2.5", 19 | lg: "h-11 px-5", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | size: "default", 25 | }, 26 | }, 27 | ); 28 | 29 | const Toggle = React.forwardRef< 30 | React.ElementRef, 31 | React.ComponentPropsWithoutRef & 32 | VariantProps 33 | >(({ className, variant, size, ...props }, ref) => ( 34 | 39 | )); 40 | 41 | Toggle.displayName = TogglePrimitive.Root.displayName; 42 | 43 | export { Toggle, toggleVariants }; 44 | -------------------------------------------------------------------------------- /app/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 3 | 4 | import { cn } from "./lib/utils"; 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider; 7 | 8 | const Tooltip = TooltipPrimitive.Root; 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger; 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 26 | )); 27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 28 | 29 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 30 | -------------------------------------------------------------------------------- /app/src/components/utils/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Icon = { 4 | icon: React.ReactElement; 5 | className?: string; 6 | id?: string; 7 | } & React.SVGProps; 8 | 9 | function Icon({ icon, className, id, ...props }: Icon) { 10 | return React.cloneElement(icon, { 11 | className: className, 12 | id: id, 13 | ...props, 14 | }); 15 | } 16 | 17 | export default Icon; 18 | -------------------------------------------------------------------------------- /app/src/conf/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getMemory } from "@/utils/memory.ts"; 3 | 4 | type AxiosConfig = { 5 | endpoint: string; 6 | token: string; 7 | }; 8 | 9 | export function setAxiosConfig(config: AxiosConfig) { 10 | axios.defaults.baseURL = config.endpoint; 11 | axios.defaults.headers.post["Content-Type"] = "application/json"; 12 | axios.defaults.headers.common["Authorization"] = getMemory(config.token); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/conf/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getDev, 3 | getRestApi, 4 | getTokenField, 5 | getWebsocketApi, 6 | } from "@/conf/env.ts"; 7 | import { syncSiteInfo } from "@/admin/api/info.ts"; 8 | import { setAxiosConfig } from "@/conf/api.ts"; 9 | 10 | export const version = "3.11.2"; // version of the current build 11 | export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin) 12 | export const deploy: boolean = true; // is production environment (for api endpoint) 13 | export const tokenField = getTokenField(deploy); // token field name for storing token 14 | 15 | export let apiEndpoint: string = getRestApi(deploy); // api endpoint for rest api calls 16 | export let websocketEndpoint: string = getWebsocketApi(deploy); // api endpoint for websocket calls 17 | 18 | setAxiosConfig({ 19 | endpoint: apiEndpoint, 20 | token: tokenField, 21 | }); 22 | 23 | syncSiteInfo(); 24 | -------------------------------------------------------------------------------- /app/src/conf/deeptrain.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | deeptrainAppName, 3 | deeptrainEndpoint, 4 | useDeeptrain, 5 | } from "@/conf/env.ts"; 6 | import { dev } from "@/conf/bootstrap.ts"; 7 | import React from "react"; 8 | 9 | export function goDeepLogin() { 10 | location.href = `${deeptrainEndpoint}/login?app=${ 11 | dev ? "dev" : deeptrainAppName 12 | }`; 13 | } 14 | 15 | export function DeeptrainOnly({ children }: { children: React.ReactNode }) { 16 | return useDeeptrain ? <>{children} : null; 17 | } 18 | -------------------------------------------------------------------------------- /app/src/conf/model.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "@/api/types.tsx"; 2 | 3 | export function getModelFromId(market: Model[], id: string): Model | undefined { 4 | return market.find((model) => model.id === id); 5 | } 6 | 7 | export function isHighContextModel(market: Model[], id: string): boolean { 8 | const model = getModelFromId(market, id); 9 | return !!model && model.high_context; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/dialogs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "@/components/ui/toaster.tsx"; 2 | import QuotaDialog from "./QuotaDialog.tsx"; 3 | import ApikeyDialog from "./ApikeyDialog.tsx"; 4 | import PackageDialog from "./PackageDialog.tsx"; 5 | import SubscriptionDialog from "./SubscriptionDialog.tsx"; 6 | import ShareManagementDialog from "./ShareManagementDialog.tsx"; 7 | import InvitationDialog from "./InvitationDialog.tsx"; 8 | import SettingsDialog from "@/dialogs/SettingsDialog.tsx"; 9 | import MaskDialog from "@/dialogs/MaskDialog.tsx"; 10 | 11 | function DialogManager() { 12 | return ( 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default DialogManager; 28 | -------------------------------------------------------------------------------- /app/src/events/announcement.ts: -------------------------------------------------------------------------------- 1 | import { EventCommitter } from "@/events/struct.ts"; 2 | 3 | export type AnnouncementEvent = { 4 | message: string; 5 | firstReceived: boolean; 6 | }; 7 | 8 | export const announcementEvent = new EventCommitter({ 9 | name: "announcement", 10 | }); 11 | -------------------------------------------------------------------------------- /app/src/events/blob.ts: -------------------------------------------------------------------------------- 1 | import { EventCommitter } from "@/events/struct.ts"; 2 | 3 | export const blobEvent = new EventCommitter({ 4 | name: "blob", 5 | }); 6 | -------------------------------------------------------------------------------- /app/src/events/info.ts: -------------------------------------------------------------------------------- 1 | import { EventCommitter } from "@/events/struct.ts"; 2 | 3 | export type InfoForm = { 4 | mail: boolean; 5 | contact: string; 6 | footer: string; 7 | auth_footer: boolean; 8 | article: string[]; 9 | generation: string[]; 10 | relay_plan: boolean; 11 | }; 12 | 13 | export const infoEvent = new EventCommitter({ 14 | name: "info", 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/events/model.ts: -------------------------------------------------------------------------------- 1 | import { EventCommitter } from "./struct.ts"; 2 | 3 | export const modelEvent = new EventCommitter({ 4 | name: "model", 5 | }); 6 | -------------------------------------------------------------------------------- /app/src/events/spinner.ts: -------------------------------------------------------------------------------- 1 | import { EventCommitter } from "@/events/struct.ts"; 2 | 3 | export type SpinnerEvent = { 4 | id: number; 5 | type: boolean; 6 | }; 7 | 8 | export const openSpinnerType = true; 9 | export const closeSpinnerType = false; 10 | 11 | export const spinnerEvent = new EventCommitter({ 12 | name: "spinner", 13 | }); 14 | -------------------------------------------------------------------------------- /app/src/events/struct.ts: -------------------------------------------------------------------------------- 1 | export type EventCommitterProps = { 2 | name: string; 3 | destroyedAfterTrigger?: boolean; 4 | }; 5 | 6 | export class EventCommitter { 7 | name: string; 8 | trigger: ((data: T) => void) | undefined; 9 | listeners: ((data: T) => void)[] = []; 10 | destroyedAfterTrigger: boolean; 11 | 12 | constructor({ name, destroyedAfterTrigger = false }: EventCommitterProps) { 13 | this.name = name; 14 | this.destroyedAfterTrigger = destroyedAfterTrigger; 15 | } 16 | 17 | protected setTrigger(trigger: (data: T) => void) { 18 | this.trigger = trigger; 19 | } 20 | 21 | protected clearTrigger() { 22 | this.trigger = undefined; 23 | } 24 | 25 | protected triggerEvent(data: T) { 26 | this.trigger && this.trigger(data); 27 | if (this.destroyedAfterTrigger) this.clearTrigger(); 28 | 29 | this.listeners.forEach((listener) => listener(data)); 30 | } 31 | 32 | public emit(data: T) { 33 | this.triggerEvent(data); 34 | } 35 | 36 | public bind(trigger: (data: T) => void) { 37 | this.setTrigger(trigger); 38 | } 39 | 40 | public addEventListener(listener: (data: T) => void) { 41 | this.listeners.push(listener); 42 | } 43 | 44 | public removeEventListener(listener: (data: T) => void) { 45 | this.listeners = this.listeners.filter((item) => item !== listener); 46 | } 47 | 48 | public clearEventListener() { 49 | this.listeners = []; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/events/theme.ts: -------------------------------------------------------------------------------- 1 | import { EventCommitter } from "@/events/struct.ts"; 2 | 3 | export const themeEvent = new EventCommitter({ 4 | name: "theme", 5 | }); 6 | -------------------------------------------------------------------------------- /app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import App from "./App.tsx"; 3 | import "./conf/bootstrap.ts"; 4 | import "./i18n.ts"; 5 | import "./assets/main.less"; 6 | import "./assets/globals.less"; 7 | import "./conf/bootstrap.ts"; 8 | import ReloadPrompt from "./components/ReloadService.tsx"; 9 | 10 | ReactDOM.createRoot(document.getElementById("root")!).render( 11 | <> 12 | 13 | 14 | , 15 | ); 16 | -------------------------------------------------------------------------------- /app/src/masks/types.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@/api/types.tsx"; 2 | 3 | export type MaskMessage = { 4 | role: string; 5 | content: string; 6 | }; 7 | 8 | export type Mask = { 9 | avatar: string; 10 | name: string; 11 | description?: string; 12 | lang?: string; 13 | builtin?: boolean; 14 | context: MaskMessage[]; 15 | }; 16 | 17 | export type CustomMask = Mask & { 18 | id: number; 19 | }; 20 | 21 | export const initialCustomMask: CustomMask = { 22 | id: -1, 23 | avatar: "1f9d0", 24 | name: "", 25 | context: [{ role: UserRole, content: "" }], 26 | }; 27 | -------------------------------------------------------------------------------- /app/src/routes/Admin.tsx: -------------------------------------------------------------------------------- 1 | import "@/assets/admin/all.less"; 2 | import MenuBar from "@/components/admin/MenuBar.tsx"; 3 | import { Outlet } from "react-router-dom"; 4 | import { useSelector } from "react-redux"; 5 | import { selectAdmin, selectInit } from "@/store/auth.ts"; 6 | import { useEffect } from "react"; 7 | import router from "@/router.tsx"; 8 | import { ScrollArea } from "@/components/ui/scroll-area.tsx"; 9 | 10 | function Admin() { 11 | const init = useSelector(selectInit); 12 | const admin = useSelector(selectAdmin); 13 | 14 | useEffect(() => { 15 | if (init && !admin) router.navigate("/"); 16 | }, [init]); 17 | 18 | return ( 19 |
20 | 21 | 22 | 23 | 24 |
25 | ); 26 | } 27 | 28 | export default Admin; 29 | -------------------------------------------------------------------------------- /app/src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import "@/assets/pages/home.less"; 2 | import "@/assets/pages/chat.less"; 3 | import ChatWrapper from "@/components/home/ChatWrapper.tsx"; 4 | import SideBar from "@/components/home/SideBar.tsx"; 5 | import { useSelector } from "react-redux"; 6 | import { selectMarket } from "@/store/chat.ts"; 7 | import ModelMarket from "@/components/home/ModelMarket.tsx"; 8 | import ErrorBoundary from "@/components/ErrorBoundary.tsx"; 9 | 10 | function Home() { 11 | const market = useSelector(selectMarket); 12 | 13 | return ( 14 | 15 |
16 | 17 | {market ? : } 18 |
19 |
20 | ); 21 | } 22 | 23 | export default Home; 24 | -------------------------------------------------------------------------------- /app/src/routes/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import "@/assets/common/404.less"; 2 | import { Button } from "@/components/ui/button.tsx"; 3 | import { HelpCircle } from "lucide-react"; 4 | import router from "@/router.tsx"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | function NotFound() { 8 | const { t } = useTranslation(); 9 | 10 | return ( 11 |
12 | 13 |

404

14 |

{t("not-found")}

15 | 16 |
17 | ); 18 | } 19 | 20 | export default NotFound; 21 | -------------------------------------------------------------------------------- /app/src/routes/admin/Broadcast.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardHeader, 5 | CardTitle, 6 | } from "@/components/ui/card.tsx"; 7 | import { useTranslation } from "react-i18next"; 8 | import BroadcastTable from "@/components/admin/assemblies/BroadcastTable.tsx"; 9 | 10 | function Broadcast() { 11 | const { t } = useTranslation(); 12 | return ( 13 |
14 | 15 | 16 | {t("admin.broadcast")} 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | 26 | export default Broadcast; 27 | -------------------------------------------------------------------------------- /app/src/routes/admin/Channel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardHeader, 5 | CardTitle, 6 | } from "@/components/ui/card.tsx"; 7 | import { useTranslation } from "react-i18next"; 8 | import ChannelSettings from "@/components/admin/ChannelSettings.tsx"; 9 | 10 | function Channel() { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 |
15 | 16 | 17 | {t("admin.channel")} 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default Channel; 28 | -------------------------------------------------------------------------------- /app/src/routes/admin/Charge.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardHeader, 5 | CardTitle, 6 | } from "@/components/ui/card.tsx"; 7 | import { useTranslation } from "react-i18next"; 8 | import ChargeWidget from "@/components/admin/ChargeWidget.tsx"; 9 | 10 | function Charge() { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 |
15 | 16 | 17 | {t("admin.prize")} 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default Charge; 28 | -------------------------------------------------------------------------------- /app/src/routes/admin/DashBoard.tsx: -------------------------------------------------------------------------------- 1 | import InfoBox from "@/components/admin/InfoBox.tsx"; 2 | import ChartBox from "@/components/admin/ChartBox.tsx"; 3 | import CommunityBanner from "@/components/admin/CommunityBanner.tsx"; 4 | 5 | function DashBoard() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default DashBoard; 16 | -------------------------------------------------------------------------------- /app/src/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { NProgress } from "@tanem/react-nprogress"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { decreaseTask, increaseTask, selectIsTasking } from "@/store/auth.ts"; 4 | import { useEffect } from "react"; 5 | import { 6 | closeSpinnerType, 7 | openSpinnerType, 8 | spinnerEvent, 9 | } from "@/events/spinner.ts"; 10 | 11 | function Spinner() { 12 | const dispatch = useDispatch(); 13 | 14 | useEffect(() => { 15 | spinnerEvent.bind((event) => { 16 | switch (event.type) { 17 | case openSpinnerType: 18 | dispatch(increaseTask(event.id)); 19 | break; 20 | case closeSpinnerType: 21 | dispatch(decreaseTask(event.id)); 22 | break; 23 | } 24 | }); 25 | }, []); 26 | 27 | const isAnimating = useSelector(selectIsTasking); 28 | 29 | return ( 30 | 31 | {({ animationDuration, isFinished, progress }) => ( 32 |
40 | )} 41 |
42 | ); 43 | } 44 | 45 | export default Spinner; 46 | -------------------------------------------------------------------------------- /app/src/store/api.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { getKey, regenerateKey } from "@/api/addition.ts"; 3 | import { AppDispatch, RootState } from "./index.ts"; 4 | 5 | export const apiSlice = createSlice({ 6 | name: "api", 7 | initialState: { 8 | dialog: false, 9 | key: "", 10 | }, 11 | reducers: { 12 | toggleDialog: (state) => { 13 | state.dialog = !state.dialog; 14 | }, 15 | setDialog: (state, action) => { 16 | state.dialog = action.payload as boolean; 17 | }, 18 | openDialog: (state) => { 19 | state.dialog = true; 20 | }, 21 | closeDialog: (state) => { 22 | state.dialog = false; 23 | }, 24 | setKey: (state, action) => { 25 | state.key = action.payload as string; 26 | }, 27 | }, 28 | }); 29 | 30 | export const { toggleDialog, setDialog, openDialog, closeDialog, setKey } = 31 | apiSlice.actions; 32 | export default apiSlice.reducer; 33 | 34 | export const dialogSelector = (state: RootState): boolean => state.api.dialog; 35 | export const keySelector = (state: RootState): string => state.api.key; 36 | 37 | export const getApiKey = async (dispatch: AppDispatch, retries?: boolean) => { 38 | const response = await getKey(); 39 | if (response.status) { 40 | if (response.key.length === 0 && retries !== false) { 41 | await getApiKey(dispatch, false); 42 | return; 43 | } 44 | dispatch(setKey(response.key)); 45 | } 46 | }; 47 | 48 | export const regenerateApiKey = async (dispatch: AppDispatch) => { 49 | const response = await regenerateKey(); 50 | if (response.status) { 51 | dispatch(setKey(response.key)); 52 | } 53 | 54 | return response; 55 | }; 56 | -------------------------------------------------------------------------------- /app/src/store/globals.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { Plans } from "@/api/types.tsx"; 3 | import { AppDispatch, RootState } from "@/store/index.ts"; 4 | import { getOfflinePlans, setOfflinePlans } from "@/conf/storage.ts"; 5 | import { getTheme, Theme } from "@/components/ThemeProvider.tsx"; 6 | 7 | type GlobalState = { 8 | theme: Theme; 9 | subscription: Plans; 10 | }; 11 | 12 | export const globalSlice = createSlice({ 13 | name: "global", 14 | initialState: { 15 | theme: getTheme(), 16 | subscription: getOfflinePlans(), 17 | } as GlobalState, 18 | reducers: { 19 | setSubscription: (state, action) => { 20 | const plans = action.payload as Plans; 21 | state.subscription = plans; 22 | setOfflinePlans(plans); 23 | }, 24 | setTheme: (state, action) => { 25 | state.theme = action.payload; 26 | }, 27 | }, 28 | }); 29 | 30 | export const { setSubscription, setTheme } = globalSlice.actions; 31 | 32 | export default globalSlice.reducer; 33 | 34 | export const subscriptionDataSelector = (state: RootState): Plans => 35 | state.global.subscription; 36 | export const themeSelector = (state: RootState): Theme => state.global.theme; 37 | 38 | export const dispatchSubscriptionData = ( 39 | dispatch: AppDispatch, 40 | subscription: Plans, 41 | ) => { 42 | dispatch(setSubscription(subscription)); 43 | }; 44 | -------------------------------------------------------------------------------- /app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import infoReducer from "./info"; 3 | import globalReducer from "./globals"; 4 | import menuReducer from "./menu"; 5 | import authReducer from "./auth"; 6 | import chatReducer from "./chat"; 7 | import quotaReducer from "./quota"; 8 | import packageReducer from "./package"; 9 | import subscriptionReducer from "./subscription"; 10 | import apiReducer from "./api"; 11 | import sharingReducer from "./sharing"; 12 | import invitationReducer from "./invitation"; 13 | import settingsReducer from "./settings"; 14 | 15 | const store = configureStore({ 16 | reducer: { 17 | info: infoReducer, 18 | global: globalReducer, 19 | menu: menuReducer, 20 | auth: authReducer, 21 | chat: chatReducer, 22 | quota: quotaReducer, 23 | package: packageReducer, 24 | subscription: subscriptionReducer, 25 | api: apiReducer, 26 | sharing: sharingReducer, 27 | invitation: invitationReducer, 28 | settings: settingsReducer, 29 | }, 30 | }); 31 | 32 | type RootState = ReturnType; 33 | type AppDispatch = typeof store.dispatch; 34 | 35 | export type { RootState, AppDispatch }; 36 | export default store; 37 | -------------------------------------------------------------------------------- /app/src/store/invitation.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { RootState } from "./index.ts"; 3 | 4 | export const invitationSlice = createSlice({ 5 | name: "invitation", 6 | initialState: { 7 | dialog: false, 8 | }, 9 | reducers: { 10 | toggleDialog: (state) => { 11 | state.dialog = !state.dialog; 12 | }, 13 | setDialog: (state, action) => { 14 | state.dialog = action.payload as boolean; 15 | }, 16 | openDialog: (state) => { 17 | state.dialog = true; 18 | }, 19 | closeDialog: (state) => { 20 | state.dialog = false; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { toggleDialog, setDialog, openDialog, closeDialog } = 26 | invitationSlice.actions; 27 | export default invitationSlice.reducer; 28 | 29 | export const dialogSelector = (state: RootState): boolean => 30 | state.invitation.dialog; 31 | -------------------------------------------------------------------------------- /app/src/store/menu.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { mobile } from "@/utils/device.ts"; 3 | 4 | export const menuSlice = createSlice({ 5 | name: "menu", 6 | initialState: { 7 | open: !mobile, // mobile: false, desktop: true 8 | }, 9 | reducers: { 10 | toggleMenu: (state) => { 11 | state.open = !state.open; 12 | }, 13 | closeMenu: (state) => { 14 | state.open = false; 15 | }, 16 | openMenu: (state) => { 17 | state.open = true; 18 | }, 19 | setMenu: (state, action) => { 20 | state.open = action.payload as boolean; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { toggleMenu, closeMenu, openMenu, setMenu } = menuSlice.actions; 26 | export default menuSlice.reducer; 27 | 28 | export const selectMenu = (state: any) => state.menu.open; 29 | -------------------------------------------------------------------------------- /app/src/store/package.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { getPackage } from "@/api/addition.ts"; 3 | import { AppDispatch } from "./index.ts"; 4 | 5 | export const packageSlice = createSlice({ 6 | name: "package", 7 | initialState: { 8 | dialog: false, 9 | cert: false, 10 | teenager: false, 11 | }, 12 | reducers: { 13 | toggleDialog: (state) => { 14 | state.dialog = !state.dialog; 15 | }, 16 | setDialog: (state, action) => { 17 | state.dialog = action.payload as boolean; 18 | }, 19 | openDialog: (state) => { 20 | state.dialog = true; 21 | }, 22 | closeDialog: (state) => { 23 | state.dialog = false; 24 | }, 25 | refreshState: (state, action) => { 26 | state.cert = action.payload.cert; 27 | state.teenager = action.payload.teenager; 28 | }, 29 | }, 30 | }); 31 | 32 | export const { 33 | toggleDialog, 34 | setDialog, 35 | openDialog, 36 | closeDialog, 37 | refreshState, 38 | } = packageSlice.actions; 39 | export default packageSlice.reducer; 40 | 41 | export const dialogSelector = (state: any): boolean => state.package.dialog; 42 | export const certSelector = (state: any): boolean => state.package.cert; 43 | export const teenagerSelector = (state: any): boolean => state.package.teenager; 44 | 45 | export const refreshPackage = async (dispatch: AppDispatch) => { 46 | const response = await getPackage(); 47 | if (response.status) dispatch(refreshState(response)); 48 | }; 49 | -------------------------------------------------------------------------------- /app/src/store/quota.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { AppDispatch, RootState } from "./index.ts"; 3 | import { getQuota } from "@/api/quota.ts"; 4 | 5 | export const quotaSlice = createSlice({ 6 | name: "quota", 7 | initialState: { 8 | dialog: false, 9 | quota: 0, 10 | }, 11 | reducers: { 12 | toggleDialog: (state) => { 13 | state.dialog = !state.dialog; 14 | }, 15 | setDialog: (state, action) => { 16 | state.dialog = action.payload as boolean; 17 | }, 18 | openDialog: (state) => { 19 | state.dialog = true; 20 | }, 21 | closeDialog: (state) => { 22 | state.dialog = false; 23 | }, 24 | setQuota: (state, action) => { 25 | state.quota = action.payload as number; 26 | }, 27 | increaseQuota: (state, action) => { 28 | state.quota += action.payload as number; 29 | }, 30 | decreaseQuota: (state, action) => { 31 | state.quota -= action.payload as number; 32 | }, 33 | }, 34 | }); 35 | 36 | export const { 37 | toggleDialog, 38 | setDialog, 39 | openDialog, 40 | closeDialog, 41 | setQuota, 42 | increaseQuota, 43 | decreaseQuota, 44 | } = quotaSlice.actions; 45 | export default quotaSlice.reducer; 46 | 47 | export const dialogSelector = (state: RootState): boolean => state.quota.dialog; 48 | export const quotaValueSelector = (state: RootState): number => 49 | state.quota.quota; 50 | export const quotaSelector = (state: RootState): number => state.quota.quota; 51 | 52 | export const refreshQuota = async (dispatch: AppDispatch) => { 53 | const quota = await getQuota(); 54 | dispatch(setQuota(quota)); 55 | }; 56 | -------------------------------------------------------------------------------- /app/src/store/utils.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { RootState } from "./index.ts"; 3 | 4 | export function dispatchWrapper( 5 | action: (state: RootState, payload?: any) => any, 6 | ) { 7 | return (payload?: any) => { 8 | const dispatch = useDispatch(); 9 | dispatch(action(payload)); 10 | }; 11 | } 12 | 13 | export function getSelector(reducer: string, key: string) { 14 | return useSelector((state: any) => state[reducer][key]); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/translator/adapter.ts: -------------------------------------------------------------------------------- 1 | // format language code to name/ISO 639-1 code map 2 | const languageTranslatorMap: Record = { 3 | cn: "zh-CN", 4 | tw: "zh-TW", 5 | en: "en", 6 | ru: "ru", 7 | ja: "ja", 8 | ko: "ko", 9 | fr: "fr", 10 | de: "de", 11 | es: "es", 12 | pt: "pt", 13 | it: "it", 14 | }; 15 | 16 | export function getFormattedLanguage(lang: string): string { 17 | return languageTranslatorMap[lang.toLowerCase()] || lang; 18 | } 19 | 20 | type translationResponse = { 21 | responseData: { 22 | translatedText: string; 23 | }; 24 | }; 25 | 26 | async function translate( 27 | text: string, 28 | from: string, 29 | to: string, 30 | ): Promise { 31 | if (from === to || text.length === 0) return text; 32 | const resp = await fetch( 33 | `https://api.mymemory.translated.net/get?q=${encodeURIComponent( 34 | text, 35 | )}&langpair=${from}|${to}`, 36 | ); 37 | const data: translationResponse = await resp.json(); 38 | 39 | return data.responseData.translatedText; 40 | } 41 | 42 | export function doTranslate( 43 | content: string, 44 | from: string, 45 | to: string, 46 | ): Promise { 47 | from = getFormattedLanguage(from); 48 | to = getFormattedLanguage(to); 49 | 50 | if (content.startsWith("!!")) content = content.substring(2); 51 | 52 | return translate(content, from, to); 53 | } 54 | -------------------------------------------------------------------------------- /app/src/translator/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, ResolvedConfig } from "vite"; 2 | import { processTranslation } from "./translator"; 3 | 4 | export function createTranslationPlugin(): Plugin { 5 | return { 6 | name: "translate-plugin", 7 | apply: "build", 8 | async configResolved(config: ResolvedConfig) { 9 | try { 10 | console.info("[i18n] start translation process"); 11 | await processTranslation(config); 12 | } catch (e) { 13 | console.warn(`error during translation: ${e}`); 14 | } finally { 15 | console.info("[i18n] translation process finished"); 16 | } 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /app/src/types/performance.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | // see https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory 3 | 4 | interface PerformanceMemory { 5 | usedJSHeapSize: number; 6 | totalJSHeapSize: number; 7 | jsHeapSizeLimit: number; 8 | } 9 | 10 | interface Performance { 11 | memory: PerformanceMemory; 12 | } 13 | 14 | interface Window { 15 | __TAURI__: Tauri; 16 | } 17 | } 18 | 19 | export declare function getMemoryPerformance(): number; 20 | -------------------------------------------------------------------------------- /app/src/types/service.d.ts: -------------------------------------------------------------------------------- 1 | declare module "virtual:pwa-register/react" { 2 | // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error 3 | // @ts-expect-error ignore when React is not installed 4 | import type { Dispatch, SetStateAction } from "react"; 5 | import type { RegisterSWOptions } from "vite-plugin-pwa/types"; 6 | 7 | export type { RegisterSWOptions }; 8 | 9 | export function useRegisterSW(options?: RegisterSWOptions): { 10 | needRefresh: [boolean, Dispatch>]; 11 | offlineReady: [boolean, Dispatch>]; 12 | updateServiceWorker: (reloadPage?: boolean) => Promise; 13 | onRegistered: (registration: ServiceWorkerRegistration) => void; 14 | }; 15 | } 16 | 17 | interface BeforeInstallPromptEvent extends Event { 18 | readonly platforms: string[]; 19 | readonly userChoice: Promise<{ 20 | outcome: "accepted" | "dismissed"; 21 | platform: string; 22 | }>; 23 | prompt(): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /app/src/types/ui.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@radix-ui/react-select-area"; 2 | 3 | declare module "sonner" { 4 | export interface ToastProps { 5 | description: string; 6 | } 7 | 8 | export const Toaster: React.FC; 9 | 10 | export function toast(title: string, content?: ToastProps): void; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/utils/desktop.ts: -------------------------------------------------------------------------------- 1 | export function isTauri(): boolean { 2 | return window.__TAURI__ !== undefined; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/utils/dev.ts: -------------------------------------------------------------------------------- 1 | export function inWaiting(duration: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, duration); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/utils/device.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { addEventListeners } from "@/utils/dom.ts"; 3 | 4 | export let mobile = isMobile(); 5 | 6 | window.addEventListener("resize", () => { 7 | mobile = isMobile(); 8 | }); 9 | 10 | export function isMobile(): boolean { 11 | return ( 12 | (document.documentElement.clientWidth || window.innerWidth) <= 668 || 13 | (document.documentElement.clientHeight || window.innerHeight) <= 468 || 14 | navigator.userAgent.includes("Mobile") 15 | ); 16 | } 17 | 18 | export function useMobile(): boolean { 19 | const [mobile, setMobile] = useState(isMobile); 20 | 21 | useEffect(() => { 22 | const handler = () => setMobile(isMobile); 23 | 24 | return addEventListeners( 25 | window, 26 | [ 27 | "resize", 28 | "orientationchange", 29 | "touchstart", 30 | "touchmove", 31 | "touchend", 32 | "touchcancel", 33 | "gesturestart", 34 | "gesturechange", 35 | "gestureend", 36 | ], 37 | handler, 38 | ); 39 | }, []); 40 | 41 | return mobile; 42 | } 43 | 44 | export function openWindow(url: string, target?: string): void { 45 | /** 46 | * Open a new window with the given URL. 47 | * If the device does not support opening a new window, the URL will be opened in the current window. 48 | * @param url The URL to open. 49 | * @param target The target of the URL. 50 | */ 51 | 52 | if (mobile) { 53 | window.location.href = url; 54 | } else { 55 | window.open(url, target); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/utils/form.ts: -------------------------------------------------------------------------------- 1 | export function setKey(state: T, key: string, value: any): T { 2 | const segment = key.split("."); 3 | if (segment.length === 1) { 4 | return { ...state, [key]: value }; 5 | } else if (segment.length > 1) { 6 | const [k, ...v] = segment; 7 | return { ...state, [k]: setKey(state[k as keyof T], v.join("."), value) }; 8 | } 9 | 10 | // segment.length is zero 11 | throw new Error("invalid key"); 12 | } 13 | 14 | export const formReducer = () => { 15 | return (state: T, action: any): T => { 16 | action.payload = action.payload ?? action.value; 17 | 18 | switch (action.type) { 19 | case "update": 20 | return { ...state, ...action.payload } as T; 21 | case "reset": 22 | return { ...action.payload } as T; 23 | case "set": 24 | return action.payload as T; 25 | default: 26 | if (action.type.startsWith("update:")) { 27 | const key = action.type.slice(7); 28 | return setKey(state, key, action.payload) as T; 29 | } 30 | 31 | return state; 32 | } 33 | }; 34 | }; 35 | 36 | export function isEmailValid(email: string) { 37 | return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 255; 38 | } 39 | 40 | export function isInRange(value: number, min: number, max: number) { 41 | return value >= min && value <= max; 42 | } 43 | 44 | export function isTextInRange(value: string, min: number, max: number) { 45 | return value.trim().length >= min && value.trim().length <= max; 46 | } 47 | -------------------------------------------------------------------------------- /app/src/utils/groups.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { selectAdmin, selectAuthenticated } from "@/store/auth.ts"; 3 | import { levelSelector } from "@/store/subscription.ts"; 4 | import { useMemo } from "react"; 5 | 6 | export const AnonymousType = "anonymous"; 7 | export const NormalType = "normal"; 8 | export const BasicType = "basic"; 9 | export const StandardType = "standard"; 10 | export const ProType = "pro"; 11 | export const AdminType = "admin"; 12 | 13 | export const allGroups: string[] = [ 14 | AnonymousType, 15 | NormalType, 16 | BasicType, 17 | StandardType, 18 | ProType, 19 | AdminType, 20 | ]; 21 | 22 | export function useGroup(): string { 23 | const auth = useSelector(selectAuthenticated); 24 | const level = useSelector(levelSelector); 25 | 26 | return useMemo(() => { 27 | if (!auth) return AnonymousType; 28 | switch (level) { 29 | case 1: 30 | return BasicType; 31 | case 2: 32 | return StandardType; 33 | case 3: 34 | return ProType; 35 | default: 36 | return NormalType; 37 | } 38 | }, [auth, level]); 39 | } 40 | 41 | export function hitGroup(group: string[]): boolean { 42 | const current = useGroup(); 43 | const admin = useSelector(selectAdmin); 44 | 45 | return useMemo(() => { 46 | if (group.includes(AdminType) && admin) return true; 47 | return group.includes(current); 48 | }, [group, current, admin]); 49 | } 50 | -------------------------------------------------------------------------------- /app/src/utils/loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | closeSpinnerType, 4 | openSpinnerType, 5 | spinnerEvent, 6 | } from "@/events/spinner.ts"; 7 | import { generateListNumber } from "@/utils/base.ts"; 8 | 9 | export function lazyFactor>( 10 | factor: () => Promise<{ default: T }>, 11 | ): React.LazyExoticComponent { 12 | /** 13 | * Lazy load factor 14 | * @see https://reactjs.org/docs/code-splitting.html#reactlazy 15 | * 16 | * @example 17 | * lazyFactor(() => import("./factor.tsx")); 18 | */ 19 | 20 | return React.lazy(() => { 21 | return new Promise((resolve, reject) => { 22 | const task = generateListNumber(6); 23 | const id = setTimeout( 24 | () => 25 | spinnerEvent.emit({ 26 | id: task, 27 | type: openSpinnerType, 28 | }), 29 | 1000, 30 | ); 31 | 32 | factor() 33 | .then((module) => { 34 | clearTimeout(id); 35 | spinnerEvent.emit({ 36 | id: task, 37 | type: closeSpinnerType, 38 | }); 39 | resolve(module); 40 | }) 41 | .catch((error) => { 42 | console.warn(`[factor] cannot load factor: ${error}`); 43 | reject(error); 44 | }); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /app/src/utils/memory.ts: -------------------------------------------------------------------------------- 1 | export function setMemory(key: string, value: string) { 2 | const data = value.trim(); 3 | localStorage.setItem(key, data); 4 | } 5 | 6 | export function setBooleanMemory(key: string, value: boolean) { 7 | setMemory(key, String(value)); 8 | } 9 | 10 | export function setNumberMemory(key: string, value: number) { 11 | setMemory(key, value.toString()); 12 | } 13 | 14 | export function setArrayMemory(key: string, value: string[]) { 15 | setMemory(key, value.join(",")); 16 | } 17 | 18 | export function getMemory(key: string, defaultValue?: string): string { 19 | return (localStorage.getItem(key) || (defaultValue ?? "")).trim(); 20 | } 21 | 22 | export function getBooleanMemory(key: string, defaultValue: boolean): boolean { 23 | const value = getMemory(key); 24 | return value ? value === "true" : defaultValue; 25 | } 26 | 27 | export function getNumberMemory(key: string, defaultValue: number): number { 28 | const value = getMemory(key); 29 | return value ? Number(value) : defaultValue; 30 | } 31 | 32 | export function getArrayMemory(key: string): string[] { 33 | const value = getMemory(key); 34 | return value ? value.split(",") : []; 35 | } 36 | 37 | export function forgetMemory(key: string) { 38 | localStorage.removeItem(key); 39 | } 40 | 41 | export function clearMemory() { 42 | localStorage.clear(); 43 | } 44 | 45 | export function popMemory(key: string): string { 46 | const value = getMemory(key); 47 | forgetMemory(key); 48 | return value; 49 | } 50 | -------------------------------------------------------------------------------- /app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "paths": { 23 | "@/*": ["./src/*"], 24 | } 25 | }, 26 | "include": [ 27 | "src", 28 | ], 29 | "baseUrl": ".", 30 | "references": [{ "path": "./tsconfig.node.json" }], 31 | } 32 | -------------------------------------------------------------------------------- /app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | import path from "path" 4 | import { createHtmlPlugin } from 'vite-plugin-html' 5 | import { createTranslationPlugin } from "./src/translator"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | react(), 11 | createHtmlPlugin({ 12 | minify: true, 13 | }), 14 | createTranslationPlugin(), 15 | ], 16 | resolve: { 17 | alias: { 18 | "@": path.resolve(__dirname, "./src"), 19 | }, 20 | }, 21 | css: { 22 | preprocessorOptions: { 23 | less: { 24 | javascriptEnabled: true, 25 | } 26 | } 27 | }, 28 | build: { 29 | manifest: true, 30 | chunkSizeWarningLimit: 2048, 31 | rollupOptions: { 32 | output: { 33 | entryFileNames: `assets/[name].[hash].js`, 34 | chunkFileNames: `assets/[name].[hash].js`, 35 | }, 36 | }, 37 | }, 38 | server: { 39 | proxy: { 40 | "/api": { 41 | target: "http://localhost:8094", 42 | changeOrigin: true, 43 | rewrite: (path) => path.replace(/^\/api/, ""), 44 | ws: true, 45 | }, 46 | "/v1": { 47 | target: "http://localhost:8094", 48 | changeOrigin: true, 49 | } 50 | } 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /auth/analysis.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "chat/utils" 5 | "fmt" 6 | "github.com/go-redis/redis/v8" 7 | "time" 8 | ) 9 | 10 | func getMonth() string { 11 | date := time.Now() 12 | return date.Format("2006-01") 13 | } 14 | 15 | func getDay() string { 16 | date := time.Now() 17 | return date.Format("2006-01-02") 18 | } 19 | 20 | func getBillingFormat(t string) string { 21 | return fmt.Sprintf("nio:billing-analysis-%s", t) 22 | } 23 | 24 | func getMonthBillingFormat(t string) string { 25 | return fmt.Sprintf("nio:billing-analysis-%s", t) 26 | } 27 | 28 | func incrBillingRequest(cache *redis.Client, amount int64) { 29 | utils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*30*2) 30 | utils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*30*2) 31 | } 32 | -------------------------------------------------------------------------------- /auth/apikey.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "database/sql" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | func (u *User) CreateApiKey(db *sql.DB) string { 12 | salt := utils.Sha2Encrypt(fmt.Sprintf("%s-%s", u.Username, utils.GenerateChar(utils.GetRandomInt(720, 1024)))) 13 | key := fmt.Sprintf("sk-%s", salt[:64]) // 64 bytes 14 | if _, err := globals.ExecDb(db, "INSERT INTO apikey (user_id, api_key) VALUES (?, ?)", u.GetID(db), key); err != nil { 15 | return "" 16 | } 17 | return key 18 | } 19 | 20 | func (u *User) GetApiKey(db *sql.DB) string { 21 | var key string 22 | if err := globals.QueryRowDb(db, "SELECT api_key FROM apikey WHERE user_id = ?", u.GetID(db)).Scan(&key); err != nil { 23 | return u.CreateApiKey(db) 24 | } 25 | return key 26 | } 27 | 28 | func (u *User) ResetApiKey(db *sql.DB) (string, error) { 29 | if _, err := globals.ExecDb(db, "DELETE FROM apikey WHERE user_id = ?", u.GetID(db)); err != nil && !errors.Is(err, sql.ErrNoRows) { 30 | return "", err 31 | } 32 | return u.CreateApiKey(db), nil 33 | } 34 | -------------------------------------------------------------------------------- /auth/call.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "chat/utils" 5 | "github.com/goccy/go-json" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type ValidateUserResponse struct { 10 | Status bool `json:"status" required:"true"` 11 | Username string `json:"username" required:"true"` 12 | ID int `json:"id" required:"true"` 13 | } 14 | 15 | func getDeeptrainApi(path string) string { 16 | return viper.GetString("auth.endpoint") + path 17 | } 18 | 19 | func useDeeptrain() bool { 20 | return viper.GetBool("auth.use_deeptrain") 21 | } 22 | 23 | func Validate(token string) *ValidateUserResponse { 24 | res, err := utils.Post(getDeeptrainApi("/app/validate"), map[string]string{ 25 | "Content-Type": "application/json", 26 | }, map[string]interface{}{ 27 | "password": viper.GetString("auth.access"), 28 | "token": token, 29 | "hash": utils.Sha2Encrypt(token + viper.GetString("auth.salt")), 30 | }) 31 | 32 | if err != nil || res == nil || res.(map[string]interface{})["status"] == false { 33 | return nil 34 | } 35 | 36 | converter, _ := json.Marshal(res) 37 | resp, _ := utils.Unmarshal[ValidateUserResponse](converter) 38 | return &resp 39 | } 40 | -------------------------------------------------------------------------------- /auth/cert.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "chat/utils" 5 | "github.com/goccy/go-json" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type CertResponse struct { 10 | Status bool `json:"status" required:"true"` 11 | Cert bool `json:"cert"` 12 | Teenager bool `json:"teenager"` 13 | } 14 | 15 | func Cert(username string) *CertResponse { 16 | res, err := utils.Post(getDeeptrainApi("/app/cert"), map[string]string{ 17 | "Content-Type": "application/json", 18 | }, map[string]interface{}{ 19 | "password": viper.GetString("auth.access"), 20 | "user": username, 21 | "hash": utils.Sha2Encrypt(username + viper.GetString("auth.salt")), 22 | }) 23 | 24 | if err != nil || res == nil || res.(map[string]interface{})["status"] == false { 25 | return nil 26 | } 27 | 28 | converter, _ := json.Marshal(res) 29 | resp, _ := utils.Unmarshal[CertResponse](converter) 30 | return &resp 31 | } 32 | -------------------------------------------------------------------------------- /auth/router.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func Register(app *gin.RouterGroup) { 6 | app.Any("/", IndexAPI) 7 | app.POST("/verify", VerifyAPI) 8 | app.POST("/reset", ResetAPI) 9 | app.POST("/register", RegisterAPI) 10 | app.POST("/login", LoginAPI) 11 | app.POST("/state", StateAPI) 12 | app.GET("/apikey", KeyAPI) 13 | app.POST("/resetkey", ResetKeyAPI) 14 | app.GET("/package", PackageAPI) 15 | app.GET("/quota", QuotaAPI) 16 | app.POST("/buy", BuyAPI) 17 | app.GET("/subscription", SubscriptionAPI) 18 | app.POST("/subscribe", SubscribeAPI) 19 | app.GET("/invite", InviteAPI) 20 | app.GET("/redeem", RedeemAPI) 21 | } 22 | -------------------------------------------------------------------------------- /auth/usage.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "chat/channel" 5 | "database/sql" 6 | "github.com/go-redis/redis/v8" 7 | ) 8 | 9 | func (u *User) GetSubscriptionUsage(db *sql.DB, cache *redis.Client) channel.UsageMap { 10 | plan := u.GetPlan(db) 11 | return plan.GetUsage(u, db, cache) 12 | } 13 | -------------------------------------------------------------------------------- /auth/validators.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | func isInRange(content string, min, max int) bool { 9 | content = strings.TrimSpace(content) 10 | return len(content) >= min && len(content) <= max 11 | } 12 | 13 | func validateUsername(username string) bool { 14 | return isInRange(username, 2, 24) 15 | } 16 | 17 | func validateUsernameOrEmail(username string) bool { 18 | return isInRange(username, 1, 255) 19 | } 20 | 21 | func validatePassword(password string) bool { 22 | return isInRange(password, 6, 36) 23 | } 24 | 25 | func validateEmail(email string) bool { 26 | if !isInRange(email, 1, 255) { 27 | return false 28 | } 29 | 30 | exp := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) 31 | return exp.MatchString(email) 32 | } 33 | 34 | func validateCode(code string) bool { 35 | return isInRange(code, 1, 64) 36 | } 37 | -------------------------------------------------------------------------------- /channel/router.go: -------------------------------------------------------------------------------- 1 | package channel 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func Register(app *gin.RouterGroup) { 6 | app.GET("/info", GetInfo) 7 | app.GET("/attachments/:hash", AttachmentService) 8 | 9 | app.GET("/admin/channel/list", GetChannelList) 10 | app.POST("/admin/channel/create", CreateChannel) 11 | app.GET("/admin/channel/get/:id", GetChannel) 12 | app.POST("/admin/channel/update/:id", UpdateChannel) 13 | app.GET("/admin/channel/delete/:id", DeleteChannel) 14 | app.GET("/admin/channel/activate/:id", ActivateChannel) 15 | app.GET("/admin/channel/deactivate/:id", DeactivateChannel) 16 | 17 | app.GET("/admin/charge/list", GetChargeList) 18 | app.POST("/admin/charge/set", SetCharge) 19 | app.GET("/admin/charge/delete/:id", DeleteCharge) 20 | app.POST("/admin/charge/sync", SyncCharge) 21 | 22 | app.GET("/admin/config/view", GetConfig) 23 | app.POST("/admin/config/update", UpdateConfig) 24 | 25 | app.GET("/admin/plan/view", GetPlanConfig) 26 | app.POST("/admin/plan/update", UpdatePlanConfig) 27 | } 28 | -------------------------------------------------------------------------------- /channel/sequence.go: -------------------------------------------------------------------------------- 1 | package channel 2 | 3 | import "sort" 4 | 5 | func (s *Sequence) Len() int { 6 | return len(*s) 7 | } 8 | 9 | func (s *Sequence) Less(i, j int) bool { 10 | return (*s)[i].GetPriority() > (*s)[j].GetPriority() 11 | } 12 | 13 | func (s *Sequence) Swap(i, j int) { 14 | (*s)[i], (*s)[j] = (*s)[j], (*s)[i] 15 | } 16 | 17 | func (s *Sequence) GetChannelById(id int) *Channel { 18 | for _, channel := range *s { 19 | if channel.Id == id { 20 | return channel 21 | } 22 | } 23 | return nil 24 | } 25 | 26 | func (s *Sequence) Sort() { 27 | // sort by priority 28 | sort.Sort(s) 29 | } 30 | -------------------------------------------------------------------------------- /cli/admin.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "chat/admin" 5 | "chat/connection" 6 | "errors" 7 | ) 8 | 9 | func UpdateRootCommand(args []string) { 10 | db := connection.ConnectDatabase() 11 | cache := connection.ConnectRedis() 12 | 13 | if len(args) == 0 { 14 | outputError(errors.New("invalid arguments, please provide a new root password")) 15 | return 16 | } 17 | 18 | password := args[0] 19 | if err := admin.UpdateRootPassword(db, cache, password); err != nil { 20 | outputError(err) 21 | return 22 | } 23 | 24 | outputInfo("root", "root password updated") 25 | } 26 | -------------------------------------------------------------------------------- /cli/exec.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | func Run() bool { 4 | args := GetArgs() 5 | if len(args) == 0 { 6 | return false 7 | } 8 | 9 | param := args[1:] 10 | switch args[0] { 11 | case "help": 12 | Help() 13 | case "invite": 14 | CreateInvitationCommand(param) 15 | case "token": 16 | CreateTokenCommand(param) 17 | case "root": 18 | UpdateRootCommand(param) 19 | default: 20 | return false 21 | } 22 | 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /cli/help.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "fmt" 4 | 5 | var Prompt = ` 6 | Commands: 7 | - help 8 | - invite 9 | - token 10 | - root 11 | ` 12 | 13 | func Help() { 14 | fmt.Println(fmt.Sprintf("%s", Prompt)) 15 | } 16 | -------------------------------------------------------------------------------- /cli/invite.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "chat/auth" 5 | "chat/connection" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | func CreateInvitationCommand(args []string) { 11 | db := connection.ConnectDatabase() 12 | 13 | var ( 14 | t = GetArgString(args, 0) 15 | num = GetArgInt(args, 1) 16 | quota = GetArgFloat32(args, 2) 17 | ) 18 | 19 | resp, err := auth.GenerateInvitations(db, num, quota, t) 20 | if err != nil { 21 | outputError(err) 22 | return 23 | } 24 | 25 | outputInfo("invite", fmt.Sprintf("%d invitation codes generated", len(resp))) 26 | fmt.Println(strings.Join(resp, "\n")) 27 | } 28 | -------------------------------------------------------------------------------- /cli/token.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "chat/auth" 5 | "chat/connection" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | func CreateTokenCommand(args []string) { 11 | db := connection.ConnectDatabase() 12 | id, _ := strconv.Atoi(args[0]) 13 | 14 | user := auth.GetUserById(db, int64(id)) 15 | token, err := user.GenerateTokenSafe(db) 16 | if err != nil { 17 | outputError(err) 18 | return 19 | } 20 | 21 | outputInfo("token", "token generated") 22 | fmt.Println(token) 23 | } 24 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | mysql: 2 | db: chatnio 3 | host: localhost 4 | password: chatnio123456 5 | port: 3306 6 | user: root 7 | tls: false 8 | 9 | redis: 10 | host: localhost 11 | port: 6379 12 | db: 0 13 | password: "" 14 | 15 | 16 | secret: secret 17 | serve_static: true 18 | 19 | server: 20 | port: 8094 21 | system: 22 | general: 23 | backend: "" 24 | mail: 25 | host: "" 26 | port: 465 27 | username: "" 28 | password: "" 29 | from: "" 30 | search: 31 | endpoint: https://duckduckgo-api.vercel.app 32 | query: 5 33 | -------------------------------------------------------------------------------- /connection/cache.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "chat/globals" 5 | "context" 6 | "fmt" 7 | "github.com/go-redis/redis/v8" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var Cache *redis.Client 12 | 13 | func InitRedisSafe() *redis.Client { 14 | ConnectRedis() 15 | 16 | // using Cache as a global variable to point to the latest redis connection 17 | RedisWorker(Cache) 18 | return Cache 19 | } 20 | 21 | func ConnectRedis() *redis.Client { 22 | // connect to redis 23 | Cache = redis.NewClient(&redis.Options{ 24 | Addr: fmt.Sprintf("%s:%d", viper.GetString("redis.host"), viper.GetInt("redis.port")), 25 | Password: viper.GetString("redis.password"), 26 | DB: viper.GetInt("redis.db"), 27 | }) 28 | 29 | if err := pingRedis(Cache); err != nil { 30 | globals.Warn( 31 | fmt.Sprintf( 32 | "[connection] failed to connect to redis host: %s (message: %s), will retry in 5 seconds", 33 | viper.GetString("redis.host"), 34 | err.Error(), 35 | ), 36 | ) 37 | } else { 38 | globals.Debug(fmt.Sprintf("[connection] connected to redis (host: %s)", viper.GetString("redis.host"))) 39 | } 40 | 41 | if viper.GetBool("debug") { 42 | Cache.FlushAll(context.Background()) 43 | globals.Debug(fmt.Sprintf("[connection] flush redis cache (host: %s)", viper.GetString("redis.host"))) 44 | } 45 | return Cache 46 | } 47 | -------------------------------------------------------------------------------- /connection/db_migration.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "chat/globals" 5 | "database/sql" 6 | "strings" 7 | ) 8 | 9 | func validSqlError(err error) bool { 10 | if err == nil { 11 | return false 12 | } 13 | 14 | content := err.Error() 15 | 16 | // Error 1060: Duplicate column name 17 | // Error 1050: Table already exists 18 | 19 | return !(strings.Contains(content, "Error 1060") || strings.Contains(content, "Error 1050")) 20 | } 21 | 22 | func checkSqlError(_ sql.Result, err error) error { 23 | if validSqlError(err) { 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func execSql(db *sql.DB, sql string, args ...interface{}) error { 31 | return checkSqlError(globals.ExecDb(db, sql, args...)) 32 | } 33 | 34 | func doMigration(db *sql.DB) error { 35 | if globals.SqliteEngine { 36 | return doSqliteMigration(db) 37 | } 38 | 39 | // v3.10 migration 40 | 41 | // update `quota`, `used` field in `quota` table 42 | // migrate `DECIMAL(16, 4)` to `DECIMAL(24, 6)` 43 | 44 | if err := execSql(db, ` 45 | ALTER TABLE quota 46 | MODIFY COLUMN quota DECIMAL(24, 6), 47 | MODIFY COLUMN used DECIMAL(24, 6); 48 | `); err != nil { 49 | return err 50 | } 51 | 52 | // add new field `is_banned` in `auth` table 53 | if err := execSql(db, ` 54 | ALTER TABLE auth 55 | ADD COLUMN is_banned BOOLEAN DEFAULT FALSE; 56 | `); err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func doSqliteMigration(db *sql.DB) error { 64 | // v3.10 added sqlite support, no migration needed before this version 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /connection/worker.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/go-redis/redis/v8" 6 | "time" 7 | ) 8 | 9 | var tick time.Duration = 5 * time.Second // tick every 5 second 10 | 11 | func MysqlWorker(db *sql.DB) { 12 | go func() { 13 | for { 14 | if db == nil || db.Ping() != nil { 15 | db = ConnectDatabase() 16 | } 17 | 18 | time.Sleep(tick) 19 | } 20 | }() 21 | } 22 | 23 | func pingRedis(client *redis.Client) error { 24 | if _, err := client.Ping(client.Context()).Result(); err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func RedisWorker(cache *redis.Client) { 31 | go func() { 32 | for { 33 | if cache == nil || pingRedis(cache) != nil { 34 | cache = ConnectRedis() 35 | } 36 | 37 | time.Sleep(tick) 38 | } 39 | }() 40 | } 41 | -------------------------------------------------------------------------------- /docker-compose.stable.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mysql: 4 | image: mysql:latest 5 | container_name: db 6 | restart: always 7 | environment: 8 | MYSQL_ROOT_PASSWORD: root 9 | MYSQL_DATABASE: chatnio 10 | MYSQL_USER: chatnio 11 | MYSQL_PASSWORD: chatnio123456! 12 | TZ: Asia/Shanghai 13 | expose: 14 | - "3306" 15 | volumes: 16 | - ./db:/var/lib/mysql 17 | networks: 18 | - chatnio-network 19 | 20 | redis: 21 | image: redis:latest 22 | container_name: redis 23 | restart: always 24 | expose: 25 | - "6379" 26 | volumes: 27 | - ./redis:/data 28 | networks: 29 | - chatnio-network 30 | 31 | chatnio: 32 | image: programzmh/chatnio:stable 33 | container_name: chatnio 34 | restart: always 35 | ports: 36 | - "8000:8094" 37 | depends_on: 38 | - mysql 39 | - redis 40 | links: 41 | - mysql 42 | - redis 43 | ulimits: 44 | nofile: 45 | soft: 65535 46 | hard: 65535 47 | environment: 48 | MYSQL_HOST: mysql 49 | MYSQL_USER: chatnio 50 | MYSQL_PASSWORD: chatnio123456! 51 | MYSQL_DB: chatnio 52 | REDIS_HOST: redis 53 | REDIS_PORT: 6379 54 | REDIS_PASSWORD: "" 55 | REDIS_DB: 0 56 | SERVE_STATIC: "true" 57 | volumes: 58 | - ./config:/config 59 | - ./logs:/logs 60 | - ./storage:/storage 61 | networks: 62 | - chatnio-network 63 | 64 | networks: 65 | chatnio-network: 66 | driver: bridge 67 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mysql: 4 | image: mysql:latest 5 | container_name: db 6 | restart: always 7 | environment: 8 | MYSQL_ROOT_PASSWORD: root 9 | MYSQL_DATABASE: chatnio 10 | MYSQL_USER: chatnio 11 | MYSQL_PASSWORD: chatnio123456! 12 | TZ: Asia/Shanghai 13 | expose: 14 | - "3306" 15 | volumes: 16 | - ./db:/var/lib/mysql 17 | networks: 18 | - chatnio-network 19 | 20 | redis: 21 | image: redis:latest 22 | container_name: redis 23 | restart: always 24 | expose: 25 | - "6379" 26 | volumes: 27 | - ./redis:/data 28 | networks: 29 | - chatnio-network 30 | 31 | chatnio: 32 | image: programzmh/chatnio 33 | container_name: chatnio 34 | restart: always 35 | ports: 36 | - "8000:8094" 37 | depends_on: 38 | - mysql 39 | - redis 40 | links: 41 | - mysql 42 | - redis 43 | ulimits: 44 | nofile: 45 | soft: 65535 46 | hard: 65535 47 | environment: 48 | MYSQL_HOST: mysql 49 | MYSQL_USER: chatnio 50 | MYSQL_PASSWORD: chatnio123456! 51 | MYSQL_DB: chatnio 52 | REDIS_HOST: redis 53 | REDIS_PORT: 6379 54 | REDIS_PASSWORD: "" 55 | REDIS_DB: 0 56 | SERVE_STATIC: "true" 57 | volumes: 58 | - ./config:/config 59 | - ./logs:/logs 60 | - ./storage:/storage 61 | networks: 62 | - chatnio-network 63 | 64 | networks: 65 | chatnio-network: 66 | driver: bridge 67 | -------------------------------------------------------------------------------- /globals/constant.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | const ( 4 | System = "system" 5 | User = "user" 6 | Assistant = "assistant" 7 | Tool = "tool" 8 | Function = "function" 9 | ) 10 | 11 | const ( 12 | OpenAIChannelType = "openai" 13 | AzureOpenAIChannelType = "azure" 14 | ClaudeChannelType = "claude" 15 | SlackChannelType = "slack" 16 | SparkdeskChannelType = "sparkdesk" 17 | ChatGLMChannelType = "chatglm" 18 | HunyuanChannelType = "hunyuan" 19 | QwenChannelType = "qwen" 20 | ZhinaoChannelType = "zhinao" 21 | BaichuanChannelType = "baichuan" 22 | SkylarkChannelType = "skylark" 23 | BingChannelType = "bing" 24 | PalmChannelType = "palm" 25 | MidjourneyChannelType = "midjourney" 26 | MoonshotChannelType = "moonshot" 27 | GroqChannelType = "groq" 28 | DeepseekChannelType = "deepseek" 29 | DifyChannelType = "dify" 30 | CozeChannelType = "coze" 31 | ) 32 | 33 | const ( 34 | NonBilling = "non-billing" 35 | TimesBilling = "times-billing" 36 | TokenBilling = "token-billing" 37 | ) 38 | 39 | const ( 40 | AnonymousType = "anonymous" 41 | NormalType = "normal" 42 | BasicType = "basic" // basic subscription 43 | StandardType = "standard" // standard subscription 44 | ProType = "pro" // pro subscription 45 | AdminType = "admin" 46 | ) 47 | 48 | const ( 49 | NoneProxyType = iota 50 | HttpProxyType 51 | HttpsProxyType 52 | Socks5ProxyType 53 | ) 54 | 55 | const ( 56 | WebTokenType = "web" 57 | ApiTokenType = "api" 58 | SystemToken = "system" 59 | ) 60 | -------------------------------------------------------------------------------- /globals/interface.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | import "database/sql" 4 | 5 | type ChannelConfig interface { 6 | GetType() string 7 | GetModelReflect(model string) string 8 | GetRetry() int 9 | GetRandomSecret() string 10 | SplitRandomSecret(num int) []string 11 | GetEndpoint() string 12 | ProcessError(err error) error 13 | GetId() int 14 | GetProxy() ProxyConfig 15 | } 16 | 17 | type AuthLike interface { 18 | GetID(db *sql.DB) int64 19 | HitID() int64 20 | } 21 | -------------------------------------------------------------------------------- /globals/logger.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/natefinch/lumberjack" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | const DefaultLoggerFile = "chatnio.log" 13 | 14 | var Logger *logrus.Logger 15 | 16 | type AppLogger struct { 17 | *logrus.Logger 18 | } 19 | 20 | func (l *AppLogger) Format(entry *logrus.Entry) ([]byte, error) { 21 | data := fmt.Sprintf( 22 | "[%s] - [%s] - %s\n", 23 | strings.ToUpper(entry.Level.String()), 24 | entry.Time.Format("2006-01-02 15:04:05"), 25 | entry.Message, 26 | ) 27 | 28 | if !viper.GetBool("log.ignore_console") { 29 | fmt.Print(data) 30 | } 31 | 32 | return []byte(data), nil 33 | } 34 | 35 | func init() { 36 | Logger = logrus.New() 37 | Logger.SetFormatter(&AppLogger{ 38 | Logger: Logger, 39 | }) 40 | 41 | Logger.SetOutput(&lumberjack.Logger{ 42 | Filename: fmt.Sprintf("logs/%s", DefaultLoggerFile), 43 | MaxSize: 1, 44 | MaxBackups: 500, 45 | MaxAge: 21, // 3 weeks 46 | }) 47 | 48 | Logger.SetLevel(logrus.DebugLevel) 49 | } 50 | 51 | func Output(args ...interface{}) { 52 | Logger.Println(args...) 53 | } 54 | 55 | func Debug(args ...interface{}) { 56 | Logger.Debugln(args...) 57 | } 58 | 59 | func Info(args ...interface{}) { 60 | Logger.Infoln(args...) 61 | } 62 | 63 | func Warn(args ...interface{}) { 64 | Logger.Warnln(args...) 65 | } 66 | 67 | func Error(args ...interface{}) { 68 | Logger.Errorln(args...) 69 | } 70 | 71 | func Fatal(args ...interface{}) { 72 | Logger.Fatalln(args...) 73 | } 74 | 75 | func Panic(args ...interface{}) { 76 | Logger.Panicln(args...) 77 | } 78 | -------------------------------------------------------------------------------- /globals/method.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | func (c *Chunk) IsEmpty() bool { 4 | return len(c.Content) == 0 && c.ToolCall == nil && c.FunctionCall == nil 5 | } 6 | -------------------------------------------------------------------------------- /globals/params.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | var V1ListModels ListModels 4 | var SupportModels []string 5 | -------------------------------------------------------------------------------- /globals/usage.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func GetSubscriptionLimitFormat(t string, id int64) string { 8 | return fmt.Sprintf("usage-%s:%d", t, id) 9 | } 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "chat/adapter" 5 | "chat/addition" 6 | "chat/admin" 7 | "chat/auth" 8 | "chat/channel" 9 | "chat/cli" 10 | "chat/globals" 11 | "chat/manager" 12 | "chat/manager/conversation" 13 | "chat/middleware" 14 | "chat/utils" 15 | "fmt" 16 | "github.com/gin-gonic/gin" 17 | "github.com/spf13/viper" 18 | "net/url" 19 | ) 20 | 21 | func readCorsOrigins() { 22 | origins := viper.GetStringSlice("allow_origins") 23 | if len(origins) > 0 { 24 | globals.AllowedOrigins = utils.Each(origins, func(origin string) string { 25 | // remove protocol and trailing slash 26 | // e.g. https://chatnio.net/ -> chatnio.net 27 | 28 | if host, err := url.Parse(origin); err == nil { 29 | return host.Host 30 | } 31 | 32 | return origin 33 | }) 34 | } 35 | } 36 | 37 | func registerApiRouter(engine *gin.Engine) { 38 | var app *gin.RouterGroup 39 | if !viper.GetBool("serve_static") { 40 | app = engine.Group("") 41 | } else { 42 | app = engine.Group("/api") 43 | } 44 | 45 | { 46 | auth.Register(app) 47 | admin.Register(app) 48 | adapter.Register(app) 49 | manager.Register(app) 50 | addition.Register(app) 51 | conversation.Register(app) 52 | } 53 | } 54 | 55 | func main() { 56 | utils.ReadConf() 57 | admin.InitInstance() 58 | channel.InitManager() 59 | 60 | if cli.Run() { 61 | return 62 | } 63 | 64 | app := utils.NewEngine() 65 | worker := middleware.RegisterMiddleware(app) 66 | defer worker() 67 | 68 | utils.RegisterStaticRoute(app) 69 | registerApiRouter(app) 70 | readCorsOrigins() 71 | 72 | if err := app.Run(fmt.Sprintf(":%s", viper.GetString("server.port"))); err != nil { 73 | panic(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /manager/broadcast/controller.go: -------------------------------------------------------------------------------- 1 | package broadcast 2 | 3 | import ( 4 | "chat/auth" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func ViewBroadcastAPI(c *gin.Context) { 10 | c.JSON(http.StatusOK, getLatestBroadcast(c)) 11 | } 12 | 13 | func CreateBroadcastAPI(c *gin.Context) { 14 | user := auth.RequireAdmin(c) 15 | if user == nil { 16 | return 17 | } 18 | 19 | var form createRequest 20 | if err := c.ShouldBindJSON(&form); err != nil { 21 | c.JSON(http.StatusOK, createResponse{ 22 | Status: false, 23 | Error: err.Error(), 24 | }) 25 | } 26 | 27 | err := createBroadcast(c, user, form.Content) 28 | if err != nil { 29 | c.JSON(http.StatusOK, createResponse{ 30 | Status: false, 31 | Error: err.Error(), 32 | }) 33 | return 34 | } 35 | 36 | c.JSON(http.StatusOK, createResponse{ 37 | Status: true, 38 | }) 39 | } 40 | 41 | func GetBroadcastListAPI(c *gin.Context) { 42 | user := auth.RequireAdmin(c) 43 | if user == nil { 44 | return 45 | } 46 | 47 | data, err := getBroadcastList(c) 48 | if err != nil { 49 | c.JSON(http.StatusOK, listResponse{ 50 | Data: []Info{}, 51 | }) 52 | return 53 | } 54 | 55 | c.JSON(http.StatusOK, listResponse{ 56 | Data: data, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /manager/broadcast/manage.go: -------------------------------------------------------------------------------- 1 | package broadcast 2 | 3 | import ( 4 | "chat/auth" 5 | "chat/globals" 6 | "chat/utils" 7 | "context" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func createBroadcast(c *gin.Context, user *auth.User, content string) error { 12 | db := utils.GetDBFromContext(c) 13 | cache := utils.GetCacheFromContext(c) 14 | 15 | if _, err := globals.ExecDb(db, `INSERT INTO broadcast (poster_id, content) VALUES (?, ?)`, user.GetID(db), content); err != nil { 16 | return err 17 | } 18 | 19 | cache.Del(context.Background(), ":broadcast") 20 | 21 | return nil 22 | } 23 | 24 | func getBroadcastList(c *gin.Context) ([]Info, error) { 25 | db := utils.GetDBFromContext(c) 26 | 27 | var broadcastList []Info 28 | rows, err := globals.QueryDb(db, ` 29 | SELECT broadcast.id, broadcast.content, auth.username, broadcast.created_at 30 | FROM broadcast 31 | INNER JOIN auth ON broadcast.poster_id = auth.id 32 | ORDER BY broadcast.id DESC 33 | `) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | for rows.Next() { 39 | var broadcast Info 40 | var createdAt []uint8 41 | if err := rows.Scan(&broadcast.Index, &broadcast.Content, &broadcast.Poster, &createdAt); err != nil { 42 | return nil, err 43 | } 44 | broadcast.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05") 45 | broadcastList = append(broadcastList, broadcast) 46 | } 47 | 48 | return broadcastList, nil 49 | } 50 | -------------------------------------------------------------------------------- /manager/broadcast/router.go: -------------------------------------------------------------------------------- 1 | package broadcast 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func Register(app *gin.RouterGroup) { 6 | app.GET("/broadcast/view", ViewBroadcastAPI) 7 | app.GET("/broadcast/list", GetBroadcastListAPI) 8 | app.POST("/broadcast/create", CreateBroadcastAPI) 9 | } 10 | -------------------------------------------------------------------------------- /manager/broadcast/types.go: -------------------------------------------------------------------------------- 1 | package broadcast 2 | 3 | type Broadcast struct { 4 | Index int `json:"index"` 5 | Content string `json:"content"` 6 | } 7 | 8 | type Info struct { 9 | Index int `json:"index"` 10 | Content string `json:"content"` 11 | Poster string `json:"poster"` 12 | CreatedAt string `json:"created_at"` 13 | } 14 | 15 | type listResponse struct { 16 | Data []Info `json:"data"` 17 | } 18 | 19 | type createRequest struct { 20 | Content string `json:"content"` 21 | } 22 | 23 | type createResponse struct { 24 | Status bool `json:"status"` 25 | Error string `json:"error"` 26 | } 27 | -------------------------------------------------------------------------------- /manager/broadcast/view.go: -------------------------------------------------------------------------------- 1 | package broadcast 2 | 3 | import ( 4 | "chat/globals" 5 | "chat/utils" 6 | "context" 7 | "github.com/gin-gonic/gin" 8 | "time" 9 | ) 10 | 11 | func getLatestBroadcast(c *gin.Context) *Broadcast { 12 | db := utils.GetDBFromContext(c) 13 | cache := utils.GetCacheFromContext(c) 14 | 15 | if data, err := cache.Get(context.Background(), ":broadcast").Result(); err == nil { 16 | if broadcast := utils.UnmarshalForm[Broadcast](data); broadcast != nil { 17 | return broadcast 18 | } 19 | } 20 | 21 | var broadcast Broadcast 22 | if err := globals.QueryRowDb(db, ` 23 | SELECT id, content FROM broadcast ORDER BY id DESC LIMIT 1; 24 | `).Scan(&broadcast.Index, &broadcast.Content); err != nil { 25 | return nil 26 | } 27 | 28 | cache.Set(context.Background(), ":broadcast", utils.Marshal(broadcast), 10*time.Minute) 29 | return &broadcast 30 | } 31 | -------------------------------------------------------------------------------- /manager/completions.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | adaptercommon "chat/adapter/common" 5 | "chat/addition/web" 6 | "chat/admin" 7 | "chat/auth" 8 | "chat/channel" 9 | "chat/globals" 10 | "chat/utils" 11 | "fmt" 12 | "runtime/debug" 13 | 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func NativeChatHandler(c *gin.Context, user *auth.User, model string, message []globals.Message, enableWeb bool) (string, float32) { 18 | defer func() { 19 | if err := recover(); err != nil { 20 | stack := debug.Stack() 21 | globals.Warn(fmt.Sprintf("caught panic from chat handler: %s (instance: %s, client: %s)\n%s", 22 | err, model, c.ClientIP(), stack, 23 | )) 24 | } 25 | }() 26 | 27 | segment := web.ToSearched(enableWeb, message) 28 | 29 | db := utils.GetDBFromContext(c) 30 | cache := utils.GetCacheFromContext(c) 31 | check, plan := auth.CanEnableModelWithSubscription(db, cache, user, model, segment) 32 | 33 | if check != nil { 34 | return check.Error(), 0 35 | } 36 | 37 | buffer := utils.NewBuffer(model, segment, channel.ChargeInstance.GetCharge(model)) 38 | hit, err := channel.NewChatRequestWithCache( 39 | cache, buffer, 40 | auth.GetGroup(db, user), 41 | adaptercommon.CreateChatProps(&adaptercommon.ChatProps{ 42 | Model: model, 43 | Message: segment, 44 | }, buffer), 45 | func(resp *globals.Chunk) error { 46 | buffer.WriteChunk(resp) 47 | return nil 48 | }, 49 | ) 50 | 51 | admin.AnalyseRequest(model, buffer, err) 52 | if err != nil { 53 | auth.RevertSubscriptionUsage(db, cache, user, model) 54 | return err.Error(), 0 55 | } 56 | 57 | if !hit { 58 | CollectQuota(c, user, buffer, plan, err) 59 | } 60 | 61 | return buffer.ReadWithDefault(defaultMessage), buffer.GetQuota() 62 | } 63 | -------------------------------------------------------------------------------- /manager/conversation/router.go: -------------------------------------------------------------------------------- 1 | package conversation 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func Register(app *gin.RouterGroup) { 6 | router := app.Group("/conversation") 7 | { 8 | router.GET("/list", ListAPI) 9 | router.GET("/load", LoadAPI) 10 | router.POST("/rename", RenameAPI) 11 | router.GET("/delete", DeleteAPI) 12 | router.GET("/clean", CleanAPI) 13 | 14 | // share 15 | router.POST("/share", ShareAPI) 16 | router.GET("/view", ViewAPI) 17 | router.GET("/share/list", ListSharingAPI) 18 | router.GET("/share/delete", DeleteSharingAPI) 19 | 20 | router.GET("/mask/view", LoadMaskAPI) 21 | router.POST("/mask/save", SaveMaskAPI) 22 | router.POST("/mask/delete", DeleteMaskAPI) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /manager/relay.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "chat/admin" 5 | "chat/channel" 6 | "chat/globals" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | func ModelAPI(c *gin.Context) { 12 | c.JSON(http.StatusOK, globals.V1ListModels) 13 | } 14 | 15 | func MarketAPI(c *gin.Context) { 16 | c.JSON(http.StatusOK, admin.MarketInstance.GetModels()) 17 | } 18 | 19 | func ChargeAPI(c *gin.Context) { 20 | c.JSON(http.StatusOK, channel.ChargeInstance.ListRules()) 21 | } 22 | 23 | func PlanAPI(c *gin.Context) { 24 | c.JSON(http.StatusOK, channel.PlanInstance.GetPlans()) 25 | } 26 | 27 | func sendErrorResponse(c *gin.Context, err error, types ...string) { 28 | var errType string 29 | if len(types) > 0 { 30 | errType = types[0] 31 | } else { 32 | errType = "chatnio_api_error" 33 | } 34 | 35 | c.JSON(http.StatusServiceUnavailable, RelayErrorResponse{ 36 | Error: TranshipmentError{ 37 | Message: err.Error(), 38 | Type: errType, 39 | }, 40 | }) 41 | } 42 | 43 | func abortWithErrorResponse(c *gin.Context, err error, types ...string) { 44 | sendErrorResponse(c, err, types...) 45 | c.Abort() 46 | } 47 | -------------------------------------------------------------------------------- /manager/router.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "chat/manager/broadcast" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func Register(app *gin.RouterGroup) { 9 | app.GET("/chat", ChatAPI) 10 | app.GET("/v1/models", ModelAPI) 11 | app.GET("/v1/market", MarketAPI) 12 | app.GET("/v1/charge", ChargeAPI) 13 | app.GET("/v1/plans", PlanAPI) 14 | app.GET("/dashboard/billing/usage", GetBillingUsage) 15 | app.GET("/dashboard/billing/subscription", GetSubscription) 16 | app.POST("/v1/chat/completions", ChatRelayAPI) 17 | app.POST("/v1/images/generations", ImagesRelayAPI) 18 | 19 | broadcast.Register(app) 20 | } 21 | -------------------------------------------------------------------------------- /manager/usage.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "chat/auth" 5 | "chat/utils" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | type BillingResponse struct { 11 | Object string `json:"object"` 12 | TotalUsage float32 `json:"total_usage"` 13 | } 14 | 15 | type SubscriptionResponse struct { 16 | Object string `json:"object"` 17 | SoftLimit int64 `json:"soft_limit"` 18 | HardLimit int64 `json:"hard_limit"` 19 | SystemHardLimit int64 `json:"system_hard_limit"` 20 | SoftLimitUSD float32 `json:"soft_limit_usd"` 21 | HardLimitUSD float32 `json:"hard_limit_usd"` 22 | SystemHardLimitUSD float32 `json:"system_hard_limit_usd"` 23 | } 24 | 25 | func GetBillingUsage(c *gin.Context) { 26 | user := auth.RequireAuth(c) 27 | if user == nil { 28 | return 29 | } 30 | 31 | db := utils.GetDBFromContext(c) 32 | usage := user.GetUsedQuota(db) 33 | 34 | c.JSON(http.StatusOK, BillingResponse{ 35 | Object: "list", 36 | TotalUsage: usage, 37 | }) 38 | } 39 | 40 | func GetSubscription(c *gin.Context) { 41 | user := auth.RequireAuth(c) 42 | if user == nil { 43 | return 44 | } 45 | 46 | db := utils.GetDBFromContext(c) 47 | quota := user.GetQuota(db) 48 | used := user.GetUsedQuota(db) 49 | total := quota + used 50 | 51 | c.JSON(http.StatusOK, SubscriptionResponse{ 52 | Object: "billing_subscription", 53 | SoftLimit: int64(quota * 100), 54 | HardLimit: int64(total * 100), 55 | SystemHardLimit: 100000000, 56 | SoftLimitUSD: quota / 7.3 / 10, 57 | HardLimitUSD: total / 7.3 / 10, 58 | SystemHardLimitUSD: 1000000, 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /middleware/builtins.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-redis/redis/v8" 7 | ) 8 | 9 | func BuiltinMiddleWare(db *sql.DB, cache *redis.Client) gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | c.Set("db", db) 12 | c.Set("cache", cache) 13 | c.Next() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "chat/globals" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | ) 8 | 9 | func CORSMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | origin := c.Request.Header.Get("Origin") 12 | if globals.OriginIsOpen(c) || globals.OriginIsAllowed(origin) { 13 | c.Writer.Header().Set("Access-Control-Allow-Origin", origin) 14 | c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 15 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Auth-Token, X-Requested-With, X-Forwarded-For, X-Real-IP, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port") 16 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 17 | 18 | if c.Request.Method == "OPTIONS" { 19 | c.Writer.Header().Set("Access-Control-Max-Age", "7200") 20 | c.AbortWithStatus(http.StatusOK) 21 | return 22 | } 23 | } 24 | 25 | c.Next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "chat/connection" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func RegisterMiddleware(app *gin.Engine) func() { 9 | db := connection.InitMySQLSafe() 10 | cache := connection.InitRedisSafe() 11 | 12 | app.Use(CORSMiddleware()) 13 | app.Use(BuiltinMiddleWare(db, cache)) 14 | app.Use(ThrottleMiddleware()) 15 | app.Use(AuthMiddleware()) 16 | 17 | return func() { 18 | db.Close() 19 | cache.Close() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migration/3.6.sql: -------------------------------------------------------------------------------- 1 | DELIMITER $$ 2 | 3 | CREATE PROCEDURE Migration() 4 | BEGIN 5 | DECLARE _count INT; 6 | 7 | SELECT COUNT(*) INTO _count 8 | FROM INFORMATION_SCHEMA.COLUMNS 9 | WHERE TABLE_SCHEMA = DATABASE() 10 | AND TABLE_NAME = 'subscription' 11 | AND COLUMN_NAME = 'level'; 12 | 13 | IF _count = 0 THEN 14 | ALTER TABLE subscription ADD COLUMN level INT DEFAULT 1; 15 | UPDATE subscription SET level = 3; 16 | END IF; 17 | 18 | END $$ 19 | 20 | DELIMITER ; 21 | 22 | CALL Migration(); 23 | -------------------------------------------------------------------------------- /migration/3.8.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE auth 2 | ADD COLUMN email VARCHAR(255) UNIQUE, 3 | ADD COLUMN is_banned BOOLEAN DEFAULT FALSE; 4 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server 2 | { 3 | # this is a sample configuration for nginx 4 | listen 80; 5 | 6 | location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md|package.json|package-lock.json|\.env) { 7 | return 404; 8 | } 9 | 10 | if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { 11 | return 403; 12 | } 13 | 14 | location ~ /purge(/.*) { 15 | proxy_cache_purge cache_one 127.0.0.1$request_uri$is_args$args; 16 | } 17 | 18 | location / { 19 | # if you are using compile deployment mode, please use the http://localhost:8094 instead 20 | proxy_pass http://127.0.0.1:8000; 21 | proxy_set_header Host 127.0.0.1:$server_port; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_set_header REMOTE-HOST $remote_addr; 25 | add_header X-Cache $upstream_cache_status; 26 | proxy_set_header X-Host $host:$server_port; 27 | proxy_set_header X-Scheme $scheme; 28 | proxy_connect_timeout 30s; 29 | proxy_read_timeout 86400s; 30 | proxy_send_timeout 30s; 31 | proxy_http_version 1.1; 32 | proxy_set_header Upgrade $http_upgrade; 33 | proxy_set_header Connection "upgrade"; 34 | } 35 | 36 | access_log /www/wwwlogs/chatnio.log; 37 | error_log /www/wwwlogs/chatnio.error.log; 38 | } 39 | -------------------------------------------------------------------------------- /screenshot/chatnio-pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/screenshot/chatnio-pro.png -------------------------------------------------------------------------------- /screenshot/chatnio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/screenshot/chatnio.png -------------------------------------------------------------------------------- /utils/ctx.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-redis/redis/v8" 7 | ) 8 | 9 | func GetDBFromContext(c *gin.Context) *sql.DB { 10 | return c.MustGet("db").(*sql.DB) 11 | } 12 | 13 | func GetCacheFromContext(c *gin.Context) *redis.Client { 14 | return c.MustGet("cache").(*redis.Client) 15 | } 16 | 17 | func GetUserFromContext(c *gin.Context) string { 18 | return c.MustGet("user").(string) 19 | } 20 | 21 | func GetAdminFromContext(c *gin.Context) bool { 22 | return c.MustGet("admin").(bool) 23 | } 24 | 25 | func GetAgentFromContext(c *gin.Context) string { 26 | return c.MustGet("agent").(string) 27 | } 28 | -------------------------------------------------------------------------------- /utils/templates/code.html: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 |
41 |

{{.Title}}

42 |

Your One-Time Password is {{.Code}}

43 |
44 | The code will expire in 10 minutes.
45 | If it is not operated by yourself, please ignore it.
46 |
47 |
48 |
49 | © {{.Title}} 50 |
51 |
52 | --------------------------------------------------------------------------------