├── .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 |
44 |
45 |
--------------------------------------------------------------------------------
/addition/card/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coaidev/coai/1fcde51da41c1a93e5ca5f938dc4e8cf9bf2c52a/addition/card/favicon.ico
--------------------------------------------------------------------------------
/addition/card/utils.php:
--------------------------------------------------------------------------------
1 | [^\S ]+/', '/[^\S ]+', '/(\s)+/', '/> ', '/:\s+/', '/\{\s+/', '/\s+}/');
8 | $replace = array('>', '<', '\\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 |
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 |
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 |
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 |
51 |
52 |
--------------------------------------------------------------------------------