Repository: coaidev/coai Branch: main Commit: 3048a493eedc Files: 535 Total size: 2.2 MB Directory structure: gitextract__4v8y3mp/ ├── .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 │ │ └── videos.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 │ │ └── utils.php │ ├── generation/ │ │ ├── api.go │ │ ├── build.go │ │ ├── data/ │ │ │ └── .gitkeep │ │ ├── generate.go │ │ └── prompt.go │ ├── router.go │ └── web/ │ ├── call.go │ └── search.go ├── admin/ │ ├── analysis/ │ │ ├── analysis.go │ │ ├── format.go │ │ ├── reflect.go │ │ ├── statistic.go │ │ └── types.go │ ├── controller.go │ ├── format.go │ ├── instance.go │ ├── invitation.go │ ├── logger.go │ ├── market.go │ ├── redeem.go │ ├── router.go │ ├── statistic.go │ ├── types.go │ └── user.go ├── app/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc.json │ ├── components.json │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public/ │ │ ├── manifest.json │ │ ├── robots.txt │ │ ├── service.js │ │ ├── site.webmanifest │ │ └── workbox.js │ ├── qodana.yaml │ ├── 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 │ │ │ ├── mask.ts │ │ │ ├── plugin.ts │ │ │ ├── quota.ts │ │ │ ├── record.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 │ │ │ │ └── plugin.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 │ │ │ │ ├── navbar.less │ │ │ │ ├── notify.less │ │ │ │ ├── package.less │ │ │ │ ├── preset.less │ │ │ │ ├── quota.less │ │ │ │ ├── record.less │ │ │ │ ├── settings.less │ │ │ │ ├── share-manager.less │ │ │ │ ├── sharing.less │ │ │ │ └── subscription.less │ │ │ └── ui.less │ │ ├── components/ │ │ │ ├── Avatar.tsx │ │ │ ├── EditorProvider.tsx │ │ │ ├── Emoji.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── FileProvider.tsx │ │ │ ├── FileViewer.tsx │ │ │ ├── I18nProvider.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MCPResultDebug.tsx │ │ │ ├── MCPResultPanel.tsx │ │ │ ├── Markdown.tsx │ │ │ ├── Message.tsx │ │ │ ├── ModelAvatar.tsx │ │ │ ├── OperationAction.tsx │ │ │ ├── Paragraph.tsx │ │ │ ├── PopupDialog.tsx │ │ │ ├── ProjectLink.tsx │ │ │ ├── ReloadService.tsx │ │ │ ├── Require.tsx │ │ │ ├── SelectGroup.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── ThinkContent.tsx │ │ │ ├── TickButton.tsx │ │ │ ├── Tips.tsx │ │ │ ├── TrendBadge.tsx │ │ │ ├── VoiceProvider.tsx │ │ │ ├── WarningButton.tsx │ │ │ ├── admin/ │ │ │ │ ├── ChannelSettings.tsx │ │ │ │ ├── ChargeWidget.tsx │ │ │ │ ├── ChartBox.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 │ │ │ │ ├── Contact.tsx │ │ │ │ ├── MenuBar.tsx │ │ │ │ └── NavBar.tsx │ │ │ ├── home/ │ │ │ │ ├── ChatInterface.tsx │ │ │ │ ├── ChatSpace.tsx │ │ │ │ ├── ChatWrapper.tsx │ │ │ │ ├── ConversationItem.tsx │ │ │ │ ├── MaskEditor.tsx │ │ │ │ ├── ModelArea.tsx │ │ │ │ ├── SideBar.tsx │ │ │ │ ├── assemblies/ │ │ │ │ │ ├── ActionButton.tsx │ │ │ │ │ ├── ChatAction.tsx │ │ │ │ │ ├── ChatInput.tsx │ │ │ │ │ └── ScrollAction.tsx │ │ │ │ └── subscription/ │ │ │ │ ├── SubscriptionUsage.tsx │ │ │ │ └── UpgradePlan.tsx │ │ │ ├── markdown/ │ │ │ │ ├── Code.tsx │ │ │ │ ├── Image.tsx │ │ │ │ ├── Label.tsx │ │ │ │ ├── Link.tsx │ │ │ │ ├── Reference.tsx │ │ │ │ ├── Video.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 │ │ │ │ ├── clickable.tsx │ │ │ │ ├── combo-box.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── date-picker.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── icons/ │ │ │ │ │ ├── Github.tsx │ │ │ │ │ └── Send.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── lib/ │ │ │ │ │ └── utils.ts │ │ │ │ ├── multi-combobox.tsx │ │ │ │ ├── number-input.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-box.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── step.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 │ │ │ └── version.json │ │ ├── dialogs/ │ │ │ ├── SettingsDialog.tsx │ │ │ └── index.tsx │ │ ├── events/ │ │ │ ├── blob.ts │ │ │ ├── info.ts │ │ │ ├── model.ts │ │ │ ├── spinner.ts │ │ │ ├── struct.ts │ │ │ └── theme.ts │ │ ├── i18n.ts │ │ ├── main.tsx │ │ ├── masks/ │ │ │ ├── prompts.ts │ │ │ └── types.ts │ │ ├── payment/ │ │ │ ├── icons.tsx │ │ │ ├── request.ts │ │ │ └── utils.ts │ │ ├── plugin/ │ │ │ └── types.ts │ │ ├── resources/ │ │ │ └── i18n/ │ │ │ ├── cn.json │ │ │ ├── en.json │ │ │ ├── ja.json │ │ │ ├── ru.json │ │ │ └── tw.json │ │ ├── router.tsx │ │ ├── routes/ │ │ │ ├── Account.tsx │ │ │ ├── Admin.tsx │ │ │ ├── Article.tsx │ │ │ ├── Auth.tsx │ │ │ ├── Forgot.tsx │ │ │ ├── Generation.tsx │ │ │ ├── Home.tsx │ │ │ ├── Index.tsx │ │ │ ├── Model.tsx │ │ │ ├── NotFound.tsx │ │ │ ├── Register.tsx │ │ │ ├── Sharing.tsx │ │ │ ├── Wallet.tsx │ │ │ ├── admin/ │ │ │ │ ├── Broadcast.tsx │ │ │ │ ├── Channel.tsx │ │ │ │ ├── Charge.tsx │ │ │ │ ├── DashBoard.tsx │ │ │ │ ├── License.tsx │ │ │ │ ├── Logger.tsx │ │ │ │ ├── Market.tsx │ │ │ │ ├── Notification.tsx │ │ │ │ ├── Subscription.tsx │ │ │ │ ├── System.tsx │ │ │ │ ├── Users.tsx │ │ │ │ └── common/ │ │ │ │ └── CommonAdminPage.tsx │ │ │ └── wallet/ │ │ │ ├── AmountItem.tsx │ │ │ └── WalletQuotaBox.tsx │ │ ├── spinner.tsx │ │ ├── store/ │ │ │ ├── api.ts │ │ │ ├── auth.ts │ │ │ ├── avatar.ts │ │ │ ├── chat.ts │ │ │ ├── globals.ts │ │ │ ├── index.ts │ │ │ ├── info.ts │ │ │ ├── menu.ts │ │ │ ├── package.ts │ │ │ ├── quota.ts │ │ │ ├── record.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/ │ │ │ ├── analytics.ts │ │ │ ├── app.ts │ │ │ ├── base.ts │ │ │ ├── date.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 │ ├── src-tauri/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── icons/ │ │ │ └── icon.icns │ │ ├── src/ │ │ │ └── main.rs │ │ └── tauri.conf.json │ ├── 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 │ └── videos.go ├── middleware/ │ ├── auth.go │ ├── builtins.go │ ├── cors.go │ ├── middleware.go │ └── throttle.go ├── migration/ │ ├── 3.6.sql │ └── 3.8.sql ├── nginx.conf ├── utils/ │ ├── base.go │ ├── bootstrap.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ app/node_modules app/src-tauri app/.idea app/.vscode app/dist app/dev-dist app/dist-ssr app/target app/tauri.conf.json app/tauri.js screenshot .vscode .idea config.yaml config.dev.yaml # current in ~/storage addition/generation/data/* !addition/generation/data/.gitkeep addition/article/data/* !addition/article/data/.gitkeep sdk logs chat chat.exe # for reverse engine reverse access.json access/*.json db cache config README.md .gitignore screenshot LICENSE .github ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: 报告问题 | Bug Report about: 使用简练详细的语言描述你遇到的问题 | Describe the issue you encountered in detail title: '' labels: bug assignees: '' --- [//]: # (方框内删除已有的空格,填 x 号) + [ ] 我已确认目前没有类似 issue + [ ] 我已确认我已升级到最新版本 + [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案 + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 + [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block) + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** **问题描述** **复现步骤** **预期结果** **日志信息** **相关截图 (如果有)** ================================================ FILE: .github/ISSUE_TEMPLATE/channel_update.md ================================================ --- name: 渠道更新 | Channel Update about: 新大模型供应商格式增加、更新请求 | Request to add or update a new llm provider format title: '' labels: feature assignees: '' --- [//]: # (方框内删除已有的空格,填 x 号) + [ ] 我已确认目前没有类似 issue + [ ] 我已确认我已升级到最新版本 + [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案 + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 + [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block) + [ ] 如果为新供应商格式,我已确认此供应商有一定的用户群体和知名度,借此以广告和推广类的名义的中转站点请求将被直接关闭 + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** **供应商名称** **描述** **供应商网址 / 截图 / 样例 (如果愿意提供)** ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Discord url: https://discord.gg/rpzNSmqaF2 about: Join Discord Community ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 功能请求 | Feature Request about: 使用简练详细的语言描述希望加入的新功能 | Describe the feature you would like to request title: '' labels: feature assignees: '' --- [//]: # (方框内删除已有的空格,填 x 号) + [ ] 我已确认目前没有类似 issue + [ ] 我已确认我已升级到最新版本 + [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案 + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 + [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block) + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** **功能描述** **相关截图 (如果有)** ================================================ FILE: .github/workflows/app.yaml ================================================ name: Release App on: workflow_dispatch: release: types: [published] jobs: create-release: permissions: contents: write runs-on: ubuntu-latest outputs: release_id: ${{ steps.create-release.outputs.result }} steps: - uses: actions/checkout@v3 - name: setup node uses: actions/setup-node@v3 with: node-version: 16 - name: get version run: | cd app echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV - name: create release id: create-release uses: actions/github-script@v6 with: script: | const { data } = await github.rest.repos.getLatestRelease({ owner: context.repo.owner, repo: context.repo.repo, }) return data.id build-tauri: needs: create-release permissions: contents: write strategy: fail-fast: false matrix: config: - os: ubuntu-latest arch: x86_64 rust_target: x86_64-unknown-linux-gnu - os: macos-latest arch: x86_64 rust_target: x86_64-apple-darwin - os: macos-latest arch: aarch64 rust_target: aarch64-apple-darwin - os: windows-latest arch: x86_64 rust_target: x86_64-pc-windows-msvc runs-on: ${{ matrix.config.os }} steps: - uses: actions/checkout@v3 - name: setup node uses: actions/setup-node@v3 with: node-version: 16 - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.config.rust_target }} - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.config.rust_target }} - name: install dependencies (ubuntu only) if: matrix.config.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - name: install frontend dependencies run: | cd app npm install -g pnpm pnpm install - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} with: releaseId: ${{ needs.create-release.outputs.release_id }} projectPath: ./app publish-release: permissions: contents: write runs-on: ubuntu-latest needs: [create-release, build-tauri] steps: - name: publish release id: publish-release uses: actions/github-script@v6 env: release_id: ${{ needs.create-release.outputs.release_id }} with: script: | github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: process.env.release_id, draft: false, prerelease: false }) ================================================ FILE: .github/workflows/build.yaml ================================================ name: Build Test on: push: branches: - '*' pull_request: branches: - '*' jobs: release: runs-on: ubuntu-latest strategy: matrix: node-version: [ 18.x ] steps: - name: Checkout uses: actions/checkout@v3 - name: Use Golang uses: actions/setup-go@v4 with: go-version: '1.20' - name: Build Backend run: | go build . - name: Use Node.js uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Build Frontend run: | cd app npm install -g pnpm pnpm install pnpm build - name: Upload a Build Artifact uses: actions/upload-artifact@v4.0.0 with: name: Build result path: app/dist ================================================ FILE: .github/workflows/docker-cd.yaml ================================================ name: Docker CD on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Build and push Docker images uses: docker/build-push-action@v2 with: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: programzmh/chatnio:stable ================================================ FILE: .github/workflows/docker-ci.yaml ================================================ name: Docker Image CI on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 with: platforms: "arm64,amd64" - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Get version run: echo "VERSION=$(node -p "require('./app/src/conf/version.json').version")" >> $GITHUB_ENV - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build and push Docker images uses: docker/build-push-action@v3 with: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: | programzmh/chatnio:latest programzmh/chatnio:${{ env.VERSION }} cache-from: | type=registry,ref=programzmh/chatnio:buildcache type=gha cache-to: | type=registry,ref=programzmh/chatnio:buildcache,mode=max type=gha,mode=max ================================================ FILE: .github/workflows/issue-translator.yaml ================================================ name: Issue Translator on: issue_comment: types: [created] issues: types: [opened] jobs: build: runs-on: ubuntu-latest steps: - uses: usthe/issues-translate-action@v2.7 with: IS_MODIFY_TITLE: false CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. ================================================ FILE: .gitignore ================================================ app/node_modules .vscode .idea config.yaml config.dev.yaml storage addition/generation/data/* !addition/generation/data/.gitkeep addition/article/data/* !addition/article/data/.gitkeep sdk logs chat *.exe chat.exe # for reverse engine reverse access.json access/*.json db redis config presets key/* !key/.gitkeep # for https://github.com/di-sukharev/opencommit .env ================================================ FILE: Dockerfile ================================================ # Author: ProgramZmh # License: Apache-2.0 # Description: Dockerfile for chatnio FROM --platform=$TARGETPLATFORM golang:1.20-alpine AS backend WORKDIR /backend COPY . . # Set go proxy to https://goproxy.cn (open for vps in China Mainland) # RUN go env -w GOPROXY=https://goproxy.cn,direct ARG TARGETARCH ARG TARGETOS ENV GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on CGO_ENABLED=1 # Install build dependencies RUN apk update && \ apk add --no-cache \ gcc \ musl-dev \ g++ \ make \ linux-headers # Build backend RUN go build -o chat -a -ldflags="-extldflags=-static" . FROM node:18 AS frontend WORKDIR /app COPY ./app . RUN npm install -g pnpm && \ pnpm install && \ pnpm run build && \ rm -rf node_modules src FROM alpine # Install dependencies RUN apk upgrade --no-cache && \ apk add --no-cache wget ca-certificates tzdata && \ update-ca-certificates 2>/dev/null || true # Set timezone RUN echo "Asia/Shanghai" > /etc/timezone && \ ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime WORKDIR / # Copy dist COPY --from=backend /backend/chat /chat COPY --from=backend /backend/config.example.yaml /config.example.yaml COPY --from=backend /backend/utils/templates /utils/templates COPY --from=backend /backend/addition/article/template.docx /addition/article/template.docx COPY --from=frontend /app/dist /app/dist # Volumes VOLUME ["/config", "/logs", "/storage"] # Expose port EXPOSE 8094 # Run application CMD ["./chat"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================
![chatnio](/app/public/logo.png) # [🥳 CoAI.Dev](https://coai.dev) #### 🚀 Next Generation AIGC One-Stop Business Solution #### *"CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api)"* English · [简体中文](./README_zh-CN.md) · [日本語](./README_ja-JP.md) · [Docs](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [Deployment Guide](https://coai.dev/docs/deploy) [![CoAI.Dev: #1 Repo Of The Day](https://trendshift.io/api/badge/repositories/6369)](https://trendshift.io/repositories/6369) CoAI.Dev Preview
## 📝 Features 1. 🤖️ **Rich Model Support**: Multi-model service provider support (OpenAI / Anthropic / Gemini / Midjourney and more than ten compatible formats & private LLM support) 2. 🤯 **Beautiful UI Design**: UI compatible with PC / Pad / Mobile, following [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) design standards, rich and beautiful interface design and backend dashboard 3. 🎃 **Complete Markdown Support**: Support for **LaTeX formulas** / **Mermaid mind maps** / table rendering / code highlighting / chart drawing / progress bars and other advanced Markdown syntax support 4. 👀 **Multi-theme Support**: Support for multiple theme switching, including **Light Mode** for light themes and **Dark Mode** for dark themes. 👉 [Custom Color Scheme](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less) 5. 📚 **Internationalization Support**: Support for internationalization, multi-language switching 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 Welcome to contribute translations [Pull Request](https://github.com/coaidev/coai/pulls) 6. 🎨 **Text-to-Image Support**: Support for multiple text-to-image models: **OpenAI DALL-E**✅ & **Midjourney** (support for **U/V/R** operations)✅ & Stable Diffusion✅ etc. 7. 📡 **Powerful Conversation Sync**: **Zero-cost cross-device conversation sync support for users**, support for **conversation sharing** (link sharing & save as image & share management), **no need for WebDav / WebRTC and other dependencies and complex learning costs** 8. 🎈 **Model Market & Preset System**: Support for customizable model market in the backend, providing model introductions, tags, and other parameters. Site owners can customize model introductions according to the situation. Also supports a preset system, including **custom presets** and **cloud synchronization** functions. 9. 📖 **Rich File Parsing**: **Out-of-the-box**, supports file parsing for **all models** (PDF / Docx / Pptx / Excel / image formats parsing), **supports more cloud image storage solutions** (S3 / R2 / MinIO etc.), **supports OCR image recognition** 👉 See project [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) for details (supports Vercel / Docker one-click deployment) 10. 🌏 **Full Model Internet Search**: Based on the [SearXNG](https://github.com/searxng/searxng) open-source engine, supports rich search engines such as Google / Bing / DuckDuckGo / Yahoo / Wikipedia / Arxiv / Qwant, supports safe search mode, content truncation, image proxy, test search availability, and other functions. 11. 💕 **Progressive Web App (PWA)**: Supports PWA applications & desktop support (desktop based on [Tauri](https://github.com/tauri-apps/tauri)) 12. 🤩 **Comprehensive Backend Management**: Supports beautiful and rich dashboard, announcement & notification management, user management, subscription management, gift code & redemption code management, price setting, subscription setting, custom model market, custom site name & logo, SMTP email settings, and other functions 13. 🤑 **Multiple Billing Methods**: Supports 💴 **Subscription** and 💴 **Elastic Billing** two billing methods. Elastic billing supports per-request billing / token billing / no billing / anonymous calls and **minimum request points** detection and other powerful features 14. 🎉 **Innovative Model Caching**: Supports enabling model caching: i.e., under the same request parameter hash, if it has been requested before, it will directly return the cached result (hitting the cache will not be billed), reducing the number of requests. You can customize whether to cache models, cache time, multiple cache result numbers, and other advanced cache settings 15. 🥪 **Additional Features** (Support Discontinued): 🍎 **AI Project Generator Function** / 📂 **Batch Article Generation Function** / 🥪 **AI Card Function** (Deprecated) 16. 😎 **Excellent Channel Management**: Self-written excellent channel algorithm, supports ⚡ **multi-channel management**, supports 🥳**priority** setting for channel call order, supports 🥳**weight** setting for load balancing probability distribution of channels at the same priority, supports 🥳**user grouping**, 🥳**automatic retry on failure**, 🥳**model redirection**, 🥳**built-in upstream hiding**, 🥳**channel status management** and other powerful **enterprise-level functions** 17. ⭐ **OpenAI API Distribution & Proxy System**: Supports calling various large models in **OpenAI API** standard format, integrates powerful channel management functions, only needs to deploy one site to achieve simultaneous development of B/C-end business💖 18. 👌 **Quick Upstream Synchronization**: Channel settings, model market, price settings, and other settings can quickly synchronize with upstream sites, modify your site configuration based on this, quickly build your site, save time and effort, one-click synchronization, quick launch 19. 👋 **SEO Optimization**: Supports SEO optimization, supports custom site name, site logo, and other SEO optimization settings to make search engines crawl faster, making your site stand out👋 20. 🎫 **Multiple Redemption Code Systems**: Supports multiple redemption code systems, supports gift codes and redemption codes, supports batch generation, gift codes are suitable for promotional distribution, redemption codes are suitable for card sales, for gift codes of one type, a user can only redeem one code, which to some extent reduces the situation of one user redeeming multiple times in promotions😀 21. 🥰 **Business-Friendly License**: Adopts the **Apache-2.0** open-source license, friendly for commercial secondary development & distribution (please also comply with the provisions of the Apache-2.0 license, do not use for illegal purposes) > ### ✨ CoAI.Dev Business > > ![Pro Version Preview](./screenshot/coai-pro.png) > > - ✅ Beautiful commercial-grade UI, elegant frontend interface and backend management > - ✅ Supports TTS & STT, plugin marketplace, RAG knowledge base and other rich features and modules > - ✅ More payment providers, more billing models and advanced order management > - ✅ Supports more authentication methods, including SMS login, OAuth login, etc. > - ✅ Supports model monitoring, channel health detection, fault alarm automatic channel switching > - ✅ Supports multi-tenant API Key distribution system, enterprise-level token permission management and visitor restrictions > - ✅ Supports security auditing, logging, model rate limiting, API Gateway and other advanced features > - ✅ Supports promotion rewards, professional data statistics, user profile analysis and other business analysis capabilities > - ✅ Supports Discord/Telegram/Feishu and other bot integration capabilities (extension modules) > - ... > > [👉 Learn More](https://www.coai.dev/docs/contact) ## 🔨 Supported Models 1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)* 2. Anthropic Claude *(✅ Vision ✅ Function Calling)* 3. Google Gemini & PaLM2 *(✅ Vision)* 4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)* 5. iFlytek SparkDesk *(✅ Vision ✅ Function Calling)* 6. Zhipu AI ChatGLM *(✅ Vision)* 7. Alibaba Tongyi Qwen 8. Tencent Hunyuan 9. Baichuan AI 10. Moonshot AI (👉 OpenAI) 11. DeepSeek AI (👉 OpenAI) 12. ByteDance Skylark *(✅ Function Calling)* 13. Groq Cloud AI 14. OpenRouter (👉 OpenAI) 15. 360 GPT 16. LocalAI / Ollama (👉 OpenAI) ## 👻 OpenAI Compatible API Proxy - [x] Chat Completions _(/v1/chat/completions)_ - [x] Image Generation _(/v1/images)_ - [x] Model List _(/v1/models)_ - [x] Dashboard Billing _(/v1/billing)_ ## 📦 Deployment > [!TIP] > **After successful deployment, the admin account is `root`, with the default password `chatnio123456`** ### ✨ Zeabur (One-Click) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/M86XJI) > Zeabur provides a certain free quota, you can use non-paid regions for one-click deployment, and also supports plan subscriptions and elastic billing for flexible expansion. > 1. Click `Deploy` to deploy, and enter the domain name you wish to bind, wait for the deployment to complete. > 2. After deployment is complete, please visit your domain name and log in to the backend management using the username `root` and password `chatnio123456`. Please follow the prompts to change the password in the chatnio backend in a timely manner. ### 🐳 BTPanel (One-Click) 1. Install Baota Panel by visiting [BTPanel](https://www.bt.cn/new/download.html) and install using the stable version script. 2. Log in to the panel and click **Docker** on the left to enter Docker management. 3. If prompted that Docker / Docker Compose is not installed, you can install according to the guide above. 4. After installation is complete, enter **App Store**, search for `CoAI` and click **Install**. 5. Configure basic application information such as your domain name, port, etc., and click **Confirm** (default configuration can be used). 6. First-time installation may take 1-2 minutes to complete database initialization. If you encounter issues, please check the panel running logs for troubleshooting. 7. Visit your configured domain or server `http://[ip]:[port]`, log in to the backend management using username `root` and password `chatnio123456`. ### AlibabaCloud ComputeNest (One-Click) [![Deploy on AlibabaCloud ComputeNest International Edition](https://service-info-public.oss-cn-hangzhou.aliyuncs.com/computenest-en.svg)](https://computenest.console.aliyun.com/service/instance/create/ap-southeast-1?type=user&ServiceName=CoAI%20%20Community%20Edition) 1. Access the CoAI service on [ComputeNest International Edition](https://computenest.console.aliyun.com/service/instance/create/ap-southeast-1?type=user&ServiceName=CoAI%20%20Community%20Edition). If you are in China, please visit [ComputeNest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=CoAI社区版), and fill in the deployment parameters as prompted. 2. Select the payment type, fill in the instance parameters and network parameters, and click **Next: Confirm Order**. 3. After confirming the deployment parameters and checking the estimated price, click Create Now and wait for the service instance to be deployed. 4. Click **Service Instance** on the left. After the service instance is deployed, click the instance ID to enter the details interface. 5. Click the address in **Use Now** on the details interface to enter the CoAI interface. The default username is `root` and the password is `chatnio123456` to log in to the backend management. 6. For more operation details and payment information, see:[Service Details](https://computenest.console.aliyun.com/service/detail/ap-southeast-1/service-27e11d3a5c9b40628505/1?type=user&isRecommend=true). ### ⚡ Docker Compose Installation (Recommended) > [!NOTE] > After successful execution, the host machine mapping address is `http://localhost:8000` ```shell git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git cd chatnio docker-compose up -d # Run the service # To use the stable version, use docker-compose -f docker-compose.stable.yaml up -d instead # To use Watchtower for automatic updates, use docker-compose -f docker-compose.watch.yaml up -d instead ``` Version update (_If Watchtower automatic updates are enabled, manual updates are not necessary_): ```shell docker-compose down docker-compose pull docker-compose up -d ``` > - MySQL database mount directory: ~/**db** > - Redis database mount directory: ~/**redis** > - Configuration file mount directory: ~/**config** ### ⚡ Docker Installation (Lightweight runtime, commonly used for external _MYSQL/RDS_ services) > [!NOTE] > After successful execution, the host machine address is `http://localhost:8094`. > > To use the stable version, use `programzmh/chatnio:stable` instead of `programzmh/chatnio:latest` ```shell docker run -d --name chatnio \ --network host \ -v ~/config:/config \ -v ~/logs:/logs \ -v ~/storage:/storage \ -e MYSQL_HOST=localhost \ -e MYSQL_PORT=3306 \ -e MYSQL_DB=chatnio \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=chatnio123456 \ -e REDIS_HOST=localhost \ -e REDIS_PORT=6379 \ -e SECRET=secret \ -e SERVE_STATIC=true \ programzmh/chatnio:latest ``` > - *--network host* means using the host machine's network, allowing the Docker container to use the host's network. You can modify this as needed. > - SECRET: JWT secret key, generate a random string and modify accordingly > - SERVE_STATIC: Whether to enable static file serving (normally this doesn't need to be changed, see FAQ below for details) > - *-v ~/config:/config* mounts the configuration file, *-v ~/logs:/logs* mounts the host machine directory for log files, *-v ~/storage:/storage* mounts the directory for additional feature generated files > - MySQL and Redis services need to be configured. Please refer to the information above to modify the environment variables accordingly Version update (_After enabling Watchtower, manual updates are not necessary. After execution, follow the steps above to run again_): ```shell docker stop chatnio docker rm chatnio docker pull programzmh/chatnio:latest ``` ### ⚒ Compile and Install > [!NOTE] > After successful deployment, the default port is **8094**, and the access address is `http://localhost:8094` > > Config settings (~/config/**config.yaml**) can be overridden using environment variables. For example, the `MYSQL_HOST` environment variable can override the `mysql.host` configuration item ```shell git clone https://github.com/coaidev/coai.git cd chatnio cd app npm install -g pnpm pnpm install pnpm build cd .. go build -o chatnio # e.g. using nohup (you can also use systemd or other service manager) nohup ./chatnio > output.log & # using nohup to run in background ``` ## 📦 Tech Stack - 🥗 Frontend: React + Redux + Radix UI + Tailwind CSS - 🍎 Backend: Golang + Gin + Redis + MySQL - 🍒 Application Technology: PWA + WebSocket ## 🤯 Why Create This Project & Project Advantages - We found that most AIGC commercial sites on the market are frontend-oriented lightweight deployment projects with beautiful UI interface designs, such as the commercial version of [Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web). Due to its personal privatization-oriented design, there are some limitations in secondary commercial development, presenting some issues, such as: 1. **Difficult conversation synchronization**, for example, requiring services like WebDav, high user learning costs, and difficulties in real-time cross-device synchronization. 2. **Insufficient billing**, for example, only supporting elastic billing or only subscription-based, unable to meet the needs of different users. 3. **Inconvenient file parsing**, for example, only supporting uploading images to an image hosting service first, then returning to the site to input the URL direct link in the input box, without built-in file parsing functionality. 4. **No support for conversation URL sharing**, for example, only supporting conversation screenshot sharing, unable to support conversation URL sharing (or only supporting tools like ShareGPT, which cannot promote the site). 5. **Insufficient channel management**, for example, the backend only supports OpenAI format channels, making it difficult to be compatible with other format channels. And only one channel can be filled in, unable to support multi-channel management. 6. **No API call support**, for example, only supporting user interface calls, unable to support API proxying and management. - Another type is API distribution-oriented sites with powerful distribution systems, such as projects based on [One API](https://github.com/songquanpeng/one-api). Although these projects support powerful API proxying and management, they lack interface design and some C-end features, such as: 1. **Insufficient user interface**, for example, only supporting API calls, without built-in user interface chat. User interface chat requires manually copying the key and going to other sites to use, which has a high learning cost for ordinary users. 2. **No subscription system**, for example, only supporting elastic billing, lacking billing design for C-end users, unable to meet different user needs, and not user-friendly in terms of cost perception for users without a foundation. 3. **Insufficient C-end features**, for example, only supporting API calls, not supporting conversation synchronization, conversation sharing, file parsing, and other functions. 4. **Insufficient load balancing**, the open-source version does not support the **weight** parameter, unable to achieve balanced load distribution probability for channels at the same priority ([New API](https://github.com/Calcium-Ion/new-api) also solves this pain point, with a more beautiful UI). Therefore, we hope to combine the advantages of these two types of projects to create a project that has both a powerful API distribution system and a rich user interface design, thus meeting the needs of C-end users while developing B-end business, improving user experience, reducing user learning costs, and increasing user stickiness. Thus, **CoAI.Dev** was born. We hope to create a project that has both a powerful API distribution system and a rich user interface design, becoming the next-generation open-source AIGC project's one-stop commercial solution. ## ❤ Donations If you find this project helpful, you can give it a Star to show your support! ================================================ FILE: README_ja-JP.md ================================================
![chatnio](/app/public/logo.png) # [🥳 CoAI.Dev](https://coai.dev) #### 🚀 次世代AIGCワンストップビジネスソリューション #### *"CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api)"* English · [简体中文](./README_zh-CN.md) · [日本語](./README_ja-JP.md) · [ドキュメント](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [デプロイガイド](https://coai.dev/docs/deploy) [![CoAI.Dev: #1 Repo Of The Day](https://trendshift.io/api/badge/repositories/6369)](https://trendshift.io/repositories/6369) CoAI.Dev Preview
## 📝 機能 1. 🤖️ **豊富なモデルサポート**: 複数のモデルサービスプロバイダーのサポート (OpenAI / Anthropic / Gemini / Midjourney など十数種類の互換フォーマット & プライベートLLMサポート) 2. 🤯 **美しいUIデザイン**: PC / Pad / モバイルに対応したUI、[Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) のデザイン基準に従った豊富で美しいインターフェースデザインとバックエンドダッシュボード 3. 🎃 **完全なMarkdownサポート**: **LaTeX数式** / **Mermaidマインドマップ** / テーブルレンダリング / コードハイライト / チャート描画 / プログレスバーなどの高度なMarkdown構文サポート 4. 👀 **マルチテーマサポート**: 複数のテーマ切り替えをサポート、ライトテーマの**ライトモード**とダークテーマの**ダークモード**を含む。 👉 [カスタムカラースキーム](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less) 5. 📚 **国際化サポート**: 国際化をサポートし、複数の言語切り替えをサポート 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 翻訳の貢献を歓迎します [Pull Request](https://github.com/coaidev/coai/pulls) 6. 🎨 **テキストから画像へのサポート**: 複数のテキストから画像へのモデルをサポート: **OpenAI DALL-E**✅ & **Midjourney** ( **U/V/R** 操作をサポート)✅ & Stable Diffusion✅ など 7. 📡 **強力な会話同期**: **ユーザーのゼロコストクロスデバイス会話同期サポート**、**会話共有** (リンク共有 & 画像として保存 & 共有管理) をサポート、**WebDav / WebRTCなどの依存関係や複雑な学習コストは不要** 8. 🎈 **モデル市場 & プリセットシステム**: バックエンドでカスタマイズ可能なモデル市場をサポートし、モデルの紹介、タグなどのパラメータを提供。サイトオーナーは状況に応じてモデルの紹介をカスタマイズできます。また、**カスタムプリセット**と**クラウド同期**機能を含むプリセットシステムもサポート。 9. 📖 **豊富なファイル解析**: **すぐに使える**、**すべてのモデル**のファイル解析をサポート (PDF / Docx / Pptx / Excel / 画像形式の解析)、**より多くのクラウド画像ストレージソリューション** (S3 / R2 / MinIO など) をサポート、**OCR画像認識**をサポート 👉 詳細はプロジェクト [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) を参照 (Vercel / Dockerのワンクリックデプロイをサポート) 10. 🌏 **全モデルインターネット検索**: [SearXNG](https://github.com/searxng/searxng) オープンソースエンジンに基づき、Google / Bing / DuckDuckGo / Yahoo / Wikipedia / Arxiv / Qwant などの豊富な検索エンジン検索をサポート、安全検索モード、コンテンツ切り捨て、画像プロキシ、検索可用性テストなどの機能をサポート。 11. 💕 **プログレッシブウェブアプリ (PWA)**: PWAアプリケーションをサポートし、デスクトップサポート (デスクトップは [Tauri](https://github.com/tauri-apps/tauri) に基づく) 12. 🤩 **包括的なバックエンド管理**: 美しく豊富なダッシュボード、公告 & 通知管理、ユーザー管理、サブスクリプション管理、ギフトコード & 交換コード管理、価格設定、サブスクリプション設定、カスタムモデル市場、カスタムサイト名 & ロゴ、SMTPメール設定などの機能をサポート 13. 🤑 **複数の課金方法**: 💴 **サブスクリプション** と 💴 **エラスティック課金** の2つの課金方法をサポート。エラスティック課金は、リクエストごとの課金 / トークン課金 / 無課金 / 匿名通話をサポートし、**最小リクエストポイント** 検出などの強力な機能をサポート 14. 🎉 **革新的なモデルキャッシュ**: モデルキャッシュの有効化をサポート: 同じリクエストパラメータハッシュの下で、以前にリクエストされた場合、キャッシュ結果を直接返します (キャッシュヒットは課金されません)、リクエスト回数を減らします。キャッシュするモデル、キャッシュ時間、複数のキャッシュ結果数などの高度なキャッシュ設定をカスタマイズできます 15. 🥪 **追加機能** (サポート終了): 🍎 **AIプロジェクトジェネレーター機能** / 📂 **バッチ記事生成機能** / 🥪 **AIカード機能** (廃止) 16. 😎 **優れたチャネル管理**: 自作の優れたチャネルアルゴリズム、⚡ **マルチチャネル管理** をサポート、チャネル呼び出し順序の設定をサポート、同じ優先度のチャネルの負荷分散確率の設定をサポート、**ユーザーグループ化**、**失敗時の自動再試行**、**モデルリダイレクト**、**組み込みの上流非表示**、**チャネル状態管理** などの強力な**企業レベルの機能**をサポート 17. ⭐ **OpenAI API分散 & プロキシシステム**: **OpenAI API** 標準フォーマットでさまざまな大規模モデルを呼び出すことをサポートし、強力なチャネル管理機能を統合。1つのサイトをデプロイするだけで、B/Cエンドビジネスの同時開発を実現💖 18. 👌 **迅速な上流同期**: チャネル設定、モデル市場、価格設定などの設定を迅速に上流サイトと同期し、これに基づいてサイト設定を変更し、迅速にサイトを構築し、時間と労力を節約し、ワンクリック同期、迅速なオンライン化を実現 19. 👋 **SEO最適化**: SEO最適化をサポートし、カスタムサイト名、サイトロゴなどのSEO最適化設定をサポートし、検索エンジンがより速くクロールできるようにし、サイトを際立たせます👋 20. 🎫 **複数の交換コードシステム**: 複数の交換コードシステムをサポートし、ギフトコードと交換コードをサポートし、バッチ生成をサポート。ギフトコードはプロモーション配布に適しており、交換コードはカード販売に適しています。ギフトコードの1つのタイプの複数のコードは、1人のユーザーが1つのコードしか交換できないため、プロモーション中に1人のユーザーが複数回交換する状況をある程度減らします😀 21. 🥰 **ビジネスフレンドリーなライセンス**: **Apache-2.0** オープンソースライセンスを採用し、商用の二次開発 & 配布にフレンドリー (Apache-2.0ライセンスの規定を遵守し、違法な目的で使用しないでください) > ### ✨ CoAI.Dev ビジネス版 > > ![Pro Version Preview](./screenshot/coai-pro.png) > > - ✅ 美しい商用グレードのUI、エレガントなフロントエンドインターフェースとバックエンド管理 > - ✅ TTS & STT、プラグインマーケットプレイス、RAGナレッジベースなどの豊富な機能とモジュールをサポート > - ✅ より多くの支払いプロバイダー、より多くの課金モデルと高度な注文管理をサポート > - ✅ SMSログイン、OAuthログインなど、より多くの認証方法をサポート > - ✅ モデル監視、チャネルの健康状態検出、障害アラーム自動チャネル切り替えをサポート > - ✅ マルチテナントAPIキー配布システム、企業レベルのトークン権限管理と訪問者制限をサポート > - ✅ セキュリティ監査、ログ記録、モデルレート制限、APIゲートウェイなどの高度な機能をサポート > - ✅ プロモーション報酬、プロフェッショナルなデータ統計、ユーザープロファイル分析などのビジネス分析能力をサポート > - ✅ Discord/Telegram/Feishuなどのボット統合機能をサポート (拡張モジュール) > - ... > > [👉 詳細はこちら](https://www.coai.dev/docs/contact) ## 🔨 サポートされているモデル 1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)* 2. Anthropic Claude *(✅ Vision ✅ Function Calling)* 3. Google Gemini & PaLM2 *(✅ Vision)* 4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)* 5. iFlytek SparkDesk *(✅ Vision ✅ Function Calling)* 6. Zhipu AI ChatGLM *(✅ Vision)* 7. Alibaba Tongyi Qwen 8. Tencent Hunyuan 9. Baichuan AI 10. Moonshot AI (👉 OpenAI) 11. DeepSeek AI (👉 OpenAI) 12. ByteDance Skylark *(✅ Function Calling)* 13. Groq Cloud AI 14. OpenRouter (👉 OpenAI) 15. 360 GPT 16. LocalAI / Ollama (👉 OpenAI) ## 👻 OpenAI互換APIプロキシ - [x] Chat Completions _(/v1/chat/completions)_ - [x] Image Generation _(/v1/images)_ - [x] Model List _(/v1/models)_ - [x] Dashboard Billing _(/v1/billing)_ ## 📦 デプロイ > [!TIP] > **デプロイが成功した後、管理者アカウントは `root` で、デフォルトのパスワードは `chatnio123456` です** ### ✨ Zeabur (ワンクリック) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/M86XJI) > Zeaburは一定の無料クォータを提供しており、非有料地域でのワンクリックデプロイをサポートし、プランサブスクリプションとエラスティック課金による柔軟な拡張もサポートしています。 > 1. `Deploy` をクリックしてデプロイし、バインドしたいドメイン名を入力し、デプロイが完了するのを待ちます。 > 2. デプロイが完了したら、ドメイン名にアクセスし、ユーザー名 `root` とパスワード `chatnio123456` を使用してバックエンド管理にログインします。チャットニオのバックエンドでパスワードを変更するように指示に従ってください。 ### ⚡ Docker Composeインストール (推奨) > [!NOTE] > 実行が成功した後、ホストマシンのマッピングアドレスは `http://localhost:8000` です ```shell git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git cd chatnio docker-compose up -d # サービスを実行 # 安定版を使用するには、docker-compose -f docker-compose.stable.yaml up -d を使用してください # Watchtowerを使用して自動更新するには、docker-compose -f docker-compose.watch.yaml up -d を使用してください ``` バージョン更新 (_Watchtower自動更新が有効な場合、手動更新は不要です_): ```shell docker-compose down docker-compose pull docker-compose up -d ``` > - MySQLデータベースマウントディレクトリ: ~/**db** > - Redisデータベースマウントディレクトリ: ~/**redis** > - 設定ファイルマウントディレクトリ: ~/**config** ### ⚡ Dockerインストール (軽量ランタイム、外部 _MYSQL/RDS_ サービスでよく使用されます) > [!NOTE] > 実行が成功した後、ホストマシンのアドレスは `http://localhost:8094` です。 > > 安定版を使用するには、`programzmh/chatnio:stable` を `programzmh/chatnio:latest` の代わりに使用してください ```shell docker run -d --name chatnio \ --network host \ -v ~/config:/config \ -v ~/logs:/logs \ -v ~/storage:/storage \ -e MYSQL_HOST=localhost \ -e MYSQL_PORT=3306 \ -e MYSQL_DB=chatnio \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=chatnio123456 \ -e REDIS_HOST=localhost \ -e REDIS_PORT=6379 \ -e SECRET=secret \ -e SERVE_STATIC=true \ programzmh/chatnio:latest ``` > - *--network host* はホストマシンのネットワークを使用することを意味し、Dockerコンテナがホストのネットワークを使用できるようにします。必要に応じて変更できます。 > - SECRET: JWTシークレットキー、ランダムな文字列を生成して適宜変更 > - SERVE_STATIC: 静的ファイルの提供を有効にするかどうか (通常、これを変更する必要はありません。詳細はFAQを参照) > - *-v ~/config:/config* は設定ファイルをマウントし、*-v ~/logs:/logs* はログファイルのホストマシンディレクトリをマウントし、*-v ~/storage:/storage* は追加機能の生成ファイルのディレクトリをマウント > - MySQLとRedisサービスを設定する必要があります。上記の情報を参照して環境変数を適宜変更してください バージョン更新 (_Watchtowerを有効にした後、手動更新は不要です。実行後、上記の手順に従って再度実行してください_): ```shell docker stop chatnio docker rm chatnio docker pull programzmh/chatnio:latest ``` ### ⚒ コンパイルとインストール > [!NOTE] > デプロイが成功した後、デフォルトのポートは **8094** で、アクセスアドレスは `http://localhost:8094` です > > 設定項目 (~/config/**config.yaml**) は環境変数を使用して上書きできます。例えば、`MYSQL_HOST` 環境変数は `mysql.host` 設定項目を上書きできます ```shell git clone https://github.com/coaidev/coai.git cd chatnio cd app npm install -g pnpm pnpm install pnpm build cd .. go build -o chatnio # 例: nohupを使用 (systemdや他のサービスマネージャーも使用できます) nohup ./chatnio > output.log & # バックグラウンドで実行するためにnohupを使用 ``` ## 📦 技術スタック - 🥗 フロントエンド: React + Redux + Radix UI + Tailwind CSS - 🍎 バックエンド: Golang + Gin + Redis + MySQL - 🍒 アプリケーション技術: PWA + WebSocket ## 🤯 なぜこのプロジェクトを作成したのか & プロジェクトの利点 - 市場にあるほとんどのAIGC商用サイトは、[Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) の商用版のように、美しいUIインターフェースデザインを持つフロントエンド指向の軽量デプロイプロジェクトです。個人のプライベート化指向の設計のため、二次商用開発にはいくつかの制限があり、いくつかの問題が発生します。例えば: 1. **会話の同期が難しい**: 例えば、WebDavなどのサービスが必要で、ユーザーの学習コストが高く、クロスデバイスのリアルタイム同期が困難です。 2. **課金が不十分**: 例えば、エラスティック課金のみをサポートするか、サブスクリプションのみをサポートし、異なるユーザーのニーズを満たすことができません。 3. **ファイル解析が不便**: 例えば、最初に画像ホスティングサービスに画像をアップロードし、サイトに戻って入力ボックスにURL直リンクを入力する必要があり、組み込みのファイル解析機能がありません。 4. **会話URL共有のサポートがない**: 例えば、会話のスクリーンショット共有のみをサポートし、会話URL共有をサポートしません (またはShareGPTなどのツールのみをサポートし、サイトのプロモーションには役立ちません)。 5. **チャネル管理が不十分**: 例えば、バックエンドはOpenAIフォーマットのチャネルのみをサポートし、他のフォーマットのチャネルとの互換性が難しいです。そして、1つのチャネルしか入力できず、マルチチャネル管理をサポートしません。 6. **API呼び出しのサポートがない**: 例えば、ユーザーインターフェース呼び出しのみをサポートし、APIプロキシと管理をサポートしません。 - もう1つのタイプは、[One API](https://github.com/songquanpeng/one-api) に基づくプロジェクトのように、強力な分散システムを持つAPI分散指向のサイトです。 これらのプロジェクトは強力なAPIプロキシと管理をサポートしていますが、インターフェースデザインといくつかのCエンド機能が不足しています。例えば: 1. **ユーザーインターフェースが不十分**: 例えば、API呼び出しのみをサポートし、組み込みのユーザーインターフェースチャットがありません。ユーザーインターフェースチャットは、手動でキーをコピーして他のサイトに行って使用する必要があり、普通のユーザーにとって学習コストが高いです。 2. **サブスクリプションシステムがない**: 例えば、エラスティック課金のみをサポートし、Cエンドユーザーの課金設計が不足しており、異なるユーザーのニーズを満たすことができず、基礎のないユーザーにとってコスト感知がフレンドリーではありません。 3. **Cエンド機能が不十分**: 例えば、API呼び出しのみをサポートし、会話同期、会話共有、ファイル解析などの機能をサポートしません。 4. **負荷分散が不十分**: オープンソース版は**重み**パラメータをサポートしておらず、同じ優先度のチャネルの負荷分散確率を実現できません ([New API](https://github.com/Calcium-Ion/new-api) もこの痛点を解決し、UIもより美しいです)。 したがって、これら2つのタイプのプロジェクトの利点を組み合わせて、強力なAPI分散システムと豊富なユーザーインターフェースデザインを持つプロジェクトを作成し、 Cエンドユーザーのニーズを満たしながらBエンドビジネスを開発し、ユーザーエクスペリエンスを向上させ、ユーザーの学習コストを削減し、ユーザーの粘着性を高めることを目指しています。 そのため、**CoAI.Dev** が誕生しました。強力なAPI分散システムと豊富なユーザーインターフェースデザインを持つプロジェクトを作成し、次世代のオープンソースAIGCプロジェクトのワンストップ商用ソリューションになることを目指しています。 ## ❤ 寄付 このプロジェクトが役立つと思われる場合は、Starをクリックしてサポートを示すことができます! ================================================ FILE: README_zh-CN.md ================================================
![chatnio](/app/public/logo.png) # [🥳 CoAI.Dev](https://coai.dev) #### 🚀 下一代 AIGC 一站式商业解决方案 #### *“ CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api) ”* [English](./README.md) · 简体中文 · [文档](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [部署文档](https://coai.dev/docs/deploy) [![CoAI.Dev: #1 Repo Of The Day](https://trendshift.io/api/badge/repositories/6369)](https://trendshift.io/repositories/6369) CoAI.Dev Preview
## 📝 功能 1. 🤖️ **丰富模型支持**: 多模型服务商支持 (OpenAI / Anthropic / Gemini / Midjourney 等十余种格式兼容 & 私有化 LLM 支持) 2. 🤯 **美观 UI 设计**: UI 兼容 PC / Pad / 移动三端,遵循 [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) 设计规范,丰富美观的界面设计和后台仪表盘 3. 🎃 **完整 Markdown 支持**: 支持 **LaTeX 公式** / **Mermaid 思维导图** / 表格渲染 / 代码高亮 / 图表绘制 / 进度条等进阶 Markdown 语法支持 4. 👀 **多主题支持**: 支持多种主题切换,包含亮色主题的**明亮模式**和暗色主题的**深色模式**。 👉 [自定义配色](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less) 5. 📚 **国际化支持**: 支持国际化,支持多语言切换 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 欢迎贡献翻译 [Pull Request](https://github.com/coaidev/coai/pulls) 6. 🎨 **文生图支持**: 支持多种文生图模型: **OpenAI DALL-E**✅ & **Midjourney** (支持 **U/V/R** 操作)✅ & Stable Diffusion✅ 等 7. 📡 **强大对话同步**: **用户 0 成本对话跨端同步支持**,支持**对话分享** (支持链接分享 & 保存为图片 & 分享管理), **无需 WebDav / WebRTC 等依赖和复杂学习成本** 8. 🎈 **模型市场 & 预设系统**: 支持后台可自定义的模型市场, 可提供模型介绍、标签等参数, 站长可根据情况自定义模型简介。同时支持预设系统,包含 **自定义预设** 和 **云端同步** 功能。 9. 📖 **丰富文件解析**: **开箱即用**, 支持**所有模型**的文件解析 (PDF / Docx / Pptx / Excel / 图片等格式解析), **支持更多云端图片存储方案** (S3 / R2 / MinIO 等), **支持 OCR 图片识别** 👉 详情参见项目 [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) (支持 Vercel / Docker 一键部署) 10. 🌏 **全模型联网搜索**: 基于 [SearXNG](https://github.com/searxng/searxng) 开源引擎, 支持 Google / Bing / DuckDuckGo / Yahoo / WikiPedia / Arxiv / Qwant 等丰富搜索引擎搜索, 支持安全搜索模式, 内容截断, 图片代理, 测试搜索可用性等功能。 11. 💕 **渐进式 Web 应用 (PWA)**: 支持 PWA 应用 & 支持桌面端 (桌面端基于 [Tauri](https://github.com/tauri-apps/tauri)) 12. 🤩 **齐全后台管理**: 支持美观丰富的仪表盘, 公告 & 通知管理, 用户管理, 订阅管理, 礼品码 & 兑换码管理, 价格设定, 订阅设定, 自定义模型市场, 自定义站点名称 & Logo, SMTP 发件设置等功能 13. 🤑 **多种计费方式**: 支持 💴 **订阅制** 和 💴 **弹性计费** 两种计费方式, 弹性计费支持 次数计费 / Token 计费 / 不计费 / 可匿名调用 和 **最小请求点数** 检测等强大功能 14. 🎉 **创新模型缓存**: 支持开启模型缓存:即同一个请求入参 Hash 下, 如果之前已请求过, 将直接返回缓存结果 (击中缓存将不计费), 减少请求次数。可自行自定义是否缓存的模型、缓存时间、多种缓存结果数等高级缓存设置 15. 🥪 **附加功能** (停止支持): 🍎 **AI 项目生成器功能** / 📂 **批量文章生成功能** / 🥪 **AI 卡片功能** (已废弃) 16. 😎 **优秀渠道管理**: 自写优秀渠道算法, 支持⚡ **多渠道管理**, 支持🥳**优先级**设置渠道的调用顺序, 支持🥳**权重**设置同一优先级下的渠道均衡负载分配概率, 支持🥳**用户分组**, 🥳**失败自动重试**, 🥳**模型重定向**, 🥳**内置上游隐藏**, 🥳**渠道状态管理**等强大**企业级功能** 17. ⭐ **OpenAI API 分发 & 中转系统**: 支持以 **OpenAI API** 标准格式调用各种大模型, 集成强大的渠道管理功能, 仅需部署一个站点即可实现同时发展 B/C 端业务💖 18. 👌 **快速同步上游**: 渠道设置、模型市场、价格设定等设置都可快速同步上游站点,以此基础修改自己的站点配置,快速搭建自己的站点,省时省力,一键同步,快速上线 19. 👋 **SEO 优化**: 支持 SEO 优化,支持自定义站点名称、站点 Logo 等 SEO 优化设设置使搜索引擎更快的爬取,你的站点与众不同👋 20. 🎫 **多种兑换码体系**: 支持多种兑换码体系,支持礼品码和兑换码,支持批量生成,礼品码适合宣传分发,兑换码适合发卡销售,礼品码一个类型的多个码一个用户仅能兑换一个码,在宣传中一定程度上减少一个用户兑换多次的情况😀 21. 🥰 **商用友好协议**: 采用 **Apache-2.0** 开源协议, 商用二开 & 分发友好 (也请遵守 Apache-2.0 协议的规定, 请勿用于违法用途) > ### ✨ CoAI.Dev 商业版 > ![商业版预览](./screenshot/coai-pro.png) > > - ✅ 美观商业级 UI, 漂亮的前端界面与后台管理 > - ✅ 支持 TTS & STT, 插件市场, RAG 知识库等丰富功能与模块 > - ✅ 更多支付供应商, 更多计费模式和高级订单管理 > - ✅ 支持更多鉴权方式,包括短信登录、OAuth 登录等 > - ✅ 支持模型监控,渠道健康检测,故障告警自动渠道切换 > - ✅ 支持多租户 API Key 分发系统, 企业级令牌权限管理与访问者限制 > - ✅ 支持安全审核, 日志记录, 模型限速, API Gateway 等高级功能 > - ✅ 支持推广奖励,专业数据统计,用户画像分析等商业分析能力 > - ✅ 支持Discord/Telegram/飞书等机器人对接集成能力 (扩展模块) > - ... > > [👉 了解更多](https://www.coai.dev/docs/contact) ## 🔨 支持模型 1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)* 2. Anthropic Claude *(✅ Vision ✅ Function Calling)* 3. Google Gemini & PaLM2 *(✅ Vision)* 4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)* 5. 讯飞星火 SparkDesk *(✅ Vision ✅ Function Calling)* 6. 智谱清言 ChatGLM *(✅ Vision)* 7. 通义千问 Tongyi Qwen 8. 腾讯混元 Tencent Hunyuan 9. 百川大模型 Baichuan AI 10. 月之暗面 Moonshot AI (👉 OpenAI) 11. 深度求索 DeepSeek AI (👉 OpenAI) 12. 字节云雀 ByteDance Skylark *(✅ Function Calling)* 13. Groq Cloud AI 14. OpenRouter (👉 OpenAI) 15. 360 GPT 16. LocalAI / Ollama (👉 OpenAI) ## 👻 中转 OpenAI 兼容 API - [x] Chat Completions _(/v1/chat/completions)_ - [x] Image Generation _(/v1/images)_ - [x] Model List _(/v1/models)_ - [x] Dashboard Billing _(/v1/billing)_ ## 📦 部署方式 > [!TIP] > **部署成功后, 管理员账号为 `root`, 密码默认为 `chatnio123456`** ### ✨ Zeabur (一键部署) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/M86XJI) > Zeabur 提供一定的免费额度, 可以使用非付费区域进行一键部署,同时也支持计划订阅和弹性计费等方式弹性扩展。 > 1. 点击 `Deploy` 进行部署, 并输入你希望绑定的域名,等待部署完成。 > 2. 部署完成后, 请访问你的域名, 并使用用户名 `root` 密码 `chatnio123456` 登录后台管理,请按照提示在 chatnio 后台及时修改密码。 ### 🐳 宝塔面板 (一键部署) 1. 安装宝塔面板,前往 [宝塔面板官网](https://www.bt.cn/new/download.html) 进行安装,选择正式版脚本安装。 2. 登录面板,点击左侧 **Docker** 进入 Docker 管理。 3. 如提示未安装 Docker / Docker Compose, 可根据上方引导安装。 4. 安装完成后,进入 **应用商城**,搜索 `CoAI` 并点击 **安装**。 5. 配置应用基本信息,如您的域名,端口等配置,并点击 **确认** (可使用默认配置)。 6. 首次安装可能需要等待 1-2 分钟完成数据库初始化。如遇到问题,请查看面板运行日志进行排查。 7. 访问您配置的域名或服务器 `http://[ip]:[port]`,使用用户名 `root` 和密码 `chatnio123456` 登录后台管理。 ### 阿里云计算巢 (一键部署) [![Deploy on AlibabaCloud ComputeNest](https://service-info-public.oss-cn-hangzhou.aliyuncs.com/computenest.svg)](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=CoAI社区版) 1. 访问计算巢CoAI[部署链接](https://computenest.console.aliyun.com/service/instance/create/cn-hangzhou?type=user&ServiceName=CoAI社区版),按提示填写部署参数 2. 选择付费类型,填写实例参数与网络参数,点击 **确认订单** 3. 确认部署参数并查看预估价格后,点击立即创建,等待服务实例部署完成 4. 点击左侧 **服务实例** 等待服务实例部署完成后,点击实例ID进入到详情界面 5. 点击详情界面**立即使用**中的链接,可进入CoAI社区版界面。默认用户名为`root`,密码`为chatnio123456` 登录后台管理。 6. 更多操作详情与付费信息,参见:[服务详情](https://computenest.console.aliyun.com/service/detail/cn-hangzhou/service-bfbf676bd89d434691fc/1?type=user&isRecommend=true) ### ⚡ Docker Compose 安装 (推荐) > [!NOTE] > 运行成功后, 宿主机映射地址为 `http://localhost:8000` ```shell git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git cd chatnio docker-compose up -d # 运行服务 # 如需使用 stable 版本, 请使用 docker-compose -f docker-compose.stable.yaml up -d 替代 # 如需使用 watchtower 自动更新, 请使用 docker-compose -f docker-compose.watch.yaml up -d 替代 ``` 版本更新(_开启 Watchtower 自动更新的情况下, 无需手动更新_): ```shell docker-compose down docker-compose pull docker-compose up -d ``` > - MySQL 数据库挂载目录项目 ~/**db** > - Redis 数据库挂载目录项目 ~/**redis** > - 配置文件挂载目录项目 ~/**config** ### ⚡ Docker 安装 (轻量运行时, 常用于外置 _MYSQL/RDS_ 服务) > [!NOTE] > 运行成功后, 宿主机地址为 `http://localhost:8094`。 > > 如需使用 stable 版本, 请使用 `programzmh/chatnio:stable` 替代 `programzmh/chatnio:latest` ```shell docker run -d --name chatnio \ --network host \ -v ~/config:/config \ -v ~/logs:/logs \ -v ~/storage:/storage \ -e MYSQL_HOST=localhost \ -e MYSQL_PORT=3306 \ -e MYSQL_DB=chatnio \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=chatnio123456 \ -e REDIS_HOST=localhost \ -e REDIS_PORT=6379 \ -e SECRET=secret \ -e SERVE_STATIC=true \ programzmh/chatnio:latest ``` > - *--network host* 指使用宿主机网络, 使 Docker 容器使用宿主机的网络, 可自行修改 > - SECRET: JWT 密钥, 自行生成随机字符串修改 > - SERVE_STATIC: 是否启用静态文件服务 (正常情况下不需要更改此项, 详见下方常见问题解答) > - *-v ~/config:/config* 挂载配置文件, *-v ~/logs:/logs* 挂载日志文件的宿主机目录, *-v ~/storage:/storage* 挂载附加功能的生成文件 > - 需配置 MySQL 和 Redis 服务, 请自行参考上方信息修改环境变量 版本更新 (_开启 Watchtower 后无需手动更新, 执行后按照上述步骤重新运行即可_): ```shell docker stop chatnio docker rm chatnio docker pull programzmh/chatnio:latest ``` ### ⚒ 编译安装 > [!NOTE] > 部署成功后, 默认端口为 **8094**, 访问地址为 `http://localhost:8094` > > Config 配置项 (~/config/**config.yaml**) 可以使用环境变量进行覆盖, 如 `MYSQL_HOST` 环境变量可覆盖 `mysql.host` 配置项 ```shell git clone https://github.com/coaidev/coai.git cd chatnio cd app npm install -g pnpm pnpm install pnpm build cd .. go build -o chatnio # e.g. using nohup (you can also use systemd or other service manager) nohup ./chatnio > output.log & # using nohup to run in background ``` ## ❓ 常见问题 Q&A 1. **为什么我部署后的站点可以访问页面, 可以登录注册, 但是无法使用聊天 (一直在转圈)?** - 聊天等此类功能通过 websocket 进行通信, 请确保你的服务支持 websocket。 (Tip: 中转通过 Http 实现, 无需 websocket 支持) - 如果你使用了 Nginx, Apache 等反向代理, 请确保已配置 websocket 支持。 - 如果使用了端口映射, 端口转发, CDN, API Gateway 等服务, 请确保你的服务支持并开启 websocket。 2. **我配置的 Midjourney Proxy 格式的渠道一直转圈或报错 `please provide available notify url`** - 若为转圈,请确保你的 Midjourney Proxy 服务已正常运行, 并且已配置正确的上游地址。 - **Midjourney 要填渠道类型要用 Midjourney 而不是 OpenAI (不知道为什么很多人填成了 OpenAI 类型格式然后过来反馈为什么empty response, mj-chat 类除外)** - 排查完这些问题后, 请查看你的系统设置中的**后端域名**是否已经配置并配置正确。如果不配置, 将导致 Midjourney Proxy 服务无法正常回调。 3. **此项目有什么外部依赖?** - MySQL: 存储用户信息, 对话记录, 管理员信息等持久化数据。 - Redis: 存储用户快速鉴权信息, IP 速率限制, 订阅配额, 邮箱验证码等数据。 - 环境未配置好的情况下, 会导致服务无法正常运行, 请确保你的 MySQL 和 Redis 服务已正常运行 (Docker 部署, 编译部署需自行搭建外部服务)。 4. **我的机器为 ARM 架构, 该项目支持 ARM 架构吗?** - 支持。CoAI.Dev 项目使用 BuildX 构建多架构镜像, 你可以直接使用 docker-compose 或 docker 运行, 无需额外配置。 - 如果你使用编译安装, 直接在 ARM 机器上编译即可, 无需欸外配置。如果你使用 x86 机器编译, 请使用 `GOARCH=arm64 go build -o chatnio` 进行交叉编译并上传至 ARM 机器上运行。 5. **如何修改 Root 默认密码?** - 请点击右上角头像或侧边栏底部用户框进入后台管理, 点击系统设置下常规设置操作栏的 修改 Root 密码 进行修改。或者选择在 用户管理 中选定 root 用户进行修改密码操作。 6. **系统设置中的后端域名是什么?** - 后端域名是指后端 API 服务的地址, 默认为你访问站点后加 `/api` 的地址, 如 `https://example.com/api` 。 - 如果设置为非 *SERVE_STATIC* 模式, 开启前后端分离部署, 请将后端域名设置为你的后端 API 服务地址, 如 `https://api.example.com`。 - 后端域名此处用于 Midjourney Proxy 服务的后端回调地址, 如无需使用 Midjourney Proxy 服务, 请忽略此设置。 7. **如何配置支付方式?** - CoAI.Dev 开源版支持发卡模式, 设置系统设置中的购买链接为你的发卡地址即可。卡密可通过用户管理中兑换码管理中批量生成。 8. **礼品码和兑换码有什么区别?** - 礼品码一种类型只能一个用户只能绑定一次, 而非 aff code, 发福利等方式可使用礼品码, 可在头像下拉菜单中的礼品码中兑换。 - 兑换码一种类型可以多个用户绑定, 可作为正常购买和发卡使用, 可在用户管理中的兑换码管理中批量生成, 在头像下拉菜单的点数(菜单第一个)内输入兑换码进行兑换。 - 一个例子:比如我发了一个类型为 *新年快乐* 的福利, 此时推荐使用礼品码, 假设发放 100 个 66 点数, 如果为兑换码, 手快的一个用户就批量把所有兑换码的 6600 点数都用完了, 而礼品码则可以保证每个用户只能使用一次 (获得 66 点数)。 - 而搭建发卡的时, 如果用礼品码, 因为一个类型只能兑换一次, 购买多个礼品码会导致兑换失败, 而兑换码则可以在此场景下使用。 9. **该项目支持 Vercel 部署吗?** - CoAI.Dev 本身并不支持 Vercel 部署, 但是你可以使用前后端分离模式, Vercel 部署前端部分, 后端部分使用 Docker 部署或编译部署。 10. **前后端分离部署模式是什么?** - 正常情况下, 前后端在同一服务内, 后端地址为 `/api`。前后端分离部署指前端和后端分别部署在不同的服务上, 前端服务为静态文件服务, 后端服务为 API 服务。 - 举个例子, 前端使用 Nginx (或 Vercel 等) 部署, 部署的域名为 `https://www.chatnio.net`。 - 后端使用 Docker 部署, 部署的域名为 `https://api.chatnio.net`。 - 此种部署方式需自行打包前端, 配置环境变量 `VITE_BACKEND_ENDPOINT` 为你的后端地址, 如 `https://api.chatnio.net`。 - 配置后端环境变量的 `SERVE_STATIC=false` 使后端服务不提供静态文件服务。 11. **弹性计费和订阅详解** - 弹性计费, 即 `点数`, 其图标类似于**云**, 模型计费通用方式, 为了防止虚假汇率, 写死 10 点数 = 1 元, 汇率可以在计费规则中的 **应用内置模板** 中自定义汇率。 - 订阅, 即订阅计划, 为固定价格计费方式按次配额, 订阅计费扣取点数 (举例: 如果站点的用户想订阅 32 元的计划, 则需要保证点数大于等于 320 点数) - 订阅是 Item 的组合, 每个 Item 都可设置涵盖的模型, 订阅配额 (-1 为无限使用), 名称, ID (用于区分不同的 Item), 图标等。可在后台的订阅管理中进行操作, 是否开启订阅, 订阅价格等, 修改每个订阅等级的 Item, 以及支持直接导入其他订阅等级的 Item。 - 订阅支持分层并写死为三个等级。 等级分别为: _普通用户 (0)_, _基础版订阅 (1)_, _标准版订阅 (2)_, _专业版订阅 (3)_, 订阅等级即为用户分组, 可在渠道管理中进行高级设置, 选择勾选可使用此模型的用户分组。 - 订阅配额设置, 可在订阅管理中进行操作, 是否支持中转 API (默认关闭) 12. **可请求最小点数检测 `user quota is not enough` 详解** - 为防止站点用户滥用站点模型, 当请求点数低于最小请求点数时将返回点数不足的错误信息, 大于等于最小请求点数时将正常请求。 - 模型的最小可请求点数规则: - 不计费模型无限制 - 次数计费模型最小点数为该模型的 1 次请求点数 (e.g. 若一个模型的单次请求点数为 0.1 点数, 则最小请求点数为 0.1 点数) - Token 弹性计费模型为 1K 输入 Tokens 价格 + 1K 输出 Tokens 价格 (e.g. 若一个模型的 1K 输入 Tokens 价格为 0.05 点数, 1K 输出 Tokens 价格 0.1 点数, 则最小请求点数为 0.15 点数) 13. **为何我的 GPT-4-All 等逆向模型无法使用上传文件中的图片?** - 上传模型图片为 Base64 格式, 如果逆向不支持 Base64 格式, 请使用 URL 直链而非上传文件做法。 14. **如何开始域名严格跨域检测?** - 正常情况下,后端对所有域名开放跨域。如果非特殊需求,无需开启严格跨域检测。 - 如果需要开启严格跨域检测,可以在后端环境变量中 并配置 `ALLOW_ORIGINS`, 如 `ALLOW_ORIGINS=chatnio.net,chatnio.app` (不需要加协议前缀, www 解析无需手动添加, 后端将自动识别并允许跨域), 这样就会支持严格跨域检测 (如 *http://www.chatnio.app*, *https://chatnio.net* 等将会被允许, 其他域名将会被拒绝)。 - 即使在开启严格跨域检测的情况下, /v1 接口会被仍然允许所有域的跨域请求, 以保证中转 API 的正常使用。 15. **模型映射功能是如何使用的?** - 渠道内的模型映射格式为 `[from]>[to]`, 多个映射之间换行, **from** 为请求的模型, **to** 为真实向上游发送的模型并且需要上游真实支持 - 如: 我有一个逆向渠道, 填写 `gpt-4-all>gpt-4`, 则我的用户请求 **gpt-4-all** 模型到该渠道时, 后端则会模型映射至 **gpt-4** 向该渠道请求 **gpt-4**, 此时该渠道支持 2 个模型, **gpt-4** 和 **gpt-4-all** (本质上都为 **gpt-4**) - 如果我不想让我的这个逆向渠道影响到 **gpt-4** 的渠道组, 可以加前缀 `!gpt-4-all>gpt-4`, 该渠道 **gpt-4** 则会被忽略, 此时该渠道将只支持 1 个模型, **gpt-4-all** (但本质上为 **gpt-4**) ## 📦 技术栈 - 🥗 前端: React + Redux + Radix UI + Tailwind CSS - 🍎 后端: Golang + Gin + Redis + MySQL - 🍒 应用技术: PWA + WebSocket ## 🤯 为什么写此项目 & 项目优势 我们发现,市面上的 AIGC 商业站点,大多数都是偏向于前端轻量部署的项目,有精美的 UI 界面设计, 比如 [Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) 的二开商业版本, 由于其偏向个人私有化的设计,在二开商业化时有一定的局限性,呈现出一些问题,比如: - **对话同步难**, 比如需要 WebDav 等服务,用户学习成本高,跨端实时同步困难。 - **计费不够完善**, 比如只支持弹性计费或只支持订阅制,无法满足不同用户的需求。 - **文件解析不便捷**, 比如只支持先在图床上传图片,返回站点后再在输入框中输入 URL 直链,无内置文件解析功能。 - **不支持对话 URL 分享**, 比如只支持对话截图分享,无法支持对话 URL 分享 (或仅支持 ShareGPT 等工具,无法对站点起到推广作用)。 - **渠道管理不够强大**, 比如后台仅支持 OpenAI 格式渠道,兼容其他格式渠道困难。且只能填入一个渠道,无法支持多渠道管理。 - **不支持 API 调用**, 比如只支持用户界面调用,无法支持 API 中转和管理。 另一种是偏向于 API 分发的站点,有强大的分发系统,比如基于 [One API](https://github.com/songquanpeng/one-api) 等项目, 这类项目虽然支持强大的 API 中转和管理,但是缺少界面设计,且缺少一些 C 端功能,比如: - **用户界面不够丰富**, 比如只支持 API 调用,不内置用户界面聊天。用户界面聊天需要自行复制密钥并前往其他站点才能使用,这对于普通用户来说,学习成本较高。 - **没有订阅制**, 比如只支持弹性计费,缺少对 C 端用户的计费设计,无法满足用户的不同需求,对于无基础的用户来说,成本感知不够友好。 - **C 端功能不够丰富**, 比如只支持 API 调用,不支持对话同步,不支持对话分享,不支持文件解析等功能。 - **均衡负载不够强大**, 开源版不支持**权重**参数, 无法实现同优先级的渠道均衡负载分配概率 ([New API](https://github.com/Calcium-Ion/new-api) 也解决了此痛点, UI 也更美观)。 因此,我们希望能够将这两种项目的优势结合起来,做出一个既有强大的 API 分发系统,又有丰富的用户界面设计的项目, 这样既能满足 C 端用户的需求,又能发展 B 端业务,提高用户体验,降低用户学习成本,提高用户粘性。 于是,**CoAI.Dev** 应运而生,我们希望能够做出一个既有强大的 API 分发系统,又有丰富的用户界面设计的项目,成为下一代开源 AIGC 项目的商业一站式解决方案。 ## ❤ 捐助 如果您觉得这个项目对您有所帮助, 您可以点个 Star 支持一下! ================================================ FILE: adapter/adapter.go ================================================ package adapter import ( "chat/adapter/azure" "chat/adapter/baichuan" "chat/adapter/bing" "chat/adapter/claude" adaptercommon "chat/adapter/common" "chat/adapter/coze" "chat/adapter/dashscope" "chat/adapter/deepseek" "chat/adapter/dify" "chat/adapter/hunyuan" "chat/adapter/midjourney" "chat/adapter/openai" "chat/adapter/palm2" "chat/adapter/skylark" "chat/adapter/slack" "chat/adapter/sparkdesk" "chat/adapter/zhinao" "chat/adapter/zhipuai" "chat/globals" "fmt" ) var channelFactories = map[string]adaptercommon.FactoryCreator{ globals.OpenAIChannelType: openai.NewChatInstanceFromConfig, globals.AzureOpenAIChannelType: azure.NewChatInstanceFromConfig, globals.ClaudeChannelType: claude.NewChatInstanceFromConfig, globals.SlackChannelType: slack.NewChatInstanceFromConfig, globals.BingChannelType: bing.NewChatInstanceFromConfig, globals.PalmChannelType: palm2.NewChatInstanceFromConfig, globals.SparkdeskChannelType: sparkdesk.NewChatInstanceFromConfig, globals.ChatGLMChannelType: zhipuai.NewChatInstanceFromConfig, globals.QwenChannelType: dashscope.NewChatInstanceFromConfig, globals.HunyuanChannelType: hunyuan.NewChatInstanceFromConfig, globals.BaichuanChannelType: baichuan.NewChatInstanceFromConfig, globals.SkylarkChannelType: skylark.NewChatInstanceFromConfig, globals.ZhinaoChannelType: zhinao.NewChatInstanceFromConfig, globals.MidjourneyChannelType: midjourney.NewChatInstanceFromConfig, globals.DeepseekChannelType: deepseek.NewChatInstanceFromConfig, globals.DifyChannelType: dify.NewChatInstanceFromConfig, globals.CozeChannelType: coze.NewChatInstanceFromConfig, globals.MoonshotChannelType: openai.NewChatInstanceFromConfig, // openai format globals.GroqChannelType: openai.NewChatInstanceFromConfig, // openai format } func createChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error { props.Model = conf.GetModelReflect(props.OriginalModel) props.Proxy = conf.GetProxy() factoryType := conf.GetType() if factory, ok := channelFactories[factoryType]; ok { return factory(conf).CreateStreamChatRequest(props, hook) } return fmt.Errorf("unknown channel type %s (channel #%d)", conf.GetType(), conf.GetId()) } func createVideoRequest(conf globals.ChannelConfig, props *adaptercommon.VideoProps, hook globals.Hook) error { props.Model = conf.GetModelReflect(props.OriginalModel) props.Proxy = conf.GetProxy() factoryType := conf.GetType() if creator, ok := channelFactories[factoryType]; ok { inst := creator(conf) if v, ok := inst.(adaptercommon.VideoFactory); ok { return v.CreateVideoRequest(props, hook) } return fmt.Errorf("video request not supported by channel type %s (channel #%d)", conf.GetType(), conf.GetId()) } return fmt.Errorf("unknown channel type %s (channel #%d)", conf.GetType(), conf.GetId()) } ================================================ FILE: adapter/azure/chat.go ================================================ package azure import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "strings" ) func (c *ChatInstance) GetChatEndpoint(props *adaptercommon.ChatProps) string { model := strings.ReplaceAll(props.Model, ".", "") if props.Model == globals.GPT3TurboInstruct { return fmt.Sprintf("%s/openai/deployments/%s/completions?api-version=%s", c.GetResource(), model, c.GetEndpoint()) } return fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", c.GetResource(), model, c.GetEndpoint()) } func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string { result := "" for _, message := range messages { result += fmt.Sprintf("%s: %s\n", message.Role, message.Content) } return result } func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string { if len(props.Message) == 0 { return "" } return props.Message[len(props.Message)-1].Content } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { if props.Model == globals.GPT3TurboInstruct { // for completions return CompletionRequest{ Prompt: c.GetCompletionPrompt(props.Message), MaxToken: props.MaxTokens, Stream: stream, } } return ChatRequest{ Messages: formatMessages(props), MaxToken: props.MaxTokens, Stream: stream, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, Temperature: props.Temperature, TopP: props.TopP, Tools: props.Tools, ToolChoice: props.ToolChoice, } } // CreateChatRequest is the native http request body for openai func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { if globals.IsOpenAIDalleModel(props.Model) { return c.CreateImage(props) } res, err := utils.Post( c.GetChatEndpoint(props), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("openai error: %s", err.Error()) } data := utils.MapToStruct[ChatResponse](res) if data == nil { return "", fmt.Errorf("openai error: cannot parse response") } else if data.Error.Message != "" { return "", fmt.Errorf("openai error: %s", data.Error.Message) } return data.Choices[0].Message.Content, nil } // CreateStreamChatRequest is the stream response body for openai func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { if globals.IsOpenAIDalleModel(props.Model) { if url, err := c.CreateImage(props); err != nil { return err } else { return callback(&globals.Chunk{Content: url}) } } isCompletionType := props.Model == globals.GPT3TurboInstruct ticks := 0 err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(props), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), Callback: func(data string) error { ticks += 1 partial, err := c.ProcessLine(data, isCompletionType) if err != nil { return err } return callback(partial) }, }, props.Proxy) if err != nil { if form := processChatErrorResponse(err.Body); form != nil { msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type) return errors.New(msg) } return err.Error } if ticks == 0 { return errors.New("no response") } return nil } ================================================ FILE: adapter/azure/image.go ================================================ package azure import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) type ImageProps struct { Model string Prompt string Size ImageSize Proxy globals.ProxyConfig } func (c *ChatInstance) GetImageEndpoint(model string) string { model = strings.ReplaceAll(model, ".", "") return fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", c.GetResource(), model, c.GetEndpoint()) } // CreateImageRequest will create a dalle image from prompt, return url of image, base64 data and error func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, string, error) { res, err := utils.Post( c.GetImageEndpoint(props.Model), c.GetHeader(), ImageRequest{ Prompt: props.Prompt, Size: utils.Multi[ImageSize]( props.Model == globals.Dalle3 || props.Model == globals.GPTImage1, ImageSize1024, ImageSize512, ), N: 1, }, props.Proxy) if err != nil || res == nil { return "", "", fmt.Errorf("openai error: %s", err.Error()) } data := utils.MapToStruct[ImageResponse](res) if data == nil { return "", "", fmt.Errorf("openai error: cannot parse response") } else if data.Error.Message != "" { return "", "", fmt.Errorf("openai error: %s", data.Error.Message) } // for gpt-image-1, return base64 data if available if props.Model == globals.GPTImage1 && data.Data[0].B64Json != "" { return "", data.Data[0].B64Json, nil } return data.Data[0].Url, "", nil } // CreateImage will create a dalle image from prompt, return markdown of image func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) { url, b64Json, err := c.CreateImageRequest(ImageProps{ Model: props.Model, Prompt: c.GetLatestPrompt(props), Proxy: props.Proxy, }) if err != nil { if strings.Contains(err.Error(), "safety") { return err.Error(), nil } return "", err } if b64Json != "" { return utils.GetBase64ImageMarkdown(b64Json), nil } return utils.GetImageMarkdown(url), nil } ================================================ FILE: adapter/azure/processor.go ================================================ package azure import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "regexp" ) func formatMessages(props *adaptercommon.ChatProps) interface{} { if globals.IsVisionModel(props.Model) { return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message { if message.Role == globals.User { raw, urls := utils.ExtractImages(message.Content, true) images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent { obj, err := utils.NewImage(url) if err != nil { globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "..."))) return nil } props.Buffer.AddImage(obj) return &MessageContent{ Type: "image_url", ImageUrl: &ImageUrl{ Url: url, }, } }) return Message{ Role: message.Role, Content: utils.Prepend(images, MessageContent{ Type: "text", Text: &raw, }), Name: message.Name, FunctionCall: message.FunctionCall, ToolCalls: message.ToolCalls, ToolCallId: message.ToolCallId, } } return Message{ Role: message.Role, Content: MessageContents{ MessageContent{ Type: "text", Text: &message.Content, }, }, Name: message.Name, FunctionCall: message.FunctionCall, ToolCalls: message.ToolCalls, ToolCallId: message.ToolCallId, } }) } return props.Message } func processChatResponse(data string) *ChatStreamResponse { return utils.UnmarshalForm[ChatStreamResponse](data) } func processCompletionResponse(data string) *CompletionResponse { return utils.UnmarshalForm[CompletionResponse](data) } func processChatErrorResponse(data string) *ChatStreamErrorResponse { return utils.UnmarshalForm[ChatStreamErrorResponse](data) } func getChoices(form *ChatStreamResponse) *globals.Chunk { if len(form.Choices) == 0 { return &globals.Chunk{Content: ""} } choice := form.Choices[0].Delta return &globals.Chunk{ Content: choice.Content, ToolCall: choice.ToolCalls, FunctionCall: choice.FunctionCall, } } func getCompletionChoices(form *CompletionResponse) string { if len(form.Choices) == 0 { return "" } return form.Choices[0].Text } func getRobustnessResult(chunk string) string { exp := `\"content\":\"(.*?)\"` compile, err := regexp.Compile(exp) if err != nil { return "" } matches := compile.FindStringSubmatch(chunk) if len(matches) > 1 { return utils.ProcessRobustnessChar(matches[1]) } else { return "" } } func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) { if isCompletionType { // openai legacy support if completion := processCompletionResponse(data); completion != nil { return &globals.Chunk{ Content: getCompletionChoices(completion), }, nil } globals.Warn(fmt.Sprintf("openai error: cannot parse completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response") } if form := processChatResponse(data); form != nil { return getChoices(form), nil } if form := processChatErrorResponse(data); form != nil { return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("openai error: %s (type: %s)", form.Error.Message, form.Error.Type)) } globals.Warn(fmt.Sprintf("openai error: cannot parse chat completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response") } ================================================ FILE: adapter/azure/struct.go ================================================ package azure import ( factory "chat/adapter/common" "chat/globals" ) type ChatInstance struct { Endpoint string ApiKey string Resource string } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetResource() string { return c.Resource } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "api-key": c.GetApiKey(), } } func NewChatInstance(endpoint, apiKey string, resource string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, Resource: resource, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { param := conf.SplitRandomSecret(2) return NewChatInstance( conf.GetEndpoint(), param[0], param[1], ) } ================================================ FILE: adapter/azure/types.go ================================================ package azure import "chat/globals" type ImageUrl struct { Url string `json:"url"` Detail *string `json:"detail,omitempty"` } type MessageContent struct { Type string `json:"type"` Text *string `json:"text,omitempty"` ImageUrl *ImageUrl `json:"image_url,omitempty"` } type MessageContents []MessageContent type Message struct { Role string `json:"role"` Content MessageContents `json:"content"` Name *string `json:"name,omitempty"` FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role } // ChatRequest is the request body for openai type ChatRequest struct { Model string `json:"model"` Messages interface{} `json:"messages"` MaxToken *int `json:"max_tokens,omitempty"` MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` Stream bool `json:"stream"` PresencePenalty *float32 `json:"presence_penalty,omitempty"` FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` Tools *globals.FunctionTools `json:"tools,omitempty"` ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object } // CompletionRequest is the request body for openai completion type CompletionRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` MaxToken *int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` } // ChatResponse is the native http request body for openai type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Index int `json:"index"` Message globals.Message `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Error struct { Message string `json:"message"` } `json:"error"` } // ChatStreamResponse is the stream response body for openai type ChatStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Delta globals.Message `json:"delta"` Index int `json:"index"` FinishReason string `json:"finish_reason"` } `json:"choices"` } // CompletionResponse is the native http request body / stream response body for openai completion type CompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Text string `json:"text"` Index int `json:"index"` } `json:"choices"` } type ChatStreamErrorResponse struct { Error struct { Message string `json:"message"` Type string `json:"type"` } `json:"error"` } type ImageSize string // ImageRequest is the request body for openai dalle image generation type ImageRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` Size ImageSize `json:"size"` N int `json:"n"` } type ImageResponse struct { Data []struct { Url string `json:"url,omitempty"` B64Json string `json:"b64_json,omitempty"` } `json:"data"` Error struct { Message string `json:"message"` } `json:"error"` Usage *struct { InputTokens int `json:"input_tokens"` InputTokensDetails struct { ImageTokens int `json:"image_tokens"` TextTokens int `json:"text_tokens"` } `json:"input_tokens_details"` OutputTokens int `json:"output_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage,omitempty"` } var ( ImageSize256 ImageSize = "256x256" ImageSize512 ImageSize = "512x512" ImageSize1024 ImageSize = "1024x1024" ) ================================================ FILE: adapter/baichuan/chat.go ================================================ package baichuan import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" ) func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/v1/chat/completions", c.GetEndpoint()) } func (c *ChatInstance) GetModel(model string) string { switch model { case globals.Baichuan53B: return "Baichuan2" default: return model } } func (c *ChatInstance) GetMessages(messages []globals.Message) []globals.Message { for _, message := range messages { if message.Role == globals.System || message.Role == globals.Tool { message.Role = globals.User } } return messages } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) ChatRequest { return ChatRequest{ Model: c.GetModel(props.Model), Messages: c.GetMessages(props.Message), Stream: stream, TopP: props.TopP, TopK: props.TopK, Temperature: props.Temperature, } } // CreateChatRequest is the native http request body for baichuan func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { res, err := utils.Post( c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("baichuan error: %s", err.Error()) } data := utils.MapToStruct[ChatResponse](res) if data == nil { return "", fmt.Errorf("baichuan error: cannot parse response") } else if data.Error.Message != "" { return "", fmt.Errorf("baichuan error: %s", data.Error.Message) } return data.Choices[0].Message.Content, nil } // CreateStreamChatRequest is the stream response body for baichuan func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), Callback: func(data string) error { partial, err := c.ProcessLine(data) if err != nil { return err } return callback(partial) }, }, props.Proxy) if err != nil { if form := processChatErrorResponse(err.Body); form != nil { msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type) return errors.New(msg) } return err.Error } return nil } ================================================ FILE: adapter/baichuan/processor.go ================================================ package baichuan import ( "chat/globals" "chat/utils" "errors" "fmt" ) func processChatResponse(data string) *ChatStreamResponse { return utils.UnmarshalForm[ChatStreamResponse](data) } func processChatErrorResponse(data string) *ChatStreamErrorResponse { return utils.UnmarshalForm[ChatStreamErrorResponse](data) } func getChoices(form *ChatStreamResponse) *globals.Chunk { if len(form.Choices) == 0 { return &globals.Chunk{Content: ""} } choice := form.Choices[0].Delta return &globals.Chunk{ Content: choice.Content, } } func (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) { if form := processChatResponse(data); form != nil { return getChoices(form), nil } if form := processChatErrorResponse(data); form != nil { return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("baichuan error: %s (type: %s)", form.Error.Message, form.Error.Type)) } globals.Warn(fmt.Sprintf("baichuan error: cannot parse chat completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response") } ================================================ FILE: adapter/baichuan/struct.go ================================================ package baichuan import ( factory "chat/adapter/common" "chat/globals" "fmt" ) type ChatInstance struct { Endpoint string ApiKey string } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), } } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/baichuan/types.go ================================================ package baichuan import "chat/globals" // Baichuan AI API is similar to OpenAI API type ChatRequest struct { Model string `json:"model"` Messages []globals.Message `json:"messages"` Stream bool `json:"stream"` TopP *float32 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` Temperature *float32 `json:"temperature,omitempty"` WithSearchEnhance *bool `json:"with_search_enhance,omitempty"` } // ChatResponse is the native http request body for baichuan type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Message struct { Content string `json:"content"` } } `json:"choices"` Error struct { Message string `json:"message"` } `json:"error"` } // ChatStreamResponse is the stream response body for baichuan type ChatStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Delta struct { Content string `json:"content"` } Index int `json:"index"` } `json:"choices"` } type ChatStreamErrorResponse struct { Error struct { Message string `json:"message"` Type string `json:"type"` } `json:"error"` } ================================================ FILE: adapter/bing/chat.go ================================================ package bing import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error { var conn *utils.WebSocket if conn = utils.NewWebsocketClient(c.GetEndpoint()); conn == nil { return fmt.Errorf("bing error: websocket connection failed") } defer conn.DeferClose() model := strings.TrimPrefix(props.Model, "bing-") prompt := props.Message[len(props.Message)-1].Content if err := conn.SendJSON(&ChatRequest{ Prompt: prompt, Hash: c.Secret, Model: model, }); err != nil { return err } for { form, err := utils.ReadForm[ChatResponse](conn) if err != nil { if strings.Contains(err.Error(), "websocket: close 1000") { return nil } globals.Debug(fmt.Sprintf("bing error: %s", err.Error())) return nil } if err := hook(&globals.Chunk{ Content: form.Response, }); err != nil { return err } } } ================================================ FILE: adapter/bing/struct.go ================================================ package bing import ( factory "chat/adapter/common" "chat/globals" "fmt" ) type ChatInstance struct { Endpoint string Secret string } func (c *ChatInstance) GetEndpoint() string { return fmt.Sprintf("%s/chat", c.Endpoint) } func NewChatInstance(endpoint, secret string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, Secret: secret, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/bing/types.go ================================================ package bing // see https://github.com/Deeptrain-Community/chatnio-bing-service type ChatRequest struct { Prompt string `json:"prompt"` Hash string `json:"hash"` Model string `json:"model"` } type ChatResponse struct { Response string `json:"response"` } ================================================ FILE: adapter/claude/chat.go ================================================ package claude import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" ) const defaultTokens = 2500 func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/v1/messages", c.GetEndpoint()) } func (c *ChatInstance) GetChatHeaders() map[string]string { return map[string]string{ "content-type": "application/json", "anthropic-version": "2023-06-01", "x-api-key": c.GetApiKey(), } } // ConvertCompletionMessage converts the completion message to anthropic complete format (deprecated) func (c *ChatInstance) ConvertCompletionMessage(message []globals.Message) string { mapper := map[string]string{ globals.System: "Assistant", globals.User: "Human", globals.Assistant: "Assistant", } var result string for i, item := range message { if item.Role == globals.Tool { continue } if i == 0 && item.Role == globals.Assistant { // skip first assistant message continue } result += fmt.Sprintf("\n\n%s: %s", mapper[item.Role], item.Content) } return fmt.Sprintf("%s\n\nAssistant:", result) } func (c *ChatInstance) GetTokens(props *adaptercommon.ChatProps) int { if props.MaxTokens == nil || *props.MaxTokens <= 0 { return defaultTokens } return *props.MaxTokens } func (c *ChatInstance) ConvertMessages(props *adaptercommon.ChatProps) []globals.Message { // anthropic api: top message must be user message, only `user` and `assistant` role messages are allowd start := false result := make([]globals.Message, 0) for _, message := range props.Message { if message.Role == globals.System { continue } // if is first message, set it to user message if !start { start = true result = append(result, globals.Message{ Role: globals.User, Content: message.Content, }) continue } // anthropic api does not allow multi-same role messages if len(result) > 0 && result[len(result)-1].Role == message.Role { result[len(result)-1].Content += "\n" + message.Content continue } result = append(result, message) } return result } func (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message { converted := c.ConvertMessages(props) return utils.Each(converted, func(message globals.Message) Message { if !globals.IsVisionModel(props.Model) || message.Role != globals.User { return Message{ Role: message.Role, Content: message.Content, } } content, urls := utils.ExtractImages(message.Content, true) images := utils.EachNotNil(urls, func(url string) *MessageContent { obj, err := utils.NewImage(url) props.Buffer.AddImage(obj) if err != nil { globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "..."))) } i := utils.NewImageContent(url) return &MessageContent{ Type: "image", Source: &MessageImage{ Type: "base64", MediaType: i.GetType(), Data: i.ToRawBase64(), }, } }) return Message{ Role: message.Role, Content: utils.Prepend(images, MessageContent{ Type: "text", Text: &content, }), } }) } func (c *ChatInstance) GetSystemPrompt(props *adaptercommon.ChatProps) (prompt string) { for _, message := range props.Message { if message.Role == globals.System { prompt += message.Content } } return } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) *ChatBody { messages := c.GetMessages(props) return &ChatBody{ Messages: messages, MaxTokens: c.GetTokens(props), Model: props.Model, System: c.GetSystemPrompt(props), Stream: stream, Temperature: props.Temperature, TopP: props.TopP, TopK: props.TopK, } } func (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) { if form := processChatResponse(data); form != nil { return &globals.Chunk{ Content: form.Delta.Text, }, nil } if form := processChatErrorResponse(data); form != nil { return &globals.Chunk{Content: ""}, fmt.Errorf("anthropic error: %s (type: %s)", form.Error.Message, form.Error.Type) } return &globals.Chunk{Content: ""}, nil } func processChatErrorResponse(data string) *ChatErrorResponse { if form := utils.UnmarshalForm[ChatErrorResponse](data); form != nil { return form } return nil } func processChatResponse(data string) *ChatStreamResponse { if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil { return form } return nil } // CreateStreamChatRequest is the stream request for anthropic claude func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error { err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(), Headers: c.GetChatHeaders(), Body: c.GetChatBody(props, true), Callback: func(data string) error { partial, err := c.ProcessLine(data) if err != nil { return err } return hook(partial) }, }, props.Proxy, ) if err != nil { if form := processChatErrorResponse(err.Body); form != nil { if form.Error.Type == "" && form.Error.Message == "" { return errors.New(utils.ToMarkdownCode("json", err.Body)) } return errors.New(fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type)) } return fmt.Errorf("%s\n%s", err.Error, errors.New(utils.ToMarkdownCode("json", err.Body))) } return nil } ================================================ FILE: adapter/claude/struct.go ================================================ package claude import ( factory "chat/adapter/common" "chat/globals" ) type ChatInstance struct { Endpoint string ApiKey string } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } ================================================ FILE: adapter/claude/types.go ================================================ package claude // ChatBody is the request body for anthropic claude type Message struct { Role string `json:"role"` Content interface{} `json:"content"` } type MessageImage struct { Type string `json:"type"` MediaType interface{} `json:"media_type"` Data interface{} `json:"data"` } type MessageContent struct { Type string `json:"type"` Text *string `json:"text,omitempty"` Source *MessageImage `json:"source,omitempty"` } type ChatBody struct { Messages []Message `json:"messages"` MaxTokens int `json:"max_tokens"` Model string `json:"model"` System string `json:"system"` Stream bool `json:"stream"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` } type ChatStreamResponse struct { Type string `json:"type"` Index int `json:"index"` Delta struct { Type string `json:"type"` Text string `json:"text"` } `json:"delta"` } type ChatErrorResponse struct { Error struct { Type string `json:"type" binding:"required"` Message string `json:"message"` } `json:"error"` } ================================================ FILE: adapter/common/interface.go ================================================ package adaptercommon import ( "chat/globals" ) type Factory interface { CreateStreamChatRequest(props *ChatProps, hook globals.Hook) error } type VideoFactory interface { CreateVideoRequest(props *VideoProps, hook globals.Hook) error } type FactoryCreator func(globals.ChannelConfig) Factory ================================================ FILE: adapter/common/types.go ================================================ package adaptercommon import ( "chat/globals" "chat/utils" ) type RequestProps struct { MaxRetries *int `json:"-"` Current int `json:"-"` Group string `json:"-"` Proxy globals.ProxyConfig `json:"-"` } type VideoProps struct { RequestProps Model string `json:"model,omitempty"` OriginalModel string `json:"-"` Prompt string `json:"prompt"` Seconds *string `json:"seconds,omitempty"` Size *string `json:"size,omitempty"` InputReference *string `json:"input_reference,omitempty"` User string `json:"-"` } type ChatProps struct { RequestProps Model string `json:"model,omitempty"` OriginalModel string `json:"-"` Message []globals.Message `json:"messages,omitempty"` MaxTokens *int `json:"max_tokens,omitempty"` PresencePenalty *float32 `json:"presence_penalty,omitempty"` FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` Tools *globals.FunctionTools `json:"tools,omitempty"` ToolChoice *interface{} `json:"tool_choice,omitempty"` Buffer *utils.Buffer `json:"-"` } func (c *ChatProps) SetupBuffer(buf *utils.Buffer) { buf.SetPrompts(c) c.Buffer = buf } func CreateChatProps(props *ChatProps, buffer *utils.Buffer) *ChatProps { props.SetupBuffer(buffer) return props } func CreateVideoProps(props *VideoProps) *VideoProps { return props } ================================================ FILE: adapter/coze/chat.go ================================================ package coze import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "encoding/json" "errors" "fmt" "strings" "sync" "time" ) type ChatInstance struct { Endpoint string ApiKey string AutoSaveHistory bool responseComplete bool } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), } } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, AutoSaveHistory: false, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/v3/chat", c.GetEndpoint()) } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { additionalMessages := []EnterMessage{} for _, msg := range props.Message { enterMsg := EnterMessage{ Role: msg.Role, Content: msg.Content, ContentType: "text", } if msg.Role == "user" { enterMsg.Type = "question" } else if msg.Role == "assistant" { enterMsg.Type = "answer" } additionalMessages = append(additionalMessages, enterMsg) } // `user_id` is required in coze timestamp := time.Now().UnixNano() userID := fmt.Sprintf("user_%d", timestamp) return ChatRequest{ BotID: props.Model, UserID: userID, AdditionalMessages: additionalMessages, Stream: stream, AutoSaveHistory: c.AutoSaveHistory, } } func (c *ChatInstance) ProcessLine(data string) (string, error) { if c.responseComplete { return "", nil } if data == "" { return "", nil } chunk, complete, err := processStreamResponse(data) if err != nil { return "", err } if complete { c.responseComplete = true } return chunk.Content, nil } func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { // TODO: use standard non-stream request c.AutoSaveHistory = true res, err := utils.Post( c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("coze error: %s", err.Error()) } responseBody := utils.Marshal(res) response := processChatResponse(responseBody) if response == nil { return "", fmt.Errorf("coze error: cannot parse response") } if response.Code != 0 { return "", fmt.Errorf("coze error: %s (code: %d)", response.Msg, response.Code) } var responseContent string var responseMutex sync.Mutex err = c.CreateStreamChatRequest(props, func(chunk *globals.Chunk) error { responseMutex.Lock() defer responseMutex.Unlock() responseContent += chunk.Content return nil }) if err != nil { return "", err } if responseContent == "" { return "", fmt.Errorf("coze error: empty response from API") } return responseContent, nil } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { c.responseComplete = false c.AutoSaveHistory = false err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), FullSSE: true, Callback: func(data string) error { partial, err := c.ProcessLine(data) if err != nil { return err } if partial != "" { err = callback(&globals.Chunk{Content: partial}) if err != nil { return err } } return nil }, }, props.Proxy) c.responseComplete = true if err != nil { if strings.Contains(err.Body, "\"code\":") { errorResp := processChatErrorResponse(err.Body) if errorResp != nil && errorResp.Data.Code != 0 { return errors.New(fmt.Sprintf("coze error: %s (code: %d)", errorResp.Data.Msg, errorResp.Data.Code)) } var genericResp map[string]interface{} if jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil { errMsg, _ := json.Marshal(genericResp) return errors.New(fmt.Sprintf("coze error: %s", string(errMsg))) } } if err.Error != nil { return err.Error } return errors.New(fmt.Sprintf("coze error: unexpected error in stream request")) } return nil } ================================================ FILE: adapter/coze/processor.go ================================================ package coze import ( "chat/globals" "chat/utils" "errors" "fmt" "strconv" "strings" ) func processChatResponse(data string) *ChatResponse { if form := utils.UnmarshalForm[ChatResponse](data); form != nil { return form } return nil } func processChatStreamResponse(data string) *ChatStreamResponse { if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil { return form } return nil } func processChatStreamData(data string) *ChatStreamData { if form := utils.UnmarshalForm[ChatStreamData](data); form != nil { return form } return nil } func processChatErrorResponse(data string) *ChatStreamErrorResponse { if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { return form } return nil } func processSSEData(data string) (event string, eventData string, err error) { if data == "" { return "", "", nil } sseLines := strings.Split(data, "\n") for _, line := range sseLines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "event:") { event = strings.TrimSpace(strings.TrimPrefix(line, "event:")) } else if strings.HasPrefix(line, "data:") { eventData = strings.TrimSpace(strings.TrimPrefix(line, "data:")) } } if eventData == "" { return "", "", nil } if strings.HasPrefix(eventData, "\"") && strings.HasSuffix(eventData, "\"") && len(eventData) > 2 { unquoted, err := strconv.Unquote(eventData) if err == nil { eventData = unquoted } } return event, eventData, nil } func processEventContent(event string, eventData string) (content string, complete bool, err error) { switch event { case "conversation.message.delta": content, _ := parseEventContent(event, eventData) if content != "" { return content, false, nil } streamData := processChatStreamData(eventData) if streamData != nil && streamData.Type == "answer" && streamData.Role == "assistant" && streamData.Content != "" { return streamData.Content, false, nil } case "conversation.message.completed": return "", false, nil case "conversation.chat.completed": return "", true, nil case "conversation.chat.failed": streamData := processChatStreamData(eventData) if streamData != nil { if streamData.Code != 0 && streamData.Msg != "" { return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", streamData.Msg, streamData.Code)) } } return "", false, errors.New("coze error: conversation failed") case "done": return "", true, nil } errorResp := processChatErrorResponse(eventData) if errorResp != nil && errorResp.Data.Code != 0 { return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", errorResp.Data.Msg, errorResp.Data.Code)) } streamData := processChatStreamData(eventData) if streamData != nil { if streamData.Code != 0 && streamData.Msg != "" { return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", streamData.Msg, streamData.Code)) } if streamData.LastError.Code != 0 && streamData.LastError.Msg != "" { return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", streamData.LastError.Msg, streamData.LastError.Code)) } } return "", false, nil } func parseEventContent(eventType string, eventData string) (string, error) { if eventType == "conversation.message.delta" { streamResp := processChatStreamResponse(fmt.Sprintf(`{"event":"%s","data":%s}`, eventType, eventData)) if streamResp != nil { streamData := processChatStreamData(streamResp.Data) if streamData != nil && streamData.Type == "answer" && streamData.Role == "assistant" && streamData.Content != "" { return streamData.Content, nil } } } return "", nil } func processStreamResponse(data string) (*globals.Chunk, bool, error) { event, eventData, err := processSSEData(data) if err != nil { return nil, false, err } if event == "" || eventData == "" { return &globals.Chunk{Content: ""}, false, nil } content, complete, err := processEventContent(event, eventData) if err != nil { return nil, false, err } return &globals.Chunk{ Content: content, }, complete, nil } ================================================ FILE: adapter/coze/struct.go ================================================ package coze type ChatRequest struct { BotID string `json:"bot_id"` UserID string `json:"user_id"` AdditionalMessages []EnterMessage `json:"additional_messages,omitempty"` Stream bool `json:"stream"` CustomVariables map[string]string `json:"custom_variables,omitempty"` AutoSaveHistory bool `json:"auto_save_history"` MetaData map[string]string `json:"meta_data,omitempty"` ExtraParams map[string]string `json:"extra_params,omitempty"` ShortcutCommand *ShortcutCommand `json:"shortcut_command,omitempty"` } type EnterMessage struct { Role string `json:"role"` Type string `json:"type,omitempty"` Content string `json:"content,omitempty"` ContentType string `json:"content_type,omitempty"` MetaData map[string]string `json:"meta_data,omitempty"` } type ShortcutCommand struct { // TODO: support for adding this on demand } type ObjectString struct { Type string `json:"type"` Text string `json:"text,omitempty"` FileID string `json:"file_id,omitempty"` FileURL string `json:"file_url,omitempty"` } type ChatResponse struct { Data struct { ID string `json:"id"` ConversationID string `json:"conversation_id"` BotID string `json:"bot_id"` CreatedAt int64 `json:"created_at"` CompletedAt int64 `json:"completed_at"` LastError interface{} `json:"last_error"` MetaData map[string]string `json:"meta_data"` Status string `json:"status"` Usage *Usage `json:"usage"` } `json:"data"` Code int `json:"code"` Msg string `json:"msg"` } type Usage struct { TokenCount int `json:"token_count"` OutputTokens int `json:"output_tokens"` InputTokens int `json:"input_tokens"` } type ChatStreamResponse struct { Event string `json:"event"` Data string `json:"data"` } type ChatStreamData struct { ID string `json:"id,omitempty"` Role string `json:"role,omitempty"` Type string `json:"type,omitempty"` Content string `json:"content,omitempty"` ContentType string `json:"content_type,omitempty"` ChatID string `json:"chat_id,omitempty"` ConversationID string `json:"conversation_id,omitempty"` BotID string `json:"bot_id,omitempty"` SectionID string `json:"section_id,omitempty"` CreatedAt int64 `json:"created_at,omitempty"` CompletedAt int64 `json:"completed_at,omitempty"` UpdatedAt int64 `json:"updated_at,omitempty"` Status string `json:"status,omitempty"` LastError struct { Code int `json:"code"` Msg string `json:"msg"` } `json:"last_error,omitempty"` Code int `json:"code"` Msg string `json:"msg"` Usage *Usage `json:"usage,omitempty"` MetaData map[string]string `json:"meta_data,omitempty"` FromModule interface{} `json:"from_module,omitempty"` FromUnit interface{} `json:"from_unit,omitempty"` } type ChatStreamErrorResponse struct { Event string `json:"event"` Data struct { Code int `json:"code"` Msg string `json:"msg"` } `json:"data"` } ================================================ FILE: adapter/dashscope/chat.go ================================================ package dashscope import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) const defaultMaxTokens = 1500 func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), "X-DashScope-SSE": "enable", } } func (c *ChatInstance) FormatMessages(message []globals.Message) []Message { var messages []Message var start bool for _, v := range message { if v.Role == globals.Tool { continue } if !start { start = true // dashscope first message should be [`user`, `system`] role, convert other roles to `user` if v.Role != globals.User && v.Role != globals.System { v.Role = globals.User } } messages = append(messages, Message{ Role: v.Role, Content: v.Content, }) } return messages } func (c *ChatInstance) GetMaxTokens(props *adaptercommon.ChatProps) int { // dashscope has a restriction of 1500 tokens in completion if props.MaxTokens == nil || *props.MaxTokens <= 0 || *props.MaxTokens > 1500 { return defaultMaxTokens } return *props.MaxTokens } func (c *ChatInstance) GetTopP(props *adaptercommon.ChatProps) *float32 { // range of top_p should be (0.0, 1.0) if props.TopP == nil { return nil } if *props.TopP <= 0.0 { return utils.ToPtr[float32](0.1) } else if *props.TopP >= 1.0 { return utils.ToPtr[float32](0.9) } return props.TopP } func (c *ChatInstance) GetRepeatPenalty(props *adaptercommon.ChatProps) *float32 { // range of repetition_penalty should greater than 0.0 if props.RepetitionPenalty == nil { return nil } if *props.RepetitionPenalty <= 0.0 { return utils.ToPtr[float32](0.1) } return props.RepetitionPenalty } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps) ChatRequest { return ChatRequest{ Model: strings.TrimSuffix(props.Model, "-net"), Input: ChatInput{ Messages: c.FormatMessages(props.Message), }, Parameters: ChatParam{ MaxTokens: c.GetMaxTokens(props), Temperature: props.Temperature, TopP: c.GetTopP(props), TopK: props.TopK, RepetitionPenalty: c.GetRepeatPenalty(props), EnableSearch: utils.ToPtr(strings.HasSuffix(props.Model, "-net")), IncrementalOutput: true, }, } } func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/api/v1/services/aigc/text-generation/generation", c.Endpoint) } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { return utils.EventSource( "POST", c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props), func(data string) error { // example: // id:1 // event:result // :HTTP_STATUS/200 // data:{"output":{"finish_reason":"null","text":"hi"},"usage":{"total_tokens":15,"input_tokens":14,"output_tokens":1},"request_id":"08da1369-e009-9f8f-8363-54b966f80daf"} data = strings.TrimSpace(data) if !strings.HasPrefix(data, "data:") { return nil } slice := strings.TrimSpace(strings.TrimPrefix(data, "data:")) if form := utils.UnmarshalForm[ChatResponse](slice); form != nil { if form.Output.Text == "" && form.Message != "" { return fmt.Errorf("dashscope error: %s", form.Message) } if err := callback(&globals.Chunk{Content: form.Output.Text}); err != nil { return err } return nil } globals.Debug(fmt.Sprintf("dashscope error: cannot unmarshal data %s", slice)) return nil }, props.Proxy, ) } ================================================ FILE: adapter/dashscope/struct.go ================================================ package dashscope import ( factory "chat/adapter/common" "chat/globals" ) type ChatInstance struct { Endpoint string ApiKey string } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func NewChatInstance(endpoint string, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/dashscope/types.go ================================================ package dashscope // ChatRequest is the request body for dashscope type ChatRequest struct { Model string `json:"model"` Input ChatInput `json:"input"` Parameters ChatParam `json:"parameters"` } type Message struct { Role string `json:"role"` Content string `json:"content"` } type ChatInput struct { Messages []Message `json:"messages"` } type ChatParam struct { IncrementalOutput bool `json:"incremental_output"` EnableSearch *bool `json:"enable_search,omitempty"` MaxTokens int `json:"max_tokens"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` } // ChatResponse is the response body for dashscope type ChatResponse struct { Output struct { FinishReason string `json:"finish_reason"` Text string `json:"text"` } `json:"output"` RequestId string `json:"request_id"` Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } `json:"usage"` Message string `json:"message"` } ================================================ FILE: adapter/deepseek/chat.go ================================================ package deepseek import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" ) type ChatInstance struct { Endpoint string ApiKey string isFirstReasoning bool isReasonOver bool } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), } } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, isFirstReasoning: true, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/chat/completions", c.GetEndpoint()) } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { messages := props.Message // because of deepseek first message must be user role // convert assistant message to user message if len(messages) > 0 && messages[0].Role == globals.Assistant { messages = make([]globals.Message, len(props.Message)) copy(messages, props.Message) messages[0].Role = globals.User } return ChatRequest{ Model: props.Model, Messages: messages, MaxTokens: props.MaxTokens, Stream: stream, Temperature: props.Temperature, TopP: props.TopP, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, } } func processChatResponse(data string) *ChatResponse { if form := utils.UnmarshalForm[ChatResponse](data); form != nil { return form } return nil } func processChatStreamResponse(data string) *ChatStreamResponse { if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil { return form } return nil } func processChatErrorResponse(data string) *ChatStreamErrorResponse { if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { return form } return nil } func (c *ChatInstance) ProcessLine(data string) (string, error) { if form := processChatStreamResponse(data); form != nil { if len(form.Choices) == 0 { return "", nil } delta := form.Choices[0].Delta if c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil { c.isReasonOver = true if delta.Content != "" { return fmt.Sprintf("\n\n\n%s", delta.Content), nil } return "\n\n\n", nil } if delta.ReasoningContent != nil { content := *delta.ReasoningContent if c.isFirstReasoning { c.isFirstReasoning = false return fmt.Sprintf("\n%s", content), nil } return content, nil } return delta.Content, nil } if form := processChatErrorResponse(data); form != nil { if form.Error.Message != "" { return "", errors.New(fmt.Sprintf("deepseek error: %s", form.Error.Message)) } } return "", nil } func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { res, err := utils.Post( c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("deepseek error: %s", err.Error()) } data := utils.MapToStruct[ChatResponse](res) if data == nil { return "", fmt.Errorf("deepseek error: cannot parse response") } if len(data.Choices) == 0 { return "", fmt.Errorf("deepseek error: no choices") } message := data.Choices[0].Message content := message.Content if message.ReasoningContent != nil { content = fmt.Sprintf("\n%s\n\n\n%s", *message.ReasoningContent, content) } return content, nil } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { c.isFirstReasoning = true c.isReasonOver = false err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), Callback: func(data string) error { partial, err := c.ProcessLine(data) if err != nil { return err } return callback(&globals.Chunk{Content: partial}) }, }, props.Proxy) if err != nil { if form := processChatErrorResponse(err.Body); form != nil { if form.Error.Type == "" && form.Error.Message == "" { return errors.New(utils.ToMarkdownCode("json", err.Body)) } return errors.New(fmt.Sprintf("deepseek error: %s (type: %s)", form.Error.Message, form.Error.Type)) } return err.Error } return nil } ================================================ FILE: adapter/deepseek/struct.go ================================================ package deepseek import ( "chat/globals" ) // DeepSeek API is similar to OpenAI API with additional reasoning content type ChatRequest struct { Model string `json:"model"` Messages []globals.Message `json:"messages"` MaxTokens *int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` PresencePenalty *float32 `json:"presence_penalty,omitempty"` FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` } // ChatResponse is the native http request body for deepseek type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Index int `json:"index"` Message globals.Message `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` } // ChatStreamResponse is the stream response body for deepseek type ChatStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Delta globals.Message `json:"delta"` Index int `json:"index"` FinishReason string `json:"finish_reason"` } `json:"choices"` } type ChatStreamErrorResponse struct { Error struct { Message string `json:"message"` Type string `json:"type"` } `json:"error"` } ================================================ FILE: adapter/dify/chat.go ================================================ package dify import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "encoding/json" "errors" "fmt" "strings" "time" ) type ChatInstance struct { Endpoint string ApiKey string responseComplete bool } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), } } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/chat-messages", c.GetEndpoint()) } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { timestamp := time.Now().UnixNano() userID := fmt.Sprintf("user_%d", timestamp) query := "" for _, msg := range props.Message { if msg.Role == "user" { query = msg.Content break } } return ChatRequest{ Inputs: map[string]interface{}{}, Query: query, ResponseMode: "streaming", User: userID, AutoGenerateName: true, } } func (c *ChatInstance) ProcessLine(data string) (string, error) { if c.responseComplete { return "", nil } if data == "" { return "", nil } chunk, complete, err := processStreamResponse(data) if err != nil { return "", err } if complete { c.responseComplete = true } return chunk.Content, nil } func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { res, err := utils.Post( c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("dify error: %s", err.Error()) } responseBody := utils.Marshal(res) response := processChatResponse(responseBody) if response == nil { return "", fmt.Errorf("dify error: cannot parse response") } return response.Answer, nil } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { c.responseComplete = false err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), Callback: func(data string) error { partial, err := c.ProcessLine(data) if err != nil { return err } if partial != "" { err = callback(&globals.Chunk{Content: partial}) if err != nil { return err } } return nil }, }, props.Proxy) c.responseComplete = true if err != nil { if strings.Contains(err.Body, "\"code\":") { errorResp := processChatErrorResponse(err.Body) if errorResp != nil { return errors.New(fmt.Sprintf("dify error: %s (code: %s)", errorResp.Message, errorResp.Code)) } var genericResp map[string]interface{} if jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil { errMsg, _ := json.Marshal(genericResp) return errors.New(fmt.Sprintf("dify error: %s", string(errMsg))) } } if err.Error != nil { return err.Error } return errors.New(fmt.Sprintf("dify error: unexpected error in stream request")) } return nil } ================================================ FILE: adapter/dify/processor.go ================================================ package dify import ( "chat/globals" "chat/utils" "errors" "fmt" ) func processChatResponse(data string) *ChatResponse { if form := utils.UnmarshalForm[ChatResponse](data); form != nil { return form } return nil } func processChatStreamResponse(data string) *ChatStreamResponse { if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil { return form } return nil } func processChatErrorResponse(data string) *ChatStreamErrorResponse { if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { return form } return nil } func processStreamResponse(data string) (*globals.Chunk, bool, error) { if data == "" { return &globals.Chunk{Content: ""}, false, nil } streamData := processChatStreamResponse(data) if streamData == nil { return &globals.Chunk{Content: ""}, false, nil } switch streamData.Event { case "message": if streamData.Answer != "" { return &globals.Chunk{ Content: streamData.Answer, }, false, nil } case "message_end": return &globals.Chunk{ Content: "", }, true, nil case "error": if streamData.Code != "" && streamData.Message != "" { return nil, false, errors.New(fmt.Sprintf("dify error: %s (code: %s)", streamData.Message, streamData.Code)) } return nil, false, errors.New("dify error: conversation failed") case "workflow_started", "node_started", "node_finished", "workflow_finished", "iteration_started", "iteration_next", "iteration_finished", "iteration_completed", "parallel_branch_started", "parallel_branch_finished", "ping": return &globals.Chunk{Content: ""}, false, nil } errorResp := processChatErrorResponse(data) if errorResp != nil { return nil, false, errors.New(fmt.Sprintf("dify error: %s (code: %s)", errorResp.Message, errorResp.Code)) } return &globals.Chunk{Content: ""}, false, nil } ================================================ FILE: adapter/dify/struct.go ================================================ package dify type ChatRequest struct { Inputs map[string]interface{} `json:"inputs"` Query string `json:"query"` ResponseMode string `json:"response_mode"` ConversationID string `json:"conversation_id,omitempty"` User string `json:"user"` Files []File `json:"files,omitempty"` AutoGenerateName bool `json:"auto_generate_name,omitempty"` } type File struct { Type string `json:"type"` TransferMethod string `json:"transfer_method"` URL string `json:"url,omitempty"` UploadFileID string `json:"upload_file_id,omitempty"` } type ChatResponse struct { MessageID string `json:"message_id"` ConversationID string `json:"conversation_id"` Mode string `json:"mode"` Answer string `json:"answer"` Metadata map[string]interface{} `json:"metadata"` Usage Usage `json:"usage"` RetrieverResources []RetrieverResource `json:"retriever_resources"` CreatedAt int64 `json:"created_at"` } type Usage struct { TokenCount int `json:"token_count"` OutputTokens int `json:"output_tokens"` InputTokens int `json:"input_tokens"` } type RetrieverResource struct { SegmentID string `json:"segment_id"` Content string `json:"content"` Source string `json:"source"` } type ChatStreamResponse struct { Event string `json:"event"` TaskID string `json:"task_id"` MessageID string `json:"message_id,omitempty"` ConversationID string `json:"conversation_id,omitempty"` Answer string `json:"answer,omitempty"` CreatedAt int64 `json:"created_at,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` Usage *Usage `json:"usage,omitempty"` RetrieverResources []RetrieverResource `json:"retriever_resources,omitempty"` Audio string `json:"audio,omitempty"` Status int `json:"status,omitempty"` Code string `json:"code,omitempty"` Message string `json:"message,omitempty"` } type ChatStreamErrorResponse struct { Event string `json:"event"` TaskID string `json:"task_id"` MessageID string `json:"message_id"` Status int `json:"status"` Code string `json:"code"` Message string `json:"message"` } ================================================ FILE: adapter/hunyuan/chat.go ================================================ package hunyuan import ( adaptercommon "chat/adapter/common" "chat/globals" "context" "fmt" ) func (c *ChatInstance) FormatMessages(messages []globals.Message) []globals.Message { var result []globals.Message for _, message := range messages { switch message.Role { case globals.System: result = append(result, globals.Message{Role: globals.User, Content: message.Content}) case globals.Assistant, globals.User: bound := len(result) > 0 && result[len(result)-1].Role == message.Role if bound { result[len(result)-1].Content += message.Content } else { result = append(result, message) } case globals.Tool: continue default: result = append(result, message) } } return result } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { credential := NewCredential(c.GetSecretId(), c.GetSecretKey()) client := NewInstance(c.GetAppId(), c.GetEndpoint(), credential) channel, err := client.Chat(context.Background(), NewRequest(Stream, c.FormatMessages(props.Message), props.Temperature, props.TopP)) if err != nil { return fmt.Errorf("tencent hunyuan error: %+v", err) } for chunk := range channel { if chunk.Error.Code != 0 { fmt.Printf("tencent hunyuan error: %+v\n", chunk.Error) break } if len(chunk.Choices) == 0 { continue } choice := chunk.Choices[0].Delta if err := callback(&globals.Chunk{Content: choice.Content}); err != nil { return err } } return nil } ================================================ FILE: adapter/hunyuan/sdk.go ================================================ package hunyuan /* * Copyright (c) 2017-2018 THL A29 Limited, a Tencent company. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import ( "bufio" "bytes" "chat/globals" "context" "crypto/hmac" "crypto/sha1" "encoding/base64" "encoding/json" "fmt" "github.com/google/uuid" "io" "net/http" "sort" "strconv" "strings" "time" ) const ( defaultProtocol = "https" defaultHost = "hunyuan.cloud.tencent.com" path = "/hyllm/v1/chat/completions?" ) const ( Synchronize = iota Stream ) func getUrl(endpoint string) string { return fmt.Sprintf("%s://%s%s", getProtocol(endpoint), getHost(endpoint), path) } func getProtocol(endpoint string) string { seg := strings.Split(endpoint, "://") if len(seg) > 0 && seg[0] != "" { return seg[0] } return defaultProtocol } func getHost(endpoint string) string { seg := strings.Split(endpoint, "://") if len(seg) > 1 && seg[1] != "" { return seg[1] } return defaultHost } func getFullPath(endpoint string) string { return getHost(endpoint) + path } type ResponseChoices struct { FinishReason string `json:"finish_reason,omitempty"` Messages []globals.Message `json:"messages,omitempty"` Delta globals.Message `json:"delta,omitempty"` } type ResponseUsage struct { PromptTokens int64 `json:"prompt_tokens,omitempty"` TotalTokens int64 `json:"total_tokens,omitempty"` CompletionTokens int64 `json:"completion_tokens,omitempty"` } type ResponseError struct { Message string `json:"message,omitempty"` Code int `json:"code,omitempty"` } type StreamDelta struct { Content string `json:"content"` } type ChatRequest struct { AppID int64 `json:"app_id"` SecretID string `json:"secret_id"` Timestamp int `json:"timestamp"` Expired int `json:"expired"` QueryID string `json:"query_id"` Temperature float64 `json:"temperature"` TopP float64 `json:"top_p"` Stream int `json:"stream"` Messages []globals.Message `json:"messages"` } type ChatResponse struct { Choices []ResponseChoices `json:"choices,omitempty"` Created string `json:"created,omitempty"` ID string `json:"id,omitempty"` Usage ResponseUsage `json:"usage,omitempty"` Error ResponseError `json:"error,omitempty"` Note string `json:"note,omitempty"` ReqID string `json:"req_id,omitempty"` } type Credential struct { SecretID string SecretKey string } func NewCredential(secretID, secretKey string) *Credential { return &Credential{SecretID: secretID, SecretKey: secretKey} } type Client struct { Credential *Credential AppID int64 EndPoint string } func NewInstance(appId int64, endpoint string, credential *Credential) *Client { return &Client{ Credential: credential, AppID: appId, EndPoint: endpoint, } } func NewRequest(mod int, messages []globals.Message, temperature *float32, topP *float32) ChatRequest { queryID := uuid.NewString() return ChatRequest{ Timestamp: int(time.Now().Unix()), Expired: int(time.Now().Unix()) + 24*60*60, Temperature: 0, TopP: 0.8, Messages: messages, QueryID: queryID, Stream: mod, } } func (t *Client) getHttpReq(ctx context.Context, req ChatRequest) (*http.Request, error) { req.AppID = t.AppID req.SecretID = t.Credential.SecretID signatureUrl := t.buildURL(req) signature := t.genSignature(signatureUrl) body, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("json marshal err: %+v", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", getUrl(t.EndPoint), bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("new http request err: %+v", err) } httpReq.Header.Set("Authorization", signature) httpReq.Header.Set("Content-Type", "application/json") if req.Stream == Stream { httpReq.Header.Set("Cache-Control", "no-cache") httpReq.Header.Set("Connection", "keep-alive") httpReq.Header.Set("Accept", "text/event-Stream") } return httpReq, nil } func (t *Client) Chat(ctx context.Context, req ChatRequest) (<-chan ChatResponse, error) { res := make(chan ChatResponse, 1) httpReq, err := t.getHttpReq(ctx, req) if err != nil { return nil, fmt.Errorf("do general http request err: %+v", err) } httpResp, err := http.DefaultClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("do chat request err: %+v", err) } if httpResp.StatusCode != http.StatusOK { return nil, fmt.Errorf("do chat request failed status code :%d", httpResp.StatusCode) } if req.Stream == Synchronize { err = t.synchronize(httpResp, res) return res, err } go t.stream(httpResp, res) return res, nil } func (t *Client) synchronize(httpResp *http.Response, res chan ChatResponse) (err error) { defer func() { httpResp.Body.Close() close(res) }() var chatResp ChatResponse respBody, err := io.ReadAll(httpResp.Body) if err != nil { return fmt.Errorf("read response body err: %+v", err) } if err = json.Unmarshal(respBody, &chatResp); err != nil { return fmt.Errorf("json unmarshal err: %+v", err) } res <- chatResp return } func (t *Client) stream(httpResp *http.Response, res chan ChatResponse) { defer func() { httpResp.Body.Close() close(res) }() reader := bufio.NewReader(httpResp.Body) for { raw, err := reader.ReadBytes('\n') if err != nil { if err == io.EOF { return } res <- ChatResponse{Error: ResponseError{Message: fmt.Sprintf("tencent error: read stream data failed: %+v", err), Code: 500}} return } data := strings.TrimSpace(string(raw)) if data == "" || !strings.HasPrefix(data, "data: ") { continue } var chatResponse ChatResponse if err := json.Unmarshal([]byte(data[6:]), &chatResponse); err != nil { res <- ChatResponse{Error: ResponseError{Message: fmt.Sprintf("json unmarshal err: %+v", err), Code: 500}} return } res <- chatResponse if chatResponse.Choices[0].FinishReason == "stop" { return } } } func (t *Client) genSignature(url string) string { mac := hmac.New(sha1.New, []byte(t.Credential.SecretKey)) signURL := url mac.Write([]byte(signURL)) sign := mac.Sum([]byte(nil)) return base64.StdEncoding.EncodeToString(sign) } func (t *Client) getMessages(messages []globals.Message) string { var message string for _, msg := range messages { message += fmt.Sprintf(`{"role":"%s","content":"%s"},`, msg.Role, msg.Content) } message = strings.TrimSuffix(message, ",") return message } func (t *Client) buildURL(req ChatRequest) string { params := make([]string, 0) params = append(params, "app_id="+strconv.FormatInt(req.AppID, 10)) params = append(params, "secret_id="+req.SecretID) params = append(params, "timestamp="+strconv.Itoa(req.Timestamp)) params = append(params, "query_id="+req.QueryID) params = append(params, "temperature="+strconv.FormatFloat(req.Temperature, 'f', -1, 64)) params = append(params, "top_p="+strconv.FormatFloat(req.TopP, 'f', -1, 64)) params = append(params, "stream="+strconv.Itoa(req.Stream)) params = append(params, "expired="+strconv.Itoa(req.Expired)) params = append(params, fmt.Sprintf("messages=[%s]", t.getMessages(req.Messages))) sort.Sort(sort.StringSlice(params)) return getFullPath(t.EndPoint) + strings.Join(params, "&") } ================================================ FILE: adapter/hunyuan/struct.go ================================================ package hunyuan import ( factory "chat/adapter/common" "chat/globals" "chat/utils" ) type ChatInstance struct { Endpoint string AppId int64 SecretId string SecretKey string } func (c *ChatInstance) GetAppId() int64 { return c.AppId } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetSecretId() string { return c.SecretId } func (c *ChatInstance) GetSecretKey() string { return c.SecretKey } func NewChatInstance(endpoint, appId, secretId, secretKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, AppId: utils.ParseInt64(appId), SecretId: secretId, SecretKey: secretKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { params := conf.SplitRandomSecret(3) return NewChatInstance( conf.GetEndpoint(), params[0], params[1], params[2], ) } ================================================ FILE: adapter/midjourney/api.go ================================================ package midjourney import ( "chat/globals" "chat/utils" "fmt" ) func (c *ChatInstance) GetImagineEndpoint() string { return fmt.Sprintf("%s/mj/submit/imagine", c.GetEndpoint()) } func (c *ChatInstance) GetChangeEndpoint() string { return fmt.Sprintf("%s/mj/submit/change", c.GetEndpoint()) } func (c *ChatInstance) GetImagineRequest(prompt string) *ImagineRequest { return &ImagineRequest{ NotifyHook: c.GetNotifyEndpoint(), Prompt: prompt, } } func (c *ChatInstance) GetChangeRequest(action string, task string, index *int) *ChangeRequest { return &ChangeRequest{ NotifyHook: c.GetNotifyEndpoint(), Action: action, Index: index, TaskId: task, } } func (c *ChatInstance) CreateImagineRequest(proxy globals.ProxyConfig, prompt string) (*CommonResponse, error) { content, err := utils.PostRaw( c.GetImagineEndpoint(), c.GetMidjourneyHeaders(), c.GetImagineRequest(prompt), proxy, ) if err != nil { return nil, err } if data, err := utils.UnmarshalString[CommonResponse](content); err == nil { return &data, nil } else { return nil, utils.ToMarkdownError(err, content) } } func (c *ChatInstance) CreateChangeRequest(proxy globals.ProxyConfig, action string, task string, index *int) (*CommonResponse, error) { res, err := utils.Post( c.GetChangeEndpoint(), c.GetMidjourneyHeaders(), c.GetChangeRequest(action, task, index), proxy, ) if err != nil { return nil, err } return utils.MapToStruct[CommonResponse](res), nil } ================================================ FILE: adapter/midjourney/chat.go ================================================ package midjourney import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) const maxActions = 4 const ( ImagineAction = "IMAGINE" UpscaleAction = "UPSCALE" VariationAction = "VARIATION" RerollAction = "REROLL" ) const ( ImagineCommand = "/IMAGINE" UpscaleCommand = "/UPSCALE" VariationCommand = "/VARIATION" RerollCommand = "/REROLL" ) type ChatProps struct { Messages []globals.Message Model string } func getMode(model string) string { switch model { case globals.Midjourney: // relax return RelaxMode case globals.MidjourneyFast: // fast return FastMode case globals.MidjourneyTurbo: // turbo return TurboMode default: return RelaxMode } } func (c *ChatInstance) IsIgnoreMode() bool { return strings.HasSuffix(c.Endpoint, "/mj-relax") || strings.HasSuffix(c.Endpoint, "/mj-fast") || strings.HasSuffix(c.Endpoint, "/mj-turbo") } func (c *ChatInstance) GetCleanPrompt(model string, prompt string) string { if c.IsIgnoreMode() { return prompt } arr := strings.Split(strings.TrimSpace(prompt), " ") var res []string for _, word := range arr { if utils.Contains[string](word, RendererMode) { continue } res = append(res, word) } res = append(res, getMode(model)) target := strings.Join(res, " ") return target } func (c *ChatInstance) GetPrompt(props *adaptercommon.ChatProps) string { if len(props.Message) == 0 { return "" } content := props.Message[len(props.Message)-1].Content return c.GetCleanPrompt(props.Model, content) } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { // partial response like: // ```progress // 0 // ... // 100 // ``` // ![image](...) if len(globals.NotifyUrl) == 0 { return fmt.Errorf("format error: please provide available notify url") } action, prompt := c.ExtractPrompt(c.GetPrompt(props)) if len(prompt) == 0 { return fmt.Errorf("format error: please provide available prompt") } var begin bool form, err := c.CreateStreamTask(props, action, prompt, func(form *StorageForm, progress int) error { if progress == -1 { // ping event return callback(&globals.Chunk{Content: ""}) } if !begin { begin = true if err := callback(&globals.Chunk{Content: "```progress\n"}); err != nil { return err } } else if progress == 100 && !begin { if err := callback(&globals.Chunk{Content: "```progress\n"}); err != nil { return err } } if err := callback(&globals.Chunk{Content: fmt.Sprintf("%d\n", progress)}); err != nil { return err } if progress == 100 { if err := callback(&globals.Chunk{Content: "```\n"}); err != nil { return err } } return nil }) if err != nil { return fmt.Errorf("error from midjourney: %s", err.Error()) } if err := callback(&globals.Chunk{Content: utils.GetImageMarkdown(form.Url)}); err != nil { return err } return c.CallbackActions(props, form, callback) } func toVirtualMessage(message string, model string) string { prompt := strings.Replace(message, " ", "-", -1) return fmt.Sprintf("https://chatnio.virtual%s::%s", prompt, model) } func (c *ChatInstance) CallbackActions(props *adaptercommon.ChatProps, form *StorageForm, callback globals.Hook) error { if form.Action == UpscaleAction { return nil } actions := utils.Range(1, maxActions+1) upscale := strings.Join(utils.Each(actions, func(index int) string { return fmt.Sprintf("[U%d](%s)", index, toVirtualMessage(fmt.Sprintf("/UPSCALE %s %d", form.Task, index), props.OriginalModel)) }), " ") variation := strings.Join(utils.Each(actions, func(index int) string { return fmt.Sprintf("[V%d](%s)", index, toVirtualMessage(fmt.Sprintf("/VARIATION %s %d", form.Task, index), props.OriginalModel)) }), " ") reroll := fmt.Sprintf("[REROLL](%s)", toVirtualMessage(fmt.Sprintf("/REROLL %s", form.Task), props.OriginalModel)) return callback(&globals.Chunk{ Content: fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n", upscale, variation, reroll), }) } ================================================ FILE: adapter/midjourney/expose.go ================================================ package midjourney import ( "chat/globals" "chat/utils" "fmt" "github.com/gin-gonic/gin" "net/http" "strings" ) var whiteList []string func SaveWhiteList(raw string) { arr := utils.Filter(strings.Split(raw, ","), func(s string) bool { return len(strings.TrimSpace(s)) > 0 }) for _, ip := range arr { if !utils.Contains(ip, whiteList) { whiteList = append(whiteList, ip) } } } func InWhiteList(ip string) bool { if len(whiteList) == 0 { return true } return utils.Contains(ip, whiteList) } func NotifyAPI(c *gin.Context) { if !InWhiteList(c.ClientIP()) { globals.Info(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP())) c.AbortWithStatus(http.StatusForbidden) return } var form NotifyForm if err := c.ShouldBindJSON(&form); err != nil { c.AbortWithStatus(http.StatusBadRequest) return } globals.Debug(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP())) if !utils.Contains(form.Status, []string{InProgress, Success, Failure}) { // ignore return } reason, ok := form.FailReason.(string) if !ok { reason = "unknown" } err := setStorage(form.Id, StorageForm{ Task: form.Id, Action: form.Action, Url: form.ImageUrl, FailReason: reason, Progress: form.Progress, Status: form.Status, }) c.JSON(http.StatusOK, gin.H{ "status": err == nil, }) } ================================================ FILE: adapter/midjourney/handler.go ================================================ package midjourney import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" "time" ) const maxTimeout = 30 * time.Minute // 30 min timeout func getStatusCode(action string, response *CommonResponse) error { code := response.Code switch code { case SuccessCode, QueueCode: return nil case ExistedCode: if action != ImagineCommand { return nil } return fmt.Errorf("task is existed, please try again later with another prompt") case MaxQueueCode: return fmt.Errorf("task queue is full, please try again later") case NudeCode: return fmt.Errorf("prompt violates the content policy of midjourney, the request is rejected") default: return fmt.Errorf(fmt.Sprintf("unknown error from midjourney (code: %d, description: %s)", code, response.Description)) } } func getProgress(value string) int { progress := strings.TrimSuffix(value, "%") return utils.ParseInt(progress) } func (c *ChatInstance) GetAction(command string) string { return strings.TrimLeft(command, "/") } func (c *ChatInstance) ExtractPrompt(input string) (action string, prompt string) { segment := utils.SafeSplit(input, " ", 2) action = strings.TrimSpace(segment[0]) prompt = strings.TrimSpace(segment[1]) switch action { case ImagineCommand, VariationCommand, UpscaleCommand, RerollCommand: return default: return ImagineCommand, strings.TrimSpace(input) } } func (c *ChatInstance) ExtractCommand(input string) (task string, index *int) { segment := utils.SafeSplit(input, " ", 2) task = strings.TrimSpace(segment[0]) if segment[1] != "" { data := segment[1] slice := strings.Split(segment[1], " ") if len(slice) > 1 { data = slice[0] } index = utils.ToPtr(utils.ParseInt(strings.TrimSpace(data))) } return } func (c *ChatInstance) CreateRequest(proxy globals.ProxyConfig, action string, prompt string) (*CommonResponse, error) { switch action { case ImagineCommand: return c.CreateImagineRequest(proxy, prompt) case VariationCommand, UpscaleCommand, RerollCommand: task, index := c.ExtractCommand(prompt) return c.CreateChangeRequest(proxy, c.GetAction(action), task, index) default: return nil, fmt.Errorf("unknown action: %s", action) } } func (c *ChatInstance) CreateStreamTask(props *adaptercommon.ChatProps, action string, prompt string, hook func(form *StorageForm, progress int) error) (*StorageForm, error) { res, err := c.CreateRequest(props.Proxy, action, prompt) if err != nil { return nil, err } if err := getStatusCode(action, res); err != nil { return nil, err } task := res.Result progress := -1 ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: form := getNotifyStorage(task) if form == nil { // hook for ping (in order to catch the stop signal) if err := hook(nil, -1); err != nil { return nil, err } continue } switch form.Status { case Success: if err := hook(form, 100); err != nil { return nil, err } return form, nil case Failure: return nil, fmt.Errorf("task failed: %s", form.FailReason) case InProgress: current := getProgress(form.Progress) if progress != current { if err := hook(form, current); err != nil { return nil, err } progress = current } default: // ping if err := hook(form, -1); err != nil { return nil, err } } case <-time.After(maxTimeout): return nil, fmt.Errorf("task timeout") } } } ================================================ FILE: adapter/midjourney/storage.go ================================================ package midjourney import ( "chat/connection" "chat/utils" "fmt" ) func getTaskName(task string) string { return fmt.Sprintf("nio:mj-task:%s", task) } func setStorage(task string, form StorageForm) error { return utils.SetJson(connection.Cache, getTaskName(task), form, 60*60) } func getNotifyStorage(task string) *StorageForm { return utils.GetCacheStore[StorageForm](connection.Cache, getTaskName(task)) } ================================================ FILE: adapter/midjourney/struct.go ================================================ package midjourney import ( factory "chat/adapter/common" "chat/globals" "fmt" ) var midjourneyEmptySecret = "null" type ChatInstance struct { Endpoint string ApiSecret string } func (c *ChatInstance) GetApiSecret() string { return c.ApiSecret } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetMidjourneyHeaders() map[string]string { secret := c.GetApiSecret() if secret == "" || secret == midjourneyEmptySecret { return map[string]string{ "Content-Type": "application/json", } } return map[string]string{ "Content-Type": "application/json", "mj-api-secret": secret, } } func (c *ChatInstance) GetNotifyEndpoint() string { return fmt.Sprintf("%s/mj/notify", globals.NotifyUrl) } func NewChatInstance(endpoint, apiSecret, whiteList string) *ChatInstance { SaveWhiteList(whiteList) return &ChatInstance{ Endpoint: endpoint, ApiSecret: apiSecret, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { params := conf.SplitRandomSecret(2) return NewChatInstance( conf.GetEndpoint(), params[0], params[1], ) } ================================================ FILE: adapter/midjourney/types.go ================================================ package midjourney const ( SuccessCode = 1 ExistedCode = 21 QueueCode = 22 MaxQueueCode = 23 NudeCode = 24 ) const ( NotStartStatus = "NOT_START" Submitted = "SUBMITTED" InProgress = "IN_PROGRESS" Failure = "FAILURE" Success = "SUCCESS" ) const ( TurboMode = "--turbo" FastMode = "--fast" RelaxMode = "--relax" ) var RendererMode = []string{TurboMode, FastMode, RelaxMode} type CommonHeader struct { ContentType string `json:"Content-Type"` MjApiSecret string `json:"mj-api-secret,omitempty"` } type CommonResponse struct { Code int `json:"code"` Description string `json:"description"` Result string `json:"result"` } type ImagineRequest struct { NotifyHook string `json:"notifyHook"` Prompt string `json:"prompt"` } type ChangeRequest struct { NotifyHook string `json:"notifyHook"` Action string `json:"action"` Index *int `json:"index,omitempty"` TaskId string `json:"taskId"` } type NotifyForm struct { Id string `json:"id"` Action string `json:"action"` Status string `json:"status"` Prompt string `json:"prompt"` PromptEn string `json:"promptEn"` Description string `json:"description"` SubmitTime int64 `json:"submitTime"` StartTime int64 `json:"startTime"` FinishTime int64 `json:"finishTime"` Progress string `json:"progress"` ImageUrl string `json:"imageUrl"` FailReason interface{} `json:"failReason"` } type StorageForm struct { Task string `json:"task"` Action string `json:"action"` Url string `json:"url"` FailReason string `json:"failReason"` Progress string `json:"progress"` Status string `json:"status"` } ================================================ FILE: adapter/openai/chat.go ================================================ package openai import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "regexp" "strings" ) func (c *ChatInstance) GetChatEndpoint(props *adaptercommon.ChatProps) string { if props.Model == globals.GPT3TurboInstruct { return fmt.Sprintf("%s/v1/completions", c.GetEndpoint()) } return fmt.Sprintf("%s/v1/chat/completions", c.GetEndpoint()) } func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string { result := "" for _, message := range messages { result += fmt.Sprintf("%s: %s\n", message.Role, message.Content) } return result } func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string { if len(props.Message) == 0 { return "" } return props.Message[len(props.Message)-1].Content } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { if props.Model == globals.GPT3TurboInstruct { // for completions return CompletionRequest{ Model: props.Model, Prompt: c.GetCompletionPrompt(props.Message), MaxToken: props.MaxTokens, Stream: stream, } } messages := formatMessages(props) // o1, o3, gpt-5 compatibility isNewModel := len(props.Model) >= 2 && (props.Model[:2] == "o1" || props.Model[:2] == "o3") || strings.HasPrefix(props.Model, "gpt-5") var temperature *float32 if isNewModel { temp := float32(1.0) temperature = &temp } else { temperature = props.Temperature } request := ChatRequest{ Model: props.Model, Messages: messages, Stream: stream, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, Temperature: temperature, TopP: props.TopP, Tools: props.Tools, ToolChoice: props.ToolChoice, } if isNewModel { request.MaxCompletionTokens = props.MaxTokens } else { request.MaxToken = props.MaxTokens } return request } // CreateChatRequest is the native http request body for openai func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { if globals.IsOpenAIDalleModel(props.Model) { return c.CreateImage(props) } res, err := utils.Post( c.GetChatEndpoint(props), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("openai error: %s", err.Error()) } data := utils.MapToStruct[ChatResponse](res) if data == nil { return "", fmt.Errorf("openai error: cannot parse response") } else if data.Error.Message != "" { return "", fmt.Errorf("openai error: %s", data.Error.Message) } return data.Choices[0].Message.Content, nil } func hideRequestId(message string) string { // xxx (request id: 2024020311120561344953f0xfh0TX) exp := regexp.MustCompile(`\(request id: [a-zA-Z0-9]+\)`) return exp.ReplaceAllString(message, "") } // CreateStreamChatRequest is the stream response body for openai func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { if globals.IsOpenAIDalleModel(props.Model) { if url, err := c.CreateImage(props); err != nil { return err } else { return callback(&globals.Chunk{ Content: url, }) } } isCompletionType := props.Model == globals.GPT3TurboInstruct ticks := 0 err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(props), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), Callback: func(data string) error { ticks += 1 partial, err := c.ProcessLine(data, isCompletionType) if err != nil { return err } return callback(partial) }, }, props.Proxy) if err != nil { if form := processChatErrorResponse(err.Body); form != nil { if form.Error.Type == "" && form.Error.Message == "" { return errors.New(utils.ToMarkdownCode("json", err.Body)) } msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type) return errors.New(hideRequestId(msg)) } return err.Error } if ticks == 0 { return errors.New("no response") } return nil } ================================================ FILE: adapter/openai/image.go ================================================ package openai import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) type ImageProps struct { Model string Prompt string Size ImageSize Proxy globals.ProxyConfig } func (c *ChatInstance) GetImageEndpoint() string { return fmt.Sprintf("%s/v1/images/generations", c.GetEndpoint()) } // CreateImageRequest will create a dalle image from prompt, return url of image, base64 data and error func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, string, error) { res, err := utils.Post( c.GetImageEndpoint(), c.GetHeader(), ImageRequest{ Model: props.Model, Prompt: props.Prompt, Size: utils.Multi[ImageSize]( props.Model == globals.Dalle3 || props.Model == globals.GPTImage1, ImageSize1024, ImageSize512, ), N: 1, }, props.Proxy) if err != nil || res == nil { return "", "", fmt.Errorf(err.Error()) } data := utils.MapToStruct[ImageResponse](res) if data == nil { return "", "", fmt.Errorf("openai error: cannot parse response") } else if data.Error.Message != "" { return "", "", fmt.Errorf(data.Error.Message) } // for gpt-image-1, return base64 data if available if props.Model == globals.GPTImage1 && data.Data[0].B64Json != "" { return "", data.Data[0].B64Json, nil } return data.Data[0].Url, "", nil } // CreateImage will create a dalle image from prompt, return markdown of image func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) { url, b64Json, err := c.CreateImageRequest(ImageProps{ Model: props.Model, Prompt: c.GetLatestPrompt(props), Proxy: props.Proxy, }) if err != nil { if strings.Contains(err.Error(), "safety") { return err.Error(), nil } return "", err } if b64Json != "" { return utils.GetBase64ImageMarkdown(b64Json), nil } storedUrl := utils.StoreImage(url) return utils.GetImageMarkdown(storedUrl), nil } ================================================ FILE: adapter/openai/processor.go ================================================ package openai import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "regexp" ) func formatMessages(props *adaptercommon.ChatProps) interface{} { if globals.IsVisionModel(props.Model) { return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message { if message.Role == globals.User { content, urls := utils.ExtractImages(message.Content, true) images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent { obj, err := utils.NewImage(url) props.Buffer.AddImage(obj) if err != nil { globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "..."))) } return &MessageContent{ Type: "image_url", ImageUrl: &ImageUrl{ Url: url, }, } }) return Message{ Role: message.Role, Content: utils.Prepend(images, MessageContent{ Type: "text", Text: &content, }), Name: message.Name, FunctionCall: message.FunctionCall, ToolCalls: message.ToolCalls, ToolCallId: message.ToolCallId, } } return Message{ Role: message.Role, Content: MessageContents{ MessageContent{ Type: "text", Text: &message.Content, }, }, Name: message.Name, FunctionCall: message.FunctionCall, ToolCalls: message.ToolCalls, ToolCallId: message.ToolCallId, } }) } return props.Message } func processChatResponse(data string) *ChatStreamResponse { return utils.UnmarshalForm[ChatStreamResponse](data) } func processCompletionResponse(data string) *CompletionResponse { return utils.UnmarshalForm[CompletionResponse](data) } func processChatErrorResponse(data string) *ChatStreamErrorResponse { return utils.UnmarshalForm[ChatStreamErrorResponse](data) } func getChoices(form *ChatStreamResponse) *globals.Chunk { if len(form.Choices) == 0 { return &globals.Chunk{Content: ""} } choice := form.Choices[0].Delta return &globals.Chunk{ Content: choice.Content, ToolCall: choice.ToolCalls, FunctionCall: choice.FunctionCall, } } func getCompletionChoices(form *CompletionResponse) string { if len(form.Choices) == 0 { return "" } return form.Choices[0].Text } func getRobustnessResult(chunk string) string { exp := `\"content\":\"(.*?)\"` compile, err := regexp.Compile(exp) if err != nil { return "" } matches := compile.FindStringSubmatch(chunk) if len(matches) > 1 { return utils.ProcessRobustnessChar(matches[1]) } else { return "" } } func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) { if isCompletionType { // openai legacy support if completion := processCompletionResponse(data); completion != nil { return &globals.Chunk{ Content: getCompletionChoices(completion), }, nil } globals.Warn(fmt.Sprintf("openai error: cannot parse completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response") } if form := processChatResponse(data); form != nil { return getChoices(form), nil } if form := processChatErrorResponse(data); form != nil { return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("openai error: %s (type: %s)", form.Error.Message, form.Error.Type)) } globals.Warn(fmt.Sprintf("openai error: cannot parse chat completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response") } ================================================ FILE: adapter/openai/struct.go ================================================ package openai import ( factory "chat/adapter/common" "chat/globals" "fmt" ) type ChatInstance struct { Endpoint string ApiKey string } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), } } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/openai/types.go ================================================ package openai import "chat/globals" type ImageUrl struct { Url string `json:"url"` Detail *string `json:"detail,omitempty"` } type MessageContent struct { Type string `json:"type"` Text *string `json:"text,omitempty"` ImageUrl *ImageUrl `json:"image_url,omitempty"` } type MessageContents []MessageContent type Message struct { Role string `json:"role"` Content MessageContents `json:"content"` Name *string `json:"name,omitempty"` FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role ReasoningContent *string `json:"reasoning,omitempty"` // only for claude reasoning models } // ChatRequest is the request body for openai type ChatRequest struct { Model string `json:"model"` Messages interface{} `json:"messages"` MaxToken *int `json:"max_tokens,omitempty"` MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` Stream bool `json:"stream"` PresencePenalty *float32 `json:"presence_penalty,omitempty"` FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` Tools *globals.FunctionTools `json:"tools,omitempty"` ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object } // CompletionRequest is the request body for openai completion type CompletionRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` MaxToken *int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` } // ChatResponse is the native http request body for openai type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Index int `json:"index"` Message globals.Message `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Error struct { Message string `json:"message"` } `json:"error"` } // ChatStreamResponse is the stream response body for openai type ChatStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Delta globals.Message `json:"delta"` Index int `json:"index"` FinishReason string `json:"finish_reason"` } `json:"choices"` } // CompletionResponse is the native http request body / stream response body for openai completion type CompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Text string `json:"text"` Index int `json:"index"` } `json:"choices"` } type ChatStreamErrorResponse struct { Error struct { Message string `json:"message"` Type string `json:"type"` } `json:"error"` } type ImageSize string // ImageRequest is the request body for openai dalle image generation type ImageRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` Size ImageSize `json:"size"` N int `json:"n"` } type ImageResponse struct { Data []struct { Url string `json:"url,omitempty"` B64Json string `json:"b64_json,omitempty"` } `json:"data"` Error struct { Message string `json:"message"` } `json:"error"` } var ( ImageSize256 ImageSize = "256x256" ImageSize512 ImageSize = "512x512" ImageSize1024 ImageSize = "1024x1024" ) ================================================ FILE: adapter/openai/videos.go ================================================ package openai import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "time" ) type VideoRequest struct { Prompt string `json:"prompt"` Model string `json:"model,omitempty"` Seconds *string `json:"seconds,omitempty"` Size *string `json:"size,omitempty"` InputReference *string `json:"input_reference,omitempty"` } type VideoJob struct { CompletedAt *int64 `json:"completed_at,omitempty"` CreatedAt int64 `json:"created_at"` ExpiresAt *int64 `json:"expires_at,omitempty"` Id string `json:"id"` Model string `json:"model"` Object string `json:"object"` Progress *int `json:"progress,omitempty"` Prompt string `json:"prompt"` RemixedFromVideoId *string `json:"remixed_from_video_id,omitempty"` Seconds string `json:"seconds"` Size string `json:"size"` Status string `json:"status"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error,omitempty"` } func (c *ChatInstance) getVideoCreateEndpoint() string { return fmt.Sprintf("%s/v1/videos", c.GetEndpoint()) } func (c *ChatInstance) getVideoQueryEndpoint(id string) string { return fmt.Sprintf("%s/v1/videos/%s", c.GetEndpoint(), id) } func (c *ChatInstance) CreateVideoRequest(props *adaptercommon.VideoProps, hook globals.Hook) error { body := VideoRequest{ Prompt: props.Prompt, Model: props.Model, Seconds: props.Seconds, Size: props.Size, InputReference: props.InputReference, } res, err := utils.Post(c.getVideoCreateEndpoint(), c.GetHeader(), body, props.Proxy) if err != nil || res == nil { if err != nil { return fmt.Errorf("openai video error: %s", err.Error()) } return fmt.Errorf("openai video error: empty response") } job := utils.MapToStruct[VideoJob](res) if job == nil { return fmt.Errorf("openai video error: cannot parse response") } if job.Error != nil && (job.Error.Message != "") { return fmt.Errorf("openai video error: %s", job.Error.Message) } const maxTimeout = 30 * time.Minute ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() deadline := time.After(maxTimeout) var begin bool var lastProgress int = -1 for { if job.Status == "completed" { if begin { if err := hook(&globals.Chunk{Content: "```\n"}); err != nil { return err } } return hook(&globals.Chunk{Content: utils.Marshal(job)}) } if job.Status == "failed" { if begin { if err := hook(&globals.Chunk{Content: "```\n"}); err != nil { return err } } if job.Error != nil && job.Error.Message != "" { return fmt.Errorf("openai video job failed: %s", job.Error.Message) } return fmt.Errorf("openai video job failed") } select { case <-ticker.C: if job.Id == "" { return hook(&globals.Chunk{Content: utils.Marshal(job)}) } data, gErr := utils.Get(c.getVideoQueryEndpoint(job.Id), c.GetHeader(), props.Proxy) if gErr != nil || data == nil { continue } if j := utils.MapToStruct[VideoJob](data); j != nil { job = j } progress := 0 if job.Progress != nil { progress = *job.Progress } if !begin { begin = true if err := hook(&globals.Chunk{Content: "```progress\n"}); err != nil { return err } } if progress != lastProgress { if err := hook(&globals.Chunk{Content: fmt.Sprintf("%d\n", progress)}); err != nil { return err } lastProgress = progress } case <-deadline: if begin { if err := hook(&globals.Chunk{Content: "```\n"}); err != nil { return err } } return fmt.Errorf("openai video job timeout") } } } ================================================ FILE: adapter/palm2/chat.go ================================================ package palm2 import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "strings" ) var geminiMaxImages = 16 func (c *ChatInstance) GetChatEndpoint(model string, stream bool) string { if model == globals.ChatBison001 { return fmt.Sprintf("%s/v1beta2/models/%s:generateMessage?key=%s", c.Endpoint, model, c.ApiKey) } if stream { return fmt.Sprintf("%s/v1beta/models/%s:streamGenerateContent?alt=sse&key=%s", c.Endpoint, model, c.ApiKey) } return fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", c.Endpoint, model, c.ApiKey) } func (c *ChatInstance) ConvertMessage(message []globals.Message) []PalmMessage { var result []PalmMessage for i, item := range message { if len(item.Content) == 0 { // palm model: message must include non empty content continue } if item.Role == globals.Tool { continue } if i > 0 && item.Role == result[len(result)-1].Author { // palm model: messages must alternate between authors result[len(result)-1].Content += " " + item.Content continue } result = append(result, PalmMessage{ Author: item.Role, Content: item.Content, }) } return result } func (c *ChatInstance) GetPalm2ChatBody(props *adaptercommon.ChatProps) *PalmChatBody { return &PalmChatBody{ Prompt: PalmPrompt{ Messages: c.ConvertMessage(props.Message), }, } } func (c *ChatInstance) GetGeminiChatBody(props *adaptercommon.ChatProps) *GeminiChatBody { return &GeminiChatBody{ Contents: c.GetGeminiContents(props.Model, props.Message), GenerationConfig: GeminiConfig{ Temperature: props.Temperature, MaxOutputTokens: props.MaxTokens, TopP: props.TopP, TopK: props.TopK, }, } } func (c *ChatInstance) GetPalm2ChatResponse(data interface{}) (string, error) { if form := utils.MapToStruct[PalmChatResponse](data); form != nil { if len(form.Candidates) == 0 { return "", fmt.Errorf("palm2 error: the content violates content policy") } return form.Candidates[0].Content, nil } return "", fmt.Errorf("palm2 error: cannot parse response") } func (c *ChatInstance) GetGeminiChatResponse(data interface{}) (string, error) { if form := utils.MapToStruct[GeminiChatResponse](data); form != nil { if len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 { return form.Candidates[0].Content.Parts[0].Text, nil } } if form := utils.MapToStruct[GeminiChatErrorResponse](data); form != nil { return "", fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status) } return "", fmt.Errorf("gemini: cannot parse response") } func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { uri := c.GetChatEndpoint(props.Model, false) if props.Model == globals.ChatBison001 { data, err := utils.Post(uri, map[string]string{ "Content-Type": "application/json", }, c.GetPalm2ChatBody(props), props.Proxy) if err != nil { return "", fmt.Errorf("palm2 error: %s", err.Error()) } return c.GetPalm2ChatResponse(data) } data, err := utils.Post(uri, map[string]string{ "Content-Type": "application/json", }, c.GetGeminiChatBody(props), props.Proxy) if err != nil { return "", fmt.Errorf("gemini error: %s", err.Error()) } return c.GetGeminiChatResponse(data) } // CreateStreamChatRequest is the stream request for palm2 func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { // Handle imagen models if globals.IsGoogleImagenModel(props.Model) { response, err := c.CreateImage(props) if err != nil { return err } return callback(&globals.Chunk{Content: response}) } // Handle chat models if props.Model == globals.ChatBison001 { response, err := c.CreateChatRequest(props) if err != nil { return err } for _, item := range utils.SplitItem(response, " ") { if err := callback(&globals.Chunk{Content: item}); err != nil { return err } } return nil } ticks := 0 scanErr := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(props.Model, true), Headers: map[string]string{ "Content-Type": "application/json", }, Body: c.GetGeminiChatBody(props), Callback: func(data string) error { ticks += 1 if form := utils.UnmarshalForm[GeminiStreamResponse](data); form != nil { if len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 { return callback(&globals.Chunk{ Content: form.Candidates[0].Content.Parts[0].Text, }) } return nil } if form := utils.UnmarshalForm[GeminiChatErrorResponse](data); form != nil { return fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status) } return nil }, }, props.Proxy) if scanErr != nil { if scanErr.Error != nil && strings.Contains(scanErr.Error.Error(), "status code: 404") { // downgrade to non-stream request response, err := c.CreateChatRequest(props) if err != nil { return err } return callback(&globals.Chunk{Content: response}) } if scanErr.Body != "" { if form := utils.UnmarshalForm[GeminiChatErrorResponse](scanErr.Body); form != nil { return fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status) } return fmt.Errorf("gemini error: %s", scanErr.Body) } return fmt.Errorf("gemini error: %v", scanErr.Error) } if ticks == 0 { return errors.New("no response") } return nil } func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string { if len(props.Message) == 0 { return "" } return props.Message[len(props.Message)-1].Content } ================================================ FILE: adapter/palm2/formatter.go ================================================ package palm2 import ( "chat/globals" "chat/utils" "strings" ) func getGeminiRole(role string) string { switch role { case globals.User: return GeminiUserType case globals.Assistant, globals.Tool, globals.System: return GeminiModelType default: return GeminiUserType } } func getMimeType(content string) string { segment := strings.Split(content, ".") if len(segment) == 0 || len(segment) == 1 { return "image/png" } suffix := strings.TrimSpace(strings.ToLower(segment[len(segment)-1])) switch suffix { case "png": return "image/png" case "jpg", "jpeg": return "image/jpeg" case "gif": return "image/gif" case "webp": return "image/webp" case "heif": return "image/heif" case "heic": return "image/heic" default: return "image/png" } } func getGeminiContent(parts []GeminiChatPart, content string, model string) []GeminiChatPart { if model == globals.GeminiPro { return append(parts, GeminiChatPart{ Text: &content, }) } raw, urls := utils.ExtractImages(content, true) if len(urls) > geminiMaxImages { urls = urls[:geminiMaxImages] } parts = append(parts, GeminiChatPart{ Text: &raw, }) for _, url := range urls { data, err := utils.ConvertToBase64(url) if err != nil { continue } parts = append(parts, GeminiChatPart{ InlineData: &GeminiInlineData{ MimeType: getMimeType(url), Data: data, }, }) } return parts } func (c *ChatInstance) GetGeminiContents(model string, message []globals.Message) []GeminiContent { // gemini role should be user-model result := make([]GeminiContent, 0) for _, item := range message { role := getGeminiRole(item.Role) if len(item.Content) == 0 { // gemini model: message must include non empty content continue } if len(result) == 0 && getGeminiRole(item.Role) == GeminiModelType { // gemini model: first message must be user result = append(result, GeminiContent{ Role: GeminiUserType, Parts: getGeminiContent(make([]GeminiChatPart, 0), "", model), }) } if len(result) > 0 && role == result[len(result)-1].Role { // gemini model: messages must alternate between authors result[len(result)-1].Parts = getGeminiContent(result[len(result)-1].Parts, item.Content, model) continue } result = append(result, GeminiContent{ Role: getGeminiRole(item.Role), Parts: getGeminiContent(make([]GeminiChatPart, 0), item.Content, model), }) } return result } ================================================ FILE: adapter/palm2/image.go ================================================ package palm2 import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) type ImageProps struct { Model string Prompt string Proxy globals.ProxyConfig } func (c *ChatInstance) GetImageEndpoint(model string) string { return fmt.Sprintf("%s/v1beta/models/%s:predict?key=%s", c.Endpoint, model, c.ApiKey) } // CreateImageRequest will create a gemini imagen from prompt, return base64 of image and error func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) { res, err := utils.Post( c.GetImageEndpoint(props.Model), map[string]string{ "Content-Type": "application/json", }, ImageRequest{ Instances: []ImageInstance{ { Prompt: props.Prompt, }, }, Parameters: ImageParameters{ SampleCount: 1, AspectRatio: "1:1", PersonGeneration: "allow_adult", }, }, props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("gemini error: %s", err.Error()) } data := utils.MapToStruct[ImageResponse](res) if data == nil { return "", fmt.Errorf("gemini error: cannot parse response") } if len(data.Predictions) == 0 { return "", fmt.Errorf("gemini error: no image generated") } return data.Predictions[0].BytesBase64Encoded, nil } // CreateImage will create a gemini imagen from prompt, return markdown of image func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) { if !globals.IsGoogleImagenModel(props.Model) { return "", nil } base64Data, err := c.CreateImageRequest(ImageProps{ Model: props.Model, Prompt: c.GetLatestPrompt(props), Proxy: props.Proxy, }) if err != nil { if strings.Contains(err.Error(), "safety") { return err.Error(), nil } return "", err } // Convert base64 to data URL format dataUrl := fmt.Sprintf("data:image/png;base64,%s", base64Data) url := utils.StoreImage(dataUrl) return utils.GetImageMarkdown(url), nil } ================================================ FILE: adapter/palm2/struct.go ================================================ package palm2 import ( factory "chat/adapter/common" "chat/globals" ) type ChatInstance struct { Endpoint string ApiKey string } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func NewChatInstance(endpoint string, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/palm2/types.go ================================================ package palm2 const ( GeminiUserType = "user" GeminiModelType = "model" ) type PalmMessage struct { Author string `json:"author"` Content string `json:"content"` } // PalmChatBody is the native http request body for palm2 type PalmChatBody struct { Prompt PalmPrompt `json:"prompt"` } type PalmPrompt struct { Messages []PalmMessage `json:"messages"` } // PalmChatResponse is the native http response body for palm2 type PalmChatResponse struct { Candidates []PalmMessage `json:"candidates"` } // GeminiChatBody is the native http request body for gemini type GeminiChatBody struct { Contents []GeminiContent `json:"contents"` GenerationConfig GeminiConfig `json:"generationConfig"` } type GeminiConfig struct { Temperature *float32 `json:"temperature,omitempty"` MaxOutputTokens *int `json:"maxOutputTokens,omitempty"` TopP *float32 `json:"topP,omitempty"` TopK *int `json:"topK,omitempty"` } type GeminiContent struct { Role string `json:"role"` Parts []GeminiChatPart `json:"parts"` } type GeminiChatPart struct { Text *string `json:"text,omitempty"` InlineData *GeminiInlineData `json:"inline_data,omitempty"` } type GeminiInlineData struct { MimeType string `json:"mime_type"` Data string `json:"data"` } type GeminiChatResponse struct { Candidates []struct { Content struct { Parts []struct { Text string `json:"text"` } `json:"parts"` Role string `json:"role"` } `json:"content"` } `json:"candidates"` } type GeminiChatErrorResponse struct { Error struct { Code int `json:"code"` Message string `json:"message"` Status string `json:"status"` } `json:"error"` } type GeminiStreamResponse struct { Candidates []struct { Content struct { Parts []struct { Text string `json:"text"` } `json:"parts"` Role string `json:"role"` } `json:"content"` } `json:"candidates"` } // ImageRequest is the native http request body for imagen type ImageRequest struct { Instances []ImageInstance `json:"instances"` Parameters ImageParameters `json:"parameters"` } type ImageInstance struct { Prompt string `json:"prompt"` } type ImageParameters struct { SampleCount int `json:"sampleCount,omitempty"` AspectRatio string `json:"aspectRatio,omitempty"` PersonGeneration string `json:"personGeneration,omitempty"` } // ImageResponse is the native http response body for imagen type ImageResponse struct { Predictions []ImagePrediction `json:"predictions"` } type ImagePrediction struct { MimeType string `json:"mimeType"` BytesBase64Encoded string `json:"bytesBase64Encoded"` } ================================================ FILE: adapter/request.go ================================================ package adapter import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" "time" ) func IsAvailableError(err error) bool { return err != nil && (err.Error() != "signal" && !strings.Contains(err.Error(), "signal")) } func IsSkipError(err error) bool { return err == nil || (err.Error() == "signal" || strings.Contains(err.Error(), "signal")) } func isQPSOverLimit(model string, err error) bool { if strings.Contains(model, "spark-desk") { return strings.Contains(err.Error(), "AppIdQpsOverFlowError") } return false } func NewChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error { err := createChatRequest(conf, props, hook) retries := conf.GetRetry() props.Current++ if IsAvailableError(err) { if isQPSOverLimit(props.OriginalModel, err) { // sleep for 0.5s to avoid qps limit globals.Info(fmt.Sprintf("qps limit for %s, sleep and retry (times: %d)", props.OriginalModel, props.Current)) time.Sleep(500 * time.Millisecond) return NewChatRequest(conf, props, hook) } if props.Current < retries { content := strings.Replace(err.Error(), "\n", "", -1) globals.Warn(fmt.Sprintf("retrying chat request for %s (attempt %d/%d, error: %s)", props.OriginalModel, props.Current+1, retries, content)) return NewChatRequest(conf, props, hook) } } return conf.ProcessError(err) } func NewVideoRequest(conf globals.ChannelConfig, props *adaptercommon.VideoProps, hook globals.Hook) error { err := createVideoRequest(conf, props, hook) retries := conf.GetRetry() props.Current++ if IsAvailableError(err) { if isQPSOverLimit(props.OriginalModel, err) { // sleep for 0.5s to avoid qps limit globals.Info(fmt.Sprintf("qps limit for %s, sleep and retry (times: %d)", props.OriginalModel, props.Current)) time.Sleep(500 * time.Millisecond) return NewVideoRequest(conf, props, hook) } if props.Current < retries { content := strings.Replace(err.Error(), "\n", "", -1) globals.Info(fmt.Sprintf("retrying error request for %s (attempt %d/%d, error: %s)", props.OriginalModel, props.Current+1, retries, content)) return NewVideoRequest(conf, props, hook) } } return conf.ProcessError(err) } func ClearMessages(model string, messages []globals.Message) []globals.Message { if globals.IsVisionModel(model) { return messages } return utils.Each[globals.Message](messages, func(message globals.Message) globals.Message { if message.Role != globals.User { return message } images := utils.ExtractBase64Images(message.Content) for _, image := range images { if len(image) <= 46 { continue } message.Content = strings.Replace(message.Content, image, utils.Extract(image, 46, " ..."), -1) } return message }) } ================================================ FILE: adapter/router.go ================================================ package adapter import ( "chat/adapter/midjourney" "github.com/gin-gonic/gin" ) func Register(app *gin.RouterGroup) { app.POST("/mj/notify", midjourney.NotifyAPI) } ================================================ FILE: adapter/skylark/chat.go ================================================ package skylark import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "context" "fmt" "io" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" "github.com/volcengine/volcengine-go-sdk/volcengine" ) const defaultMaxTokens int = 4096 func getMessages(messages []globals.Message) []*model.ChatCompletionMessage { result := make([]*model.ChatCompletionMessage, 0) for _, message := range messages { if message.Role == globals.Tool { message.Role = model.ChatMessageRoleTool } msg := &model.ChatCompletionMessage{ Role: message.Role, Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(message.Content)}, FunctionCall: getFunctionCall(message.ToolCalls), ReasoningContent: message.ReasoningContent, } hasPrevious := len(result) > 0 // a message should not followed by the same role message, merge them if hasPrevious && result[len(result)-1].Role == message.Role { prev := result[len(result)-1] prev.Content.StringValue = volcengine.String(*prev.Content.StringValue + *msg.Content.StringValue) if message.ToolCalls != nil { prev.FunctionCall = msg.FunctionCall } continue } // `assistant` message should follow a user or function message, if not has previous message, change the role to `user` if !hasPrevious && message.Role == model.ChatMessageRoleAssistant { msg.Role = model.ChatMessageRoleUser } result = append(result, msg) } return result } func (c *ChatInstance) GetMaxTokens(token *int) int { if token == nil || *token < 0 { return defaultMaxTokens } return *token } func (c *ChatInstance) CreateRequest(props *adaptercommon.ChatProps) *model.ChatCompletionRequest { return &model.ChatCompletionRequest{ Model: props.Model, Messages: getMessages(props.Message), Temperature: utils.GetPtrVal(props.Temperature, 0.), TopP: utils.GetPtrVal(props.TopP, 0.), // skylark v3 not support TopK PresencePenalty: utils.GetPtrVal(props.PresencePenalty, 0.), FrequencyPenalty: utils.GetPtrVal(props.FrequencyPenalty, 0.), RepetitionPenalty: utils.GetPtrVal(props.RepetitionPenalty, 0.), MaxTokens: c.GetMaxTokens(props.MaxTokens), FunctionCall: getFunctions(props.Tools), } } func getToolCalls(id string, choiceDelta model.ChatCompletionStreamChoiceDelta) *globals.ToolCalls { calls := choiceDelta.FunctionCall if calls == nil { return nil } return &globals.ToolCalls{ globals.ToolCall{ Type: "function", Id: fmt.Sprintf("%s-%s", calls.Name, id), Function: globals.ToolCallFunction{ Name: calls.Name, Arguments: calls.Arguments, }, }, } } func getChoice(choice model.ChatCompletionStreamResponse) *globals.Chunk { if len(choice.Choices) == 0 { return &globals.Chunk{Content: ""} } message := choice.Choices[0].Delta return &globals.Chunk{ Content: message.Content, ToolCall: getToolCalls(choice.ID, message), } } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { req := c.CreateRequest(props) c.isFirstReasoning = true c.isReasonOver = false if globals.DebugMode { globals.Debug(fmt.Sprintf("[skylark] request: %v", utils.Marshal(req))) } stream, err := c.Instance.CreateChatCompletionStream(context.Background(), req) if err != nil { if globals.DebugMode { globals.Debug(fmt.Sprintf("[skylark] stream error: %v", err)) } return err } defer stream.Close() for { recv, err2 := stream.Recv() if err2 == io.EOF { return nil } if err2 != nil { if globals.DebugMode { globals.Debug(fmt.Sprintf("[skylark] receive error: %v", err2)) } return err2 } if globals.DebugMode { globals.Debug(fmt.Sprintf("[skylark] response: %v", utils.Marshal(recv))) } choice := getChoice(recv) if len(recv.Choices) > 0 { delta := recv.Choices[0].Delta // Handle reasoning content if c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil { c.isReasonOver = true if delta.Content != "" { choice.Content = fmt.Sprintf("\n\n\n%s", delta.Content) } else { choice.Content = "\n\n\n" } } if delta.ReasoningContent != nil { if c.isFirstReasoning { c.isFirstReasoning = false choice.Content = fmt.Sprintf("\n%s", *delta.ReasoningContent) } else { choice.Content = *delta.ReasoningContent } } } if err = callback(choice); err != nil { return err } } return nil } ================================================ FILE: adapter/skylark/formatter.go ================================================ package skylark import ( "chat/globals" "chat/utils" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" structpb "github.com/golang/protobuf/ptypes/struct" "github.com/volcengine/volc-sdk-golang/service/maas/models/api" ) func getFunctionCall(calls *globals.ToolCalls) *model.FunctionCall { if calls == nil || len(*calls) == 0 { return nil } call := (*calls)[0] return &model.FunctionCall{ Name: call.Function.Name, Arguments: call.Function.Arguments, } } func getType(p globals.ToolProperty) string { t, ok := p["type"] if !ok { return "string" } return t.(string) } func getDescription(p globals.ToolProperty) string { desc, ok := p["description"] if !ok { return "" } return desc.(string) } func getValue(p globals.ToolProperty) *structpb.Value { switch getType(p) { case "string", "enum": return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: getDescription(p)}} case "number": return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 0}} case "boolean": return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: false}} case "object": return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: map[string]*structpb.Value{}}}} case "array": return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{}}}} default: return nil } } func getFunctions(tools *globals.FunctionTools) []*api.Function { if tools == nil || len(*tools) == 0 { return nil } return utils.Each[globals.ToolObject, *api.Function](*tools, func(tool globals.ToolObject) *api.Function { param := &structpb.Struct{ Fields: map[string]*structpb.Value{}, } for k, v := range tool.Function.Parameters.Properties { param.Fields[k] = getValue(v) } return &api.Function{ Name: tool.Function.Name, Description: tool.Function.Description, Parameters: param, } }) } ================================================ FILE: adapter/skylark/struct.go ================================================ package skylark import ( factory "chat/adapter/common" "chat/globals" "github.com/volcengine/volcengine-go-sdk/service/arkruntime" ) type ChatInstance struct { Instance *arkruntime.Client isFirstReasoning bool isReasonOver bool } func NewChatInstance(endpoint, apiKey string) *ChatInstance { //https://ark.cn-beijing.volces.com/api/v3 instance := arkruntime.NewClientWithApiKey(apiKey, arkruntime.WithBaseUrl(endpoint)) return &ChatInstance{ Instance: instance, isFirstReasoning: true, isReasonOver: false, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { params := conf.SplitRandomSecret(1) return NewChatInstance( conf.GetEndpoint(), params[0], ) } ================================================ FILE: adapter/slack/chat.go ================================================ package slack import ( adaptercommon "chat/adapter/common" "chat/globals" "context" ) func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error { if err := c.Instance.NewChannel(c.GetChannel()); err != nil { return err } resp, err := c.Instance.Reply(context.Background(), c.FormatMessage(props.Message), nil) if err != nil { return err } return c.ProcessPartialResponse(resp, hook) } ================================================ FILE: adapter/slack/struct.go ================================================ package slack import ( factory "chat/adapter/common" "chat/globals" "fmt" "github.com/bincooo/claude-api" "github.com/bincooo/claude-api/types" "github.com/bincooo/claude-api/vars" "strings" ) type ChatInstance struct { BotId string Token string Channel string Instance types.Chat } func (c *ChatInstance) GetBotId() string { return c.BotId } func (c *ChatInstance) GetToken() string { return c.Token } func (c *ChatInstance) GetChannel() string { return c.Channel } func (c *ChatInstance) GetInstance() types.Chat { return c.Instance } func NewChatInstance(botId, token, channel string) *ChatInstance { options := claude.NewDefaultOptions(token, botId, vars.Model4Slack) if instance, err := claude.New(options); err != nil { return nil } else { return &ChatInstance{ BotId: botId, Token: token, Channel: channel, Instance: instance, } } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { params := conf.SplitRandomSecret(2) return NewChatInstance( params[0], params[1], conf.GetEndpoint(), ) } func (c *ChatInstance) FormatMessage(message []globals.Message) string { result := make([]string, len(message)) for i, item := range message { if item.Role == globals.Tool { continue } result[i] = fmt.Sprintf("%s: %s", item.Role, item.Content) } return strings.Join(result, "\n\n") } func (c *ChatInstance) ProcessPartialResponse(res chan types.PartialResponse, hook globals.Hook) error { for { select { case data, ok := <-res: if !ok { return nil } if data.Error != nil { return data.Error } else if data.Text != "" { if err := hook(&globals.Chunk{Content: data.Text}); err != nil { return err } } } } } ================================================ FILE: adapter/sparkdesk/chat.go ================================================ package sparkdesk import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) var FunctionCallingModels = []string{ globals.SparkDeskMax, globals.SparkDeskV4Ultra, } func GetToken(props *adaptercommon.ChatProps) *int { if props.MaxTokens == nil { return nil } switch props.Model { case globals.SparkDeskLite, globals.SparkDeskPro128K: if *props.MaxTokens > 4096 { return utils.ToPtr(4096) } case globals.SparkDeskPro, globals.SparkDeskMax, globals.SparkDeskMax32K, globals.SparkDeskV4Ultra: if *props.MaxTokens > 8192 { return utils.ToPtr(8192) } } return props.MaxTokens } func GetTopK(props *adaptercommon.ChatProps) *int { if props.TopK == nil { return nil } // topk max value is 6 if *props.TopK > 6 { return utils.ToPtr(6) } return props.TopK } func (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message { var messages []Message for _, message := range props.Message { if message.Role == globals.Tool { continue } if message.Role == globals.System { message.Role = globals.Assistant } messages = append(messages, Message{ Role: message.Role, Content: message.Content, }) } return messages } func (c *ChatInstance) GetFunctionCalling(props *adaptercommon.ChatProps) *FunctionsPayload { if props.Tools == nil { return nil } if !utils.Contains(props.Model, FunctionCallingModels) { return nil } return &FunctionsPayload{ Text: utils.Each[globals.ToolObject, globals.ToolFunction](*props.Tools, func(tool globals.ToolObject) globals.ToolFunction { return tool.Function }), } } func getFunctionCall(call *FunctionCall) *globals.ToolCalls { if call == nil { return nil } return &globals.ToolCalls{ globals.ToolCall{ Type: "function", Id: fmt.Sprintf("%s-%s", call.Name, call.Arguments), Function: globals.ToolCallFunction{ Name: call.Name, Arguments: call.Arguments, }, }, } } func getChoice(form *ChatResponse) *globals.Chunk { if form == nil || len(form.Payload.Choices.Text) == 0 { return &globals.Chunk{Content: ""} } choices := form.Payload.Choices.Text if len(choices) == 0 { return &globals.Chunk{Content: ""} } choice := choices[0] return &globals.Chunk{ Content: choice.Content, ToolCall: getFunctionCall(choice.FunctionCall), } } func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error { var endpoint string switch props.Model { case globals.SparkDeskPro128K, globals.SparkDeskMax32K: endpoint = fmt.Sprintf("%s/chat/%s", c.Endpoint, TransformModel(props.Model)) default: endpoint = fmt.Sprintf("%s/%s/chat", c.Endpoint, TransformAddr(props.Model)) } var conn *utils.WebSocket if conn = utils.NewWebsocketClient(c.GenerateUrl(endpoint)); conn == nil { return fmt.Errorf("sparkdesk error: websocket connection failed") } defer conn.DeferClose() if err := conn.SendJSON(&ChatRequest{ Header: RequestHeader{ AppId: c.AppId, }, Payload: RequestPayload{ Message: MessagePayload{ Text: c.GetMessages(props), }, Functions: c.GetFunctionCalling(props), }, Parameter: RequestParameter{ Chat: ChatParameter{ Domain: TransformModel(props.Model), MaxToken: GetToken(props), Temperature: props.Temperature, TopK: GetTopK(props), }, }, }); err != nil { return err } for { form, err := utils.ReadForm[ChatResponse](conn) if err != nil { if strings.Contains(err.Error(), "websocket: close 1000") { return nil } globals.Debug(fmt.Sprintf("sparkdesk error: %s", err.Error())) return nil } if form.Header.Code != 0 { return fmt.Errorf("sparkdesk error: %s (sid: %s)", form.Header.Message, form.Header.Sid) } if err := hook(getChoice(form)); err != nil { return err } } } ================================================ FILE: adapter/sparkdesk/struct.go ================================================ package sparkdesk import ( factory "chat/adapter/common" "chat/globals" "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "net/url" "strings" "time" ) type ChatInstance struct { AppId string ApiSecret string ApiKey string Endpoint string } func TransformAddr(model string) string { switch model { case globals.SparkDeskLite: return "v1.1" case globals.SparkDeskPro: return "v3.1" case globals.SparkDeskMax: return "v3.5" case globals.SparkDeskV4Ultra: return "v4.0" default: return "v1.1" } } func TransformModel(model string) string { switch model { case globals.SparkDeskLite: return "general" case globals.SparkDeskPro: return "generalv3" case globals.SparkDeskPro128K: return "pro-128k" case globals.SparkDeskMax: return "generalv3.5" case globals.SparkDeskMax32K: return "max-32k" case globals.SparkDeskV4Ultra: return "4.0Ultra" default: return "general" } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { params := conf.SplitRandomSecret(3) return &ChatInstance{ AppId: params[0], ApiSecret: params[1], ApiKey: params[2], Endpoint: conf.GetEndpoint(), } } func (c *ChatInstance) CreateUrl(endpoint, host, date, auth string) string { v := make(url.Values) v.Add("host", host) v.Add("date", date) v.Add("authorization", auth) return fmt.Sprintf("%s?%s", endpoint, v.Encode()) } func (c *ChatInstance) Sign(data, key string) string { mac := hmac.New(sha256.New, []byte(key)) mac.Write([]byte(data)) return base64.StdEncoding.EncodeToString(mac.Sum(nil)) } // GenerateUrl will generate the signed url for sparkdesk api func (c *ChatInstance) GenerateUrl(endpoint string) string { uri, err := url.Parse(endpoint) if err != nil { return "" } date := time.Now().UTC().Format(time.RFC1123) data := strings.Join([]string{ fmt.Sprintf("host: %s", uri.Host), fmt.Sprintf("date: %s", date), fmt.Sprintf("GET %s HTTP/1.1", uri.Path), }, "\n") signature := c.Sign(data, c.ApiSecret) authorization := base64.StdEncoding.EncodeToString([]byte( fmt.Sprintf( "hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", c.ApiKey, "hmac-sha256", "host date request-line", signature, ), )) return c.CreateUrl(endpoint, uri.Host, date, authorization) } ================================================ FILE: adapter/sparkdesk/types.go ================================================ package sparkdesk import "chat/globals" // ChatRequest is the request body for sparkdesk type ChatRequest struct { Header RequestHeader `json:"header"` Payload RequestPayload `json:"payload"` Parameter RequestParameter `json:"parameter"` } type RequestHeader struct { AppId string `json:"app_id"` } type RequestPayload struct { Message MessagePayload `json:"message"` Functions *FunctionsPayload `json:"functions,omitempty"` } type FunctionsPayload struct { Text []globals.ToolFunction `json:"text"` } type Message struct { Role string `json:"role"` Content string `json:"content"` FunctionCall *FunctionCall `json:"function_call,omitempty"` } type FunctionCall struct { Name string `json:"name"` Arguments string `json:"arguments"` } type MessagePayload struct { Text []Message `json:"text"` } type RequestParameter struct { Chat ChatParameter `json:"chat"` } type ChatParameter struct { Domain string `json:"domain"` MaxToken *int `json:"max_tokens,omitempty"` Temperature *float32 `json:"temperature,omitempty"` TopK *int `json:"top_k,omitempty"` } // ChatResponse is the websocket partial response body for sparkdesk type ChatResponse struct { Header struct { Code int `json:"code" required:"true"` Message string `json:"message"` Sid string `json:"sid"` Status int `json:"status"` } `json:"header"` Payload struct { Choices struct { Status int `json:"status"` Seq int `json:"seq"` Text []Message `json:"text"` } `json:"choices"` Usage struct { Text struct { QuestionTokens int `json:"question_tokens"` PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } } } } ================================================ FILE: adapter/zhinao/chat.go ================================================ package zhinao import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" ) func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/v1/chat/completions", c.GetEndpoint()) } func (c *ChatInstance) GetModel(model string) string { switch model { case globals.GPT360V9: return "360GPT_S2_V9" default: return model } } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { // 2048 is the max token for 360GPT if props.MaxTokens != nil && *props.MaxTokens > 2048 { props.MaxTokens = utils.ToPtr(2048) } return ChatRequest{ Model: c.GetModel(props.Model), Messages: utils.EachNotNil(props.Message, func(message globals.Message) *globals.Message { if message.Role == globals.Tool { return nil } return &message }), MaxToken: props.MaxTokens, Stream: stream, Temperature: props.Temperature, TopP: props.TopP, TopK: props.TopK, RepetitionPenalty: props.RepetitionPenalty, } } // CreateChatRequest is the native http request body for zhinao func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { res, err := utils.Post( c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("zhinao error: %s", err.Error()) } data := utils.MapToStruct[ChatResponse](res) if data == nil { return "", fmt.Errorf("zhinao error: cannot parse response") } else if data.Error.Message != "" { return "", fmt.Errorf("zhinao error: %s", data.Error.Message) } return data.Choices[0].Message.Content, nil } // CreateStreamChatRequest is the stream response body for zhinao func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { buf := "" cursor := 0 chunk := "" err := utils.EventSource( "POST", c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, true), func(data string) error { data, err := c.ProcessLine(buf, data) chunk += data if err != nil { if strings.HasPrefix(err.Error(), "zhinao error") { return err } // error when break line buf = buf + data return nil } buf = "" if data != "" { cursor += 1 if err := callback(&globals.Chunk{Content: data}); err != nil { return err } } return nil }, props.Proxy, ) if err != nil { return err } else if len(chunk) == 0 { return fmt.Errorf("empty response") } return nil } ================================================ FILE: adapter/zhinao/processor.go ================================================ package zhinao import ( "chat/globals" "chat/utils" "errors" "fmt" "strings" ) func processFormat(data string) string { rep := strings.NewReplacer( "data: {", "\"data\": {", ) item := rep.Replace(data) if !strings.HasPrefix(item, "{") { item = "{" + item } if !strings.HasSuffix(item, "}}") { item = item + "}" } return item } func processChatResponse(data string) *ChatStreamResponse { if strings.HasPrefix(data, "{") { var form *ChatStreamResponse if form = utils.UnmarshalForm[ChatStreamResponse](data); form != nil { return form } if form = utils.UnmarshalForm[ChatStreamResponse](data[:len(data)-1]); form != nil { return form } if form = utils.UnmarshalForm[ChatStreamResponse](data + "}"); form != nil { return form } } return nil } func processChatErrorResponse(data string) *ChatStreamErrorResponse { if strings.HasPrefix(data, "{") { var form *ChatStreamErrorResponse if form = utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil { return form } if form = utils.UnmarshalForm[ChatStreamErrorResponse](data + "}"); form != nil { return form } } return nil } func isDone(data string) bool { return utils.Contains[string](data, []string{ "{data: [DONE]}", "{data: [DONE]}}", "null}}", "{null}", "{[DONE]}", "{data:}", "{data:}}", "data: [DONE]}}", }) } func getChoices(form *ChatStreamResponse) string { if len(form.Data.Choices) == 0 { return "" } return form.Data.Choices[0].Delta.Content } func (c *ChatInstance) ProcessLine(buf, data string) (string, error) { item := processFormat(buf + data) if isDone(item) { return "", nil } if form := processChatResponse(item); form == nil { // recursive call if len(buf) > 0 { return c.ProcessLine("", buf+item) } if err := processChatErrorResponse(item); err == nil || err.Data.Error.Message == "" { globals.Warn(fmt.Sprintf("zhinao error: cannot parse response: %s", item)) return data, errors.New("parser error: cannot parse response") } else { return "", fmt.Errorf("zhinao error: %s (type: %s)", err.Data.Error.Message, err.Data.Error.Type) } } else { return getChoices(form), nil } } ================================================ FILE: adapter/zhinao/struct.go ================================================ package zhinao import ( factory "chat/adapter/common" "chat/globals" "fmt" ) type ChatInstance struct { Endpoint string ApiKey string } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()), } } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/zhinao/types.go ================================================ package zhinao import "chat/globals" // 360 ZhiNao API is similar to OpenAI API // ChatRequest is the request body for zhinao type ChatRequest struct { Model string `json:"model"` Messages []globals.Message `json:"messages"` MaxToken *int `json:"max_tokens,omitempty"` TopP *float32 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` Temperature *float32 `json:"temperature,omitempty"` RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"` Stream bool `json:"stream"` } // ChatResponse is the native http request body for zhinao type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Message struct { Content string `json:"content"` } } `json:"choices"` Error struct { Message string `json:"message"` } `json:"error"` } // ChatStreamResponse is the stream response body for zhinao type ChatStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Data struct { Choices []struct { Delta struct { Content string `json:"content"` } Index int `json:"index"` } `json:"choices"` } `json:"data"` } type ChatStreamErrorResponse struct { Data struct { Error struct { Message string `json:"message"` Type string `json:"type"` } `json:"error"` } `json:"data"` } ================================================ FILE: adapter/zhipuai/chat.go ================================================ package zhipuai import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "regexp" ) func (c *ChatInstance) GetChatEndpoint() string { return fmt.Sprintf("%s/api/paas/v4/chat/completions", c.GetEndpoint()) } func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string { result := "" for _, message := range messages { result += fmt.Sprintf("%s: %s\n", message.Role, message.Content) } return result } func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string { if len(props.Message) == 0 { return "" } return props.Message[len(props.Message)-1].Content } func (c *ChatInstance) ConvertModel(model string) string { // for v3 legacy adapter switch model { case globals.ZhiPuChatGLMTurbo: return GLMTurbo case globals.ZhiPuChatGLMPro: return GLMPro case globals.ZhiPuChatGLMStd: return GLMStd case globals.ZhiPuChatGLMLite: return GLMLite default: return GLMStd } } func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} { if props.Model == globals.GPT3TurboInstruct { // for completions return CompletionRequest{ Model: c.ConvertModel(props.Model), Prompt: c.GetCompletionPrompt(props.Message), MaxToken: props.MaxTokens, Stream: stream, } } messages := formatMessages(props) // chatglm top_p should be (0.0, 1.0) and cannot be 0 or 1 if props.TopP != nil && *props.TopP >= 1.0 { props.TopP = utils.ToPtr[float32](0.99) } else if props.TopP != nil && *props.TopP <= 0.0 { props.TopP = utils.ToPtr[float32](0.01) } return ChatRequest{ Model: props.Model, Messages: messages, MaxToken: props.MaxTokens, Stream: stream, PresencePenalty: props.PresencePenalty, FrequencyPenalty: props.FrequencyPenalty, Temperature: props.Temperature, TopP: props.TopP, Tools: props.Tools, ToolChoice: props.ToolChoice, } } // CreateChatRequest is the native http request body for chatglm func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) { res, err := utils.Post( c.GetChatEndpoint(), c.GetHeader(), c.GetChatBody(props, false), props.Proxy, ) if err != nil || res == nil { return "", fmt.Errorf("chatglm error: %s", err.Error()) } data := utils.MapToStruct[ChatResponse](res) if data == nil { return "", fmt.Errorf("chatglm error: cannot parse response") } else if data.Error.Message != "" { return "", fmt.Errorf("chatglm error: %s", data.Error.Message) } return data.Choices[0].Message.Content, nil } func hideRequestId(message string) string { // xxx (request id: 2024020311120561344953f0xfh0TX) exp := regexp.MustCompile(`\(request id: [a-zA-Z0-9]+\)`) return exp.ReplaceAllString(message, "") } // CreateStreamChatRequest is the stream response body for chatglm func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error { ticks := 0 err := utils.EventScanner(&utils.EventScannerProps{ Method: "POST", Uri: c.GetChatEndpoint(), Headers: c.GetHeader(), Body: c.GetChatBody(props, true), Callback: func(data string) error { ticks += 1 partial, err := c.ProcessLine(data, false) if err != nil { return err } return callback(partial) }, }, props.Proxy) if err != nil { if form := processChatErrorResponse(err.Body); form != nil { if form.Error.Type == "" && form.Error.Message == "" { return errors.New(utils.ToMarkdownCode("json", err.Body)) } msg := fmt.Sprintf("%s (code: %s)", form.Error.Message, form.Error.Code) return errors.New(hideRequestId(msg)) } return err.Error } if ticks == 0 { return errors.New("no response") } return nil } ================================================ FILE: adapter/zhipuai/processor.go ================================================ package zhipuai import ( adaptercommon "chat/adapter/common" "chat/globals" "chat/utils" "errors" "fmt" "regexp" "strings" ) func formatMessages(props *adaptercommon.ChatProps) interface{} { if globals.IsVisionModel(props.Model) { return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message { if message.Role == globals.User { content, urls := utils.ExtractImages(message.Content, true) images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent { obj, err := utils.NewImage(url) props.Buffer.AddImage(obj) if err != nil { globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "..."))) } if strings.HasPrefix(url, "data:image/") { // remove base64 image prefix if idx := strings.Index(url, "base64,"); idx != -1 { url = url[idx+7:] } } return &MessageContent{ Type: "image_url", ImageUrl: &ImageUrl{ Url: url, }, } }) return Message{ Role: message.Role, Content: utils.Prepend(images, MessageContent{ Type: "text", Text: &content, }), Name: message.Name, FunctionCall: message.FunctionCall, ToolCalls: message.ToolCalls, ToolCallId: message.ToolCallId, } } return Message{ Role: message.Role, Content: message.Content, Name: message.Name, FunctionCall: message.FunctionCall, ToolCalls: message.ToolCalls, ToolCallId: message.ToolCallId, } }) } return props.Message } func processChatResponse(data string) *ChatStreamResponse { return utils.UnmarshalForm[ChatStreamResponse](data) } func processCompletionResponse(data string) *CompletionResponse { return utils.UnmarshalForm[CompletionResponse](data) } func processChatErrorResponse(data string) *ChatStreamErrorResponse { return utils.UnmarshalForm[ChatStreamErrorResponse](data) } func getChoices(form *ChatStreamResponse) *globals.Chunk { if len(form.Choices) == 0 { return &globals.Chunk{Content: ""} } choice := form.Choices[0].Delta return &globals.Chunk{ Content: choice.Content, ToolCall: choice.ToolCalls, FunctionCall: choice.FunctionCall, } } func getCompletionChoices(form *CompletionResponse) string { if len(form.Choices) == 0 { return "" } return form.Choices[0].Text } func getRobustnessResult(chunk string) string { exp := `\"content\":\"(.*?)\"` compile, err := regexp.Compile(exp) if err != nil { return "" } matches := compile.FindStringSubmatch(chunk) if len(matches) > 1 { return utils.ProcessRobustnessChar(matches[1]) } else { return "" } } func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) { if isCompletionType { // chatglm legacy support if completion := processCompletionResponse(data); completion != nil { return &globals.Chunk{ Content: getCompletionChoices(completion), }, nil } globals.Warn(fmt.Sprintf("chatglm error: cannot parse completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response") } if form := processChatResponse(data); form != nil { return getChoices(form), nil } if form := processChatErrorResponse(data); form != nil { return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("chatglm error: %s (type: %s)", form.Error.Message, form.Error.Type)) } globals.Warn(fmt.Sprintf("chatglm error: cannot parse chat completion response: %s", data)) return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response") } ================================================ FILE: adapter/zhipuai/struct.go ================================================ package zhipuai import ( factory "chat/adapter/common" "chat/globals" "chat/utils" "fmt" "strings" "time" "github.com/dgrijalva/jwt-go" ) type ChatInstance struct { Endpoint string ApiKey string } type Payload struct { ApiKey string `json:"api_key"` Exp int64 `json:"exp"` TimeStamp int64 `json:"timestamp"` } func (c *ChatInstance) GetEndpoint() string { return c.Endpoint } func (c *ChatInstance) GetApiKey() string { return c.ApiKey } func (c *ChatInstance) GetHeader() map[string]string { return map[string]string{ "Content-Type": "application/json", "Authorization": fmt.Sprintf("Bearer %s", c.GetToken()), } } func (c *ChatInstance) GetToken() string { // get jwt token for zhipuai api segment := strings.Split(c.ApiKey, ".") if len(segment) != 2 { return "" } id, secret := segment[0], segment[1] payload := utils.MapToStruct[jwt.MapClaims](Payload{ ApiKey: id, Exp: time.Now().Add(time.Minute*5).Unix() * 1000, TimeStamp: time.Now().Unix() * 1000, }) instance := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) instance.Header = map[string]interface{}{ "alg": "HS256", "sign_type": "SIGN", } token, _ := instance.SignedString([]byte(secret)) return token } func NewChatInstance(endpoint, apiKey string) *ChatInstance { return &ChatInstance{ Endpoint: endpoint, ApiKey: apiKey, } } func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory { return NewChatInstance( conf.GetEndpoint(), conf.GetRandomSecret(), ) } ================================================ FILE: adapter/zhipuai/types.go ================================================ package zhipuai import "chat/globals" const ( GLM4 = "glm-4" GLM4Vision = "glm-4v" GLMTurbo = "glm-3-turbo" // GLM3 Turbo GLMPro = "chatglm_pro" // GLM3 Pro (deprecated) GLMStd = "chatglm_std" // GLM3 Standard (deprecated) GLMLite = "chatglm_lite" // GLM3 Lite (deprecated) ) type ImageUrl struct { Url string `json:"url"` Detail *string `json:"detail,omitempty"` } type MessageContent struct { Type string `json:"type"` Text *string `json:"text,omitempty"` ImageUrl *ImageUrl `json:"image_url,omitempty"` } type MessageContents []MessageContent type Message struct { Role string `json:"role"` Content interface{} `json:"content"` Name *string `json:"name,omitempty"` FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role } // ChatRequest is the request body for chatglm type ChatRequest struct { Model string `json:"model"` Messages interface{} `json:"messages"` MaxToken *int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` PresencePenalty *float32 `json:"presence_penalty,omitempty"` FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"` Temperature *float32 `json:"temperature,omitempty"` TopP *float32 `json:"top_p,omitempty"` Tools *globals.FunctionTools `json:"tools,omitempty"` ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object } // CompletionRequest is the request body for chatglm completion type CompletionRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` MaxToken *int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` } // ChatResponse is the native http request body for chatglm type ChatResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Index int `json:"index"` Message globals.Message `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Error struct { Message string `json:"message"` } `json:"error"` } // ChatStreamResponse is the stream response body for chatglm type ChatStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Delta globals.Message `json:"delta"` Index int `json:"index"` FinishReason string `json:"finish_reason"` } `json:"choices"` } // CompletionResponse is the native http request body / stream response body for chatglm completion type CompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []struct { Text string `json:"text"` Index int `json:"index"` } `json:"choices"` } type ChatStreamErrorResponse struct { Error struct { Message string `json:"message"` Type string `json:"type"` Code string `json:"code"` } `json:"error"` } type ImageSize string // ImageRequest is the request body for chatglm dalle image generation type ImageRequest struct { Model string `json:"model"` Prompt string `json:"prompt"` Size ImageSize `json:"size"` N int `json:"n"` } type ImageResponse struct { Data []struct { Url string `json:"url"` } `json:"data"` Error struct { Message string `json:"message"` } `json:"error"` } var ( ImageSize256 ImageSize = "256x256" ImageSize512 ImageSize = "512x512" ImageSize1024 ImageSize = "1024x1024" ) ================================================ FILE: addition/article/api.go ================================================ package article import ( "chat/auth" "chat/globals" "chat/utils" "fmt" "github.com/gin-gonic/gin" "strings" ) type WebsocketArticleForm struct { Token string `json:"token" binding:"required"` Model string `json:"model" binding:"required"` Prompt string `json:"prompt" binding:"required"` Title string `json:"title" binding:"required"` Web bool `json:"web"` } type WebsocketArticleResponse struct { Hash string `json:"hash"` Data StreamProgressResponse `json:"data"` } func ProjectTarDownloadAPI(c *gin.Context) { hash := strings.TrimSpace(c.Query("hash")) c.Writer.Header().Add("Content-Disposition", "attachment; filename=article.tar.gz") c.File(fmt.Sprintf("storage/article/%s.tar.gz", hash)) } func ProjectZipDownloadAPI(c *gin.Context) { hash := strings.TrimSpace(c.Query("hash")) c.Writer.Header().Add("Content-Disposition", "attachment; filename=article.zip") c.File(fmt.Sprintf("storage/article/%s.zip", hash)) } func GenerateAPI(c *gin.Context) { var conn *utils.WebSocket if conn = utils.NewWebsocket(c, false); conn == nil { return } defer conn.DeferClose() form, err := utils.ReadForm[WebsocketArticleForm](conn) if err != nil { return } user := auth.ParseToken(c, form.Token) db := utils.GetDBFromContext(c) if !auth.HitGroups(db, user, globals.ArticlePermissionGroup) { return } if len(form.Title) == 0 { return } hash := CreateWorker(c, user, form.Model, form.Prompt, form.Title, form.Web, func(resp StreamProgressResponse) { conn.Send(WebsocketArticleResponse{ Hash: "", Data: resp, }) }) conn.Send(WebsocketArticleResponse{ Hash: hash, }) } ================================================ FILE: addition/article/data/.gitkeep ================================================ ================================================ FILE: addition/article/generate.go ================================================ package article import ( "chat/auth" "chat/globals" "chat/manager" "chat/utils" "fmt" "github.com/gin-gonic/gin" "strings" ) type StreamProgressResponse struct { Current int `json:"current"` Total int `json:"total"` Quota float32 `json:"quota"` } type Response struct { File string Quota float32 } func GenerateArticle(c *gin.Context, user *auth.User, model string, hash string, title string, prompt string, enableWeb bool) Response { message, quota := manager.NativeChatHandler(c, user, model, []globals.Message{{ Role: globals.User, Content: fmt.Sprintf("%s\n%s", prompt, title), }}, enableWeb) return Response{ File: CreateArticleFile(hash, title, message), Quota: quota, } } func ParseTitle(titles string) []string { var result []string for _, title := range strings.Split(titles, "\n") { title = strings.TrimSpace(title) if len(title) > 0 { result = append(result, title) } } return result } func CreateGenerationWorker(c *gin.Context, user *auth.User, model string, prompt string, title string, enableWeb bool, hash string) (int, chan Response) { titles := ParseTitle(title) result := make(chan Response, len(titles)) for _, name := range titles { go func(title string) { result <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb) }(name) } return len(titles), result } func CreateWorker(c *gin.Context, user *auth.User, model string, prompt string, title string, enableWeb bool, hook func(resp StreamProgressResponse)) string { hash := utils.Md5Encrypt(fmt.Sprintf("%s%s%s%v", model, prompt, title, enableWeb)) total, channel := CreateGenerationWorker(c, user, model, prompt, title, enableWeb, hash) current := 0 hook(StreamProgressResponse{Current: current, Total: total, Quota: 0}) for resp := range channel { current += 1 hook(StreamProgressResponse{Current: current, Total: total, Quota: resp.Quota}) if current == total { break } } hook(StreamProgressResponse{Current: current, Total: total, Quota: 0}) path := fmt.Sprintf("storage/article/data/%s", hash) if _, _, err := utils.GenerateCompressTask(hash, "storage/article", path, path); err != nil { globals.Debug(fmt.Sprintf("[article] error during generate compress task: %s", err.Error())) return "" } return hash } ================================================ FILE: addition/article/utils.go ================================================ package article import ( "chat/globals" "chat/utils" "fmt" "github.com/lukasjarosch/go-docx" ) func GenerateDocxFile(target, title, content string) error { data := docx.PlaceholderMap{ "title": title, "content": content, } doc, err := docx.Open("addition/article/template.docx") if err != nil { return err } if err := doc.ReplaceAll(data); err != nil { return err } if err := doc.WriteToFile(target); err != nil { return err } return nil } func CreateArticleFile(hash, title, content string) string { target := fmt.Sprintf("storage/article/data/%s/%s.docx", hash, title) utils.FileDirSafe(target) if err := GenerateDocxFile(target, title, content); err != nil { globals.Debug(fmt.Sprintf("[article] error during generate article %s: %s", title, err.Error())) } return target } ================================================ FILE: addition/card/.gitignore ================================================ .idea .vscode ================================================ FILE: addition/card/card.go ================================================ package card import ( "chat/globals" "chat/manager" "github.com/gin-gonic/gin" "github.com/russross/blackfriday/v2" "net/http" "strings" ) type RequestForm struct { Message string `json:"message" required:"true"` Web bool `json:"web"` } const maxColumnPerLine = 50 func ProcessMarkdownLine(source []byte) string { segment := strings.Split(string(source), "\n") var result []rune for _, line := range segment { data := []rune(line) length := len([]rune(line)) if length < maxColumnPerLine { result = append(result, data...) result = append(result, '\n') } else { for i := 0; i < length; i += maxColumnPerLine { if i+maxColumnPerLine < length { result = append(result, data[i:i+maxColumnPerLine]...) result = append(result, '\n') } else { result = append(result, data[i:]...) result = append(result, '\n') } } } } return string(result) } func MarkdownConvert(text string) string { if text == "" { return "" } result := blackfriday.Run([]byte(text)) return string(result) } func HandlerAPI(c *gin.Context) { var body RequestForm if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request body", }) } message := strings.TrimSpace(body.Message) if len(message) == 0 { c.JSON(http.StatusBadRequest, gin.H{ "error": "message is empty", }) return } response, quota := manager.NativeChatHandler(c, nil, globals.GPT3Turbo0613, []globals.Message{ {Role: globals.User, Content: message}, }, body.Web) c.JSON(http.StatusOK, gin.H{ "message": MarkdownConvert(response), "keyword": "", "quota": quota, }) } ================================================ FILE: addition/card/card.php ================================================ ", "

", $resp['message'])); $msgHeight = substr_count($resp['message'], "\n") * 20 + strlen($msg) * 0.15 + substr_count($resp['message'], " ChatGPT ChatGPT Card chatnio OpenAI ================================================ FILE: addition/card/error.php ================================================ Error Error Card Sorry, there is something wrong... ================================================ FILE: addition/card/utils.php ================================================ [^\S ]+/', '/[^\S ]+ ', '<', '\\1', '><', ':', '{', '}'); return preg_replace($search, $replace, $buffer); } function fetch($message, $web): array|string|null { $opts = array('http' => array( 'method' => 'POST', 'header' => 'Content-type: application/json', 'content' => json_encode(array('message' => $message, 'web' => $web)) ) ); $context = stream_context_create($opts); $response = @file_get_contents("http://localhost:8094/card", false, $context); $ok = $response !== false; return $ok ? json_decode($response, true) : null; } function get($param, $default = null) { return $_GET[$param] ?? $default; } ================================================ FILE: addition/generation/api.go ================================================ package generation import ( "chat/auth" "chat/globals" "chat/utils" "fmt" "strings" "github.com/gin-gonic/gin" ) type WebsocketGenerationForm struct { Token string `json:"token" binding:"required"` Prompt string `json:"prompt" binding:"required"` Model string `json:"model" binding:"required"` } func ProjectTarDownloadAPI(c *gin.Context) { hash := strings.TrimSpace(c.Query("hash")) c.Writer.Header().Add("Content-Disposition", "attachment; filename=code.tar.gz") c.File(fmt.Sprintf("storage/generation/%s.tar.gz", hash)) } func ProjectZipDownloadAPI(c *gin.Context) { hash := strings.TrimSpace(c.Query("hash")) c.Writer.Header().Add("Content-Disposition", "attachment; filename=code.zip") c.File(fmt.Sprintf("storage/generation/%s.zip", hash)) } func GenerateAPI(c *gin.Context) { var conn *utils.WebSocket if conn = utils.NewWebsocket(c, false); conn == nil { return } defer conn.DeferClose() form, err := utils.ReadForm[WebsocketGenerationForm](conn) if err != nil { return } user := auth.ParseToken(c, form.Token) db := utils.GetDBFromContext(c) cache := utils.GetCacheFromContext(c) if !auth.HitGroups(db, user, globals.GenerationPermissionGroup) { conn.Send(globals.GenerationSegmentResponse{ Message: "permission denied", Quota: 0, End: true, }) return } check, plan := auth.CanEnableModelWithSubscription(db, cache, user, form.Model, []globals.Message{}) if check != nil { conn.Send(globals.GenerationSegmentResponse{ Message: check.Error(), Quota: 0, End: true, }) return } var instance *utils.Buffer hash, err := CreateGenerationWithCache( auth.GetGroup(db, user), form.Model, form.Prompt, func(buffer *utils.Buffer, data string) { instance = buffer conn.Send(globals.GenerationSegmentResponse{ End: false, Message: data, Quota: buffer.GetQuota(), }) }, ) if instance != nil && !plan && instance.GetQuota() > 0 && user != nil { user.UseQuota(db, instance.GetQuota()) } if err != nil { auth.RevertSubscriptionUsage(db, cache, user, form.Model) conn.Send(globals.GenerationSegmentResponse{ End: true, Error: err.Error(), Quota: instance.GetQuota(), }) return } conn.Send(globals.GenerationSegmentResponse{ End: true, Hash: hash, Quota: instance.GetQuota(), }) } ================================================ FILE: addition/generation/build.go ================================================ package generation import ( "chat/utils" "fmt" "time" ) func GetFolder(hash string) string { return fmt.Sprintf("storage/generation/data/%s", hash) } func GetFolderByHash(model string, prompt string) (string, string) { hash := utils.Sha2Encrypt(model + prompt + time.Now().Format("2006-01-02 15:04:05")) return hash, GetFolder(hash) } func GenerateProject(path string, instance ProjectResult) bool { for name, data := range instance.Result { current := fmt.Sprintf("%s/%s", path, name) if content, ok := data.(string); ok { if utils.WriteFile(current, content, true) != nil { return false } } else { GenerateProject(current, ProjectResult{ Result: data.(map[string]interface{}), }) } } return true } ================================================ FILE: addition/generation/data/.gitkeep ================================================ ================================================ FILE: addition/generation/generate.go ================================================ package generation import ( "chat/globals" "chat/utils" "fmt" ) func CreateGenerationWithCache(group, model, prompt string, hook func(buffer *utils.Buffer, data string)) (string, error) { hash, path := GetFolderByHash(model, prompt) if !utils.Exists(path) { if err := CreateGeneration(group, model, prompt, path, hook); err != nil { globals.Info(fmt.Sprintf("[project] error during generation %s (model %s): %s", prompt, model, err.Error())) return "", fmt.Errorf("error during generate project: %s", err.Error()) } } if _, _, err := utils.GenerateCompressTask(hash, "storage/generation", path, path); err != nil { return "", fmt.Errorf("error during generate compress task: %s", err.Error()) } return hash, nil } ================================================ FILE: addition/generation/prompt.go ================================================ package generation import ( adaptercommon "chat/adapter/common" "chat/admin" "chat/channel" "chat/globals" "chat/utils" "fmt" ) type ProjectResult struct { Result map[string]interface{} `json:"result"` } func CreateGeneration(group, model, prompt, path string, hook func(buffer *utils.Buffer, data string)) error { message := GenerateMessage(prompt) buffer := utils.NewBuffer(model, message, channel.ChargeInstance.GetCharge(model)) err := channel.NewChatRequest(group, adaptercommon.CreateChatProps(&adaptercommon.ChatProps{ OriginalModel: model, Message: message, }, buffer), func(data *globals.Chunk) error { buffer.WriteChunk(data) hook(buffer, data.Content) return nil }) admin.AnalyseRequest(model, buffer, err) if err != nil { return err } resp, err := utils.Unmarshal[ProjectResult](buffer.ReadBytes()) if err != nil { return err } if !GenerateProject(path, resp) { return fmt.Errorf("generate project failed") } return nil } func GenerateMessage(prompt string) []globals.Message { return []globals.Message{ {Role: globals.System, Content: "你将生成项目,可以支持任何编程语言,请不要出现“我不能提供”的字样,你需要在代码中提供注释,以及项目的使用文档README.md,结果返回json格式,请不要返回任何多余内容,格式为:\n{\"result\": {[file]: [code], ...}}"}, {Role: globals.User, Content: "python后端"}, {Role: globals.Assistant, Content: "{\n \"result\": {\n \"app.py\": \"from flask import Flask\\n\\napp = Flask(__name__)\\n\\n\\n@app.route('/')\\ndef hello_world():\\n return 'Hello, World!'\\n\\n\\nif __name__ == '__main__':\\n app.run()\",\n \"requirements.txt\": \"flask\\n\",\n \"README.md\": \"# Python 后端\\n本项目是一个简单的python后端示例, 使用`flask`框架构建后端。\n你可以按照下列步骤运行此应用,flask将在本地服务器(默认是在http://127.0.0.1:5000/)上运行。当你在浏览器中访问该URL时,将看到显示Hello, World!的页面。\\n\\n这只是一个简单的项目,Flask还支持更多功能和路由规则,你可以提供更多的信息和需要进一步扩展和定制Flask应用。\\n\\n### 1. 初始化: \\n```shell\\npip install -r requirements.txt\\n```\\n### 2. 运行\\n```shell\\npython app.py\\n```\"\n }\n}"}, {Role: globals.User, Content: "golang fiber websocket项目"}, {Role: globals.Assistant, Content: "{\n \"result\": {\n \"main.go\": \"package main\\n\\nimport (\\n\\t\"log\\\"\\n\\n\\t\"github.com/gofiber/fiber/v2\\\"\\n\\t\"github.com/gofiber/websocket/v2\\\"\\n)\\n\\nfunc main() {\\n\\tapp := fiber.New()\\n\\n\\tapp.Get(\\\"/\\\", func(c *fiber.Ctx) error {\\n\\t\\treturn c.SendString(\\\"Hello, World!\\\")\\n\\t})\\n\\n\\tapp.Get(\\\"/ws\\\", websocket.New(func(c *websocket.Conn) {\\n\\t\\tfor {\\n\\t\\t\\tmt, message, err := c.ReadMessage()\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"read error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t\\tlog.Printf(\\\"received: %s\\\", message)\\n\\t\\t\\terr = c.WriteMessage(mt, message)\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"write error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}))\\n\\n\\tlog.Fatal(app.Listen(\\\":3000\\\"))\\n}\",\n \"go.mod\": \"module fiber-websocket\\n\\ngo 1.16\\n\\nrequire (\\n\\tgithub.com/gofiber/fiber/v2 v2.12.1\\n\\tgithub.com/gofiber/websocket/v2 v2.10.2\\n)\",\n \"README.md\": \"# Golang Fiber WebSocket项目\\n\\n这个项目是一个使用Golang和Fiber框架构建的WebSocket服务器示例。\\n\\n### 1. 初始化:\\n```shell\\ngo mod init fiber-websocket\\n```\\n\\n### 2. 安装依赖:\\n```shell\\ngo get github.com/gofiber/fiber/v2\\n``` \\n```shell\\ngo get github.com/gofiber/websocket/v2\\n```\\n\\n### 3. 创建main.go文件,将以下代码复制粘贴:\\n\\n```go\\npackage main\\n\\nimport (\\n\\t\\\"log\\\"\\n\\n\\t\\\"github.com/gofiber/fiber/v2\\\"\\n\\t\\\"github.com/gofiber/websocket/v2\\\"\\n)\\n\\nfunc main() {\\n\\tapp := fiber.New()\\n\\n\\tapp.Get(\\\"/\\\", func(c *fiber.Ctx) error {\\n\\t\\treturn c.SendString(\\\"Hello, World!\\\")\\n\\t})\\n\\n\\tapp.Get(\\\"/ws\\\", websocket.New(func(c *websocket.Conn) {\\n\\t\\tfor {\\n\\t\\t\\tmt, message, err := c.ReadMessage()\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"read error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t\\tlog.Printf(\\\"received: %s\\\", message)\\n\\t\\t\\terr = c.WriteMessage(mt, message)\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"write error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}))\\n\\n\\tlog.Fatal(app.Listen(\\\":3000\\\"))\\n}\\n```\\n\\n### 4. 运行应用程序:\\n```shell\\ngo run main.go\\n```\\n\\n应用程序将在本地服务器(默认是在http://localhost:3000)上运行。当你在浏览器中访问`http://localhost:3000`时,将看到显示\"Hello, World!\"的页面。你还可以访问`http://localhost:3000/ws`来测试WebSocket功能。\n\n这只是一个简单的示例,Fiber框架提供了更多的功能和路由规则,你可以在此基础上进行进一步扩展和定制。\n\n注意:在运行应用程序之前,请确保已经安装了Go语言开发环境。"}, {Role: globals.User, Content: prompt}, } } ================================================ FILE: addition/router.go ================================================ package addition import ( "chat/addition/article" "chat/addition/card" "chat/addition/generation" "github.com/gin-gonic/gin" ) func Register(app *gin.RouterGroup) { { app.POST("/card", card.HandlerAPI) app.GET("/generation/create", generation.GenerateAPI) app.GET("/generation/download/tar", generation.ProjectTarDownloadAPI) app.GET("/generation/download/zip", generation.ProjectZipDownloadAPI) app.GET("/article/create", article.GenerateAPI) app.GET("/article/download/tar", article.ProjectTarDownloadAPI) app.GET("/article/download/zip", article.ProjectZipDownloadAPI) } } ================================================ FILE: addition/web/call.go ================================================ package web import ( "chat/globals" "chat/manager/conversation" "chat/utils" "fmt" "time" ) type Hook func(message []globals.Message, token int) (string, error) func toWebSearchingMessage(message []globals.Message) []globals.Message { data, _ := GenerateSearchResult(message[len(message)-1].Content) return utils.Insert(message, 0, globals.Message{ Role: globals.System, 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."+ "Current time: %s, Real-time internet search results: %s", time.Now().Format("2006-01-02 15:04:05"), data, ), }) } func ToChatSearched(instance *conversation.Conversation, restart bool) []globals.Message { segment := conversation.CopyMessage(instance.GetChatMessage(restart)) if instance.IsEnableWeb() { segment = toWebSearchingMessage(segment) } return segment } func ToSearched(enable bool, message []globals.Message) []globals.Message { if enable { return toWebSearchingMessage(message) } return message } ================================================ FILE: addition/web/search.go ================================================ package web import ( "chat/globals" "chat/utils" "errors" "fmt" "net/http" "net/url" "strconv" "strings" "github.com/gin-gonic/gin" ) type SearXNGResponse struct { Query string `json:"query"` NumberOfResults int `json:"number_of_results"` Results []struct { Url string `json:"url"` Title string `json:"title"` Content string `json:"content"` PublishedDate *string `json:"publishedDate,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"` Engine string `json:"engine"` ParsedUrl []string `json:"parsed_url"` Template string `json:"template"` Engines []string `json:"engines"` Positions []int `json:"positions"` Score float64 `json:"score"` Category string `json:"category"` IframeSrc string `json:"iframe_src,omitempty"` } `json:"results"` Answers []interface{} `json:"answers"` Corrections []interface{} `json:"corrections"` Infoboxes []interface{} `json:"infoboxes"` Suggestions []interface{} `json:"suggestions"` UnresponsiveEngines [][]string `json:"unresponsive_engines"` } func formatResponse(data *SearXNGResponse) string { res := make([]string, 0) for _, item := range data.Results { if item.Content == "" || item.Url == "" || item.Title == "" { continue } res = append(res, fmt.Sprintf("%s (%s): %s", item.Title, item.Url, item.Content)) } return strings.Join(res, "\n") } func createURLParams(query string) string { params := url.Values{} params.Add("q", query) params.Add("format", "json") params.Add("safesearch", strconv.Itoa(globals.SearchSafeSearch)) if len(globals.SearchEngines) > 0 { params.Add("engines", globals.SearchEngines) } if len(globals.SearchImageProxy) > 0 { params.Add("image_proxy", globals.SearchImageProxy) } return fmt.Sprintf("%s?%s", globals.SearchEndpoint, params.Encode()) } func createSearXNGRequest(query string) (*SearXNGResponse, error) { data, err := utils.Get(createURLParams(query), nil) if err != nil { return nil, err } return utils.MapToRawStruct[SearXNGResponse](data) } func GenerateSearchResult(q string) (string, error) { res, err := createSearXNGRequest(q) if err != nil { globals.Warn(fmt.Sprintf("[web] failed to get search result: %s (query: %s)", err.Error(), utils.Extract(q, 20, "..."))) content := fmt.Sprintf("search failed: %s", err.Error()) return content, errors.New(content) } content := formatResponse(res) globals.Debug(fmt.Sprintf("[web] search result: %s (query: %s)", utils.Extract(content, 50, "..."), q)) if globals.SearchCrop { globals.Debug(fmt.Sprintf("[web] crop search result length %d to %d max", len(content), globals.SearchCropLength)) return utils.Extract(content, globals.SearchCropLength, "..."), nil } return content, nil } func TestSearch(c *gin.Context) { // get `query` param from query query := c.Query("query") fmt.Println(query) res, err := GenerateSearchResult(query) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) } else { c.JSON(http.StatusOK, gin.H{ "status": true, "result": res, }) } } ================================================ FILE: admin/analysis/analysis.go ================================================ package analysis import ( "chat/channel" "chat/globals" "chat/utils" "database/sql" "time" "github.com/go-redis/redis/v8" ) type UserTypeForm struct { Normal int64 `json:"normal"` ApiPaid int64 `json:"api_paid"` BasicPlan int64 `json:"basic_plan"` StandardPlan int64 `json:"standard_plan"` ProPlan int64 `json:"pro_plan"` Total int64 `json:"total"` } func getDates(t []time.Time) []string { return utils.Each[time.Time, string](t, func(date time.Time) string { return date.Format("1/2") }) } func getFormat(t time.Time) string { return t.Format("2006-01-02") } func getMinuteFormat(t time.Time) string { return t.Format("2006-01-02 15:04") } func GetSubscriptionUsers(db *sql.DB) int64 { var count int64 err := globals.QueryRowDb(db, ` SELECT COUNT(*) FROM subscription WHERE expired_at > NOW() `).Scan(&count) if err != nil { return 0 } return count } func GetBillingToday(cache *redis.Client) float32 { return float32(utils.MustInt(cache, getBillingFormat(getDay()))) / 100 } func GetBillingYesterday(cache *redis.Client) float32 { return float32(utils.MustInt(cache, getBillingFormat(getLastDay()))) / 100 } func GetBillingMonth(cache *redis.Client) float32 { return float32(utils.MustInt(cache, getMonthBillingFormat(getMonth()))) / 100 } func GetBillingLastMonth(cache *redis.Client) float32 { return float32(utils.MustInt(cache, getMonthBillingFormat(getLastMonth()))) / 100 } func GetTpmToday(cache *redis.Client, user string) int64 { // this minute and last minute return utils.MustInt(cache, getTpmFormat(getMinuteFormat(time.Now()), user)) + utils.MustInt(cache, getTpmFormat(getMinuteFormat(time.Now().Add(-time.Minute)), user)) } func GetRpmToday(cache *redis.Client, user string) int64 { // this minute and last minute return utils.MustInt(cache, getRpmFormat(getMinuteFormat(time.Now()), user)) + utils.MustInt(cache, getRpmFormat(getMinuteFormat(time.Now().Add(-time.Minute)), user)) } func GetModelData(cache *redis.Client) ModelChartForm { dates := getDays(7) return ModelChartForm{ Date: getDates(dates), Value: utils.EachNotNil[string, ModelData](globals.SupportModels, func(model string) *ModelData { data := ModelData{ Model: model, Data: utils.Each[time.Time, int64](dates, func(date time.Time) int64 { return utils.MustInt(cache, getModelFormat(getFormat(date), model)) }), } if utils.Sum(data.Data) == 0 { return nil } return &data }), } } func GetSortedModelData(cache *redis.Client) ModelChartForm { form := GetModelData(cache) data := utils.Sort(form.Value, func(a ModelData, b ModelData) bool { return utils.Sum(a.Data) > utils.Sum(b.Data) }) form.Value = data return form } func GetRequestData(cache *redis.Client) RequestChartForm { dates := getDays(7) return RequestChartForm{ Date: getDates(dates), Value: utils.Each[time.Time, int64](dates, func(date time.Time) int64 { return utils.MustInt(cache, getRequestFormat(getFormat(date))) }), } } func GetBillingData(cache *redis.Client) BillingChartForm { dates := getDays(30) return BillingChartForm{ Date: getDates(dates), Value: utils.Each[time.Time, float32](dates, func(date time.Time) float32 { return float32(utils.MustInt(cache, getBillingFormat(getFormat(date)))) / 100. }), } } func GetErrorData(cache *redis.Client) ErrorChartForm { dates := getDays(7) return ErrorChartForm{ Date: getDates(dates), Value: utils.Each[time.Time, int64](dates, func(date time.Time) int64 { return utils.MustInt(cache, getErrorFormat(getFormat(date))) }), } } func GetUserTypeData(db *sql.DB) (UserTypeForm, error) { var form UserTypeForm // get total users if err := globals.QueryRowDb(db, ` SELECT COUNT(*) FROM auth `).Scan(&form.Total); err != nil { return form, err } // get subscription users count (current subscription) // level 1: basic plan, level 2: standard plan, level 3: pro plan if err := globals.QueryRowDb(db, ` SELECT (SELECT COUNT(*) FROM subscription WHERE level = 1 AND expired_at > NOW()), (SELECT COUNT(*) FROM subscription WHERE level = 2 AND expired_at > NOW()), (SELECT COUNT(*) FROM subscription WHERE level = 3 AND expired_at > NOW()) `).Scan(&form.BasicPlan, &form.StandardPlan, &form.ProPlan); err != nil { return form, err } // get normal users count (no subscription in `subscription` table and `quota` + `used` < initial quota in `quota` table) initialQuota := channel.SystemInstance.GetInitialQuota() if err := globals.QueryRowDb(db, ` SELECT COUNT(*) FROM auth WHERE id NOT IN (SELECT user_id FROM subscription WHERE total_month > 0) AND id IN (SELECT user_id FROM quota WHERE quota + used <= ?) `, initialQuota).Scan(&form.Normal); err != nil { return form, err } form.ApiPaid = form.Total - form.Normal - form.BasicPlan - form.StandardPlan - form.ProPlan return form, nil } ================================================ FILE: admin/analysis/format.go ================================================ package analysis import ( "fmt" "time" ) func getMonth() string { date := time.Now() return date.Format("2006-01") } func getLastMonth() string { date := time.Now().AddDate(0, -1, 0) return date.Format("2006-01") } func getDay() string { date := time.Now() return date.Format("2006-01-02") } func getLastDay() string { date := time.Now().AddDate(0, 0, -1) return date.Format("2006-01-02") } func getDays(n int) []time.Time { current := time.Now() var days []time.Time for i := n; i > 0; i-- { days = append(days, current.AddDate(0, 0, -i+1)) } return days } func getErrorFormat(t string) string { return fmt.Sprintf("nio:err-analysis-%s", t) } func getBillingFormat(t string) string { return fmt.Sprintf("nio:billing-analysis-%s", t) } func getMonthBillingFormat(t string) string { return fmt.Sprintf("nio:billing-analysis-%s", t) } func getRequestFormat(t string) string { return fmt.Sprintf("nio:request-analysis-%s", t) } func getModelFormat(t string, model string) string { return fmt.Sprintf("nio:model-analysis-%s-%s", model, t) } func getTpmFormat(t string, user string) string { return fmt.Sprintf("nio:tpm-analysis-%s-%s", user, t) } func getRpmFormat(t string, user string) string { return fmt.Sprintf("nio:rpm-analysis-%s-%s", user, t) } ================================================ FILE: admin/analysis/reflect.go ================================================ package analysis import "reflect" var _ = reflect.TypeOf(UserTypeForm{}) var _ = reflect.TypeOf(ModelData{}) var _ = reflect.TypeOf(ModelChartForm{}) var _ = reflect.TypeOf(RequestChartForm{}) var _ = reflect.TypeOf(BillingChartForm{}) var _ = reflect.TypeOf(ErrorChartForm{}) ================================================ FILE: admin/analysis/statistic.go ================================================ package analysis import ( "chat/adapter" "chat/connection" "chat/utils" "time" "github.com/go-redis/redis/v8" ) func IncrErrorRequest(cache *redis.Client) { utils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2) } func IncrBillingRequest(cache *redis.Client, amount int64, isAdmin bool) { if isAdmin { // do not count billing for admin user return } utils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*90) // 90 days utils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*90) // 90 days } func IncrRequest(cache *redis.Client) { utils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2) } func IncrModelRequest(cache *redis.Client, model string, tokens int64) { utils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2) } func AnalyseRequest(model string, user string, buffer *utils.Buffer, err error) { instance := connection.Cache if adapter.IsAvailableError(err) { IncrErrorRequest(instance) return } token := int64(buffer.CountInputToken() + buffer.CountOutputToken(false)) IncrRequest(instance) IncrModelRequest(instance, model, token) if buffer != nil { IncrRpm(instance, user, 1) IncrTpm(instance, user, token) // add rpm/tpm to root IncrRpm(instance, "root", 1) IncrTpm(instance, "root", token) } } func IncrTpm(cache *redis.Client, user string, n int64) { utils.IncrWithExpire(cache, getTpmFormat(getMinuteFormat(time.Now()), user), n, time.Minute*5) } func IncrRpm(cache *redis.Client, user string, n int64) { utils.IncrWithExpire(cache, getRpmFormat(getMinuteFormat(time.Now()), user), n, time.Minute*5) } ================================================ FILE: admin/analysis/types.go ================================================ package analysis type ModelData struct { Model string `json:"model"` Data []int64 `json:"data"` } type ModelChartForm struct { Date []string `json:"date"` Value []ModelData `json:"value"` } type RequestChartForm struct { Date []string `json:"date"` Value []int64 `json:"value"` } type BillingChartForm struct { Date []string `json:"date"` Value []float32 `json:"value"` } type ErrorChartForm struct { Date []string `json:"date"` Value []int64 `json:"value"` } ================================================ FILE: admin/controller.go ================================================ package admin import ( "chat/admin/analysis" "chat/utils" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) type GenerateInvitationForm struct { Type string `json:"type"` Quota float32 `json:"quota"` Number int `json:"number"` } type DeleteInvitationForm struct { Code string `json:"code"` } type GenerateRedeemForm struct { Quota float32 `json:"quota"` Number int `json:"number"` } type PasswordMigrationForm struct { Id int64 `json:"id"` Password string `json:"password"` } type EmailMigrationForm struct { Id int64 `json:"id"` Email string `json:"email"` } type SetAdminForm struct { Id int64 `json:"id"` Admin bool `json:"admin"` } type BanForm struct { Id int64 `json:"id"` Ban bool `json:"ban"` } type QuotaOperationForm struct { Id int64 `json:"id" binding:"required"` Quota *float32 `json:"quota" binding:"required"` Override bool `json:"override"` } type SubscriptionOperationForm struct { Id int64 `json:"id" binding:"required"` Expired string `json:"expired" binding:"required"` } type SubscriptionLevelForm struct { Id int64 `json:"id" binding:"required"` Level *int64 `json:"level" binding:"required"` } type ReleaseUsageForm struct { Id int64 `json:"id" binding:"required"` } type UpdateRootPasswordForm struct { Password string `json:"password" binding:"required"` } func UpdateMarketAPI(c *gin.Context) { var form MarketModelList if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } err := MarketInstance.SetModels(form) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func InfoAPI(c *gin.Context) { db := utils.GetDBFromContext(c) cache := utils.GetCacheFromContext(c) c.JSON(http.StatusOK, InfoForm{ OnlineChats: utils.GetConns(), SubscriptionCount: analysis.GetSubscriptionUsers(db), BillingToday: analysis.GetBillingToday(cache), BillingMonth: analysis.GetBillingMonth(cache), BillingYesterday: analysis.GetBillingYesterday(cache), BillingLastMonth: analysis.GetBillingLastMonth(cache), }) } func ModelAnalysisAPI(c *gin.Context) { cache := utils.GetCacheFromContext(c) c.JSON(http.StatusOK, analysis.GetSortedModelData(cache)) } func RequestAnalysisAPI(c *gin.Context) { cache := utils.GetCacheFromContext(c) c.JSON(http.StatusOK, analysis.GetRequestData(cache)) } func BillingAnalysisAPI(c *gin.Context) { cache := utils.GetCacheFromContext(c) c.JSON(http.StatusOK, analysis.GetBillingData(cache)) } func ErrorAnalysisAPI(c *gin.Context) { cache := utils.GetCacheFromContext(c) c.JSON(http.StatusOK, analysis.GetErrorData(cache)) } func UserTypeAnalysisAPI(c *gin.Context) { db := utils.GetDBFromContext(c) if form, err := analysis.GetUserTypeData(db); err != nil { c.JSON(http.StatusOK, &analysis.UserTypeForm{}) } else { c.JSON(http.StatusOK, form) } } func RedeemListAPI(c *gin.Context) { db := utils.GetDBFromContext(c) page, _ := strconv.Atoi(c.Query("page")) c.JSON(http.StatusOK, GetRedeemData(db, int64(page))) } func DeleteRedeemAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form DeleteInvitationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } err := DeleteRedeemCode(db, form.Code) c.JSON(http.StatusOK, gin.H{ "status": err == nil, "error": err, }) } func InvitationPaginationAPI(c *gin.Context) { db := utils.GetDBFromContext(c) page, _ := strconv.Atoi(c.Query("page")) c.JSON(http.StatusOK, GetInvitationPagination(db, int64(page))) } func DeleteInvitationAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form DeleteInvitationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } err := DeleteInvitationCode(db, form.Code) c.JSON(http.StatusOK, gin.H{ "status": err == nil, "error": err, }) } func GenerateInvitationAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form GenerateInvitationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, GenerateInvitations(db, form.Number, form.Quota, form.Type)) } func GenerateRedeemAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form GenerateRedeemForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, GenerateRedeemCodes(db, form.Number, form.Quota)) } func UserPaginationAPI(c *gin.Context) { db := utils.GetDBFromContext(c) page, _ := strconv.Atoi(c.Query("page")) search := strings.TrimSpace(c.Query("search")) c.JSON(http.StatusOK, getUsersForm(db, int64(page), search)) } func UpdatePasswordAPI(c *gin.Context) { db := utils.GetDBFromContext(c) cache := utils.GetCacheFromContext(c) var form PasswordMigrationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } err := passwordMigration(db, cache, form.Id, form.Password) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func UpdateEmailAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form EmailMigrationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } err := emailMigration(db, form.Id, form.Email) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func SetAdminAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form SetAdminForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } err := setAdmin(db, form.Id, form.Admin) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func BanAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form BanForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } err := banUser(db, form.Id, form.Ban) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func UserQuotaAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form QuotaOperationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } err := quotaMigration(db, form.Id, *form.Quota, form.Override) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func UserSubscriptionAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form SubscriptionOperationForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } // convert to time if _, err := time.Parse("2006-01-02 15:04:05", form.Expired); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } if err := subscriptionMigration(db, form.Id, form.Expired); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func SubscriptionLevelAPI(c *gin.Context) { db := utils.GetDBFromContext(c) var form SubscriptionLevelForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } err := subscriptionLevelMigration(db, form.Id, *form.Level) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func ReleaseUsageAPI(c *gin.Context) { db := utils.GetDBFromContext(c) cache := utils.GetCacheFromContext(c) var form ReleaseUsageForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } err := releaseUsage(db, cache, form.Id) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func UpdateRootPasswordAPI(c *gin.Context) { var form UpdateRootPasswordForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } db := utils.GetDBFromContext(c) cache := utils.GetCacheFromContext(c) err := UpdateRootPassword(db, cache, form.Password) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func ListLoggerAPI(c *gin.Context) { c.JSON(http.StatusOK, ListLogs()) } func DownloadLoggerAPI(c *gin.Context) { path := c.Query("path") getBlobFile(c, path) } func DeleteLoggerAPI(c *gin.Context) { path := c.Query("path") if err := deleteLogFile(path); err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "status": true, }) } func ConsoleLoggerAPI(c *gin.Context) { n := utils.ParseInt(c.Query("n")) content := getLatestLogs(n) c.JSON(http.StatusOK, gin.H{ "status": true, "content": content, }) } ================================================ FILE: admin/format.go ================================================ package admin import ( "fmt" "time" ) func getMonth() string { date := time.Now() return date.Format("2006-01") } func getDay() string { date := time.Now() return date.Format("2006-01-02") } func getDays(n int) []time.Time { current := time.Now() var days []time.Time for i := n; i > 0; i-- { days = append(days, current.AddDate(0, 0, -i+1)) } return days } func getErrorFormat(t string) string { return fmt.Sprintf("nio:err-analysis-%s", t) } func getBillingFormat(t string) string { return fmt.Sprintf("nio:billing-analysis-%s", t) } func getMonthBillingFormat(t string) string { return fmt.Sprintf("nio:billing-analysis-%s", t) } func getRequestFormat(t string) string { return fmt.Sprintf("nio:request-analysis-%s", t) } func getModelFormat(t string, model string) string { return fmt.Sprintf("nio:model-analysis-%s-%s", model, t) } ================================================ FILE: admin/instance.go ================================================ package admin var MarketInstance *Market func InitInstance() { MarketInstance = NewMarket() } ================================================ FILE: admin/invitation.go ================================================ package admin import ( "chat/globals" "chat/utils" "database/sql" "errors" "fmt" "math" "strings" ) func GetInvitationPagination(db *sql.DB, page int64) PaginationForm { var invitations []interface{} var total int64 if err := globals.QueryRowDb(db, ` SELECT COUNT(*) FROM invitation `).Scan(&total); err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } // get used_user from auth table by `user_id` rows, err := globals.QueryDb(db, ` SELECT invitation.code, invitation.quota, invitation.type, invitation.used, invitation.created_at, invitation.updated_at, COALESCE(auth.username, '-') as username FROM invitation LEFT JOIN auth ON auth.id = invitation.used_id ORDER BY invitation.id DESC LIMIT ? OFFSET ? `, pagination, page*pagination) if err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } for rows.Next() { var invitation InvitationData var createdAt []uint8 var updatedAt []uint8 if err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &createdAt, &updatedAt, &invitation.Username); err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } invitation.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05") invitation.UpdatedAt = utils.ConvertTime(updatedAt).Format("2006-01-02 15:04:05") invitations = append(invitations, invitation) } return PaginationForm{ Status: true, Total: int(math.Ceil(float64(total) / float64(pagination))), Data: invitations, } } func DeleteInvitationCode(db *sql.DB, code string) error { _, err := globals.ExecDb(db, ` DELETE FROM invitation WHERE code = ? `, code) return err } func NewInvitationCode(db *sql.DB, code string, quota float32, t string) error { _, err := globals.ExecDb(db, ` INSERT INTO invitation (code, quota, type) VALUES (?, ?, ?) `, code, quota, t) return err } func GenerateInvitations(db *sql.DB, num int, quota float32, t string) InvitationGenerateResponse { arr := make([]string, 0) idx := 0 retry := 0 for idx < num { code := fmt.Sprintf("%s-%s", t, utils.GenerateChar(24)) if err := NewInvitationCode(db, code, quota, t); err != nil { // ignore duplicate code if errors.Is(err, sql.ErrNoRows) { continue } if retry < 100 && strings.Contains(err.Error(), "Duplicate entry") { retry++ continue } retry = 0 return InvitationGenerateResponse{ Status: false, Message: err.Error(), } } arr = append(arr, code) idx++ } return InvitationGenerateResponse{ Status: true, Data: arr, } } ================================================ FILE: admin/logger.go ================================================ package admin import ( "chat/globals" "chat/utils" "fmt" "github.com/gin-gonic/gin" "strings" ) type LogFile struct { Path string `json:"path"` Size int64 `json:"size"` } func ListLogs() []LogFile { return utils.Each(utils.Walk("logs"), func(path string) LogFile { return LogFile{ Path: strings.TrimLeft(path, "logs/"), Size: utils.GetFileSize(path), } }) } func getLogPath(path string) string { return fmt.Sprintf("logs/%s", path) } func getBlobFile(c *gin.Context, path string) { c.File(getLogPath(path)) } func deleteLogFile(path string) error { return utils.DeleteFile(getLogPath(path)) } func getLatestLogs(n int) string { if n <= 0 { n = 100 } content, err := utils.ReadFileLatestLines(getLogPath(globals.DefaultLoggerFile), n) if err != nil { return fmt.Sprintf("read error: %s", err.Error()) } return content } ================================================ FILE: admin/market.go ================================================ package admin import ( "chat/globals" "chat/utils" "fmt" "github.com/spf13/viper" ) type ModelTag []string type MarketModel struct { Id string `json:"id" mapstructure:"id" required:"true"` Name string `json:"name" mapstructure:"name" required:"true"` Description string `json:"description" mapstructure:"description"` Default bool `json:"default" mapstructure:"default"` HighContext bool `json:"high_context" mapstructure:"highcontext"` Avatar string `json:"avatar" mapstructure:"avatar"` Tag ModelTag `json:"tag" mapstructure:"tag"` } type MarketModelList []MarketModel type Market struct { Models MarketModelList `json:"models" mapstructure:"models"` } func NewMarket() *Market { var models MarketModelList if err := viper.UnmarshalKey("market", &models); err != nil { globals.Warn(fmt.Sprintf("[market] read config error: %s, use default config", err.Error())) models = MarketModelList{} } return &Market{ Models: models, } } func (m *Market) GetModels() MarketModelList { return m.Models } func (m *Market) GetModel(id string) *MarketModel { for _, model := range m.Models { if model.Id == id { return &model } } return nil } func (m *Market) SaveConfig() error { return utils.SaveConfig("market", m.Models) } func (m *Market) SetModels(models MarketModelList) error { m.Models = models return m.SaveConfig() } ================================================ FILE: admin/redeem.go ================================================ package admin import ( "chat/globals" "chat/utils" "database/sql" "fmt" "math" "strings" ) func GetRedeemData(db *sql.DB, page int64) PaginationForm { var data []interface{} var total int64 if err := globals.QueryRowDb(db, ` SELECT COUNT(*) FROM redeem `).Scan(&total); err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } rows, err := globals.QueryDb(db, ` SELECT code, quota, used, created_at, updated_at FROM redeem ORDER BY id DESC LIMIT ? OFFSET ? `, pagination, page*pagination) if err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } for rows.Next() { var redeem RedeemData var createdAt []uint8 var updatedAt []uint8 if err := rows.Scan(&redeem.Code, &redeem.Quota, &redeem.Used, &createdAt, &updatedAt); err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } redeem.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05") redeem.UpdatedAt = utils.ConvertTime(updatedAt).Format("2006-01-02 15:04:05") data = append(data, redeem) } return PaginationForm{ Status: true, Total: int(math.Ceil(float64(total) / float64(pagination))), Data: data, } } func DeleteRedeemCode(db *sql.DB, code string) error { _, err := globals.ExecDb(db, ` DELETE FROM redeem WHERE code = ? `, code) return err } func GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse { arr := make([]string, 0) idx := 0 for idx < num { code, err := CreateRedeemCode(db, quota) if err != nil { return RedeemGenerateResponse{ Status: false, Message: err.Error(), } } arr = append(arr, code) idx++ } return RedeemGenerateResponse{ Status: true, Data: arr, } } func CreateRedeemCode(db *sql.DB, quota float32) (string, error) { code := fmt.Sprintf("nio-%s", utils.GenerateChar(32)) _, err := globals.ExecDb(db, ` INSERT INTO redeem (code, quota) VALUES (?, ?) `, code, quota) if err != nil && strings.Contains(err.Error(), "Duplicate entry") { // code name is duplicate return CreateRedeemCode(db, quota) } return code, err } ================================================ FILE: admin/router.go ================================================ package admin import ( "chat/addition/web" "chat/channel" "github.com/gin-gonic/gin" ) func Register(app *gin.RouterGroup) { channel.Register(app) app.GET("/admin/config/test/search", web.TestSearch) app.GET("/admin/analytics/info", InfoAPI) app.GET("/admin/analytics/model", ModelAnalysisAPI) app.GET("/admin/analytics/request", RequestAnalysisAPI) app.GET("/admin/analytics/billing", BillingAnalysisAPI) app.GET("/admin/analytics/error", ErrorAnalysisAPI) app.GET("/admin/analytics/user", UserTypeAnalysisAPI) app.GET("/admin/invitation/list", InvitationPaginationAPI) app.POST("/admin/invitation/generate", GenerateInvitationAPI) app.POST("/admin/invitation/delete", DeleteInvitationAPI) app.GET("/admin/redeem/list", RedeemListAPI) app.POST("/admin/redeem/generate", GenerateRedeemAPI) app.POST("/admin/redeem/delete", DeleteRedeemAPI) app.GET("/admin/user/list", UserPaginationAPI) app.POST("/admin/user/quota", UserQuotaAPI) app.POST("/admin/user/subscription", UserSubscriptionAPI) app.POST("/admin/user/level", SubscriptionLevelAPI) app.POST("/admin/user/release", ReleaseUsageAPI) app.POST("/admin/user/password", UpdatePasswordAPI) app.POST("/admin/user/email", UpdateEmailAPI) app.POST("/admin/user/ban", BanAPI) app.POST("/admin/user/admin", SetAdminAPI) app.POST("/admin/user/root", UpdateRootPasswordAPI) app.POST("/admin/market/update", UpdateMarketAPI) app.GET("/admin/logger/list", ListLoggerAPI) app.GET("/admin/logger/download", DownloadLoggerAPI) app.GET("/admin/logger/console", ConsoleLoggerAPI) app.POST("/admin/logger/delete", DeleteLoggerAPI) } ================================================ FILE: admin/statistic.go ================================================ package admin import ( "chat/adapter" "chat/connection" "chat/utils" "time" "github.com/go-redis/redis/v8" ) func IncrErrorRequest(cache *redis.Client) { utils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2) } func IncrBillingRequest(cache *redis.Client, amount int64) { utils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*30*2) utils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*30*2) } func IncrRequest(cache *redis.Client) { utils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2) } func IncrModelRequest(cache *redis.Client, model string, tokens int64) { utils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2) } func AnalyseRequest(model string, buffer *utils.Buffer, err error) { instance := connection.Cache if adapter.IsAvailableError(err) { IncrErrorRequest(instance) return } IncrRequest(instance) IncrModelRequest(instance, model, int64(buffer.CountToken())) } ================================================ FILE: admin/types.go ================================================ package admin var pagination int64 = 10 type InfoForm struct { BillingToday float32 `json:"billing_today"` BillingMonth float32 `json:"billing_month"` BillingYesterday float32 `json:"billing_yesterday"` BillingLastMonth float32 `json:"billing_last_month"` SubscriptionCount int64 `json:"subscription_count"` OnlineChats int64 `json:"online_chats"` } type ModelData struct { Model string `json:"model"` Data []int64 `json:"data"` } type ModelChartForm struct { Date []string `json:"date"` Value []ModelData `json:"value"` } type RequestChartForm struct { Date []string `json:"date"` Value []int64 `json:"value"` } type BillingChartForm struct { Date []string `json:"date"` Value []float32 `json:"value"` } type ErrorChartForm struct { Date []string `json:"date"` Value []int64 `json:"value"` } type PaginationForm struct { Status bool `json:"status"` Total int `json:"total"` Data []interface{} `json:"data"` Message string `json:"message"` } type InvitationData struct { Code string `json:"code"` Quota float32 `json:"quota"` Type string `json:"type"` Used bool `json:"used"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Username string `json:"username"` } type RedeemData struct { Code string `json:"code"` Quota float32 `json:"quota"` Used bool `json:"used"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type InvitationGenerateResponse struct { Status bool `json:"status"` Message string `json:"message"` Data []string `json:"data"` } type RedeemGenerateResponse struct { Status bool `json:"status"` Message string `json:"message"` Data []string `json:"data"` } type UserData struct { Id int64 `json:"id"` Username string `json:"username"` Email string `json:"email"` IsAdmin bool `json:"is_admin"` Quota float32 `json:"quota"` UsedQuota float32 `json:"used_quota"` ExpiredAt string `json:"expired_at"` IsSubscribed bool `json:"is_subscribed"` TotalMonth int64 `json:"total_month"` Enterprise bool `json:"enterprise"` Level int `json:"level"` IsBanned bool `json:"is_banned"` } ================================================ FILE: admin/user.go ================================================ package admin import ( "chat/channel" "chat/globals" "chat/utils" "context" "database/sql" "fmt" "math" "strings" "time" "github.com/go-redis/redis/v8" ) // AuthLike is to solve the problem of import cycle type AuthLike struct { ID int64 `json:"id"` } func (a *AuthLike) GetID(_ *sql.DB) int64 { return a.ID } func (a *AuthLike) HitID() int64 { return a.ID } func getUsersForm(db *sql.DB, page int64, search string) PaginationForm { // if search is empty, then search all users var users []interface{} var total int64 if err := globals.QueryRowDb(db, ` SELECT COUNT(*) FROM auth WHERE username LIKE ? `, "%"+search+"%").Scan(&total); err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } rows, err := globals.QueryDb(db, ` SELECT auth.id, auth.username, auth.email, auth.is_admin, quota.quota, quota.used, subscription.expired_at, subscription.total_month, subscription.enterprise, subscription.level, auth.is_banned FROM auth LEFT JOIN quota ON quota.user_id = auth.id LEFT JOIN subscription ON subscription.user_id = auth.id WHERE auth.username LIKE ? ORDER BY auth.id LIMIT ? OFFSET ? `, "%"+search+"%", pagination, page*pagination) if err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } for rows.Next() { var user UserData var ( email sql.NullString expired []uint8 quota sql.NullFloat64 usedQuota sql.NullFloat64 totalMonth sql.NullInt64 isEnterprise sql.NullBool subscriptionLevel sql.NullInt64 isBanned sql.NullBool ) if err := rows.Scan(&user.Id, &user.Username, &email, &user.IsAdmin, "a, &usedQuota, &expired, &totalMonth, &isEnterprise, &subscriptionLevel, &isBanned); err != nil { return PaginationForm{ Status: false, Message: err.Error(), } } if email.Valid { user.Email = email.String } if quota.Valid { user.Quota = float32(quota.Float64) } if usedQuota.Valid { user.UsedQuota = float32(usedQuota.Float64) } if totalMonth.Valid { user.TotalMonth = totalMonth.Int64 } if subscriptionLevel.Valid { user.Level = int(subscriptionLevel.Int64) } stamp := utils.ConvertTime(expired) if stamp != nil { user.IsSubscribed = stamp.After(time.Now()) user.ExpiredAt = stamp.Format("2006-01-02 15:04:05") } user.Enterprise = isEnterprise.Valid && isEnterprise.Bool user.IsBanned = isBanned.Valid && isBanned.Bool users = append(users, user) } return PaginationForm{ Status: true, Total: int(math.Ceil(float64(total) / float64(pagination))), Data: users, } } // clearUserCache clears all cache keys starting with nio:user: func clearUserCache(cache *redis.Client) error { ctx := context.Background() iter := cache.Scan(ctx, 0, "nio:user:*", 100).Iterator() for iter.Next(ctx) { if err := cache.Del(ctx, iter.Val()).Err(); err != nil { return fmt.Errorf("failed to delete cache key %s: %v", iter.Val(), err) } } return iter.Err() } func passwordMigration(db *sql.DB, cache *redis.Client, id int64, password string) error { password = strings.TrimSpace(password) if len(password) < 6 || len(password) > 36 { return fmt.Errorf("password length must be between 6 and 36") } hash_passwd := utils.Sha2Encrypt(password) // Update password in database _, err := globals.ExecDb(db, ` UPDATE auth SET password = ? WHERE id = ? `, hash_passwd, id) if err != nil { return err } // Clear all user related cache if err := clearUserCache(cache); err != nil { return fmt.Errorf("failed to clear user cache: %v", err) } return nil } func emailMigration(db *sql.DB, id int64, email string) error { _, err := globals.ExecDb(db, ` UPDATE auth SET email = ? WHERE id = ? `, email, id) return err } func setAdmin(db *sql.DB, id int64, isAdmin bool) error { _, err := globals.ExecDb(db, ` UPDATE auth SET is_admin = ? WHERE id = ? `, isAdmin, id) return err } func banUser(db *sql.DB, id int64, isBanned bool) error { _, err := globals.ExecDb(db, ` UPDATE auth SET is_banned = ? WHERE id = ? `, isBanned, id) return err } func quotaMigration(db *sql.DB, id int64, quota float32, override bool) error { // if quota is negative, then decrease quota // if quota is positive, then increase quota if override { _, err := globals.ExecDb(db, ` INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = ? `, id, quota, 0., quota) return err } _, err := globals.ExecDb(db, ` INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quota = quota + ? `, id, quota, 0., quota) return err } func subscriptionMigration(db *sql.DB, id int64, expired string) error { _, err := globals.ExecDb(db, ` INSERT INTO subscription (user_id, expired_at) VALUES (?, ?) ON DUPLICATE KEY UPDATE expired_at = ? `, id, expired, expired) return err } func subscriptionLevelMigration(db *sql.DB, id int64, level int64) error { if level < 0 || level > 3 { return fmt.Errorf("invalid subscription level") } _, err := globals.ExecDb(db, ` INSERT INTO subscription (user_id, level) VALUES (?, ?) ON DUPLICATE KEY UPDATE level = ? `, id, level, level) return err } func releaseUsage(db *sql.DB, cache *redis.Client, id int64) error { var level sql.NullInt64 if err := globals.QueryRowDb(db, ` SELECT level FROM subscription WHERE user_id = ? `, id).Scan(&level); err != nil { return err } if !level.Valid || level.Int64 == 0 { return fmt.Errorf("user is not subscribed") } u := &AuthLike{ID: id} plan := channel.PlanInstance.GetPlan(int(level.Int64)) if !plan.ReleaseAll(u, cache) { return fmt.Errorf("cannot release usage") } return nil } func UpdateRootPassword(db *sql.DB, cache *redis.Client, password string) error { password = strings.TrimSpace(password) if len(password) < 6 || len(password) > 36 { return fmt.Errorf("password length must be between 6 and 36") } if _, err := globals.ExecDb(db, ` UPDATE auth SET password = ? WHERE username = 'root' `, utils.Sha2Encrypt(password)); err != nil { return err } // Clear all user related cache if err := clearUserCache(cache); err != nil { return fmt.Errorf("failed to clear user cache: %v", err) } return nil } ================================================ FILE: app/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ================================================ FILE: app/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr dev-dist *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? # Libre db ================================================ FILE: app/.prettierrc.json ================================================ {} ================================================ FILE: app/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": false, "tsx": true, "tailwind": { "config": "tailwind.config.js", "css": "app/globals.css", "baseColor": "slate", "cssVariables": true }, "aliases": { "components": "src/components", "utils": "@/components/ui/lib/utils" } } ================================================ FILE: app/index.html ================================================ CoAI.Dev
================================================ FILE: app/package.json ================================================ { "name": "coai", "private": false, "version": "2.6.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build --mode production", "fast-build": "cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "prettier": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"", "preview": "vite preview" }, "dependencies": { "@fontsource-variable/inter": "^5.1.0", "@fontsource-variable/jetbrains-mono": "^5.1.1", "@headlessui/react": "^1.7.18", "@headlessui/tailwindcss": "^0.2.0", "@lobehub/icons": "1.49.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@reduxjs/toolkit": "^1.9.5", "@tanem/react-nprogress": "^5.0.51", "@tremor/react": "^3.14.0", "@types/crypto-js": "^4.2.2", "axios": "^1.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", "crypto-js": "^4.2.0", "date-fns": "^3.6.0", "emoji-picker-react": "^4.7.18", "framer-motion": "^11.3.28", "html-to-image": "^1.11.11", "i18next": "^23.4.6", "localforage": "^1.10.0", "lucide-react": "^0.483.0", "match-sorter": "^6.3.1", "mermaid": "^10.9.0", "next-themes": "^0.2.1", "normalize.css": "^8.0.1", "qrcode.react": "^4.2.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-day-picker": "^8.10.0", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", "react-i18next": "^13.2.2", "react-intersection-observer": "^9.13.1", "react-markdown": "^8.0.7", "react-redux": "^8.1.2", "react-router-dom": "^6.17.0", "react-syntax-highlighter": "^15.5.0", "react-virtuoso": "^4.7.10", "react-window": "^1.8.10", "rehype-katex": "^6.0.3", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "sonner": "^1.5.0", "sort-by": "^1.2.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", "use-debounce": "^10.0.0", "vaul": "^0.9.0", "workbox-window": "^7.0.0" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", "@tauri-apps/cli": "^1.5.6", "@types/node": "^20.5.9", "@types/react": "^18.2.15", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "^18.2.7", "@types/react-syntax-highlighter": "^15.5.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.15", "cross-env": "^7.0.3", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "less": "^4.2.0", "less-loader": "^11.1.3", "postcss": "^8.4.29", "prettier": "^3.0.3", "tailwindcss": "^3.3.3", "typescript": "^5.0.2", "vite": "^4.4.5", "vite-plugin-html": "^3.2.0" } } ================================================ FILE: app/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: app/public/manifest.json ================================================ { "_1c.7075dba8.js": { "file": "assets/1c.7075dba8.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_OperationAction.5b1f951a.js": { "file": "assets/OperationAction.5b1f951a.js", "imports": [ "index.html" ] }, "_Paragraph.4ca577cf.js": { "file": "assets/Paragraph.4ca577cf.js", "imports": [ "index.html" ] }, "_Tableau10.1b767f5e.js": { "file": "assets/Tableau10.1b767f5e.js" }, "_Tracker.20c1771e.js": { "file": "assets/Tracker.20c1771e.js", "imports": [ "index.html", "__isIndex.00b9330c.js", "_tiny-invariant.dd7d57d2.js", "_path.53f90ab3.js", "_linear.1519afab.js", "_time.f5c88166.js", "_init.a5b10ee5.js", "_band.6ffec690.js", "_ordinal.93cdc51b.js", "_array.9f3ba611.js", "_line.c9b6b09c.js" ] }, "__isIndex.00b9330c.js": { "file": "assets/_isIndex.00b9330c.js", "imports": [ "index.html" ] }, "_abnf.9bf3a7c1.js": { "file": "assets/abnf.9bf3a7c1.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_accesslog.68f2b66c.js": { "file": "assets/accesslog.68f2b66c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_actionscript.67a1e568.js": { "file": "assets/actionscript.67a1e568.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_activity.612642ea.js": { "file": "assets/activity.612642ea.js", "imports": [ "index.html" ] }, "_ada.c0e23f60.js": { "file": "assets/ada.c0e23f60.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_angelscript.410ffc68.js": { "file": "assets/angelscript.410ffc68.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_apache.006764de.js": { "file": "assets/apache.006764de.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_applescript.39299b7d.js": { "file": "assets/applescript.39299b7d.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_arc.a247e62c.js": { "file": "assets/arc.a247e62c.js", "imports": [ "_path.53f90ab3.js", "index.html" ] }, "_arcade.f590a3cc.js": { "file": "assets/arcade.f590a3cc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_arduino.c4c70db8.js": { "file": "assets/arduino.c4c70db8.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_armasm.50cdbe9a.js": { "file": "assets/armasm.50cdbe9a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_array.9f3ba611.js": { "file": "assets/array.9f3ba611.js" }, "_arrow-right-left.f4c82cc0.js": { "file": "assets/arrow-right-left.f4c82cc0.js", "imports": [ "index.html" ] }, "_asciidoc.e1e2cbee.js": { "file": "assets/asciidoc.e1e2cbee.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_aspectj.a696b879.js": { "file": "assets/aspectj.a696b879.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_asterisk-square.2615ee58.js": { "file": "assets/asterisk-square.2615ee58.js", "imports": [ "index.html" ] }, "_autohotkey.c54238fc.js": { "file": "assets/autohotkey.c54238fc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_autoit.25b13806.js": { "file": "assets/autoit.25b13806.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_avrasm.a69814df.js": { "file": "assets/avrasm.a69814df.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_awk.9f65b5a4.js": { "file": "assets/awk.9f65b5a4.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_axapta.fa0aef8d.js": { "file": "assets/axapta.fa0aef8d.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_band.6ffec690.js": { "file": "assets/band.6ffec690.js", "imports": [ "_init.a5b10ee5.js", "_ordinal.93cdc51b.js" ] }, "_bash.a8e19ccc.js": { "file": "assets/bash.a8e19ccc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_basic.6acc6ac9.js": { "file": "assets/basic.6acc6ac9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_bnf.bd11f475.js": { "file": "assets/bnf.bd11f475.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_book-dashed.798b0c5c.js": { "file": "assets/book-dashed.798b0c5c.js", "imports": [ "index.html" ] }, "_box.6dd688fe.js": { "file": "assets/box.6dd688fe.js", "imports": [ "index.html" ] }, "_brainfuck.db01d938.js": { "file": "assets/brainfuck.db01d938.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_c-like.af826c24.js": { "file": "assets/c-like.af826c24.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_c.cc4c2414.js": { "file": "assets/c.cc4c2414.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_cal.b721a23c.js": { "file": "assets/cal.b721a23c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_calendar-clock.1432b482.js": { "file": "assets/calendar-clock.1432b482.js", "imports": [ "index.html" ] }, "_calendar.2f96549a.js": { "file": "assets/calendar.2f96549a.js", "imports": [ "index.html" ] }, "_capnproto.7f37471d.js": { "file": "assets/capnproto.7f37471d.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ceylon.f41d8cb9.js": { "file": "assets/ceylon.f41d8cb9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_channel.0e442477.js": { "file": "assets/channel.0e442477.js", "imports": [ "index.html" ] }, "_chart.b43e29db.js": { "file": "assets/chart.b43e29db.js", "imports": [ "index.html" ] }, "_clean.2230529c.js": { "file": "assets/clean.2230529c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_clock.36b36b21.js": { "file": "assets/clock.36b36b21.js", "imports": [ "index.html" ] }, "_clojure-repl.24369284.js": { "file": "assets/clojure-repl.24369284.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_clojure.98540e11.js": { "file": "assets/clojure.98540e11.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_clone.2acf797d.js": { "file": "assets/clone.2acf797d.js", "imports": [ "_graph.0ef9c3b7.js" ] }, "_cmake.687d96e9.js": { "file": "assets/cmake.687d96e9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_coffeescript.09ac2f3f.js": { "file": "assets/coffeescript.09ac2f3f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_coq.18e8d32c.js": { "file": "assets/coq.18e8d32c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_core.b8b80318.js": { "file": "assets/core.b8b80318.js", "isDynamicEntry": true }, "_cos.cfa5f5a1.js": { "file": "assets/cos.cfa5f5a1.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_cpp.a6b75908.js": { "file": "assets/cpp.a6b75908.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_createText-6b48ae7d.01af0827.js": { "file": "assets/createText-6b48ae7d.01af0827.js", "imports": [ "index.html" ] }, "_crmsh.929b5790.js": { "file": "assets/crmsh.929b5790.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_crystal.520aae80.js": { "file": "assets/crystal.520aae80.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_csharp.676d187e.js": { "file": "assets/csharp.676d187e.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_csp.27c6fdf5.js": { "file": "assets/csp.27c6fdf5.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_css.aed2fec3.js": { "file": "assets/css.aed2fec3.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_d.deee4072.js": { "file": "assets/d.deee4072.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_dart.86736099.js": { "file": "assets/dart.86736099.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_delphi.b2e83fb0.js": { "file": "assets/delphi.b2e83fb0.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_diff.52ac5061.js": { "file": "assets/diff.52ac5061.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_django.acf6f5d0.js": { "file": "assets/django.acf6f5d0.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_dns.ae462891.js": { "file": "assets/dns.ae462891.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_dockerfile.a461ef01.js": { "file": "assets/dockerfile.a461ef01.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_dos.66ec8774.js": { "file": "assets/dos.66ec8774.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_download-cloud.04333a4f.js": { "file": "assets/download-cloud.04333a4f.js", "imports": [ "index.html" ] }, "_dsconfig.948d33c9.js": { "file": "assets/dsconfig.948d33c9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_dts.ace8285a.js": { "file": "assets/dts.ace8285a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_dust.61c6165d.js": { "file": "assets/dust.61c6165d.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ebnf.9f47f2bf.js": { "file": "assets/ebnf.9f47f2bf.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_edges-d32062c0.bb379725.js": { "file": "assets/edges-d32062c0.bb379725.js", "imports": [ "index.html", "_createText-6b48ae7d.01af0827.js", "_line.c9b6b09c.js" ] }, "_elixir.87b77a7b.js": { "file": "assets/elixir.87b77a7b.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_elm.62fc5561.js": { "file": "assets/elm.62fc5561.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_erb.55b07d6c.js": { "file": "assets/erb.55b07d6c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_erlang-repl.323f7160.js": { "file": "assets/erlang-repl.323f7160.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_erlang.c7e57743.js": { "file": "assets/erlang.c7e57743.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_excel.4522222f.js": { "file": "assets/excel.4522222f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_filter.0ca1fe92.js": { "file": "assets/filter.0ca1fe92.js", "imports": [ "index.html" ] }, "_fix.51ea128a.js": { "file": "assets/fix.51ea128a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_flix.c3fcc323.js": { "file": "assets/flix.c3fcc323.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_flowDb-4b19a42f.0b3ed11f.js": { "file": "assets/flowDb-4b19a42f.0b3ed11f.js", "imports": [ "index.html" ] }, "_fortran.5261c416.js": { "file": "assets/fortran.5261c416.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_fsharp.11981ee5.js": { "file": "assets/fsharp.11981ee5.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gams.49652455.js": { "file": "assets/gams.49652455.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gauss.012009e3.js": { "file": "assets/gauss.012009e3.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gcode.0c400ff3.js": { "file": "assets/gcode.0c400ff3.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gherkin.6750692c.js": { "file": "assets/gherkin.6750692c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gift.b3e4cc82.js": { "file": "assets/gift.b3e4cc82.js", "imports": [ "index.html" ] }, "_glsl.3aa8f677.js": { "file": "assets/glsl.3aa8f677.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gml.8a3d692a.js": { "file": "assets/gml.8a3d692a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_go.9e6e090c.js": { "file": "assets/go.9e6e090c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_golo.0c3cd41a.js": { "file": "assets/golo.0c3cd41a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_gradle.9b2be066.js": { "file": "assets/gradle.9b2be066.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_graph.0ef9c3b7.js": { "file": "assets/graph.0ef9c3b7.js", "imports": [ "index.html" ] }, "_groovy.e5a1e4e3.js": { "file": "assets/groovy.e5a1e4e3.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_haml.0925c5ed.js": { "file": "assets/haml.0925c5ed.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_handlebars.a705eec6.js": { "file": "assets/handlebars.a705eec6.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_haskell.c1c10057.js": { "file": "assets/haskell.c1c10057.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_haxe.bb1f4576.js": { "file": "assets/haxe.bb1f4576.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_history.44d149e5.js": { "file": "assets/history.44d149e5.js", "imports": [ "index.html" ] }, "_hook.09e5815b.js": { "file": "assets/hook.09e5815b.js", "imports": [ "index.html" ] }, "_hsp.acbd24bb.js": { "file": "assets/hsp.acbd24bb.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_htmlbars.ae56fbaf.js": { "file": "assets/htmlbars.ae56fbaf.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_http.cadf2244.js": { "file": "assets/http.cadf2244.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_hy.94f04bbe.js": { "file": "assets/hy.94f04bbe.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_icons.7e9c9414.js": { "file": "assets/icons.7e9c9414.js", "imports": [ "index.html" ] }, "_index-fc10efb0.021331b3.js": { "file": "assets/index-fc10efb0.021331b3.js", "imports": [ "_graph.0ef9c3b7.js", "_layout.991ed0c6.js", "_clone.2acf797d.js", "_edges-d32062c0.bb379725.js", "index.html", "_createText-6b48ae7d.01af0827.js" ] }, "_inform7.573ba170.js": { "file": "assets/inform7.573ba170.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ini.0ce80bb9.js": { "file": "assets/ini.0ce80bb9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_init.a5b10ee5.js": { "file": "assets/init.a5b10ee5.js" }, "_irpf90.a8791531.js": { "file": "assets/irpf90.a8791531.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_isbl.17bd57b9.js": { "file": "assets/isbl.17bd57b9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_java.de21cb0c.js": { "file": "assets/java.de21cb0c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_javascript.4b296926.js": { "file": "assets/javascript.4b296926.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_jboss-cli.f0eb5633.js": { "file": "assets/jboss-cli.f0eb5633.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_json.6e716f3a.js": { "file": "assets/json.6e716f3a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_julia-repl.6f5d2fd3.js": { "file": "assets/julia-repl.6f5d2fd3.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_julia.41a3881b.js": { "file": "assets/julia.41a3881b.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_kanban-square-dashed.c88dceb2.js": { "file": "assets/kanban-square-dashed.c88dceb2.js", "imports": [ "index.html" ] }, "_kotlin.c59af0ba.js": { "file": "assets/kotlin.c59af0ba.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_lasso.8a28f498.js": { "file": "assets/lasso.8a28f498.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_latex.058112d0.js": { "file": "assets/latex.058112d0.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_layout.991ed0c6.js": { "file": "assets/layout.991ed0c6.js", "imports": [ "_graph.0ef9c3b7.js", "index.html" ] }, "_ldif.e8f34ce7.js": { "file": "assets/ldif.e8f34ce7.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_leaf.9ed3302f.js": { "file": "assets/leaf.9ed3302f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_less.1e2f29fc.js": { "file": "assets/less.1e2f29fc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_line.c9b6b09c.js": { "file": "assets/line.c9b6b09c.js", "imports": [ "_array.9f3ba611.js", "_path.53f90ab3.js", "index.html" ] }, "_linear.1519afab.js": { "file": "assets/linear.1519afab.js", "imports": [ "index.html", "_init.a5b10ee5.js" ] }, "_lisp.ab804084.js": { "file": "assets/lisp.ab804084.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_livecodeserver.ada7c599.js": { "file": "assets/livecodeserver.ada7c599.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_livescript.09fbd630.js": { "file": "assets/livescript.09fbd630.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_llvm.c75c9c59.js": { "file": "assets/llvm.c75c9c59.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_lsl.21a37c7a.js": { "file": "assets/lsl.21a37c7a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_lua.c0a16d09.js": { "file": "assets/lua.c0a16d09.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_makefile.bb0fcfbd.js": { "file": "assets/makefile.bb0fcfbd.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_markdown.26dd2c01.js": { "file": "assets/markdown.26dd2c01.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_market.93d110b3.js": { "file": "assets/market.93d110b3.js" }, "_mathematica.2dc23a04.js": { "file": "assets/mathematica.2dc23a04.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_matlab.e39e79ce.js": { "file": "assets/matlab.e39e79ce.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_maxima.b42deb33.js": { "file": "assets/maxima.b42deb33.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_mel.30384c03.js": { "file": "assets/mel.30384c03.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_mercury.4f44fcae.js": { "file": "assets/mercury.4f44fcae.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_mipsasm.6d204ed1.js": { "file": "assets/mipsasm.6d204ed1.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_mizar.6cf15cbd.js": { "file": "assets/mizar.6cf15cbd.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_mojolicious.834a438a.js": { "file": "assets/mojolicious.834a438a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_monkey.08e49847.js": { "file": "assets/monkey.08e49847.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_moonscript.96b4ff81.js": { "file": "assets/moonscript.96b4ff81.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_more-vertical.06df6aff.js": { "file": "assets/more-vertical.06df6aff.js", "imports": [ "index.html" ] }, "_multi-combobox.51bcb343.js": { "file": "assets/multi-combobox.51bcb343.js", "imports": [ "index.html" ] }, "_n1ql.bb73836f.js": { "file": "assets/n1ql.bb73836f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_nginx.83dced37.js": { "file": "assets/nginx.83dced37.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_nim.9b03230a.js": { "file": "assets/nim.9b03230a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_nix.62253a60.js": { "file": "assets/nix.62253a60.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_node-repl.e38345ae.js": { "file": "assets/node-repl.e38345ae.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_nsis.aa91a0cd.js": { "file": "assets/nsis.aa91a0cd.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_objectivec.636246aa.js": { "file": "assets/objectivec.636246aa.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ocaml.6a0416aa.js": { "file": "assets/ocaml.6a0416aa.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_openscad.fa2654cc.js": { "file": "assets/openscad.fa2654cc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ordinal.93cdc51b.js": { "file": "assets/ordinal.93cdc51b.js", "imports": [ "_init.a5b10ee5.js" ] }, "_oxygene.8dc31ed9.js": { "file": "assets/oxygene.8dc31ed9.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_pagination.524325c1.js": { "file": "assets/pagination.524325c1.js", "imports": [ "index.html" ] }, "_parser3.5d447324.js": { "file": "assets/parser3.5d447324.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_path.53f90ab3.js": { "file": "assets/path.53f90ab3.js" }, "_perl.9060fb02.js": { "file": "assets/perl.9060fb02.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_pf.a4464222.js": { "file": "assets/pf.a4464222.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_pgsql.89d8e652.js": { "file": "assets/pgsql.89d8e652.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_php-template.02d622f8.js": { "file": "assets/php-template.02d622f8.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_php.df7925a7.js": { "file": "assets/php.df7925a7.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_plaintext.bb4a0b1b.js": { "file": "assets/plaintext.bb4a0b1b.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_pony.7125a88a.js": { "file": "assets/pony.7125a88a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_powershell.bdb91f89.js": { "file": "assets/powershell.bdb91f89.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_processing.160d59cc.js": { "file": "assets/processing.160d59cc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_profile.94a3206f.js": { "file": "assets/profile.94a3206f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_progress.b759e832.js": { "file": "assets/progress.b759e832.js", "imports": [ "index.html" ] }, "_prolog.e57149e1.js": { "file": "assets/prolog.e57149e1.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_properties.a1b0cc3a.js": { "file": "assets/properties.a1b0cc3a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_protobuf.6fd3c625.js": { "file": "assets/protobuf.6fd3c625.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_puppet.e0eaa65e.js": { "file": "assets/puppet.e0eaa65e.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_purebasic.d24275ac.js": { "file": "assets/purebasic.d24275ac.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_python-repl.0f5b514e.js": { "file": "assets/python-repl.0f5b514e.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_python.82adccf2.js": { "file": "assets/python.82adccf2.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_q.9f7c28a1.js": { "file": "assets/q.9f7c28a1.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_qml.116fd185.js": { "file": "assets/qml.116fd185.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_r.42b21dcc.js": { "file": "assets/r.42b21dcc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_radio-group.9fad45cf.js": { "file": "assets/radio-group.9fad45cf.js", "imports": [ "index.html" ] }, "_reasonml.6e89e640.js": { "file": "assets/reasonml.6e89e640.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_request.9ae92294.js": { "file": "assets/request.9ae92294.js", "imports": [ "index.html" ] }, "_rib.44b3e5d6.js": { "file": "assets/rib.44b3e5d6.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_roboconf.e09e943c.js": { "file": "assets/roboconf.e09e943c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_routeros.1e8f226f.js": { "file": "assets/routeros.1e8f226f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_rsl.1896018e.js": { "file": "assets/rsl.1896018e.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ruby.f9cd1dd5.js": { "file": "assets/ruby.f9cd1dd5.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_ruleslanguage.efb7e96b.js": { "file": "assets/ruleslanguage.efb7e96b.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_rust.eaffbb35.js": { "file": "assets/rust.eaffbb35.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_sas.791dadca.js": { "file": "assets/sas.791dadca.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_save.cc03e881.js": { "file": "assets/save.cc03e881.js", "imports": [ "index.html" ] }, "_scala.0ec867bf.js": { "file": "assets/scala.0ec867bf.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_scan-barcode.437edb2c.js": { "file": "assets/scan-barcode.437edb2c.js", "imports": [ "index.html" ] }, "_scheme.1acc1dfa.js": { "file": "assets/scheme.1acc1dfa.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_scilab.25832050.js": { "file": "assets/scilab.25832050.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_scss.e7f9a072.js": { "file": "assets/scss.e7f9a072.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_settings-2.9ad8babc.js": { "file": "assets/settings-2.9ad8babc.js", "imports": [ "index.html" ] }, "_shell.1c309f31.js": { "file": "assets/shell.1c309f31.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_smali.fddc422d.js": { "file": "assets/smali.fddc422d.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_smalltalk.9a1542dc.js": { "file": "assets/smalltalk.9a1542dc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_sml.871d736c.js": { "file": "assets/sml.871d736c.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_sqf.d9ed268b.js": { "file": "assets/sqf.d9ed268b.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_sql.c3b24f73.js": { "file": "assets/sql.c3b24f73.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_sql_more.4bd7d0c7.js": { "file": "assets/sql_more.4bd7d0c7.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_stan.7b1be381.js": { "file": "assets/stan.7b1be381.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_stata.0a068841.js": { "file": "assets/stata.0a068841.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_step21.4392b842.js": { "file": "assets/step21.4392b842.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_styles-3ed67cfa.d41aab61.js": { "file": "assets/styles-3ed67cfa.d41aab61.js", "imports": [ "_graph.0ef9c3b7.js", "index.html", "_index-fc10efb0.021331b3.js", "_channel.0e442477.js" ] }, "_styles-991ebdfc.bd043552.js": { "file": "assets/styles-991ebdfc.bd043552.js", "imports": [ "index.html" ] }, "_styles-d20c7d72.76540848.js": { "file": "assets/styles-d20c7d72.76540848.js", "imports": [ "index.html" ] }, "_stylus.7b7519f0.js": { "file": "assets/stylus.7b7519f0.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_subunit.fede25b7.js": { "file": "assets/subunit.fede25b7.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_svgDrawCommon-5ccd53ef.362ba997.js": { "file": "assets/svgDrawCommon-5ccd53ef.362ba997.js", "imports": [ "index.html" ] }, "_swift.7b385dfc.js": { "file": "assets/swift.7b385dfc.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_system.4d03cc26.js": { "file": "assets/system.4d03cc26.js", "imports": [ "index.html" ] }, "_table.b7adcb2f.js": { "file": "assets/table.b7adcb2f.js", "imports": [ "index.html", "_filter.0ca1fe92.js" ] }, "_tabs.7d7cdf4d.js": { "file": "assets/tabs.7d7cdf4d.js", "imports": [ "index.html" ] }, "_taggerscript.a5a3c164.js": { "file": "assets/taggerscript.a5a3c164.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_tap.ec001561.js": { "file": "assets/tap.ec001561.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_tcl.5faa7b55.js": { "file": "assets/tcl.5faa7b55.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_thrift.501d09ba.js": { "file": "assets/thrift.501d09ba.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_time.f5c88166.js": { "file": "assets/time.f5c88166.js", "imports": [ "_linear.1519afab.js", "_init.a5b10ee5.js" ] }, "_tiny-invariant.dd7d57d2.js": { "file": "assets/tiny-invariant.dd7d57d2.js" }, "_tp.1545d2cf.js": { "file": "assets/tp.1545d2cf.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_twig.0d7d2341.js": { "file": "assets/twig.0d7d2341.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_typescript.d128319f.js": { "file": "assets/typescript.d128319f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_upload-cloud.2f8ed3bd.js": { "file": "assets/upload-cloud.2f8ed3bd.js", "imports": [ "index.html" ] }, "_vala.c59c1f8e.js": { "file": "assets/vala.c59c1f8e.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_vbnet.6c9093e0.js": { "file": "assets/vbnet.6c9093e0.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_vbscript-html.d290f5d5.js": { "file": "assets/vbscript-html.d290f5d5.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_vbscript.3b9bcd96.js": { "file": "assets/vbscript.3b9bcd96.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_verilog.9242d940.js": { "file": "assets/verilog.9242d940.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_vhdl.aed608da.js": { "file": "assets/vhdl.aed608da.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_vim.6dc8565f.js": { "file": "assets/vim.6dc8565f.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_x86asm.ef055c19.js": { "file": "assets/x86asm.ef055c19.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_xl.dce5d508.js": { "file": "assets/xl.dce5d508.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_xml.1a48b5b5.js": { "file": "assets/xml.1a48b5b5.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_xquery.5286f839.js": { "file": "assets/xquery.5286f839.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_yaml.bff970e0.js": { "file": "assets/yaml.bff970e0.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "_zephir.5f89667a.js": { "file": "assets/zephir.5f89667a.js", "imports": [ "index.html" ], "isDynamicEntry": true }, "index.css": { "file": "assets/index-21f57514.css", "src": "index.css" }, "index.html": { "css": [ "assets/index-21f57514.css" ], "dynamicImports": [ "_1c.7075dba8.js", "_abnf.9bf3a7c1.js", "_accesslog.68f2b66c.js", "_actionscript.67a1e568.js", "_ada.c0e23f60.js", "_angelscript.410ffc68.js", "_apache.006764de.js", "_applescript.39299b7d.js", "_arcade.f590a3cc.js", "_arduino.c4c70db8.js", "_armasm.50cdbe9a.js", "_asciidoc.e1e2cbee.js", "_aspectj.a696b879.js", "_autohotkey.c54238fc.js", "_autoit.25b13806.js", "_avrasm.a69814df.js", "_awk.9f65b5a4.js", "_axapta.fa0aef8d.js", "_bash.a8e19ccc.js", "_basic.6acc6ac9.js", "_bnf.bd11f475.js", "_brainfuck.db01d938.js", "_c-like.af826c24.js", "_c.cc4c2414.js", "_cal.b721a23c.js", "_capnproto.7f37471d.js", "_ceylon.f41d8cb9.js", "_clean.2230529c.js", "_clojure-repl.24369284.js", "_clojure.98540e11.js", "_cmake.687d96e9.js", "_coffeescript.09ac2f3f.js", "_coq.18e8d32c.js", "_cos.cfa5f5a1.js", "_cpp.a6b75908.js", "_crmsh.929b5790.js", "_crystal.520aae80.js", "_csharp.676d187e.js", "_csp.27c6fdf5.js", "_css.aed2fec3.js", "_d.deee4072.js", "_dart.86736099.js", "_delphi.b2e83fb0.js", "_diff.52ac5061.js", "_django.acf6f5d0.js", "_dns.ae462891.js", "_dockerfile.a461ef01.js", "_dos.66ec8774.js", "_dsconfig.948d33c9.js", "_dts.ace8285a.js", "_dust.61c6165d.js", "_ebnf.9f47f2bf.js", "_elixir.87b77a7b.js", "_elm.62fc5561.js", "_erb.55b07d6c.js", "_erlang-repl.323f7160.js", "_erlang.c7e57743.js", "_excel.4522222f.js", "_fix.51ea128a.js", "_flix.c3fcc323.js", "_fortran.5261c416.js", "_fsharp.11981ee5.js", "_gams.49652455.js", "_gauss.012009e3.js", "_gcode.0c400ff3.js", "_gherkin.6750692c.js", "_glsl.3aa8f677.js", "_gml.8a3d692a.js", "_go.9e6e090c.js", "_golo.0c3cd41a.js", "_gradle.9b2be066.js", "_groovy.e5a1e4e3.js", "_haml.0925c5ed.js", "_handlebars.a705eec6.js", "_haskell.c1c10057.js", "_haxe.bb1f4576.js", "_hsp.acbd24bb.js", "_htmlbars.ae56fbaf.js", "_http.cadf2244.js", "_hy.94f04bbe.js", "_inform7.573ba170.js", "_ini.0ce80bb9.js", "_irpf90.a8791531.js", "_isbl.17bd57b9.js", "_java.de21cb0c.js", "_javascript.4b296926.js", "_jboss-cli.f0eb5633.js", "_json.6e716f3a.js", "_julia-repl.6f5d2fd3.js", "_julia.41a3881b.js", "_kotlin.c59af0ba.js", "_lasso.8a28f498.js", "_latex.058112d0.js", "_ldif.e8f34ce7.js", "_leaf.9ed3302f.js", "_less.1e2f29fc.js", "_lisp.ab804084.js", "_livecodeserver.ada7c599.js", "_livescript.09fbd630.js", "_llvm.c75c9c59.js", "_lsl.21a37c7a.js", "_lua.c0a16d09.js", "_makefile.bb0fcfbd.js", "_markdown.26dd2c01.js", "_mathematica.2dc23a04.js", "_matlab.e39e79ce.js", "_maxima.b42deb33.js", "_mel.30384c03.js", "_mercury.4f44fcae.js", "_mipsasm.6d204ed1.js", "_mizar.6cf15cbd.js", "_mojolicious.834a438a.js", "_monkey.08e49847.js", "_moonscript.96b4ff81.js", "_n1ql.bb73836f.js", "_nginx.83dced37.js", "_nim.9b03230a.js", "_nix.62253a60.js", "_node-repl.e38345ae.js", "_nsis.aa91a0cd.js", "_objectivec.636246aa.js", "_ocaml.6a0416aa.js", "_openscad.fa2654cc.js", "_oxygene.8dc31ed9.js", "_parser3.5d447324.js", "_perl.9060fb02.js", "_pf.a4464222.js", "_pgsql.89d8e652.js", "_php-template.02d622f8.js", "_php.df7925a7.js", "_plaintext.bb4a0b1b.js", "_pony.7125a88a.js", "_powershell.bdb91f89.js", "_processing.160d59cc.js", "_profile.94a3206f.js", "_prolog.e57149e1.js", "_properties.a1b0cc3a.js", "_protobuf.6fd3c625.js", "_puppet.e0eaa65e.js", "_purebasic.d24275ac.js", "_python-repl.0f5b514e.js", "_python.82adccf2.js", "_q.9f7c28a1.js", "_qml.116fd185.js", "_r.42b21dcc.js", "_reasonml.6e89e640.js", "_rib.44b3e5d6.js", "_roboconf.e09e943c.js", "_routeros.1e8f226f.js", "_rsl.1896018e.js", "_ruby.f9cd1dd5.js", "_ruleslanguage.efb7e96b.js", "_rust.eaffbb35.js", "_sas.791dadca.js", "_scala.0ec867bf.js", "_scheme.1acc1dfa.js", "_scilab.25832050.js", "_scss.e7f9a072.js", "_shell.1c309f31.js", "_smali.fddc422d.js", "_smalltalk.9a1542dc.js", "_sml.871d736c.js", "_sqf.d9ed268b.js", "_sql.c3b24f73.js", "_sql_more.4bd7d0c7.js", "_stan.7b1be381.js", "_stata.0a068841.js", "_step21.4392b842.js", "_stylus.7b7519f0.js", "_subunit.fede25b7.js", "_swift.7b385dfc.js", "_taggerscript.a5a3c164.js", "_tap.ec001561.js", "_tcl.5faa7b55.js", "_thrift.501d09ba.js", "_tp.1545d2cf.js", "_twig.0d7d2341.js", "_typescript.d128319f.js", "_vala.c59c1f8e.js", "_vbnet.6c9093e0.js", "_vbscript-html.d290f5d5.js", "_vbscript.3b9bcd96.js", "_verilog.9242d940.js", "_vhdl.aed608da.js", "_vim.6dc8565f.js", "_x86asm.ef055c19.js", "_xl.dce5d508.js", "_xml.1a48b5b5.js", "_xquery.5286f839.js", "_yaml.bff970e0.js", "_zephir.5f89667a.js", "_core.b8b80318.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js", "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js", "src/routes/Model.tsx", "src/routes/Wallet.tsx", "src/routes/Record.tsx", "src/routes/Preset.tsx", "src/routes/Account.tsx", "src/routes/Generation.tsx", "src/routes/Sharing.tsx", "src/routes/Article.tsx", "src/routes/Admin.tsx", "src/routes/admin/DashBoard.tsx", "src/routes/admin/Market.tsx", "src/routes/admin/Channel.tsx", "src/routes/admin/System.tsx", "src/routes/admin/Charge.tsx", "src/routes/admin/Users.tsx", "src/routes/admin/Broadcast.tsx", "src/routes/admin/Subscription.tsx", "src/routes/admin/Record.tsx", "src/routes/admin/Payment.tsx", "src/routes/admin/Logger.tsx", "src/routes/EpayNotify.tsx" ], "file": "assets/index.8d7e6a5a.js", "isEntry": true, "src": "index.html" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js": { "file": "assets/blockDiagram-91b80b7a.047c225c.js", "imports": [ "index.html", "_clone.2acf797d.js", "_edges-d32062c0.bb379725.js", "_graph.0ef9c3b7.js", "_ordinal.93cdc51b.js", "_Tableau10.1b767f5e.js", "_channel.0e442477.js", "_createText-6b48ae7d.01af0827.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js", "_init.a5b10ee5.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js": { "file": "assets/c4Diagram-b2a90758.e9bd027f.js", "imports": [ "index.html", "_svgDrawCommon-5ccd53ef.362ba997.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js": { "file": "assets/classDiagram-30eddba6.e8f5c5df.js", "imports": [ "_styles-991ebdfc.bd043552.js", "index.html", "_graph.0ef9c3b7.js", "_layout.991ed0c6.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js": { "file": "assets/classDiagram-v2-f2df5561.ba89e794.js", "imports": [ "_styles-991ebdfc.bd043552.js", "index.html", "_graph.0ef9c3b7.js", "_index-fc10efb0.021331b3.js", "_layout.991ed0c6.js", "_clone.2acf797d.js", "_edges-d32062c0.bb379725.js", "_createText-6b48ae7d.01af0827.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js": { "file": "assets/erDiagram-47591fe2.299b5ec2.js", "imports": [ "index.html", "_graph.0ef9c3b7.js", "_layout.991ed0c6.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js": { "file": "assets/flowDiagram-5540d9b9.675a4ea5.js", "imports": [ "_flowDb-4b19a42f.0b3ed11f.js", "_graph.0ef9c3b7.js", "index.html", "_layout.991ed0c6.js", "_styles-3ed67cfa.d41aab61.js", "_line.c9b6b09c.js", "_index-fc10efb0.021331b3.js", "_clone.2acf797d.js", "_edges-d32062c0.bb379725.js", "_createText-6b48ae7d.01af0827.js", "_channel.0e442477.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js": { "file": "assets/flowDiagram-v2-3b53844e.cc42dd61.js", "imports": [ "_flowDb-4b19a42f.0b3ed11f.js", "_styles-3ed67cfa.d41aab61.js", "index.html", "_graph.0ef9c3b7.js", "_layout.991ed0c6.js", "_index-fc10efb0.021331b3.js", "_clone.2acf797d.js", "_edges-d32062c0.bb379725.js", "_createText-6b48ae7d.01af0827.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js", "_channel.0e442477.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js": { "file": "assets/flowchart-elk-definition-5fe447d6.f3337586.js", "imports": [ "_flowDb-4b19a42f.0b3ed11f.js", "index.html", "_edges-d32062c0.bb379725.js", "_line.c9b6b09c.js", "_createText-6b48ae7d.01af0827.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js": { "file": "assets/ganttDiagram-9a3bba1f.f22e0aee.js", "imports": [ "index.html", "_time.f5c88166.js", "_linear.1519afab.js", "_init.a5b10ee5.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js": { "file": "assets/gitGraphDiagram-96e6b4ee.d53591a6.js", "imports": [ "index.html" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js": { "file": "assets/infoDiagram-bcd20f53.23cab256.js", "imports": [ "index.html" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js": { "file": "assets/journeyDiagram-4fe6b3dc.6ea4bcd5.js", "imports": [ "index.html", "_svgDrawCommon-5ccd53ef.362ba997.js", "_arc.a247e62c.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js": { "file": "assets/mindmap-definition-f354de21.8698e7a8.js", "imports": [ "index.html", "__isIndex.00b9330c.js", "_createText-6b48ae7d.01af0827.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js": { "file": "assets/pieDiagram-79897490.8acd8555.js", "imports": [ "index.html", "_arc.a247e62c.js", "_ordinal.93cdc51b.js", "_array.9f3ba611.js", "_path.53f90ab3.js", "_init.a5b10ee5.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js": { "file": "assets/quadrantDiagram-62f64e94.479e2729.js", "imports": [ "index.html", "_linear.1519afab.js", "_init.a5b10ee5.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js": { "file": "assets/requirementDiagram-05bf5f74.383dd160.js", "imports": [ "index.html", "_graph.0ef9c3b7.js", "_layout.991ed0c6.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js": { "file": "assets/sankeyDiagram-97764748.3efa2305.js", "imports": [ "index.html", "_ordinal.93cdc51b.js", "_Tableau10.1b767f5e.js", "_init.a5b10ee5.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js": { "file": "assets/sequenceDiagram-acc0e65c.1f830b15.js", "imports": [ "index.html", "_svgDrawCommon-5ccd53ef.362ba997.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js": { "file": "assets/stateDiagram-0ff1cf1a.af40e4d4.js", "imports": [ "_styles-d20c7d72.76540848.js", "index.html", "_graph.0ef9c3b7.js", "_layout.991ed0c6.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js": { "file": "assets/stateDiagram-v2-9a9d610d.5e37d388.js", "imports": [ "_styles-d20c7d72.76540848.js", "_graph.0ef9c3b7.js", "index.html", "_index-fc10efb0.021331b3.js", "_layout.991ed0c6.js", "_clone.2acf797d.js", "_edges-d32062c0.bb379725.js", "_createText-6b48ae7d.01af0827.js", "_line.c9b6b09c.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js": { "file": "assets/timeline-definition-fea2a41d.36bf6e4d.js", "imports": [ "index.html", "_arc.a247e62c.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js" }, "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js": { "file": "assets/xychartDiagram-ab372869.3444a59b.js", "imports": [ "index.html", "_createText-6b48ae7d.01af0827.js", "_band.6ffec690.js", "_linear.1519afab.js", "_line.c9b6b09c.js", "_init.a5b10ee5.js", "_ordinal.93cdc51b.js", "_array.9f3ba611.js", "_path.53f90ab3.js" ], "isDynamicEntry": true, "src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js" }, "src/routes/Account.css": { "file": "assets/Account-b93fa699.css", "src": "src/routes/Account.css" }, "src/routes/Account.tsx": { "css": [ "assets/Account-b93fa699.css" ], "file": "assets/Account.9f79e18e.js", "imports": [ "index.html", "_calendar-clock.1432b482.js", "_gift.b3e4cc82.js", "_clock.36b36b21.js" ], "isDynamicEntry": true, "src": "src/routes/Account.tsx" }, "src/routes/Admin.css": { "file": "assets/Admin-502861ce.css", "src": "src/routes/Admin.css" }, "src/routes/Admin.tsx": { "css": [ "assets/Admin-502861ce.css" ], "file": "assets/Admin.f2a9d119.js", "imports": [ "index.html", "_history.44d149e5.js" ], "isDynamicEntry": true, "src": "src/routes/Admin.tsx" }, "src/routes/Article.css": { "file": "assets/Article-82d9a1fc.css", "src": "src/routes/Article.css" }, "src/routes/Article.tsx": { "css": [ "assets/Article-82d9a1fc.css" ], "file": "assets/Article.58956150.js", "imports": [ "index.html", "_progress.b759e832.js" ], "isDynamicEntry": true, "src": "src/routes/Article.tsx" }, "src/routes/EpayNotify.css": { "file": "assets/EpayNotify-b82ac077.css", "src": "src/routes/EpayNotify.css" }, "src/routes/EpayNotify.tsx": { "css": [ "assets/EpayNotify-b82ac077.css" ], "file": "assets/EpayNotify.197de3d5.js", "imports": [ "index.html", "_request.9ae92294.js", "_box.6dd688fe.js", "_scan-barcode.437edb2c.js" ], "isDynamicEntry": true, "src": "src/routes/EpayNotify.tsx" }, "src/routes/Generation.css": { "file": "assets/Generation-7b5b93aa.css", "src": "src/routes/Generation.css" }, "src/routes/Generation.tsx": { "css": [ "assets/Generation-7b5b93aa.css" ], "file": "assets/Generation.e6964a34.js", "imports": [ "index.html" ], "isDynamicEntry": true, "src": "src/routes/Generation.tsx" }, "src/routes/Model.tsx": { "file": "assets/Model.beec7117.js", "imports": [ "index.html", "_market.93d110b3.js", "_arrow-right-left.f4c82cc0.js", "_box.6dd688fe.js", "_upload-cloud.2f8ed3bd.js", "_download-cloud.04333a4f.js" ], "isDynamicEntry": true, "src": "src/routes/Model.tsx" }, "src/routes/Preset.css": { "file": "assets/Preset-8710ef85.css", "src": "src/routes/Preset.css" }, "src/routes/Preset.tsx": { "css": [ "assets/Preset-8710ef85.css" ], "file": "assets/Preset.95a9b1e0.js", "imports": [ "index.html", "_arrow-right-left.f4c82cc0.js" ], "isDynamicEntry": true, "src": "src/routes/Preset.tsx" }, "src/routes/Record.css": { "file": "assets/Record-dd76ad5a.css", "src": "src/routes/Record.css" }, "src/routes/Record.tsx": { "css": [ "assets/Record-dd76ad5a.css" ], "file": "assets/Record.7203fb33.js", "imports": [ "index.html", "_table.b7adcb2f.js", "_Tracker.20c1771e.js", "_pagination.524325c1.js", "_calendar.2f96549a.js", "_upload-cloud.2f8ed3bd.js", "_clock.36b36b21.js", "_activity.612642ea.js", "_asterisk-square.2615ee58.js", "_history.44d149e5.js", "_filter.0ca1fe92.js", "__isIndex.00b9330c.js", "_tiny-invariant.dd7d57d2.js", "_path.53f90ab3.js", "_linear.1519afab.js", "_init.a5b10ee5.js", "_time.f5c88166.js", "_band.6ffec690.js", "_ordinal.93cdc51b.js", "_array.9f3ba611.js", "_line.c9b6b09c.js" ], "isDynamicEntry": true, "src": "src/routes/Record.tsx" }, "src/routes/Sharing.css": { "file": "assets/Sharing-07989aa3.css", "src": "src/routes/Sharing.css" }, "src/routes/Sharing.tsx": { "css": [ "assets/Sharing-07989aa3.css" ], "file": "assets/Sharing.b857888c.js", "imports": [ "index.html", "_clock.36b36b21.js" ], "isDynamicEntry": true, "src": "src/routes/Sharing.tsx" }, "src/routes/Wallet.css": { "file": "assets/Wallet-adc144da.css", "src": "src/routes/Wallet.css" }, "src/routes/Wallet.tsx": { "css": [ "assets/Wallet-adc144da.css" ], "file": "assets/Wallet.23e502a3.js", "imports": [ "index.html", "_request.9ae92294.js", "_icons.7e9c9414.js", "_progress.b759e832.js", "_tabs.7d7cdf4d.js", "_gift.b3e4cc82.js", "_calendar.2f96549a.js", "_calendar-clock.1432b482.js", "_box.6dd688fe.js" ], "isDynamicEntry": true, "src": "src/routes/Wallet.tsx" }, "src/routes/admin/Broadcast.tsx": { "file": "assets/Broadcast.10adc6a8.js", "imports": [ "index.html", "_table.b7adcb2f.js", "_more-vertical.06df6aff.js", "_filter.0ca1fe92.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Broadcast.tsx" }, "src/routes/admin/Channel.tsx": { "file": "assets/Channel.22f78415.js", "imports": [ "index.html", "_table.b7adcb2f.js", "_OperationAction.5b1f951a.js", "_hook.09e5815b.js", "_activity.612642ea.js", "_settings-2.9ad8babc.js", "_asterisk-square.2615ee58.js", "_Paragraph.4ca577cf.js", "_multi-combobox.51bcb343.js", "_kanban-square-dashed.c88dceb2.js", "_book-dashed.798b0c5c.js", "_filter.0ca1fe92.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Channel.tsx" }, "src/routes/admin/Charge.tsx": { "file": "assets/Charge.9dc9e031.js", "imports": [ "index.html", "_radio-group.9fad45cf.js", "_table.b7adcb2f.js", "_OperationAction.5b1f951a.js", "_hook.09e5815b.js", "_kanban-square-dashed.c88dceb2.js", "_activity.612642ea.js", "_upload-cloud.2f8ed3bd.js", "_download-cloud.04333a4f.js", "_settings-2.9ad8babc.js", "_filter.0ca1fe92.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Charge.tsx" }, "src/routes/admin/DashBoard.tsx": { "file": "assets/DashBoard.a5483326.js", "imports": [ "index.html", "_chart.b43e29db.js", "_Tracker.20c1771e.js", "_multi-combobox.51bcb343.js", "_filter.0ca1fe92.js", "__isIndex.00b9330c.js", "_tiny-invariant.dd7d57d2.js", "_path.53f90ab3.js", "_linear.1519afab.js", "_init.a5b10ee5.js", "_time.f5c88166.js", "_band.6ffec690.js", "_ordinal.93cdc51b.js", "_array.9f3ba611.js", "_line.c9b6b09c.js" ], "isDynamicEntry": true, "src": "src/routes/admin/DashBoard.tsx" }, "src/routes/admin/Logger.tsx": { "file": "assets/Logger.e813542f.js", "imports": [ "index.html", "_Paragraph.4ca577cf.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Logger.tsx" }, "src/routes/admin/Market.tsx": { "file": "assets/Market.458a4f86.js", "imports": [ "index.html", "_market.93d110b3.js", "_hook.09e5815b.js", "_tiny-invariant.dd7d57d2.js", "_system.4d03cc26.js", "_activity.612642ea.js", "_save.cc03e881.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Market.tsx" }, "src/routes/admin/Payment.tsx": { "file": "assets/Payment.f83f77b2.js", "imports": [ "index.html", "_request.9ae92294.js", "_table.b7adcb2f.js", "_pagination.524325c1.js", "_icons.7e9c9414.js", "_scan-barcode.437edb2c.js", "_more-vertical.06df6aff.js", "_filter.0ca1fe92.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Payment.tsx" }, "src/routes/admin/Record.tsx": { "file": "assets/Record.7a35bf1e.js", "imports": [ "index.html", "src/routes/Record.tsx", "_table.b7adcb2f.js", "_filter.0ca1fe92.js", "_Tracker.20c1771e.js", "__isIndex.00b9330c.js", "_tiny-invariant.dd7d57d2.js", "_path.53f90ab3.js", "_linear.1519afab.js", "_init.a5b10ee5.js", "_time.f5c88166.js", "_band.6ffec690.js", "_ordinal.93cdc51b.js", "_array.9f3ba611.js", "_line.c9b6b09c.js", "_pagination.524325c1.js", "_calendar.2f96549a.js", "_upload-cloud.2f8ed3bd.js", "_clock.36b36b21.js", "_activity.612642ea.js", "_asterisk-square.2615ee58.js", "_history.44d149e5.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Record.tsx" }, "src/routes/admin/Subscription.tsx": { "file": "assets/Subscription.facf4385.js", "imports": [ "index.html", "_multi-combobox.51bcb343.js", "_hook.09e5815b.js", "_activity.612642ea.js", "_save.cc03e881.js", "_book-dashed.798b0c5c.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Subscription.tsx" }, "src/routes/admin/System.tsx": { "file": "assets/System.1954c6c5.js", "imports": [ "index.html", "_Paragraph.4ca577cf.js", "_system.4d03cc26.js", "_multi-combobox.51bcb343.js", "_hook.09e5815b.js", "_tabs.7d7cdf4d.js", "_icons.7e9c9414.js", "_save.cc03e881.js", "_settings-2.9ad8babc.js" ], "isDynamicEntry": true, "src": "src/routes/admin/System.tsx" }, "src/routes/admin/Users.tsx": { "file": "assets/Users.ee82f0be.js", "imports": [ "index.html", "_table.b7adcb2f.js", "_chart.b43e29db.js", "_pagination.524325c1.js", "_OperationAction.5b1f951a.js", "_radio-group.9fad45cf.js", "_filter.0ca1fe92.js", "_calendar-clock.1432b482.js" ], "isDynamicEntry": true, "src": "src/routes/admin/Users.tsx" } } ================================================ FILE: app/public/robots.txt ================================================ User-Agent: * Allow: / Disallow: /admin/ ================================================ FILE: app/public/service.js ================================================ const SERVICE_NAME = "coai"; self.addEventListener('activate', function (event) { console.debug("[service] service worker activated"); }); self.addEventListener('install', function (event) { event.waitUntil( caches.open(SERVICE_NAME) .then(function (cache) { return cache.addAll([]); }) ); }); self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request) .then(function (response) { return response || fetch(event.request); }) ); }); ================================================ FILE: app/public/site.webmanifest ================================================ { "name": "CoAI", "short_name": "CoAI", "icons": [ { "src": "/service/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/service/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": "/", "theme_color": "#000", "background_color": "#000", "display": "standalone" } ================================================ FILE: app/public/workbox.js ================================================ if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/service.js').then(function (registration) { console.debug(`[service] service worker registered with scope: ${registration.scope}`); }, function (err) { console.debug(`[service] service worker registration failed: ${err}`); }); }); } ================================================ FILE: app/qodana.yaml ================================================ #-------------------------------------------------------------------------------# # Qodana analysis is configured by qodana.yaml file # # https://www.jetbrains.com/help/qodana/qodana-yaml.html # #-------------------------------------------------------------------------------# version: "1.0" #Specify inspection profile for code analysis profile: name: qodana.starter #Enable inspections #include: # - name: #Disable inspections #exclude: # - name: # paths: # - #Execute shell command before Qodana execution (Applied in CI/CD pipeline) #bootstrap: sh ./prepare-qodana.sh #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) #plugins: # - id: #(plugin id can be found at https://plugins.jetbrains.com) #Specify Qodana linter for analysis (Applied in CI/CD pipeline) linter: jetbrains/qodana-js:latest ================================================ FILE: app/src/App.tsx ================================================ import { Provider } from "react-redux"; import store from "./store/index.ts"; import AppProvider from "./components/app/AppProvider.tsx"; import { AppRouter } from "./router.tsx"; import { Toaster } from "@/components/ui/sonner"; import Spinner from "@/spinner.tsx"; import ReloadPrompt from "@/components/ReloadService.tsx"; function App() { return ( ); } export default App; ================================================ FILE: app/src/admin/api/channel.ts ================================================ import { Channel } from "@/admin/channel.ts"; import axios from "axios"; import { getErrorMessage } from "@/utils/base.ts"; import { CommonResponse } from "@/api/common.ts"; export type ChannelListResponse = CommonResponse & { data: Channel[]; }; export type GetChannelResponse = CommonResponse & { data?: Channel; }; export async function listChannel(): Promise { try { const response = await axios.get("/admin/channel/list"); return response.data as ChannelListResponse; } catch (e) { return { status: false, error: getErrorMessage(e), data: [] }; } } export async function getChannel(id: number): Promise { try { const response = await axios.get(`/admin/channel/get/${id}`); return response.data as GetChannelResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function createChannel(channel: Channel): Promise { try { const response = await axios.post("/admin/channel/create", channel); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function updateChannel( id: number, channel: Channel, ): Promise { try { const response = await axios.post(`/admin/channel/update/${id}`, channel); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function deleteChannel(id: number): Promise { try { const response = await axios.get(`/admin/channel/delete/${id}`); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function activateChannel(id: number): Promise { try { const response = await axios.get(`/admin/channel/activate/${id}`); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function deactivateChannel(id: number): Promise { try { const response = await axios.get(`/admin/channel/deactivate/${id}`); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } ================================================ FILE: app/src/admin/api/charge.ts ================================================ import { CommonResponse } from "@/api/common.ts"; import { ChargeProps } from "@/admin/charge.ts"; import { getErrorMessage } from "@/utils/base.ts"; import axios from "axios"; export type ChargeListResponse = CommonResponse & { data: ChargeProps[]; }; export type ChargeSyncRequest = { overwrite: boolean; data: ChargeProps[]; }; export type ChargeFetchRequest = { endpoint: string; system?: string; }; export type ChargeFetchResponse = CommonResponse & { data: ChargeProps[]; }; export async function listCharge(): Promise { try { const response = await axios.get("/admin/charge/list"); return response.data as ChargeListResponse; } catch (e) { return { status: false, error: getErrorMessage(e), data: [] }; } } export async function setCharge(charge: ChargeProps): Promise { try { const response = await axios.post(`/admin/charge/set`, charge); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function deleteCharge(id: number): Promise { try { const response = await axios.get(`/admin/charge/delete/${id}`); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function syncCharge( data: ChargeSyncRequest, ): Promise { try { const response = await axios.post(`/admin/charge/sync`, data); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function fetchUpstreamCharge( req: ChargeFetchRequest, ): Promise { try { const response = await axios.post(`/admin/charge/fetch`, req); const data = response.data as ChargeFetchResponse; return { status: !!data.status, error: data.error, data: data.data || [], }; } catch (e) { return { status: false, error: getErrorMessage(e), data: [] }; } } ================================================ FILE: app/src/admin/api/chart.ts ================================================ import { BillingChartResponse, CommonResponse, ErrorChartResponse, InfoResponse, InvitationGenerateResponse, InvitationResponse, ModelChartResponse, RedeemResponse, RequestChartResponse, UserResponse, UserTypeChartResponse, } from "@/admin/types.ts"; import axios from "axios"; import { getErrorMessage } from "@/utils/base.ts"; export const initialAdminInfoState: InfoResponse = { subscription_count: 0, billing_today: 0, billing_month: 0, online_chats: 0, billing_yesterday: 0, billing_last_month: 0, }; export type UserFilterProps = { plan: string; admin: string; ban: string; sort: string; }; export const initialUserFilter: UserFilterProps = { plan: "all", // all/no/yes admin: "all", // all/no/yes ban: "all", // all/no/yes sort: "id-asc", // id-asc/id-desc // quota-asc/quota-desc // used-quota-asc/used-quota-desc // plan-desc/plan-asc }; export async function getAdminInfo(): Promise { try { const response = await axios.get("/admin/analytics/info"); return response.data as InfoResponse; } catch (e) { console.warn(e); return { ...initialAdminInfoState, }; } } export async function getModelChart(): Promise { try { const response = await axios.get("/admin/analytics/model"); const data = response.data as ModelChartResponse; return { date: data.date, value: data.value || [], }; } catch (e) { console.warn(e); return { date: [], value: [] }; } } export async function getRequestChart(): Promise { try { const response = await axios.get("/admin/analytics/request"); return response.data as RequestChartResponse; } catch (e) { console.warn(e); return { date: [], value: [] }; } } export async function getBillingChart(): Promise { try { const response = await axios.get("/admin/analytics/billing"); return response.data as BillingChartResponse; } catch (e) { console.warn(e); return { date: [], value: [] }; } } export async function getErrorChart(): Promise { try { const response = await axios.get("/admin/analytics/error"); return response.data as ErrorChartResponse; } catch (e) { console.warn(e); return { date: [], value: [] }; } } export async function getUserTypeChart(): Promise { try { const response = await axios.get("/admin/analytics/user"); return response.data as UserTypeChartResponse; } catch (e) { console.warn(e); return { total: 0, normal: 0, api_paid: 0, basic_plan: 0, standard_plan: 0, pro_plan: 0, }; } } export async function getInvitationList( page: number, ): Promise { try { const response = await axios.get(`/admin/invitation/list?page=${page}`); return response.data as InvitationResponse; } catch (e) { return { status: false, message: getErrorMessage(e), data: [], total: 0, }; } } export async function deleteInvitation(code: string): Promise { try { const response = await axios.post("/admin/invitation/delete", { code }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function generateInvitation( type: string, quota: number, number: number, ): Promise { try { const response = await axios.post("/admin/invitation/generate", { type, quota, number, }); return response.data as InvitationGenerateResponse; } catch (e) { return { status: false, data: [], message: getErrorMessage(e) }; } } export async function getRedeemList(page: number): Promise { try { const response = await axios.get(`/admin/redeem/list?page=${page}`); return response.data as RedeemResponse; } catch (e) { console.warn(e); return { status: false, message: getErrorMessage(e), data: [], total: 0 }; } } export async function deleteRedeem(code: string): Promise { try { const response = await axios.post("/admin/redeem/delete", { code }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function generateRedeem( quota: number, number: number, ): Promise { try { const response = await axios.post("/admin/redeem/generate", { quota, number, }); return response.data as InvitationGenerateResponse; } catch (e) { return { status: false, data: [], message: getErrorMessage(e) }; } } export async function getUserList( page: number, search: string, params: UserFilterProps, ): Promise { try { const response = await axios.get(`/admin/user/list`, { params: { page, search, params, }, }); return response.data as UserResponse; } catch (e) { return { status: false, message: getErrorMessage(e), data: [], total: 0, }; } } export async function updatePassword( id: number, password: string, ): Promise { try { const response = await axios.post("/admin/user/password", { id, password, }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function updateEmail( id: number, email: string, ): Promise { try { const response = await axios.post("/admin/user/email", { id, email, }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function quotaOperation( id: number, quota: number, override?: boolean, ): Promise { try { const response = await axios.post("/admin/user/quota", { id, quota, override: override ?? false, }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function subscriptionOperation( id: number, expired: string, ): Promise { try { const response = await axios.post("/admin/user/subscription", { id, expired, }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function banUserOperation( id: number, ban: boolean, ): Promise { try { const response = await axios.post("/admin/user/ban", { id, ban, }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function setAdminOperation( id: number, admin: boolean, ): Promise { try { const response = await axios.post("/admin/user/admin", { id, admin, }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function subscriptionLevelOperation( id: number, level: number, ): Promise { try { const response = await axios.post("/admin/user/level", { id, level }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } export async function releaseUsageOperation( id: number, ): Promise { try { const response = await axios.post("/admin/user/release", { id }); return response.data as CommonResponse; } catch (e) { return { status: false, message: getErrorMessage(e) }; } } ================================================ FILE: app/src/admin/api/info.ts ================================================ import axios from "axios"; import { setAppLogo, setAppName, setBlobEndpoint, setBuyLink, setDocsUrl, } from "@/conf/env.ts"; import { infoEvent } from "@/events/info.ts"; import { initGoogleAnalytics } from "@/utils/analytics.ts"; import { BroadcastEvent, getBroadcast } from "@/api/broadcast"; export type SiteInfo = { title: string; logo: string; docs: string; file: string; backend?: string; currency: string; announcement: string; buy_link: string; mail: boolean; contact: string; footer: string; auth_footer: boolean; hide_key_docs?: boolean; article: string[]; generation: string[]; relay_plan: boolean; payment: string[]; payment_aggregation: boolean; ga_tracking_id?: string; broadcast?: BroadcastEvent; }; export async function getSiteInfo(): Promise { try { const response = await axios.get("/info"); return response.data as SiteInfo; } catch (e) { console.warn(e); return { title: "", logo: "", docs: "", file: "", backend: undefined, currency: "cny", announcement: "", buy_link: "", contact: "", footer: "", auth_footer: false, hide_key_docs: false, mail: false, article: [], generation: [], relay_plan: false, payment: [], payment_aggregation: false, broadcast: { message: "", firstReceived: false, }, }; } } export function syncSiteInfo() { setTimeout(async () => { const info = await getSiteInfo(); info.broadcast = await getBroadcast(); setAppName(info.title); setAppLogo(info.logo); setDocsUrl(info.docs); setBlobEndpoint(info.file); setBuyLink(info.buy_link); initGoogleAnalytics(info.ga_tracking_id); infoEvent.emit(info); }, 25); } ================================================ FILE: app/src/admin/api/logger.ts ================================================ import axios from "axios"; import { CommonResponse } from "@/api/common.ts"; import { getErrorMessage } from "@/utils/base.ts"; export type Logger = { path: string; size: number; }; export async function listLoggers(): Promise { try { const response = await axios.get("/admin/logger/list"); return (response.data || []) as Logger[]; } catch (e) { console.warn(e); return []; } } export async function getLoggerConsole(n?: number): Promise { try { const response = await axios.get(`/admin/logger/console?n=${n ?? 100}`); return response.data.content as string; } catch (e) { console.warn(e); return `failed to get info from server: ${getErrorMessage(e)}`; } } export async function downloadLogger(path: string): Promise { try { const response = await axios.get("/admin/logger/download", { responseType: "blob", params: { path }, }); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement("a"); link.href = url; link.setAttribute("download", path); document.body.appendChild(link); link.click(); } catch (e) { console.warn(e); } } export async function deleteLogger(path: string): Promise { try { const response = await axios.post(`/admin/logger/delete?path=${path}`); return response.data as CommonResponse; } catch (e) { console.warn(e); return { status: false, error: getErrorMessage(e) }; } } ================================================ FILE: app/src/admin/api/market.ts ================================================ import { Model } from "@/api/types.tsx"; import { CommonResponse } from "@/api/common.ts"; import axios from "axios"; import { getErrorMessage } from "@/utils/base.ts"; export async function updateMarket(data: Model[]): Promise { try { const resp = await axios.post("/admin/market/update", data); return resp.data as CommonResponse; } catch (e) { console.warn(e); return { status: false, error: getErrorMessage(e) }; } } ================================================ FILE: app/src/admin/api/plan.ts ================================================ import { Plan } from "@/api/types.tsx"; import axios from "axios"; import { CommonResponse } from "@/api/common.ts"; import { getErrorMessage } from "@/utils/base.ts"; import { getApiPlans } from "@/api/v1.ts"; export type PlanConfig = { enabled: boolean; plans: Plan[]; }; export async function getPlanConfig(): Promise { try { const response = await axios.get("/admin/plan/view"); const conf = response.data as PlanConfig; conf.plans = (conf.plans || []).filter((item) => item.level > 0); if (conf.plans.length === 0) conf.plans = [1, 2, 3].map( (level) => ({ level, price: 0, items: [] }) as Plan, ); return conf; } catch (e) { console.warn(e); return { enabled: false, plans: [] }; } } export async function getExternalPlanConfig( endpoint: string, ): Promise { const response = await getApiPlans({ endpoint }); return { enabled: response.length > 0, plans: response }; } export async function setPlanConfig( config: PlanConfig, ): Promise { try { const response = await axios.post(`/admin/plan/update`, config); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } ================================================ FILE: app/src/admin/api/system.ts ================================================ import { CommonResponse } from "@/api/common.ts"; import { getErrorMessage } from "@/utils/base.ts"; import axios from "axios"; import { backendEndpoint } from "@/conf/env.ts"; export type TestWebSearchResponse = CommonResponse & { result: string; }; export type whiteList = { enabled: boolean; custom: string; white_list: string[]; }; export type GeneralState = { title: string; logo: string; description: string; backend: string; docs: string; file: string; pwa_manifest: string; gravatar: string; debug_mode: boolean; realtime?: { ws?: { buffer_size?: number; aggregate?: boolean; aggregate_window_ms?: number; }; }; }; export type MailState = { host: string; protocol: boolean; port: number; username: string; password: string; from: string; white_list: whiteList; }; export type SearchState = { endpoint: string; crop: boolean; crop_len: number; engines: string[]; image_proxy: boolean; safe_search: number; llm_extract: boolean; llm_model: string; }; export type SecurityState = { check_type: string; check_models?: string[]; text_database: string; regex_database: string; baidu_api_key: string; baidu_secret_key: string; custom_endpoint: string; custom_audit_token: string; blacklist_ips: string[]; whitelist_ips: string[]; }; export type PaymentState = { stripe: { enabled: boolean; public_key: string; secret_key: string; webhook_secret: string; }; epay: { domain: string; business_id: string; business_key: string; enabled: boolean; methods: string[]; aggregation: boolean; }; wechatpay?: { enabled: boolean; app_id: string; mch_id: string; serial_no: string; apiv3_key: string; wechatcertificate: string; }; xunhupay?: { wechat_enabled: boolean; alipay_enabled: boolean; wechat_app_id: string; wechat_app_secret: string; alipay_app_id: string; alipay_app_secret: string; endpoint: string; }; affiliate?: { enabled: boolean; commission_rate: number; min_withdraw: number; allow_existing_bind: boolean; }; }; export type SiteState = { close_register: boolean; currency: string; close_relay: boolean; relay_plan: boolean; quota: number; buy_link: string; announcement: string; contact: string; footer: string; auth_footer: boolean; pre_deduct_quota: boolean; hide_key_docs: boolean; }; export type CustomState = { custom_js: string; custom_css: string; custom_html: string; ga_tracking_id: string; }; export type AutoTitleState = { enabled: boolean; model: string; max_len: number; min_msgs: number; overwrite: boolean; prompt: string; }; export type CommonState = { cache: string[]; expire: number; size: number; article: string[]; generation: string[]; prompt_store: boolean; image_store: boolean; }; export type SystemProps = { general: GeneralState; site: SiteState; mail: MailState; search: SearchState; common: CommonState; payment: PaymentState; security: SecurityState; custom: CustomState; auto_title?: AutoTitleState; }; export type SystemResponse = CommonResponse & { data?: SystemProps; }; export const initialSystemState: SystemProps = { general: { logo: "", description: "", title: "", backend: "", docs: "", file: "", pwa_manifest: "", gravatar: "", debug_mode: false, realtime: { ws: { buffer_size: 24, aggregate: true, aggregate_window_ms: 20, }, }, }, site: { close_register: false, currency: "cny", close_relay: false, relay_plan: false, quota: 0, buy_link: "", announcement: "", contact: "", footer: "", auth_footer: false, pre_deduct_quota: true, hide_key_docs: false, }, mail: { host: "", protocol: false, port: 465, username: "", password: "", from: "", white_list: { enabled: false, custom: "", white_list: [], }, }, search: { endpoint: "", crop: false, crop_len: 1000, engines: [], image_proxy: false, safe_search: 0, llm_extract: false, llm_model: "", }, common: { article: [], generation: [], cache: [], expire: 3600, size: 1, prompt_store: false, image_store: false, }, payment: { stripe: { enabled: false, public_key: "", secret_key: "", webhook_secret: "", }, epay: { domain: "", business_id: "", business_key: "", enabled: false, methods: [], aggregation: false, }, affiliate: { enabled: false, commission_rate: 0.1, min_withdraw: 10, allow_existing_bind: false, }, }, security: { check_type: "", check_models: [], text_database: "", regex_database: "", baidu_api_key: "", baidu_secret_key: "", custom_endpoint: "", custom_audit_token: "", blacklist_ips: [], whitelist_ips: [], }, custom: { custom_js: "", custom_css: "", custom_html: "", ga_tracking_id: "", }, auto_title: { enabled: false, model: "", max_len: 50, min_msgs: 6, overwrite: false, prompt: "", }, }; export async function getConfig(): Promise { try { const response = await axios.get("/admin/config/view"); const data = response.data as SystemResponse; if (data.status && data.data) { // init system data pre-format data.data.mail.white_list.white_list = data.data.mail.white_list.white_list || commonWhiteList; data.data.search.engines = data.data.search.engines || []; data.data.search.crop_len = data.data.search.crop_len && data.data.search.crop_len > 0 ? data.data.search.crop_len : 1000; data.data.site.currency = data.data.site.currency || "cny"; if ( !data.data.common.group || Object.keys(data.data.common.group).length === 0 ) { data.data.common.group = { anonymous: { buy_price: 1, consume_price: 1, description: "", }, normal: { buy_price: 1, consume_price: 1, description: "", }, basic: { buy_price: 1, consume_price: 1, description: "", }, standard: { buy_price: 1, consume_price: 1, description: "", }, pro: { buy_price: 1, consume_price: 1, description: "", }, admin: { buy_price: 1, consume_price: 1, description: "", }, }; } const rt = (data.data.general.realtime = data.data.general.realtime || {}); const ws = (rt.ws = rt.ws || {}); ws.buffer_size = typeof ws.buffer_size === "number" && ws.buffer_size > 0 ? ws.buffer_size : 1; ws.aggregate = typeof ws.aggregate === "boolean" ? ws.aggregate : true; ws.aggregate_window_ms = typeof ws.aggregate_window_ms === "number" && ws.aggregate_window_ms > 0 ? ws.aggregate_window_ms : 20; const at = (data.data.auto_title = data.data.auto_title || { enabled: false, model: "", max_len: 50, min_msgs: 6, overwrite: false, prompt: "", }); at.enabled = !!at.enabled; at.model = at.model || ""; at.max_len = typeof at.max_len === "number" && at.max_len > 0 ? at.max_len : 50; at.min_msgs = typeof at.min_msgs === "number" && at.min_msgs > 0 ? at.min_msgs : 6; at.overwrite = !!at.overwrite; at.prompt = at.prompt || ""; } return data; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function setConfig(config: SystemProps): Promise { try { const response = await axios.post(`/admin/config/update`, config); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } type UploadResponse = CommonResponse & { url?: string; }; export async function uploadFavicon(file: File): Promise { try { const formData = new FormData(); formData.append("file", file); const response = await axios.post(`/admin/favicon/upload`, formData, { headers: { "Content-Type": "multipart/form-data", }, }); return response.data as UploadResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function uploadResource(file: File): Promise { try { const formData = new FormData(); formData.append("file", file); const response = await axios.post(`/admin/resource/upload`, formData, { headers: { "Content-Type": "multipart/form-data", }, }); const data = response.data as UploadResponse; if (data.status) { data.url = backendEndpoint + data.url; } return data; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function updateRootPassword( password: string, ): Promise { try { const response = await axios.post(`/admin/user/root`, { password }); return response.data as CommonResponse; } catch (e) { return { status: false, error: getErrorMessage(e) }; } } export async function testWebSearching( query: string, ): Promise { try { const response = await axios.get( `/admin/config/test/search?query=${encodeURIComponent(query)}`, ); return response.data as TestWebSearchResponse; } catch (e) { return { status: false, error: getErrorMessage(e), result: "" }; } } export enum AuditTypes { None = "none", Dict = "dict", Regex = "regex", Baidu = "baidu", Custom = "custom", } export const auditTypes: string[] = [ AuditTypes.None, AuditTypes.Dict, AuditTypes.Regex, AuditTypes.Baidu, AuditTypes.Custom, ]; export const commonWhiteList: string[] = [ "gmail.com", "outlook.com", "yahoo.com", "hotmail.com", "foxmail.com", "icloud.com", "qq.com", "163.com", "126.com", ]; ================================================ FILE: app/src/admin/channel.ts ================================================ import { getUniqueList } from "@/utils/base.ts"; import { AnonymousType, BasicType, NormalType, ProType, StandardType, } from "@/utils/groups.ts"; export type Channel = { id: number; name: string; type: string; models: string[]; priority: number; weight: number; retry: number; secret: string; endpoint: string; mapper: string; state: boolean; group?: string[]; proxy?: { proxy: string; proxy_type: number; username: string; password: string; }; first_message_as_user?: boolean; merge_consecutive_user_messages?: boolean; }; export enum proxyType { NoneProxy = 0, HttpProxy = 1, HttpsProxy = 2, Socks5Proxy = 3, } export const ProxyTypes: Record = { [proxyType.NoneProxy]: "None Proxy", [proxyType.HttpProxy]: "HTTP Proxy", [proxyType.HttpsProxy]: "HTTPS Proxy", [proxyType.Socks5Proxy]: "SOCKS5 Proxy", }; export type ChannelInfo = { description?: string; endpoint: string; format: string; models: string[]; }; export const ChannelTypes: Record = { openai: "OpenAI", azure: "Azure OpenAI", claude: "Anthropic Claude", palm: "Google Gemini", midjourney: "Midjourney Proxy", sparkdesk: "讯飞星火 SparkDesk", chatglm: "智谱清言 ChatGLM", moonshot: "月之暗面 Moonshot", qwen: "通义千问 TongYi", hunyuan: "腾讯混元 Hunyuan", zhinao: "360智脑 360GLM", baichuan: "百川大模型 BaichuanAI", skylark: "云雀大模型 SkylarkLLM", groq: "Groq Cloud", bing: "New Bing", slack: "Slack Claude", deepseek: "深度求索 DeepSeek", coze: "扣子 Coze", dify: "Dify", }; export const ShortChannelTypes: Record = { openai: "OpenAI", azure: "Azure", claude: "Claude", palm: "Gemini", midjourney: "Midjourney", sparkdesk: "讯飞星火", chatglm: "ChatGLM", moonshot: "Moonshot", qwen: "通义千问", hunyuan: "腾讯混元", zhinao: "360 智脑", baichuan: "百川 AI", skylark: "火山方舟", groq: "Groq", bing: "Bing", slack: "Slack", deepseek: "DeepSeek", coze: "Coze", dify: "Dify", }; export const ChannelInfos: Record = { openai: { endpoint: "https://api.openai.com", format: "", models: [ "gpt-3.5-turbo", "gpt-3.5-turbo-instruct", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-16k-0301", "gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-vision-preview", "gpt-4-1106-vision-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", "gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-mini", "gpt-4o-2024-08-06", "gpt-4o-mini-2024-07-18", "dalle", "dall-e-2", "dall-e-3", ], }, azure: { endpoint: "2023-12-01-preview", format: "|", description: "> Azure 密钥 API Key 1 和 API Key 2 任填一个即可,密钥格式为 **api-key|api-endpoint**, api-endpoint 为 Azure 的 **API 端点**。\n" + "> 接入点填 **API Version**,如 2023-12-01-preview。\n" + "Azure 模型名称忽略点号等问题内部已经进行适配,无需额外任何设置。", models: [ "gpt-3.5-turbo", "gpt-3.5-turbo-instruct", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-16k-0301", "gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-vision-preview", "gpt-4-1106-vision-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", "dalle", "dall-e-2", "dall-e-3", ], }, claude: { endpoint: "https://api.anthropic.com", format: "", description: "> Anthropic Claude 密钥即为 **x-api-key**,Anthropic 对请求 IP 地域有限制,可能出现 **Request not allowed** 的错误,请尝试更换 IP 或者使用代理。\n", models: [ "claude-instant-1.2", "claude-2", "claude-2.1", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307", ], }, slack: { endpoint: "your-channel", format: "|", models: ["claude-slack"], description: "> **注意!当前个人免费版 Slack 已不支持 Claude 调用。** \n" + "> 密钥请填写 bot-id|xoxp-token,其中 bot-id 为 Slack Bot 的 ID,xoxp-token 为 Slack Bot 的 xoxp-token \n" + "> 接入点填写你的 Slack Channel 名称,如 *chatnio* \n" + "> 详情参考 [claude-api](https://github.com/bincooo/claude-api) \n", }, sparkdesk: { endpoint: "wss://spark-api.xf-yun.com", format: "||", models: [ "spark-desk-v1.5", "spark-desk-v2", "spark-desk-v3", "spark-desk-v3.5", ], }, chatglm: { endpoint: "https://open.bigmodel.cn", format: "", models: ["glm-4", "glm-4v", "glm-3-turbo"], description: "> 智谱 ChatGLM 密钥格式为 **api-key**,接入点填写 *https://open.bigmodel.cn* \n", }, qwen: { endpoint: "https://dashscope.aliyuncs.com", format: "", models: ["qwen-turbo", "qwen-plus", "qwen-turbo-net", "qwen-plus-net"], }, hunyuan: { endpoint: "https://hunyuan.cloud.tencent.com", format: "||", models: ["hunyuan"], // endpoint }, zhinao: { endpoint: "https://api.360.cn", format: "", models: ["360-gpt-v9"], }, baichuan: { endpoint: "https://api.baichuan-ai.com", format: "", models: ["baichuan-53b"], }, skylark: { endpoint: "https://ark.cn-beijing.volces.com/api/v3", format: "|", models: [ "skylark-lite-public", "skylark-plus-public", "skylark-pro-public", "skylark-chat", ], description: "> Skylark 格式密钥请填写获取到的 ak|sk 或 apikey \n" + "> 接入点填写生成的接入点,如 *https://ark.cn-beijing.volces.com/api/v3* \n" + "> Skylark API 的地域字段无需手动填写,系统会自动根据接入点获取 \n", }, bing: { endpoint: "wss://your.bing.service", format: "", models: ["bing-creative", "bing-balanced", "bing-precise"], description: "> New Bing 服务搭建详情请参考 [chatnio-bing-service](https://github.com/coaidev/chatnio-bing-service) \n " + "> bing2api (如 [bingo](https://github.com/weaigc/bingo)) 可直接使用 **OpenAI** 格式而非 **New Bing** 格式 \n " + "> 接入点填写你部署的站点即可,如 *http://localhost:8765* ", }, palm: { endpoint: "https://generativelanguage.googleapis.com", format: "", models: [ "chat-bison-001", "gemini-pro", "gemini-pro-vision", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", ], description: "> Google Gemini / PaLM2 密钥格式为 **api-key**,接入点填写 *https://generativelanguage.googleapis.com* 或其反代地址 \n" + "> Google 对请求 IP 地域有限制,可能出现 **User Location Is Not Supported** 的错误,可以看运气通过反代解决。 \n" + "> Gemini Pro 的返回结果一次性而非流式(即使 `streamGenerateContent` 接口也为假流式),系统内部做了平滑伪流式处理,但仍然无法从根本解决 Gemini Pro 自身假流式的特性。\n", }, midjourney: { endpoint: "https://your.midjourney.proxy", format: "|", models: ["midjourney", "midjourney-fast", "midjourney-turbo"], description: "> 请参考 [midjourney-proxy](https://github.com/novicezk/midjourney-proxy) 项目填入参数,可设置白名单 *white-list* 以限制回调 IP \n" + "> 密钥举例: password|localhost,127.0.0.1,196.128.0.31\n" + "> 密钥即为 *mj-api-secret* (如果没有设置 secret 请填 `null` ) \n" + "> 白名单即为 *white-list*(如果没有回调 IP 白名单默认接收所有 IP 的回调,不需要加 | 以及后面的内容) \n" + "> 接入点填写你的 Midjourney Proxy 的部署地址,如 *http://localhost:8080*, *https://example.com* \n" + "> 注意:**请在系统设置中设置后端的公网 IP / 域名,否则无法接收回调报错 please provide available notify url** \n", }, moonshot: { endpoint: "https://api.moonshot.cn", format: "", models: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"], }, groq: { endpoint: "https://api.groq.com/openai", format: "", models: ["llama2-70b-4096", "mixtral-8x7b-32768", "gemma-7b-it"], }, deepseek: { endpoint: "https://api.deepseek.com", format: "", models: ["deepseek-chat", "deepseek-reasoner"], }, coze: { endpoint: "https://api.coze.cn", format: "", models: [""], description: "> 扣子 Coze 的模型名称即为 Coze 平台的 **bot_id** \n" + "> 进入智能体的开发页面,开发页面 URL 中 bot 参数后的数字就是智能体 ID \n" + "> 例如 [https://www.coze.cn/space/341****/bot/73428668*****](https://www.coze.cn/space/341****/bot/73428668*****),智能体 ID 为 73428668***** \n" + "> 确保当前使用的访问密钥已被授予智能体所属空间的 chat 权限 \n" + "> 如果需要让系统自动适配扣子 Coze 平台的图标,请在 **模型映射** 中将 **bot_id** 映射为 **coze** 开头的模型,如 coze-chat>73428668***** \n", }, dify: { endpoint: "https://api.dify.ai/v1", format: "", models: [""], description: "> 由于 Dify 平台一个 Key 对应一个 CHATFLOW (模型),所以模型名称仅在用户调用本系统时用于标识用户调用的对象,不代表调用 Dify 平台 CHATFLOW 时被调用 CHATFLOW 的名称 \n" + "> 因此,您需要为每一个 Dify 平台的 CHATFLOW 分别创建渠道 \n" + "> 如果需要让系统自动适配 Dify 平台的图标,请将模型名称填写为 **dify** 开头的模型,如 **dify-chat** \n", }, }; export const defaultChannelModels: string[] = getUniqueList( Object.values(ChannelInfos).flatMap((info) => info.models), ); export const channelGroups: string[] = [ AnonymousType, NormalType, BasicType, StandardType, ProType, ]; export function getChannelInfo(type?: string): ChannelInfo { if (type && type in ChannelInfos) return ChannelInfos[type]; return ChannelInfos.openai; } export function getChannelType(type?: string): string { if (type && type in ChannelTypes) return ChannelTypes[type]; return ChannelTypes.openai; } export function getShortChannelType(type?: string): string { if (type && type in ShortChannelTypes) return ShortChannelTypes[type]; return ShortChannelTypes.openai; } ================================================ FILE: app/src/admin/charge.ts ================================================ export const tokenBilling = "token-billing"; export const timesBilling = "times-billing"; export const nonBilling = "non-billing"; export const defaultChargeType = tokenBilling; export const chargeTypes = [nonBilling, timesBilling, tokenBilling]; export type ChargeType = (typeof chargeTypes)[number]; export type ChargeBaseProps = { type: string; anonymous: boolean; input: number; output: number; }; export type ChargeProps = ChargeBaseProps & { id: number; models: string[]; }; ================================================ FILE: app/src/admin/colors.ts ================================================ export const modelColorMapper: Record = { // OpenAI & Azure OpenAI "gpt-3.5-turbo": "green-500", "gpt-3.5-turbo-16k": "green-600", "gpt-4": "purple-600", dalle: "green-600", "dall-e-2": "green-600", "dall-e-3": "purple-700", whisper: "gray-300", tts: "gray-300", openai: "gray-300", azure: "gray-300", // Anthropic Claude "claude-3": "orange-500", claude: "orange-400", anthropic: "orange-400", // Spark Desk "spark-desk": "blue-400", sparkdesk: "blue-400", // Moonshot moonshot: "black-500", kimi: "black-500", // Midjourney midjourney: "indigo-600", "mid-journey": "indigo-600", niji: "indigo-600", // Stable Diffusion "stable-diffusion": "gray-400", stablediffusion: "gray-400", stability: "gray-400", // Groq Cloud "llama2-70b-4096": "red-500", "mixtral-8x7b-32768": "red-500", "gemma-7b-it": "red-500", // Google Gemini & Gemma "chat-bison-001": "red-500", palm: "red-500", gemini: "red-500", gemma: "red-500", // DeepSeek deepseek: "blue-700", // New Bing bing: "blue-700", // ChatGLM zhipu: "lime-500", glm: "lime-500", // Tongyi Qwen qwen: "indigo-600", tongyi: "indigo-600", // Meta LLaMA llama: "sky-400", // Tencent Hunyuan hunyuan: "blue-500", // 360 GPT "360": "stone-500", // Baichuan AI baichuan: "orange-700", // ByteDance Skylark / Doubao / Coze skylark: "sky-300", doubao: "sky-300", coze: "sky-300", // Dify dify: "gray-300", // OpenRouter openrouter: "purple-600", }; const unknownColors = [ "gray-700", "indigo-600", "green-500", "green-600", "purple-600", "purple-700", "orange-400", "blue-400", "red-500", "blue-700", "lime-500", "sky-400", "stone-500", "orange-700", "sky-300", ]; export function getUnknownModelColor(model: string): string { const char = model.length > 0 ? model[0] : "A"; const code = char.charCodeAt(0); return unknownColors[code % unknownColors.length]; } export function getModelColor(model: string): string { for (const key in modelColorMapper) { if (model.toLowerCase().includes(key)) { return modelColorMapper[key]; } } return getUnknownModelColor(model); } ================================================ FILE: app/src/admin/datasets/charge.ts ================================================ import { ChargeProps, ChargeType, timesBilling, tokenBilling, } from "@/admin/charge.ts"; export enum Currency { CNY = "CNY", USD = "USD", } export type PricingItem = { models: string[]; input?: number; output: number; currency?: Currency; billing_type?: ChargeType; }; export type PricingDataset = PricingItem[]; export const pricing: PricingDataset = [ { models: [ "gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-instruct", ], input: 0.0015, output: 0.002, }, { models: ["gpt-3.5-turbo-1106"], input: 0.001, output: 0.002, }, { models: ["gpt-3.5-turbo-0125"], input: 0.0005, output: 0.0015, }, { models: [ "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0301", "gpt-3.5-turbo-16k-0613", ], input: 0.003, output: 0.004, }, { models: ["gpt-4", "gpt-4-0314", "gpt-4-0613"], input: 0.03, output: 0.06, }, { models: [ "gpt-4-1106-preview", "gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-1106-vision-preview", "gpt-4-vision-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", ], input: 0.01, output: 0.03, }, { models: ["gpt-4o", "gpt-4o-2024-05-13"], input: 0.005, output: 0.015, }, { models: ["gpt-4o-2024-08-06"], input: 0.0025, output: 0.01, }, { models: ["gpt-4o-mini", "gpt-4o-mini-2024-07-18"], input: 0.00015, output: 0.0006, }, { models: ["gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613"], input: 0.06, output: 0.12, }, { models: ["dalle", "dall-e-2"], // dall-e-2 512x512 size output: 0.018, billing_type: timesBilling, }, { models: ["dall-e-3"], // dall-e-3 HD 1024x1024 size output: 0.08, billing_type: timesBilling, }, { models: [ "claude-1", "claude-1-100k", "claude-1.2", "claude-1.3", "claude-instant", "claude-instant-1.2", "claude-slack", ], input: 0.0008, output: 0.0024, // input: $0.8/1m tokens, output: $2.4/1m tokens }, { models: ["claude-2", "claude-2-100k", "claude-2.1"], input: 0.008, output: 0.024, }, // claude 3 haiku $0.25/1m tokens input & $1.25/1m tokens output { models: ["claude-3-haiku-20240307"], input: 0.00025, output: 0.00125, }, // claude 3 sonnet $3/1m tokens input & $15/1m tokens output { models: ["claude-3-sonnet-20240229"], input: 0.003, output: 0.015, }, // claude 3 sonnet $15/1m tokens input & $75/1m tokens output { models: ["claude-3-opus-20240229"], input: 0.015, output: 0.075, }, { models: ["midjourney"], output: 0.1, currency: Currency.CNY, billing_type: timesBilling, }, { models: ["midjourney-fast"], output: 0.2, currency: Currency.CNY, billing_type: timesBilling, }, { models: ["midjourney-turbo"], output: 0.5, currency: Currency.CNY, billing_type: timesBilling, }, { models: ["spark-desk-v1.5"], input: 0.015, output: 0.015, currency: Currency.CNY, }, { models: ["spark-desk-v2", "spark-desk-v3", "spark-desk-v3.5"], input: 0.03, output: 0.03, currency: Currency.CNY, }, { models: ["moonshot-v1-8k"], input: 0.012, output: 0.012, currency: Currency.CNY, }, { models: ["moonshot-v1-32k"], input: 0.024, output: 0.024, currency: Currency.CNY, }, { models: ["moonshot-v1-128k"], input: 0.06, output: 0.06, currency: Currency.CNY, }, { models: ["glm-4", "glm-4v"], input: 0.1, output: 0.1, currency: Currency.CNY, }, { models: [ "zhipu-chatglm-lite", "zhipu-chatglm-std", "zhipu-chatglm-turbo", "glm-3-turbo", ], input: 0.005, output: 0.005, currency: Currency.CNY, }, { models: ["zhipu-chatglm-pro"], input: 0.01, output: 0.01, currency: Currency.CNY, }, { models: ["qwen-plus", "qwen-plus-net"], input: 0.02, output: 0.02, currency: Currency.CNY, }, { models: ["qwen-turbo", "qwen-turbo-net"], input: 0.008, output: 0.008, currency: Currency.CNY, }, { models: ["chat-bison-001"], // free marked as $0.001 output: 0.001, }, { models: [ "gemini-pro", "gemini-pro-vision", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", ], input: 0.000125, output: 0.000375, }, { models: ["hunyuan"], input: 0.1, output: 0.1, currency: Currency.CNY, }, { models: ["deepseek-chat", "deepseek-coder"], input: 0.001, output: 0.002, currency: Currency.CNY, }, { models: ["360-gpt-v9"], input: 0.12, output: 0.12, currency: Currency.CNY, }, { models: ["baichuan-53b"], input: 0.02, output: 0.02, currency: Currency.CNY, }, { models: ["skylark-lite-public"], input: 0.004, output: 0.004, currency: Currency.CNY, }, { models: ["skylark-plus-public"], input: 0.008, output: 0.008, currency: Currency.CNY, }, { models: ["skylark-pro-public", "skylark-chat"], input: 0.011, output: 0.011, currency: Currency.CNY, }, { models: ["llama2-70b-4096", "mixtral-8x7b-32768", "gemma-7b-it"], output: 0.001, // free marked as $0.001 currency: Currency.USD, }, ]; const countPricing = ( _price?: number, _currency?: Currency, usd?: number, ): number => { const price = _price ?? 0; const currency = _currency ?? Currency.USD; switch (currency) { case Currency.CNY: return price * 10; // 1 cny = 10 quota case Currency.USD: return price * 10 * (usd ?? 1); default: return countPricing(price, Currency.USD, usd); } }; export const getPricing = (currency: number): ChargeProps[] => pricing.map( (item, index): ChargeProps => ({ id: index, models: item.models, type: item.billing_type ?? tokenBilling, anonymous: false, input: countPricing(item.input, item.currency, currency), output: countPricing(item.output, item.currency, currency), }), ); ================================================ FILE: app/src/admin/hook.tsx ================================================ import { useMemo, useState } from "react"; import { getUniqueList } from "@/utils/base.ts"; import { defaultChannelModels } from "@/admin/channel.ts"; import { getApiMarket, getApiModels } from "@/api/v1.ts"; import { useEffectAsync } from "@/utils/hook.ts"; import { Model } from "@/api/types.tsx"; export type onStateChange = (state: boolean, data?: T) => void; export const useAllModels = (onStateChange?: onStateChange) => { const [allModels, setAllModels] = useState([]); const update = async () => { onStateChange?.(false, allModels); const models = await getApiModels(); onStateChange?.(true, models.data); setAllModels(models.data); }; useEffectAsync(update, []); return { allModels, update, }; }; export const useChannelModels = (onStateChange?: onStateChange) => { const { allModels, update } = useAllModels(onStateChange); const channelModels = useMemo( () => getUniqueList([...allModels, ...defaultChannelModels]), [allModels], ); return { channelModels, allModels, update, }; }; export const useSupportModels = (onStateChange?: onStateChange) => { const [supportModels, setSupportModels] = useState([]); const update = async () => { onStateChange?.(false, supportModels); const market = await getApiMarket(); onStateChange?.(true, market); setSupportModels(market); }; useEffectAsync(update, []); return { supportModels, update, }; }; ================================================ FILE: app/src/admin/market.ts ================================================ export const marketEditableTags = [ "official", "multi-modal", "web", "high-quality", "high-price", "open-source", "image-generation", "unstable", ]; export const deprecatedModelImages = [ "gpt35turbo.png", "gpt35turbo16k.webp", "gpt4.png", "gpt432k.webp", "gpt4v.png", "gpt4dalle.png", "claude.png", "claude100k.png", "stablediffusion.jpeg", "llama2.webp", "llamacode.webp", "dalle.jpeg", "midjourney.jpg", "newbing.jpg", "palm2.webp", "gemini.jpeg", "chatglm.png", "tongyi.png", "sparkdesk.jpg", "hunyuan.png", "360gpt.png", "baichuan.png", "skylark.jpg", ]; export const marketTags = ["high-context", ...marketEditableTags, "free"]; ================================================ FILE: app/src/admin/types.ts ================================================ export type CommonResponse = { status: boolean; message: string; error?: string; }; export type InfoResponse = { billing_today: number; billing_yesterday: number; billing_month: number; billing_last_month: number; subscription_count: number; online_chats: number; }; export type ModelChartResponse = { date: string[]; value: { model: string; data: number[]; }[]; }; export type RequestChartResponse = { date: string[]; value: number[]; }; export type BillingChartResponse = { date: string[]; value: number[]; }; export type ErrorChartResponse = { date: string[]; value: number[]; }; export type UserTypeChartResponse = { total: number; normal: number; api_paid: number; basic_plan: number; standard_plan: number; pro_plan: number; }; export type InvitationData = { code: string; quota: number; type: string; used: boolean; username: string; created_at: string; updated_at: string; }; export type InvitationForm = { data: InvitationData[]; total: number; }; export type InvitationResponse = { status: boolean; message: string; data: InvitationData[]; total: number; }; export type Redeem = { code: string; quota: number; used: boolean; created_at: string; updated_at: string; }; export type RedeemForm = { data: Redeem[]; total: number; }; export type RedeemResponse = CommonResponse & { data: Redeem[]; total: number; }; export type InvitationGenerateResponse = { status: boolean; data: string[]; message: string; }; export type RedeemGenerateResponse = { status: boolean; data: string[]; message: string; }; export type UserData = { id: number; username: string; email: string; is_banned: boolean; is_admin: boolean; quota: number; used_quota: number; is_subscribed: boolean; total_month: number; expired_at: string; level: number; enterprise: boolean; }; export type UserForm = { data: UserData[]; total: number; }; export type UserResponse = { status: boolean; message: string; data: UserData[]; total: number; }; ================================================ FILE: app/src/api/addition.ts ================================================ import axios from "axios"; import { getErrorMessage } from "@/utils/base.ts"; type QuotaResponse = { status: boolean; error: string; }; type PackageResponse = { status: boolean; cert: boolean; teenager: boolean; }; type SubscriptionResponse = { status: boolean; is_subscribed: boolean; expired: number; enterprise?: boolean; usage: Record; level: number; expired_at?: string; refresh?: number; refresh_at?: string; }; type BuySubscriptionResponse = { status: boolean; error: string; }; type ApiKeyResponse = { status: boolean; key: string; }; type ResetApiKeyResponse = { status: boolean; key: string; error: string; }; export async function buyQuota(quota: number): Promise { try { const resp = await axios.post(`/buy`, { quota }); return resp.data as QuotaResponse; } catch (e) { console.debug(e); return { status: false, error: "network error" }; } } export async function getPackage(): Promise { try { const resp = await axios.get(`/package`); if (resp.data.status === false) { return { status: false, cert: false, teenager: false }; } return { status: resp.data.status, cert: resp.data.data.cert, teenager: resp.data.data.teenager, }; } catch (e) { console.debug(e); return { status: false, cert: false, teenager: false }; } } export async function getSubscription(): Promise { try { const resp = await axios.get(`/subscription`); if (resp.data.status === false) { return { status: false, is_subscribed: false, level: 0, expired: 0, usage: {}, }; } return resp.data as SubscriptionResponse; } catch (e) { console.debug(e); return { status: false, is_subscribed: false, level: 0, expired: 0, usage: {}, }; } } export async function buySubscription( month: number, level: number, ): Promise { try { const resp = await axios.post(`/subscribe`, { level, month }); return resp.data as BuySubscriptionResponse; } catch (e) { console.debug(e); return { status: false, error: "network error" }; } } export async function getKey(): Promise { try { const resp = await axios.get(`/apikey`); if (resp.data.status === false) { return { status: false, key: "" }; } return { status: resp.data.status, key: resp.data.key, }; } catch (e) { console.debug(e); return { status: false, key: "" }; } } export async function regenerateKey(): Promise { try { const resp = await axios.post(`/resetkey`); return resp.data as ResetApiKeyResponse; } catch (e) { console.debug(e); return { status: false, key: "", error: getErrorMessage(e) }; } } ================================================ FILE: app/src/api/auth.ts ================================================ import axios from "axios"; import { getErrorMessage } from "@/utils/base.ts"; import { isEmailValid } from "@/utils/form.ts"; import { toast } from "sonner"; export type LoginForm = { username: string; password: string; }; export type DeepLoginForm = { token: string; }; export type LoginResponse = { status: boolean; error: string; token: string; }; export type StateResponse = { status: boolean; user: string; admin: boolean; }; export type RegisterForm = { username: string; password: string; repassword: string; email: string; code: string; }; export type RegisterResponse = { status: boolean; error: string; token: string; }; export type VerifyForm = { email: string; }; export type VerifyResponse = { status: boolean; error: string; }; export type ResetForm = { email: string; code: string; password: string; repassword: string; }; export type ResetResponse = { status: boolean; error: string; }; export type UserInfo = { id: number; register_days: number; used_quota: number; plan_total_month: number; email: string; }; export type UserInfoResponse = { status: boolean; error: string; data: UserInfo; }; export async function doLogin( data: DeepLoginForm | LoginForm, ): Promise { const response = await axios.post("/login", data); return response.data as LoginResponse; } export async function doState(): Promise { const response = await axios.post("/state"); return response.data as StateResponse; } export async function doRegister( data: RegisterForm, ): Promise { try { const response = await axios.post("/register", data); return response.data as RegisterResponse; } catch (e) { return { status: false, error: getErrorMessage(e), token: "", }; } } export async function doVerify( email: string, checkout?: boolean, ): Promise { try { const response = await axios.post("/verify", { email, checkout, } as VerifyForm); return response.data as VerifyResponse; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } export async function doReset(data: ResetForm): Promise { try { const response = await axios.post("/reset", data); return response.data as ResetResponse; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } export async function sendCode( t: any, email: string, checkout?: boolean, ): Promise { if (email.trim().length === 0 || !isEmailValid(email)) return false; const res = await doVerify(email, checkout); if (!res.status) toast.error(t("auth.send-code-failed"), { description: t("auth.send-code-failed-prompt", { reason: res.error }), }); else toast.info(t("auth.send-code-success"), { description: t("auth.send-code-success-prompt"), }); return res.status; } export const initialUserInfo: UserInfo = { id: 0, register_days: 0, used_quota: 0, plan_total_month: 0, email: "", }; export async function getUserInfo(): Promise { try { const response = await axios.get("/userinfo"); return response.data as UserInfoResponse; } catch (e) { return { status: false, error: getErrorMessage(e), data: { ...initialUserInfo }, }; } } ================================================ FILE: app/src/api/broadcast.ts ================================================ import axios from "axios"; import { getMemory, setMemory } from "@/utils/memory.ts"; export type Broadcast = { content: string; index: number; }; export type BroadcastInfo = Broadcast & { poster: string; created_at: string; }; export type BroadcastListResponse = { data: BroadcastInfo[]; }; export type CommonBroadcastResponse = { status: boolean; error: string; }; export async function getRawBroadcast(): Promise { try { const data = await axios.get("/broadcast/view"); if (data.data) return data.data as Broadcast; } catch (e) { console.warn(e); } return { content: "", index: 0, }; } export type BroadcastEvent = { message: string; firstReceived: boolean; }; export async function getBroadcast(): Promise { const data = await getRawBroadcast(); const content = data.content.trim(); if (content.length === 0) return { message: "", firstReceived: false, }; const memory = getMemory("broadcast"); if (memory === content) return { message: content, firstReceived: false, }; setMemory("broadcast", content); return { message: content, firstReceived: true, }; } export async function getBroadcastList(): Promise { try { const resp = await axios.get("/broadcast/list"); const data = resp.data as BroadcastListResponse; return data.data || []; } catch (e) { console.warn(e); return []; } } export async function createBroadcast( content: string, notify_all?: boolean, ): Promise { try { const resp = await axios.post("/broadcast/create", { content, notify_all, }); return resp.data as CommonBroadcastResponse; } catch (e) { console.warn(e); return { status: false, error: (e as Error).message, }; } } export async function removeBroadcast( index: number, ): Promise { try { const resp = await axios.post(`/broadcast/remove/${index}`); return resp.data as CommonBroadcastResponse; } catch (e) { console.warn(e); return { status: false, error: (e as Error).message, }; } } export async function updateBroadcast( id: number, content: string, ): Promise { try { const resp = await axios.post("/broadcast/update", { id, content }); return resp.data as CommonBroadcastResponse; } catch (e) { console.warn(e); return { status: false, error: (e as Error).message, }; } } ================================================ FILE: app/src/api/common.ts ================================================ import { toast } from "sonner"; export type CommonResponse = { status: boolean; error?: string; reason?: string; message?: string; data?: any; }; export function withNotify( t: any, state: CommonResponse, toastSuccess?: boolean, toastSuccessMessage?: string, ) { if (state.status) toastSuccess && toast.success(t("success"), { description: toastSuccessMessage || t("request-success"), }); else toast.error(t("error"), { description: state.error ?? state.reason ?? state.message ?? "error occurred", }); } ================================================ FILE: app/src/api/connection.ts ================================================ import { tokenField, websocketEndpoint } from "@/conf/bootstrap.ts"; import { getMemory } from "@/utils/memory.ts"; import { getErrorMessage } from "@/utils/base.ts"; import { Mask } from "@/masks/types.ts"; export const endpoint = `${websocketEndpoint}/chat`; export const maxRetry = 60; // 30s max websocket retry export const maxConnection = 5; export type StreamMessage = { conversation?: number; keyword?: string; quota?: number; message: string; end: boolean; plan?: boolean; title?: string; search_query?: { type: string; search_queries: string[]; }; search_result?: { type: string; search_results: Array<{ url: string; title: string; snippet: string; published_at?: number; site_name?: string; site_icon?: string; }>; }; search_index?: { type: string; search_indexes: Array<{ url: string; cite_index: number; }>; }; tool_call?: { name: string; arguments?: unknown; result?: string; error?: string; status: "start" | "executing" | "success" | "error"; }; response_type?: string; }; export type ChatProps = { type?: string; message: string; model: string; web?: boolean; web_search_mode?: "quick" | "detailed"; web_page_summary?: boolean; think?: boolean; context?: number; ignore_context?: boolean; // mcp related fields enable_mcp?: boolean; mcp_plugin_id?: number; max_tokens?: number; temperature?: number; top_p?: number; top_k?: number; presence_penalty?: number; frequency_penalty?: number; repetition_penalty?: number; }; type StreamCallback = (id: number, message: StreamMessage) => void; export class Connection { protected connection?: WebSocket; protected callback?: StreamCallback; protected stack?: Record; public id: number; public state: boolean; public constructor(id: number, callback?: StreamCallback) { this.state = false; this.id = id; callback && this.setCallback(callback); } public init(): void { this.connection = new WebSocket(endpoint); this.state = false; this.connection.onopen = () => { this.state = true; this.send({ token: getMemory(tokenField) || "anonymous", id: this.id, }); }; this.connection.onclose = (event) => { this.state = false; this.stack = { error: "websocket connection failed", code: event.code, reason: event.reason, endpoint: endpoint, }; setTimeout(() => { console.debug(`[connection] reconnecting... (id: ${this.id})`); this.init(); }, 3000); }; this.connection.onmessage = (event) => { const message = JSON.parse(event.data); this.triggerCallback(message as StreamMessage); }; } public reconnect(): void { this.init(); } public send(data: Record): boolean { if (!this.state || !this.connection) { if (this.connection === undefined) this.init(); console.debug("[connection] connection not ready, retrying in 500ms..."); return false; } this.connection.send(JSON.stringify(data)); return true; } public sendWithRetry(t: any, data: ChatProps, times?: number): void { try { if (!times || times < maxRetry) { if (!this.send(data)) { setTimeout(() => { this.sendWithRetry(t, data, (times ?? 0) + 1); }, 500); } return; } } catch (e) { console.warn( `[connection] failed to send message: ${getErrorMessage(e)}`, ); } const trace = JSON.stringify( this.stack ?? { message: data.message, endpoint: endpoint, }, null, 2, ); this.stack = undefined; t && this.triggerCallback({ message: `${t("request-failed")}\n\`\`\`json\n${trace}\n\`\`\`\n`, end: true, }); } public sendEvent(t: any, event: string, data?: string, props?: ChatProps) { this.sendWithRetry(t, { type: event, message: data || "", model: "event", ...props, }); } public sendStopEvent(t: any) { this.sendEvent(t, "stop"); } public sendRestartEvent(t: any, data?: ChatProps) { this.sendEvent(t, "restart", undefined, data); } public sendMaskEvent(t: any, mask: Mask) { this.sendEvent(t, "mask", JSON.stringify(mask.context)); } public sendEditEvent(t: any, id: number, message: string) { this.sendEvent(t, "edit", `${id}:${message}`); } public sendRemoveEvent(t: any, id: number) { this.sendEvent(t, "remove", id.toString()); } public sendShareEvent(t: any, refer: string) { this.sendEvent(t, "share", refer); } public close(): void { if (!this.connection) return; this.connection.close(); } public setCallback(callback?: StreamCallback): void { this.callback = callback; } protected triggerCallback(message: StreamMessage): void { this.callback && this.callback(this.id, message); } public setId(id: number): void { this.id = id; } public isReady(): boolean { return this.state; } public isRunning(): boolean { if (!this.connection || !this.state) return false; return this.connection.readyState === WebSocket.OPEN; } } export class ConnectionStack { protected connections: Connection[]; protected callback?: StreamCallback; public constructor(callback?: StreamCallback) { this.connections = []; this.callback = callback; } public getConnection(id: number): Connection | undefined { return this.connections.find((conn) => conn.id === id); } public createConnection(id: number): Connection { const conn = new Connection(id, this.triggerCallback.bind(this)); this.connections.push(conn); // max connection garbage collection if (this.connections.length > maxConnection) { const garbage = this.connections.shift(); garbage && garbage.close(); } return conn; } public send(id: number, t: any, props: ChatProps) { const conn = this.getConnection(id); if (!conn) return false; conn.sendWithRetry(t, props); return true; } public hasConnection(id: number): boolean { return this.connections.some((conn) => conn.id === id); } public setCallback(callback?: StreamCallback): void { this.callback = callback; } public sendEvent(id: number, t: any, event: string, data?: string) { const conn = this.getConnection(id); conn && conn.sendEvent(t, event, data); } public sendStopEvent(id: number, t: any) { const conn = this.getConnection(id); conn && conn.sendStopEvent(t); } public sendRestartEvent(id: number, t: any, data?: ChatProps) { const conn = this.getConnection(id); conn && conn.sendRestartEvent(t, data); } public sendMaskEvent(id: number, t: any, mask: Mask) { const conn = this.getConnection(id); conn && conn.sendMaskEvent(t, mask); } public sendEditEvent(id: number, t: any, messageId: number, message: string) { const conn = this.getConnection(id); conn && conn.sendEditEvent(t, messageId, message); } public sendRemoveEvent(id: number, t: any, messageId: number) { const conn = this.getConnection(id); conn && conn.sendRemoveEvent(t, messageId); } public sendShareEvent(id: number, t: any, refer: string) { const conn = this.getConnection(id); conn && conn.sendShareEvent(t, refer); } public close(id: number): void { const conn = this.getConnection(id); conn && conn.close(); } public closeAll(): void { this.connections.forEach((conn) => conn.close()); } public reconnect(id: number): void { const conn = this.getConnection(id); conn && conn.reconnect(); } public reconnectAll(): void { this.connections.forEach((conn) => conn.reconnect()); } public raiseConnection(id: number): void { const conn = this.getConnection(-1); if (!conn) return; conn.setId(id); } public triggerCallback(id: number, message: StreamMessage): void { this.callback && this.callback(id, message); } } ================================================ FILE: app/src/api/file.ts ================================================ import { blobEndpoint } from "@/conf/env.ts"; import { trimSuffixes } from "@/utils/base.ts"; export type BlobParserResponse = { status: boolean; content: string; error?: string; }; export type FileObject = { name: string; content: string; size?: number; }; type Model = { id: string; ocr_model?: boolean; vision_model?: boolean; reverse_model?: boolean; }; export type FileArray = FileObject[]; export async function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(new Error("Failed to read file")); }); } export function checkFileSuffix( filename: string, suffixes: string | string[], ): boolean { filename = filename.toLowerCase(); if (typeof suffixes === "string") { return filename.endsWith(suffixes); } return suffixes.some((suffix) => filename.endsWith(suffix)); } export async function quickBlobParser( file: File, model: Model, onProgress?: (progress: number) => void, ): Promise { // this function is used to parse the file quickly in local // otherwise, it will be parsed as a file if (file.size === 0 || file.name.length === 0) { throw new Error("File is empty"); } if (!model.reverse_model) { try { // if the file is an image, it will be parsed as an image by local parser first const couldLocalVision = model.vision_model; if (couldLocalVision && file.type.startsWith("image/")) { console.log("[parser] hit image/* file, using local parser"); // parse image as base64 (e.g. result: data:image/png;base64,xxx) const base64 = await fileToBase64(file); return base64; } // if the file is txt, parse it as txt if ( file.type === "text/plain" || checkFileSuffix(file.name, [ "txt", "md", "markdown", "json", "xml", "csv", "yaml", "yml", "toml", "ini", "cfg", "conf", ]) ) { console.log("[parser] hit text/plain file, using local parser"); return await file.text(); } console.log(file.type); } catch (e) { console.error( "[parser] local parser failed, switch to server parser: ", e, ); } } return blobParser(file, model, onProgress); } export async function blobParser( file: File, model: Model, onProgress?: (progress: number) => void, ): Promise { const endpoint = trimSuffixes(blobEndpoint, ["/upload", "/"]); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", file); formData.append("model", model.id); formData.append("enable_ocr", (model.ocr_model ?? false).toString()); formData.append("enable_vision", (model.vision_model ?? false).toString()); formData.append("save_all", (model.reverse_model ?? false).toString()); xhr.open("POST", `${endpoint}/upload`, true); xhr.upload.onprogress = (progressEvent) => { console.debug(progressEvent); if (progressEvent.lengthComputable) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total, ); console.debug(percentCompleted); onProgress?.(percentCompleted); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const data = JSON.parse(xhr.responseText) as BlobParserResponse; if (!data.status) { reject(new Error(data.error)); } else if (data.content.length === 0) { reject(new Error("Result is empty")); } else { resolve(data.content); } } catch (e) { reject(new Error("Invalid JSON response")); } } else { reject(new Error(`HTTP error! status: ${xhr.status}`)); } }; xhr.onerror = () => { reject(new Error("Network error")); }; xhr.send(formData); }); } ================================================ FILE: app/src/api/generation.ts ================================================ import { tokenField, websocketEndpoint } from "@/conf/bootstrap.ts"; import { getMemory } from "@/utils/memory.ts"; export const endpoint = `${websocketEndpoint}/generation/create`; export type GenerationForm = { token: string; prompt: string; model: string; }; export type GenerationSegmentResponse = { message: string; quota: number; end: boolean; error: string; hash: string; title?: string; }; export type MessageEvent = { message: string; quota: number; }; export class GenerationManager { protected processing: boolean; protected connection: WebSocket | null; protected message: string; protected onProcessingChange?: (processing: boolean) => void; protected onMessage?: (message: MessageEvent) => void; protected onError?: (error: string) => void; protected onFinished?: (hash: string) => void; constructor() { this.processing = false; this.connection = null; this.message = ""; } public setProcessingChangeHandler( handler: (processing: boolean) => void, ): void { this.onProcessingChange = handler; } public setMessageHandler(handler: (message: MessageEvent) => void): void { this.onMessage = handler; } public setErrorHandler(handler: (error: string) => void): void { this.onError = handler; } public setFinishedHandler(handler: (hash: string) => void): void { this.onFinished = handler; } public isProcessing(): boolean { return this.processing; } protected setProcessing(processing: boolean): boolean { this.processing = processing; if (!processing) { this.connection = null; this.message = ""; } this.onProcessingChange?.(processing); return processing; } public getConnection(): WebSocket | null { return this.connection; } protected handleMessage(message: GenerationSegmentResponse): void { if (message.error && message.end) { this.onError?.(message.error); this.setProcessing(false); return; } this.message += message.message; this.onMessage?.({ message: this.message, quota: message.quota, }); if (message.end) { this.onFinished?.(message.hash); this.setProcessing(false); } } public generate(prompt: string, model: string) { this.setProcessing(true); const token = getMemory(tokenField) || "anonymous"; if (token) { this.connection = new WebSocket(endpoint); this.connection.onopen = () => { this.connection?.send( JSON.stringify({ token, prompt, model } as GenerationForm), ); }; this.connection.onmessage = (event) => { this.handleMessage(JSON.parse(event.data) as GenerationSegmentResponse); }; this.connection.onclose = () => { this.setProcessing(false); }; } } public generateWithBlock(prompt: string, model: string): boolean { if (this.isProcessing()) { return false; } this.generate(prompt, model); return true; } } export const manager = new GenerationManager(); ================================================ FILE: app/src/api/history.ts ================================================ import axios from "axios"; import type { ConversationInstance } from "./types.tsx"; import { setHistory } from "@/store/chat.ts"; import { AppDispatch } from "@/store"; import { CommonResponse } from "@/api/common.ts"; import { getErrorMessage } from "@/utils/base.ts"; import { VirtualWebSearchRole, VirtualRolePrefix, Message } from "./types.tsx"; import { formatToolCallResult } from "@/api/plugin.ts"; export async function getConversationList(): Promise { try { const resp = await axios.get("/conversation/list"); return ( resp.data.status ? resp.data.data || [] : [] ) as ConversationInstance[]; } catch (e) { console.warn(e); return []; } } export async function updateConversationList( dispatch: AppDispatch, ): Promise { const resp = await getConversationList(); dispatch(setHistory(resp)); } export async function loadConversation( id: number, ): Promise { try { const resp = await axios.get(`/conversation/load?id=${id}`); if (resp.data.status) { const conversation = resp.data.data as ConversationInstance; if (conversation.message && conversation.message.length > 0) { const processedMessages: Message[] = []; for (let i = 0; i < conversation.message.length; i++) { const currentMsg = conversation.message[i]; if (currentMsg.role === VirtualWebSearchRole) { let nextMsgIndex = i + 1; while ( nextMsgIndex < conversation.message.length && conversation.message[nextMsgIndex].role.startsWith(VirtualRolePrefix) ) { nextMsgIndex++; } if (nextMsgIndex < conversation.message.length) { conversation.message[nextMsgIndex].search_query = currentMsg.search_query; conversation.message[nextMsgIndex].search_result = currentMsg.search_result; conversation.message[nextMsgIndex].search_index = currentMsg.search_index; } continue; } if (currentMsg.role === "assistant" && currentMsg.tool_calls) { processedMessages.push(currentMsg); } else if (currentMsg.role === "tool" && currentMsg.tool_call_id) { const toolCallId = currentMsg.tool_call_id; for (let j = processedMessages.length - 1; j >= 0; j--) { const prevMsg = processedMessages[j]; if (prevMsg.role === "assistant" && prevMsg.tool_calls) { const toolCall = prevMsg.tool_calls.find(tc => tc.id === toolCallId); if (toolCall) { try { const result = JSON.parse(currentMsg.content); if (result.error) { toolCall.error = result.error; toolCall.status = "error"; } else { const formattedResult = formatToolCallResult(currentMsg.content); toolCall.result = formattedResult; toolCall.status = "success"; } } catch { const formattedResult = formatToolCallResult(currentMsg.content); toolCall.result = formattedResult; toolCall.status = "success"; } } break; } } processedMessages.push(currentMsg); } else { processedMessages.push(currentMsg); } } conversation.message = processedMessages; } return conversation; } return { id, name: "", message: [] }; } catch (e) { console.warn(e); return { id, name: "", message: [] }; } } export async function deleteConversation(id: number): Promise { try { const resp = await axios.get(`/conversation/delete?id=${id}`); return resp.data.status; } catch (e) { console.warn(e); return false; } } export async function renameConversation( id: number, name: string, ): Promise { try { const resp = await axios.post("/conversation/rename", { id, name }); return resp.data as CommonResponse; } catch (e) { console.warn(e); return { status: false, error: getErrorMessage(e) }; } } export async function deleteAllConversations(): Promise { try { const resp = await axios.get("/conversation/clean"); return resp.data.status; } catch (e) { console.warn(e); return false; } } ================================================ FILE: app/src/api/mask.ts ================================================ import { CustomMask } from "@/masks/types.ts"; import axios from "axios"; import { CommonResponse } from "@/api/common.ts"; import { getErrorMessage } from "@/utils/base.ts"; type ListMaskResponse = CommonResponse & { data: CustomMask[]; }; export async function listMasks(): Promise { try { const resp = await axios.get("/conversation/mask/view"); return ( resp.data ?? { status: true, data: [], } ); } catch (e) { return { status: false, data: [], error: getErrorMessage(e), }; } } export async function saveMask(mask: CustomMask): Promise { try { const resp = await axios.post("/conversation/mask/save", mask); return resp.data; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } export async function deleteMask(id: number): Promise { try { const resp = await axios.post("/conversation/mask/delete", { id }); return resp.data; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } ================================================ FILE: app/src/api/plugin.ts ================================================ import axios from "axios"; import { CommonResponse } from "@/api/common.ts"; import { getErrorMessage } from "@/utils/base.ts"; type ListPluginResponse = CommonResponse & { data: Plugin[]; }; type TestPluginResponse = CommonResponse & { data?: { tools?: Array<{ name: string; description: string; inputSchema: Record; }>; }; }; export async function listPlugins(): Promise { try { const resp = await axios.get("/conversation/plugin/view"); return ( resp.data ?? { status: true, data: [], } ); } catch (e) { return { status: false, data: [], error: getErrorMessage(e), }; } } export async function savePlugin(plugin: Partial): Promise { try { const resp = await axios.post("/conversation/plugin/save", plugin); return resp.data; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } export async function deletePlugin(id: number): Promise { try { const resp = await axios.post("/conversation/plugin/delete", { id }); return resp.data; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } export async function testPlugin(serverUrl: string): Promise { try { const resp = await axios.get("/conversation/plugin/test", { params: { server_url: serverUrl } }); return resp.data; } catch (e) { return { status: false, error: getErrorMessage(e), }; } } export function formatToolCallResult(result: string): string { try { const parsed = JSON.parse(result); let textContent = ''; if (parsed.text) { textContent = parsed.text; } else if (typeof parsed === 'string') { textContent = parsed; } else { textContent = result; } try { const secondParsed = JSON.parse(textContent); if (secondParsed.text) { return secondParsed.text; } else if (typeof secondParsed === 'string') { return secondParsed; } else { return JSON.stringify(secondParsed, null, 2); } } catch { return textContent; } } catch { return result; } } export function parseMCPInput(input: string): { status: 'success' | 'error' | 'noop'; identifier?: string; mcpConfig?: { name: string; description: string; server_url: string; }; errorCode?: string; } { try { const parsed = JSON.parse(input); if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') { return { status: 'error', errorCode: 'plugin.import-error.invalid-format' }; } const serverKeys = Object.keys(parsed.mcpServers); if (serverKeys.length === 0) { return { status: 'error', errorCode: 'plugin.import-error.no-servers' }; } const identifier = serverKeys[0]; const serverConfig = parsed.mcpServers[identifier]; let server_url = ''; let description = ''; if (serverConfig.url) { server_url = serverConfig.url; description = `HTTP MCP Server: ${server_url}`; } else if (serverConfig.command) { return { status: 'error', errorCode: 'plugin.import-error.stdio-not-supported' }; } else { return { status: 'error', errorCode: 'plugin.import-error.unsupported-config' }; } return { status: 'success', identifier, mcpConfig: { name: identifier, description, server_url, } }; } catch (error) { return { status: 'error', errorCode: 'plugin.import-error.invalid-json' }; } } ================================================ FILE: app/src/api/quota.ts ================================================ import axios from "axios"; export async function getQuota(): Promise { try { const response = await axios.get("/quota"); if (response.data.status) { return response.data.quota as number; } } catch (e) { console.debug(e); } return NaN; } ================================================ FILE: app/src/api/record.ts ================================================ import { CommonResponse } from "@/api/common.ts"; import axios from "axios"; export type Record = { username: string; type: string; token_name: string; model: string; input_tokens: number; output_tokens: number; quota: number; duration: number; detail: string; prompts: string; response_prompts: string; channel?: number; channel_name?: string; created_at: string; }; export type RecordData = { total: number; records: Record[]; }; export type RecordStats = { billing_today: number; billing_month: number; request_today: number; request_month: number; rpm: number; tpm: number; }; export type RecordQuery = { user_id?: number; start_time?: string; end_time?: string; token_name?: string; model?: string; type?: RecordType; show_channel?: boolean; }; type ListRecordsResponse = CommonResponse & { data?: RecordData; }; type RecordStatsResponse = CommonResponse & { data?: RecordStats; }; export enum RecordType { All = "all", Topup = "topup", Consume = "consume", System = "system", } export const RecordTypes = [ RecordType.All, RecordType.Topup, RecordType.Consume, RecordType.System, ]; export async function listRecords( page: number, options?: RecordQuery, ): Promise { try { const payload: Partial = { ...options }; if (options && options.show_channel === undefined) { delete payload.show_channel; } const resp = await axios.post(`/record/view?page=${page}`, payload); return resp.data as ListRecordsResponse; } catch (e) { return { status: false, message: (e as Error).message, }; } } export async function getRecordStats(): Promise { try { const resp = await axios.post(`/record/stats`); return resp.data as RecordStatsResponse; } catch (e) { return { status: false, message: (e as Error).message, }; } } ================================================ FILE: app/src/api/redeem.ts ================================================ import axios from "axios"; import { getErrorMessage } from "@/utils/base.ts"; export type RedeemResponse = { status: boolean; error: string; quota: number; }; export async function useRedeem(code: string): Promise { try { const resp = await axios.get(`/redeem?code=${code}`); return resp.data as RedeemResponse; } catch (e) { console.debug(e); return { status: false, error: `network error: ${getErrorMessage(e)}`, quota: 0, }; } } ================================================ FILE: app/src/api/sharing.ts ================================================ import axios from "axios"; import { Message } from "./types.tsx"; export type SharingForm = { status: boolean; message: string; data: string; }; export type SharingPreviewForm = { name: string; conversation_id: number; hash: string; time: string; }; export type ViewData = { name: string; username: string; time: string; model?: string; messages: Message[]; }; export type ViewForm = { status: boolean; message: string; data: ViewData | null; }; export type ListSharingResponse = { status: boolean; message: string; data?: SharingPreviewForm[]; }; export type DeleteSharingResponse = { status: boolean; message: string; }; export async function shareConversation( id: number, refs: number[] = [-1], ): Promise { try { const resp = await axios.post("/conversation/share", { id, refs }); return resp.data; } catch (e) { return { status: false, message: (e as Error).message, data: "" }; } } export async function viewConversation(hash: string): Promise { try { const resp = await axios.get(`/conversation/view?hash=${hash}`); return resp.data as ViewForm; } catch (e) { return { status: false, message: (e as Error).message, data: null, }; } } export async function listSharing(): Promise { try { const resp = await axios.get("/conversation/share/list"); return resp.data as ListSharingResponse; } catch (e) { return { status: false, message: (e as Error).message, }; } } export async function deleteSharing( hash: string, ): Promise { try { const resp = await axios.get(`/conversation/share/delete?hash=${hash}`); return resp.data as DeleteSharingResponse; } catch (e) { return { status: false, message: (e as Error).message, }; } } export function getSharedLink(hash: string): string { return `${location.origin}/share/${hash}`; } ================================================ FILE: app/src/api/types.tsx ================================================ import { ChargeBaseProps } from "@/admin/charge.ts"; import { useMemo } from "react"; import { BotIcon, ServerIcon, UserIcon } from "lucide-react"; export const UserRole = "user"; export const AssistantRole = "assistant"; export const SystemRole = "system"; export const VirtualRolePrefix = "virtualRole::"; export const VirtualWebSearchRole = "virtualRole::websearch"; export type Role = typeof UserRole | typeof AssistantRole | typeof SystemRole; export const Roles = [UserRole, AssistantRole, SystemRole]; export const getRoleIcon = (role: string) => { return useMemo(() => { switch (role) { case UserRole: return ; case AssistantRole: return ; case SystemRole: return ; default: return ; } }, [role]); }; export type Message = { role: string; content: string; keyword?: string; quota?: number; end?: boolean; plan?: boolean; search_query?: { type: string; search_queries: string[]; }; search_result?: { type: string; search_results: Array<{ url: string; title: string; snippet: string; published_at?: number; site_name?: string; site_icon?: string; }>; }; search_index?: { type: string; search_indexes: Array<{ url: string; cite_index: number; }>; }; tool_calls?: Array<{ index: number; type: string; id: string; function: { name: string; arguments: string; }; status?: "start" | "executing" | "success" | "error"; result?: string; error?: string; }>; tool_call_id?: string; name?: string; response_type?: string; }; export type Model = { id: string; name: string; description?: string; free: boolean; auth: boolean; default: boolean; high_context: boolean; function_calling?: boolean; vision_model?: boolean; ocr_model?: boolean; reverse_model?: boolean; thinking_model?: boolean; avatar: string; tag?: string[]; price?: ChargeBaseProps; }; export type Id = number; export type ConversationInstance = { id: number; name: string; message: Message[]; model?: string; shared?: boolean; }; export type PlanItem = { id: string; name: string; value: number; icon: string; models: string[]; }; export type Plan = { level: number; price: number; items: PlanItem[]; discounts?: Record; }; export type Plans = Plan[]; export function newModel(id: string, name?: string, avatar?: string): Model { return { id, name: name ?? id, avatar: avatar ?? "", free: false, auth: false, default: false, high_context: false, }; } ================================================ FILE: app/src/api/v1.ts ================================================ import axios from "axios"; import { Model, Plan } from "@/api/types.tsx"; import { ChargeProps, nonBilling } from "@/admin/charge.ts"; import { getErrorMessage } from "@/utils/base.ts"; type v1Options = { endpoint?: string; }; type v1Models = { object: string; data: v1ModelItem[]; }; type v1ModelItem = | string | { id: string; object: string; created: number; owned_by: string; }; type v1Resp = { data: T; status: boolean; error?: string; }; export type v1ApiKey = { id: number; user_id?: number; name: string; expired_at: string; quota: number; used_quota: number; infinite_quota: boolean; ip_whitelist: string; model_whitelist: string; token_group?: string; group_id?: string; api_key?: string; disabled: boolean; created_at?: string; }; export const initialApiKey: v1ApiKey = { id: -1, name: "", expired_at: "1970-01-01 00:00:00", quota: 100.0, used_quota: 0, infinite_quota: false, ip_whitelist: "", model_whitelist: "", token_group: "default", disabled: false, }; export function getModelName(id: string): string { // replace all `-` to ` ` except first `-` keep it let begin = true; return id .replace(/-/g, (l) => { if (begin) { begin = false; return l; } return " "; }) .replace(/\b\w/g, (l) => l.toUpperCase()) .replace(/Gpt/g, "GPT") .replace(/Tts/g, "TTS") .replace(/Dall-E/g, "DALL-E") .replace(/Dalle/g, "DALLE") .replace(/Glm/g, "GLM") .trim(); } export function getV1Path(path: string, options?: v1Options): string { let endpoint = options && options.endpoint ? options.endpoint : ""; if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1); return endpoint + path; } export async function getApiModels( secret?: string, options?: v1Options, ): Promise> { try { const res = await axios.get( getV1Path("/v1/models", options), secret ? { headers: { Authorization: `Bearer ${secret}`, }, } : undefined, ); const data = res.data as v1Models; // if data.data is an array of strings, we can just return it const models = data.data ? data.data.map((model) => (typeof model === "string" ? model : model.id)) : []; return models.length > 0 ? { status: true, data: models } : { status: false, data: [], error: "No models found" }; } catch (e) { console.warn(e); return { status: false, data: [], error: getErrorMessage(e) }; } } export async function getApiPlans(options?: v1Options): Promise { try { const res = await axios.get(getV1Path("/v1/plans", options)); const plans = res.data as Plan[]; return plans.filter((plan: Plan) => plan.level !== 0); } catch (e) { console.warn(e); return []; } } export async function getApiMarket(options?: v1Options): Promise { try { const res = await axios.get(getV1Path("/v1/market", options)); return (res.data || []) as Model[]; } catch (e) { console.warn(e); return []; } } export async function getFilledApiMarket( secret?: string, options?: v1Options, ): Promise { const data = await getApiMarket(options); if (data.length > 0) return data; const resp = await getApiModels(secret, options); if (!resp.status) return []; return resp.data.map((id) => ({ id, default: true, name: getModelName(id), tag: [], avatar: "", description: id, free: false, auth: true, high_context: false, price: { type: nonBilling, anonymous: false, models: [id], input: 0, output: 0, }, })); } export async function bindMarket(options?: v1Options): Promise { const market = await getFilledApiMarket(undefined, options); const charge = await getApiCharge(options); market.forEach((item: Model) => { const instance = charge.find((i: ChargeProps) => i.models.includes(item.id), ); if (!instance) return; item.free = instance.type === nonBilling; item.auth = !item.free || !instance.anonymous; item.price = { ...instance }; }); return market; } export async function getApiCharge( options?: v1Options, ): Promise { try { const res = await axios.get(getV1Path("/v1/charge", options)); return res.data as ChargeProps[]; } catch (e) { console.warn(e); return []; } } export async function listApiKey( options?: v1Options, ): Promise> { try { const res = await axios.get(getV1Path("/v1/list_keys", options)); return res.data as v1Resp; } catch (e) { console.warn(e); return { status: false, data: [], error: getErrorMessage(e) }; } } export async function updateApiKey( data: v1ApiKey, options?: v1Options, ): Promise> { try { const res = await axios.post(getV1Path("/v1/update_key", options), data); return res.data as v1Resp; } catch (e) { console.warn(e); return { status: false, data: initialApiKey, error: getErrorMessage(e) }; } } export async function deleteApiKey( id: number, options?: v1Options, ): Promise> { try { const res = await axios.post(getV1Path(`/v1/delete_key?id=${id}`, options)); return res.data as v1Resp; } catch (e) { console.warn(e); return { status: false, data: initialApiKey, error: getErrorMessage(e) }; } } type ManifestJson = { data?: Record< string, { file: string; src: string; } >; status: boolean; error?: string; }; export async function getManifestJson(): Promise { try { const res = await axios.get("/manifest.json", { baseURL: "/", }); return { status: true, data: res.data, }; } catch (e) { console.warn(e); return { status: false, error: getErrorMessage(e), }; } } ================================================ FILE: app/src/assets/admin/all.less ================================================ @import "menu"; @import "dashboard"; @import "market"; @import "management"; @import "broadcast"; @import "channel"; @import "charge"; @import "system"; @import "subscription"; @import "logger"; .admin-page { position: relative; display: flex; flex-direction: row; width: 100%; height: 100%; max-width: 100%; } @media (orientation: landscape) { .admin-page { max-width: calc(100vw - 3.5rem); } } .admin-container { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; } .admin-card { position: relative; border: 0 !important; width: 100%; height: 100%; min-height: 20vh; } .record-card { .record-wrapper > * { width: 100%; } } @media (max-width: 1268px) { .admin-card { border-radius: 0 !important; } .user-interface, .market, .broadcast, .channel, .charge, .system, .logger, .admin-subscription, .admin-container { padding: 0 !important; & > * { margin-bottom: 0 !important; border-bottom: 1px solid hsl(var(--border)) !important; border-radius: 0 !important; } } } .object-id { display: flex; flex-direction: row; align-items: center; justify-items: center; border-radius: var(--radius); border: 1px solid hsl(var(--border)); color: hsl(var(--text-secondary)); user-select: none; font-size: 0.75rem; height: 2.5rem; padding: 0.5rem 1.25rem; cursor: pointer; transition: 0.25s; flex-shrink: 0; &:hover { color: hsl(var(--text)); border-color: hsl(var(--border-hover)); } svg { transform: translateY(1px); } } ================================================ FILE: app/src/assets/admin/broadcast.less ================================================ .broadcast { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .broadcast-card { width: 100%; height: 100%; min-height: 20vh; } .empty { color: hsl(var(--text-secondary)) !important; font-size: 14px; margin: auto; user-select: none; } } ================================================ FILE: app/src/assets/admin/channel.less ================================================ .channel { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .channel-card { width: 100%; height: 100%; min-height: 20vh; } .channel-table { .channel-id { color: hsl(var(--text-secondary)); } } } .channel-editor { position: relative; .channel-loader { position: absolute; top: 0; right: 0.25rem; } } .channel-wrapper { display: flex; flex-direction: column; margin-top: 0.5rem; margin-bottom: 2rem; & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } .channel-row { display: flex; flex-direction: column; user-select: none; white-space: nowrap; &.column-layout { flex-direction: row; align-items: center; white-space: nowrap; width: 100%; .channel-content { margin-left: 0; margin-bottom: 0; } & > * { margin-right: 1rem; margin-bottom: 0; &:last-child { margin-right: 0; } } } .channel-content { display: flex; flex-direction: row; align-items: center; margin-left: 0.25rem; margin-bottom: 0.5rem; } } } .channel-model-wrapper { display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.5rem; height: max-content; width: 100%; border-radius: var(--radius); border: 1px solid hsl(var(--border)); background: hsl(var(--background)); padding: 1rem; min-height: 5rem; .channel-model-item { display: flex; flex-direction: row; align-items: center; padding: 0.25rem 0.5rem; border: 1px solid hsl(var(--border)); border-radius: var(--radius); transition: .25s; height: max-content; white-space: break-spaces; &:hover { border-color: hsl(var(--border-hover)); } .remove-action { width: 0.75rem; height: 0.75rem; cursor: pointer; margin-left: 0.5rem; color: hsl(var(--text-secondary)); transition: .25s; flex-shrink: 0; &:hover { color: hsl(var(--text-primary)); } } } } .channel-model-action { display: flex; flex-direction: row; width: 100%; flex-wrap: wrap; gap: 0.5rem; @media (max-width: 620px) { & > * { width: 100%; } } } .channel-description { white-space: break-spaces; line-height: 1.25em; } ================================================ FILE: app/src/assets/admin/charge.less ================================================ .charge { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .charge-card { width: 100%; height: 100%; min-height: 20vh; } } .charge-widget { height: max-content; width: 100%; display: flex; flex-direction: column; & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } } .charge-alert { .model-list { display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; .model { padding: 0.5rem 0.75rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); } } } .charge-editor { padding: 1.5rem; border: 1px solid hsl(var(--border)); border-radius: var(--radius); .token { color: hsl(var(--text-secondary)); user-select: none; } } .charge-table { border: 1px solid hsl(var(--border)); border-radius: var(--radius); overflow-x: auto; scrollbar-width: thin; &::-webkit-scrollbar { width: 0.5rem; } .table { scrollbar-width: thin; } .charge-id { color: hsl(var(--text-secondary)); user-select: none; &:before { content: '#'; } } } ================================================ FILE: app/src/assets/admin/dashboard.less ================================================ .dashboard { display: flex; flex-direction: column; width: 100%; height: 100%; padding: 0.5rem 0; & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } } .info-boxes { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1rem; width: 100%; height: max-content; padding: 1rem 2rem 0; @media (max-width: 940px) { grid-template-columns: 1fr; padding: 1rem 1.5rem 0; } .info-box { display: flex; flex-direction: row; flex-grow: 1; height: max-content; padding: 0.75rem 1.5rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); box-shadow: 0.5rem 0.5rem 1rem 0 hsl(var(--shadow)); user-select: none; width: 100%; margin-right: 1.5rem; margin-left: auto; &:last-child { margin-right: auto; } & > * { flex-shrink: 0; } .box-wrapper { flex-grow: 1; .box-title { font-size: 1rem; margin-bottom: 0.5rem; } .box-value { font-size: 1.5rem; font-weight: normal; &.money::after, .box-subvalue { font-size: 1rem; font-weight: normal; margin-left: 0.5rem; content: 'CNY'; } } } .box-icon { width: max-content; height: max-content; transform: translate(0.25rem, 0.25rem); border-radius: 0.25rem; svg { width: 2rem; height: 2rem; stroke-width: 1.25; } } } } .chart-boxes { display: flex; flex-direction: row; flex-wrap: wrap; padding: 0 1.5rem 1rem; width: 100%; @media (max-width: 940px) { flex-direction: column; padding: 0 1rem 1rem; .chart-box { width: calc(100% - 1rem) !important; } } .chart-box { width: calc(50% - 1rem); height: max-content; min-height: 235px; padding: 1rem 2rem; margin: 0.5rem; border-radius: var(--radius); background: hsl(var(--background)); box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow); user-select: none; border: 1px solid hsl(var(--border)); .chart { #model-usage-chart, #user-type-chart { min-height: 8rem !important; max-height: 8rem !important; margin-top: 1.5rem; margin-bottom: 1rem; flex-shrink: 0; } .common-chart { min-height: 10rem !important; max-height: 10rem !important; flex-shrink: 0; font-size: 0.8rem !important; font-family: var(--font-family) !important; } .chart-title { display: flex; flex-direction: row; align-items: center; flex-wrap: nowrap; .chart-title-info { color: hsl(var(--text-secondary)); } svg { position: relative; top: 1px; } & > * { flex-shrink: 0; margin-right: 0.25rem; &:last-child { margin-right: 0; } } } } } } ================================================ FILE: app/src/assets/admin/logger.less ================================================ .logger { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .logger-card { width: 100%; height: 100%; min-height: 20vh; } } .logger-container { .paragraph-header { margin-bottom: 0.5rem; } } .logger-toolbar { display: flex; flex-direction: row; align-items: center; input { max-width: 4.5rem; text-align: center; } button { flex-shrink: 0; } & > * { margin-right: 0.5rem !important; white-space: nowrap; &:last-child { margin-right: 0; } } } .logger-console { position: relative; border-radius: var(--radius); font-size: 14px; width: 100%; height: max-content; overflow: hidden; pre { width: 100%; height: max-content; min-height: 20vh; max-height: 60vh; overflow-x: hidden; overflow-y: auto; touch-action: pan-y; padding: 0.5rem; white-space: pre-wrap !important; } .console-icon { position: absolute; top: 0.75rem; right: 0.75rem; user-select: none; } } .logger-list { display: flex; flex-direction: column; width: 100%; height: max-content; & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } } .logger-item { display: flex; flex-direction: row; padding: 0.75rem 1rem; flex-wrap: wrap; border-radius: var(--radius); border: 1px solid hsl(var(--border)); transition: all 0.2s ease-in-out; align-items: center; &:hover { border-color: hsl(var(--border-hover)); } & > * { margin-right: 1rem; flex-shrink: 0; white-space: nowrap; &:last-child { margin-right: 0; } } .logger-item-title { font-size: 16px; color: hsl(var(--text)); } .logger-item-size { font-size: 14px; color: hsl(var(--text-secondary)); } .logger-item-action { cursor: pointer; } } ================================================ FILE: app/src/assets/admin/management.less ================================================ .user-interface { position: relative; display: flex; flex-direction: column; width: 100%; max-width: 100%; height: 100%; padding: 2rem; & > * { margin-bottom: 2rem; &:last-child { margin-bottom: 0; } } &.mobile { padding: 1rem; } .pagination { display: flex; justify-content: center; align-items: center; margin-top: 1rem; & > * { scale: 0.8; margin: 0 0.5rem; } } .empty { user-select: none; text-align: center; font-size: 14px; margin: 4rem 0 2rem; color: hsl(var(--text-secondary)); } .action { display: flex; flex-direction: row; align-items: center; margin-top: 1rem; } } .user-row, .redeem-row, .invitation-row { display: flex; flex-direction: row; align-items: center; justify-content: center; white-space: nowrap; user-select: none; color: hsl(var(--text)); margin: 1rem 0; } .user-action, .redeem-action, .invitation-action { display: flex; margin-top: 1rem; flex-direction: row; align-items: center; & > * { margin-right: 0.5rem; &:last-child { margin-right: 0; } } } ================================================ FILE: app/src/assets/admin/market.less ================================================ .market { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .market-card { width: 100%; height: 100%; min-height: 20vh; } & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } .model-combobox { width: 320px; margin-left: auto; @media (max-width: 768px) { width: 100% !important; margin-left: 0 !important; max-width: 100vw !important; } } } .market-alert { display: flex; flex-direction: column; border: 1px solid hsl(var(--border)); border-radius: var(--radius); margin-bottom: 1rem; padding: 0.75rem; .market-alert-wrapper { display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.5rem; } } .market-list { display: flex; flex-direction: column; & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } .empty { display: flex; text-align: center; color: hsl(var(--text-secondary)); user-select: none; margin: 1.5rem auto; } .market-item { display: flex; flex-direction: row; padding: 1rem 1rem 1.5rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); user-select: none; width: 100%; height: max-content; align-items: center; background: hsl(var(--card)); transition: 0.25s; transition-property: border, background; &.error { border-color: hsl(var(--error)); } &.stacked { border-color: transparent !important; padding: 0.25rem 0.5rem; margin-bottom: 0.25rem; .market-row { width: max-content; flex-wrap: nowrap; flex-shrink: 0; gap: 0.5rem; } } .market-tags { display: flex; flex-direction: row; gap: 0.5rem; flex-wrap: wrap; width: 100%; .market-tag { white-space: nowrap; padding: 0.25rem 0.75rem !important; } } .market-image-wrapper { display: flex; flex-direction: column; width: 100%; height: max-content; & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } } .market-custom-image { display: flex; flex-direction: row; flex-wrap: wrap; gap: 0.25rem; .market-checkbox { display: flex; flex-direction: row; align-items: center; margin: 0.25rem 0; button { margin-right: 0.25rem; transform: translateY(1px); } } } .market-images { display: flex; flex-direction: row; gap: 0.5rem; flex-wrap: wrap; width: 100%; .market-image { width: 2.5rem; height: 2.5rem; padding: 0.25rem; transition: 0.1s; img { width: 2rem; height: 2rem; opacity: 0.6; border-radius: calc(var(--radius) - 2px); transition: 0.1s; } &.active { img { opacity: 1; } } } } svg { flex-shrink: 0; } .drop-icon { color: hsl(var(--text-secondary)); transition: color 0.25s ease; } &:hover { .drop-icon { color: hsl(var(--text)); } } .model-wrapper { display: flex; flex-direction: column; flex-grow: 1; flex-basis: 0; & > * { margin-bottom: 0.75rem; &:last-child { margin-bottom: 0; } } } .market-row { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; width: 100%; gap: 0.75rem; & > span { display: flex; flex-direction: row; align-items: center; font-size: 0.9rem; white-space: nowrap; min-width: 68px; text-align: center; svg { transform: translateY(1px); } } } } } ================================================ FILE: app/src/assets/admin/menu.less ================================================ .admin-menu { display: flex; flex-shrink: 0; flex-direction: column; height: 100%; padding: 0.75rem; margin: 0; background: hsl(var(--background)); transition: 0.225s ease-in-out; min-height: calc(100% - var(--navbar-height)); transition-property: width, background, box-shadow, opacity; border-right: 0; overflow-x: hidden; width: 4.25rem; border-right: 1px solid hsl(var(--border)); opacity: 1; pointer-events: all; &.open { width: 12rem; .menu-item-title { display: block !important; opacity: 1 !important; } .menu-item-badge { display: block !important; opacity: 1 !important; } } .menu-item { display: flex; flex-direction: row; width: 100%; height: fit-content; padding: 0.5rem; align-items: center; user-select: none; cursor: pointer; transition: 0.2s ease-in-out; border-radius: var(--radius); font-size: 16px; color: hsl(var(--text-secondary)); &:hover { color: hsl(var(--text)); } &.active { color: hsl(var(--text)); background: hsl(var(--card-hover)); } & > * { flex-shrink: 0; } .menu-item-title { font-size: 0.85rem; margin-left: 0.25rem; font-weight: normal; display: none; opacity: 0; } .menu-item-badge { display: none; opacity: 0; } .menu-item-icon { width: fit-content; height: fit-content; padding: 0.25rem; transform: translateY(1px); svg { width: 1.25rem; height: 1.25rem; stroke-width: 1.5; } } } @media (max-width: 668px) { &.open { width: 100%; border-right: 0; } } } .admin-content { flex-grow: 1; height: 100%; overflow-x: hidden; overflow-y: auto; touch-action: pan-y; background: hsla(var(--background-container)); & > .scrollarea-viewport > div { display: flex !important; } } ================================================ FILE: app/src/assets/admin/subscription.less ================================================ .admin-subscription { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .sub-card { width: 100%; height: 100%; min-height: 20vh; } } .plan-config { display: flex; flex-direction: column; margin-top: 0.25rem; & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } .plan-config-row { display: flex; flex-direction: row; align-items: center; } .plan-config-card { display: flex; flex-direction: column; padding: 1rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); .plan-config-title { display: flex; flex-direction: row; align-items: center; white-space: nowrap; user-select: none; margin-bottom: 0.75rem; &:before { display: inline-block; content: ''; margin-right: 0.5rem; height: 1.25rem; width: 2px; border-radius: 1px; background: hsl(var(--text-secondary)); transition: .25s; } } .plan-items-action { display: flex; flex-direction: row; gap: 1rem; align-items: center; flex-wrap: wrap; margin-top: 1rem; } .plan-items-wrapper { display: flex; flex-direction: column; width: 100%; height: max-content; margin-top: 1rem; .plan-item { padding: 1rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); &.stacked { flex-direction: row; .plan-editor-row { margin-bottom: 0; flex-grow: 1; margin-right: 1rem; .plan-editor-label { min-width: 0; margin-right: 0.75rem; flex-shrink: 0; @media (max-width: 768px) { svg { display: none; } } svg { margin: 0 0.25rem; } } } } .plan-editor-row > p { min-width: 4.25rem; } } & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } } .plan-editor-row { display: flex; flex-direction: row; align-items: center; .plan-editor-label { display: flex; flex-direction: row; align-items: center; white-space: nowrap; margin-right: 0.5rem; user-select: none; svg { display: inline-block; flex-shrink: 0; transform: translateY(-2px); } } & > p { white-space: nowrap; } } & > * { margin-bottom: 0.25rem; &:last-child { margin-bottom: 0; } } } } ================================================ FILE: app/src/assets/admin/system.less ================================================ .system { width: 100%; height: max-content; padding: 2rem; display: flex; flex-direction: column; .system-card { width: 100%; height: 100%; min-height: 50vh; } } ================================================ FILE: app/src/assets/common/404.less ================================================ .error-page { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; font-size: 30px; color: hsl(var(--tw-content)); gap: 12px; width: 100%; user-select: none; .icon { width: 58px; height: 58px; transform: translateY(-62px); } h1 { font-size: 48px; transform: translateY(-50px); } p { font-size: 24px; transform: translateY(-32px); } button { transform: translateY(-4px); } } ================================================ FILE: app/src/assets/common/editor.less ================================================ .editor-action { position: absolute; top: 50%; right: 8px; transform: translateY(-50%); background: hsl(var(--input)) !important; padding: 6px; border-radius: 50%; cursor: pointer; transition: 0.1s; outline: 0; opacity: 0; &.active { opacity: 1; } &:hover { background: hsl(var(--border-hover)) !important; } } .editor-dialog { max-width: min(90vw, 920px) !important; } .editor-container { padding: 2px 4px 0; } .editor-wrapper { padding: 4px 0; } .editor-object { position: relative; display: grid; grid-gap: 12px; height: 100%; &.show-editor { grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: 'editor'; } &.show-preview { grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: 'markdown'; } &.show-editor.show-preview { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; grid-template-areas: 'editor markdown'; } } .editor-input { grid-area: editor; scrollbar-width: thin; height: max-content; transition: .1s; min-height: 50vh !important; color: hsl(var(--text)); font-size: 16px !important; padding: 14px 12px !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; } .editor-preview { height: 100%; grid-area: markdown; overflow: auto; padding: 12px; border-radius: 4px; color: hsl(var(--text)); border: 1px solid hsl(var(--border)); scrollbar-width: thin; transition: 0.1s; min-height: 50vh !important; font-size: 16px; text-align: left; } .editor-toolbar { display: flex; width: 100%; margin: 8px 0 4px; & > * { margin-right: 4px; &:last-child { margin-right: 0; } } } ================================================ FILE: app/src/assets/common/file.less ================================================ .file-action { position: absolute; top: 50%; left: 8px; transform: translateY(-50%); background: hsl(var(--input)) !important; padding: 6px; border-radius: 50%; cursor: pointer; transition: 0.1s; outline: 0; &:hover { background: hsl(var(--border-hover)) !important; } } .file-dialog { max-width: min(90vw, 720px) !important; } .file-wrapper { max-width: calc(90vw - 3rem); } .drop-window { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; min-height: 200px; height: 20vh; border: 2px dashed hsl(var(--border)); border-radius: var(--radius); transition: 0.25s; margin-top: 1rem; cursor: pointer; color: hsl(var(--text-secondary)); } .drop-window:hover { border: 2px dashed hsl(var(--border-hover)); color: hsl(var(--text)); } .file-object { position: relative; display: flex; flex-direction: row; gap: 4px; align-items: center; padding: 10px 12px; border: 1px solid hsl(var(--border)); border-radius: var(--radius); transition: 0.25s; color: hsl(var(--text-secondary)) !important; cursor: pointer; &:hover { border: 1px solid hsl(var(--border-hover)); color: hsl(var(--text)) !important; } .close { color: hsl(var(--text-secondary)) !important; transition: .1s; &:hover { color: hsl(var(--text)) !important; } } } .file-list { display: flex; flex-direction: column; width: 100%; height: max-content; user-select: none; margin: 1rem 0; .file-item { width: 100%; height: max-content; display: flex; flex-direction: row; align-items: center; margin-bottom: 4px; color: hsl(var(--foreground)); background: hsl(var(--background)); border-radius: var(--radius); border: 1px solid hsl(var(--border)); padding: 0.5rem; .file-size { color: hsl(var(--text-secondary)); } &:last-child { margin-bottom: 0; } } } .file-name { word-wrap: anywhere; word-break: break-all; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 450px; } ================================================ FILE: app/src/assets/common/loader.less ================================================ .loader-wrapper { position: absolute; display: flex; flex-direction: column; align-items: center; vertical-align: center; text-align: center; top: 50%; left: 50%; gap: 20px; transform: translate(-50%, -50%); margin-top: -28px; p { text-align: center; user-select: none; &:after { content: '.'; color: hsl(var(--text-secondary)); animation: dots 1s steps(5, end) infinite; @keyframes dots { 0%, 20% { color: rgba(0, 0, 0, 0); text-shadow: .25em 0 0 rgba(0, 0, 0, 0), .5em 0 0 rgba(0, 0, 0, 0); } 40% { color: hsl(var(--text-secondary)); text-shadow: .25em 0 0 rgba(0, 0, 0, 0), .5em 0 0 rgba(0, 0, 0, 0); } 60% { text-shadow: .25em 0 0 hsl(var(--text-secondary)), .5em 0 0 rgba(0, 0, 0, 0); } 80%, 100% { text-shadow: .25em 0 0 hsl(var(--text-secondary)), .5em 0 0 hsl(var(--text-secondary)); } } } } .loader { position: relative; top: 0 !important; left: 0 !important; border: 4px solid hsl(var(--text)); border-left-color: transparent; border-radius: 50%; width: 46px; height: 46px; animation: SpinAnimation 1s linear infinite; @keyframes SpinAnimation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } } } ================================================ FILE: app/src/assets/common/plugin.less ================================================ .plugin-dialog { max-width: min(95vw, 1024px) !important; width: 100%; height: auto; max-height: 90vh; } .plugin-wrapper { max-width: calc(95vw - 3rem); max-height: calc(90vh - 8rem); overflow-y: auto; padding: 0.5rem; padding-top: 1rem; display: flex; flex-direction: column; } .plugin-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding: 0 0.5rem; } .plugin-header-title { display: flex; align-items: center; gap: 0.5rem; } .plugin-header-actions { display: flex; gap: 0.5rem; } .plugin-list { width: 100%; flex: 1; & > div { height: 100%; } .space-y-3 { width: 100%; height: auto; padding: 0 0.5rem; } } .plugin-item { display: flex; align-items: center; justify-content: space-between; padding: 1rem; border: 1px solid var(--border); border-radius: 0.5rem; transition: background-color 0.2s; cursor: pointer; min-height: 80px; width: 100%; box-sizing: border-box; &:hover { background-color: hsl(var(--muted-foreground) / 0.05); } } .plugin-item-content { display: flex; align-items: center; gap: 0.75rem; flex: 1; min-width: 0; } .plugin-avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; background: linear-gradient(135deg, #3b82f6, #8b5cf6); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; flex-shrink: 0; } .plugin-info { flex: 1; min-width: 0; } .plugin-name { font-weight: 500; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .plugin-description { font-size: 0.875rem; color: var(--muted-foreground); margin: 0.25rem 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .plugin-url { font-size: 0.75rem; color: var(--muted-foreground); margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .plugin-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; } .plugin-form { padding: 1rem; margin: 0 0.5rem; } .plugin-import-form { padding: 1rem; border: 1px solid var(--border); border-radius: 0.5rem; background-color: transparent; margin: 0 0.5rem; transition: background-color 0.2s, border-color 0.2s; &:hover { background-color: hsl(var(--muted-foreground) / 0.05); border-color: hsl(var(--border-hover)); } } .plugin-import-textarea { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5; background-color: transparent; border: 1px solid var(--border); border-radius: 0.375rem; transition: border-color 0.2s, background-color 0.2s; &:hover { border-color: hsl(var(--border-hover)); background-color: hsl(var(--muted-foreground) / 0.02); } &:focus { border-color: hsl(var(--border-active)); background-color: hsl(var(--background)); box-shadow: 0 0 0 2px hsl(var(--border-active) / 0.2); } &::placeholder { color: var(--muted-foreground); opacity: 0.7; } } .plugin-empty-state { text-align: center; padding: 2rem 0; color: var(--muted-foreground); margin: 0 0.5rem; } .plugin-empty-icon { width: 3rem; height: 3rem; margin: 0 auto 1rem; opacity: 0.5; } .plugin-loading { display: flex; align-items: center; justify-content: center; padding: 2rem 0; margin: 0 0.5rem; } /* 响应式设计 */ @media (max-width: 768px) { .plugin-dialog { max-width: 95vw !important; margin: 1rem; } .plugin-wrapper { max-width: calc(95vw - 2rem); padding: 0.25rem; padding-top: 1rem; } .plugin-header { flex-direction: column; gap: 1rem; align-items: stretch; text-align: center; padding: 0; } .plugin-header-title { justify-content: center; } .plugin-header-actions { justify-content: center; width: 100%; } .plugin-list { height: 60vh !important; } .plugin-item { flex-direction: column; gap: 1rem; padding: 0.75rem; align-items: stretch; } .plugin-item-content { flex-direction: column; align-items: flex-start; text-align: left; } .plugin-actions { justify-content: center; width: 100%; } .plugin-avatar { align-self: center; } .plugin-info { width: 100%; text-align: center; } .plugin-name, .plugin-description, .plugin-url { white-space: normal; word-break: break-all; } .plugin-form { padding: 0.75rem; margin: 0; } .plugin-import-form { padding: 0.75rem; margin: 0; } .plugin-import-textarea { font-size: 0.75rem; } .plugin-empty-state, .plugin-loading { margin: 0; } .plugin-form-row { flex-direction: column; gap: 0.75rem; align-items: stretch; } .plugin-form-column { width: 100%; } .plugin-avatar-picker { align-self: center; } .plugin-test-scroll-area { max-height: calc(60vh - 100px); } } @media (max-width: 480px) { .plugin-dialog { max-width: 98vw !important; margin: 0.5rem; } .plugin-wrapper { max-width: calc(98vw - 1rem); padding-top: 0.75rem; } .plugin-header { margin-bottom: 0.75rem; } .plugin-item { padding: 0.5rem; } .plugin-actions { flex-wrap: wrap; gap: 0.25rem; } .plugin-form { padding: 0.5rem; } .plugin-import-form { padding: 0.5rem; } .plugin-import-textarea { font-size: 0.75rem; } } /* Plugin Editor styles */ .plugin-drawer-viewport { width: 100%; max-width: 100%; } .plugin-form-row { display: flex; flex-direction: row; gap: 1rem; align-items: flex-end; } .plugin-form-column { display: flex; flex-direction: column; gap: 0.5rem; flex-shrink: 0; } .plugin-form-column-grow { flex: 1; min-width: 0; } .plugin-avatar-picker { width: 2.5rem !important; height: 2.5rem !important; border-radius: 0.5rem; border: 1px solid hsl(var(--border)); transition: 0.2s ease-in-out; &:hover { border-color: hsl(var(--border-hover)); background-color: hsl(var(--background-hover)); } &:focus { border-color: hsl(var(--border-active)); box-shadow: 0 0 0 2px hsl(var(--border-active) / 0.2); } } .plugin-test-scroll-area { max-height: calc(70vh - 120px); .scrollarea-viewport { padding-right: 0.5rem; } &[data-radix-scroll-area-viewport] { max-height: 100%; } } .plugin-picker-dialog { width: max-content !important; height: max-content !important; padding: 0 !important; .picker { --epr-category-navigation-button-size: 28px; --epr-search-input-bg-color: hsl(var(--background)); --epr-search-border-color: hsl(var(--border-hover)); --epr-bg-color: hsl(var(--background)); --epr-category-label-bg-color: hsl(var(--background)); img { padding: 0.5rem; } .epr-icn-search { width: 1rem; height: 1rem; transform: translateY(-0.55rem); } .epr-body { scrollbar-width: thin; -ms-overflow-style: none; &::-webkit-scrollbar { width: 6px; } } .epr-search-container { input { border: 1px solid hsl(var(--border)); } } .epr-cat-btn { &:focus:before { border: none; } } * { font-family: var(--font-family) !important; font-size: 0.85rem !important; } } } /* MCP Tool Call styles */ .mcp-container { margin: 0.5rem 0; padding: 0.5rem; border-radius: 0.5rem; background-color: rgba(var(--primary) / 0.05); border: 1px solid rgba(var(--primary) / 0.1); } .mcp-tool-call { background-color: rgba(var(--background) / 0.5); border-radius: 0.5rem; padding: 1rem; margin-top: 0.5rem; } .tool-arguments { background-color: rgba(var(--muted) / 0.3); border-radius: 0.375rem; padding: 0.75rem; margin-bottom: 0.75rem; border: 1px solid rgba(var(--border) / 0.5); } .tool-result { border-radius: 0.375rem; overflow: hidden; } .tool-result .bg-blue-500\/10 { background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); } .tool-result .bg-green-500\/10 { background-color: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2); } .tool-result .bg-red-500\/10 { background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); } .tool-result pre { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5; max-height: 400px; overflow-y: auto; } /* MCP Tool Call Button styles */ .mcp-container button { transition: all 0.2s ease-in-out; } .mcp-container button:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } /* Status indicators */ .text-blue-500 { color: rgb(59, 130, 246); } .text-green-500 { color: rgb(34, 197, 94); } .text-red-500 { color: rgb(239, 68, 68); } .text-blue-600 { color: rgb(37, 99, 235); } .text-green-700 { color: rgb(21, 128, 61); } .text-red-600 { color: rgb(220, 38, 38); } /* Animation for spinner */ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .animate-spin { animation: spin 1s linear infinite; } /* Responsive styles for MCP components */ @media (max-width: 768px) { .mcp-container { margin: 0.25rem 0; padding: 0.375rem; } .mcp-tool-call { padding: 0.75rem; } .tool-arguments, .tool-result { padding: 0.5rem; } .tool-result pre { font-size: 0.75rem; max-height: 300px; } } /* MCP Debug Tabs styles */ .mcp-debug-tabs { grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 2px; background-color: hsl(var(--muted-foreground) / 0.05); padding: 2px; border-radius: 0.5rem; border: 1px solid var(--border); } .mcp-debug-tabs [data-state="active"] { background-color: var(--background); border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: background-color 0.2s; } .mcp-debug-tabs [data-state="inactive"] { background-color: transparent; color: var(--muted-foreground); transition: background-color 0.2s; } .mcp-debug-tabs [data-state="inactive"]:hover { background-color: hsl(var(--muted-foreground) / 0.05); color: var(--foreground); } /* Debug panel tabs content */ .debug-panel [data-orientation="horizontal"][role="tablist"] { margin-bottom: 0.75rem; } .debug-panel [role="tabpanel"] { min-height: 60px; max-height: 400px; } /* MCP Debug ScrollArea styles */ .mcp-debug-scroll-area { height: 300px; max-height: 300px; width: 100%; border: 1px solid var(--border); border-radius: 0.5rem; background-color: hsl(var(--muted-foreground) / 0.02); } .mcp-debug-scroll-area [data-radix-scroll-area-viewport] { height: 100%; width: 100%; border-radius: inherit; } .mcp-debug-scroll-area pre { border: none; background-color: transparent; margin: 0; min-height: 100%; width: 100%; } /* MCP Debug Content styles (for non-scrollable content) */ .mcp-debug-content { width: 100%; border: 1px solid var(--border); border-radius: 0.5rem; background-color: hsl(var(--muted-foreground) / 0.02); } .mcp-debug-content pre { border: none; background-color: transparent; margin: 0; } /* ScrollArea scrollbar styling to match plugin-item theme */ .mcp-debug-scroll-area [data-radix-scroll-area-scrollbar] { background-color: hsl(var(--muted-foreground) / 0.05); border-radius: 0.375rem; transition: background-color 0.2s; width: 8px; height: 8px; padding: 1px; } .mcp-debug-scroll-area [data-radix-scroll-area-scrollbar]:hover { background-color: hsl(var(--muted-foreground) / 0.08); } .mcp-debug-scroll-area [data-radix-scroll-area-scrollbar][data-orientation="vertical"] { width: 8px; border-left: 1px solid transparent; border-right: 1px solid transparent; } .mcp-debug-scroll-area [data-radix-scroll-area-scrollbar][data-orientation="horizontal"] { height: 8px; border-top: 1px solid transparent; border-bottom: 1px solid transparent; } .mcp-debug-scroll-area [data-radix-scroll-area-thumb] { background-color: hsl(var(--muted-foreground) / 0.3); border-radius: 0.25rem; transition: background-color 0.2s; position: relative; } .mcp-debug-scroll-area [data-radix-scroll-area-thumb]:hover { background-color: hsl(var(--muted-foreground) / 0.5); } .mcp-debug-scroll-area [data-radix-scroll-area-corner] { background-color: hsl(var(--muted-foreground) / 0.05); } /* Responsive styles for debug tabs */ @media (max-width: 768px) { .mcp-debug-tabs { grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); gap: 1px; padding: 1px; } .debug-panel [role="tabpanel"] { min-height: 40px; max-height: 250px; } .mcp-debug-scroll-area { height: 180px; max-height: 180px; } .mcp-debug-scroll-area pre, .mcp-debug-content pre { font-size: 0.75rem; } } @media (max-width: 480px) { .mcp-debug-tabs { grid-template-columns: repeat(auto-fit, minmax(50px, 1fr)); } .debug-panel [role="tabpanel"] { min-height: 30px; max-height: 180px; } .mcp-debug-scroll-area { height: 120px; max-height: 120px; } .mcp-debug-scroll-area pre, .mcp-debug-content pre { padding: 0.5rem; } } ================================================ FILE: app/src/assets/fonts/all.less ================================================ @import "common"; @import "katex"; ================================================ FILE: app/src/assets/fonts/common.less ================================================ @import '@fontsource-variable/inter'; @import '@fontsource-variable/jetbrains-mono'; ================================================ FILE: app/src/assets/fonts/katex.less ================================================ /* stylelint-disable font-family-no-missing-generic-family-keyword */ @font-face { font-family: 'KaTeX_AMS'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Caligraphic'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.ttf) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_Caligraphic'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Fraktur'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.ttf) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_Fraktur'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Main'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.ttf) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_Main'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.ttf) format('truetype'); font-weight: bold; font-style: italic; } @font-face { font-family: 'KaTeX_Main'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.ttf) format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'KaTeX_Main'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Math'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.ttf) format('truetype'); font-weight: bold; font-style: italic; } @font-face { font-family: 'KaTeX_Math'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.ttf) format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'KaTeX_SansSerif'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.ttf) format('truetype'); font-weight: bold; font-style: normal; } @font-face { font-family: 'KaTeX_SansSerif'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.ttf) format('truetype'); font-weight: normal; font-style: italic; } @font-face { font-family: 'KaTeX_SansSerif'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Script'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size1'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size2'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size3'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Size4'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'KaTeX_Typewriter'; src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.ttf) format('truetype'); font-weight: normal; font-style: normal; } .katex { font: normal 1.21em KaTeX_Main, Times New Roman, serif; line-height: 1.2; text-indent: 0; text-rendering: auto; } .katex * { -ms-high-contrast-adjust: none !important; border-color: currentColor; } .katex .katex-version::after { content: "0.16.0"; } .katex .katex-mathml { /* Accessibility hack to only show to screen readers Found at: http://a11yproject.com/posts/how-to-hide-content/ */ position: absolute; clip: rect(1px, 1px, 1px, 1px); padding: 0; border: 0; height: 1px; width: 1px; overflow: hidden; } .katex .katex-html { /* \newline is an empty block at top level, between .base elements */ white-space: pre-wrap; } .katex .katex-html > .newline { display: block; } .katex .base { position: relative; display: inline-block; } .katex .strut { display: inline-block; } .katex .textbf { font-weight: bold; } .katex .textit { font-style: italic; } .katex .textrm { font-family: KaTeX_Main; } .katex .textsf { font-family: KaTeX_SansSerif; } .katex .texttt { font-family: KaTeX_Typewriter; } .katex .mathnormal { font-family: KaTeX_Math; font-style: italic; } .katex .mathit { font-family: KaTeX_Main; font-style: italic; } .katex .mathrm { font-style: normal; } .katex .mathbf { font-family: KaTeX_Main; font-weight: bold; } .katex .boldsymbol { font-family: KaTeX_Math; font-weight: bold; font-style: italic; } .katex .amsrm { font-family: KaTeX_AMS; } .katex .mathbb, .katex .textbb { font-family: KaTeX_AMS; } .katex .mathcal { font-family: KaTeX_Caligraphic; } .katex .mathfrak, .katex .textfrak { font-family: KaTeX_Fraktur; } .katex .mathtt { font-family: KaTeX_Typewriter; } .katex .mathscr, .katex .textscr { font-family: KaTeX_Script; } .katex .mathsf, .katex .textsf { font-family: KaTeX_SansSerif; } .katex .mathboldsf, .katex .textboldsf { font-family: KaTeX_SansSerif; font-weight: bold; } .katex .mathitsf, .katex .textitsf { font-family: KaTeX_SansSerif; font-style: italic; } .katex .mainrm { font-family: KaTeX_Main; font-style: normal; } .katex .vlist-t { display: inline-table; table-layout: fixed; border-collapse: collapse; } .katex .vlist-r { display: table-row; } .katex .vlist { display: table-cell; vertical-align: bottom; position: relative; } .katex .vlist > span { display: block; height: 0; position: relative; } .katex .vlist > span > span { display: inline-block; } .katex .vlist > span > .pstrut { overflow: hidden; width: 0; } .katex .vlist-t2 { margin-right: -2px; } .katex .vlist-s { display: table-cell; vertical-align: bottom; font-size: 1px; width: 2px; min-width: 2px; } .katex .vbox { display: inline-flex; flex-direction: column; align-items: baseline; } .katex .hbox { display: inline-flex; flex-direction: row; width: 100%; } .katex .thinbox { display: inline-flex; flex-direction: row; width: 0; max-width: 0; } .katex .msupsub { text-align: left; } .katex .mfrac > span > span { text-align: center; } .katex .mfrac .frac-line { display: inline-block; width: 100%; border-bottom-style: solid; } .katex .mfrac .frac-line, .katex .overline .overline-line, .katex .underline .underline-line, .katex .hline, .katex .hdashline, .katex .rule { min-height: 1px; } .katex .mspace { display: inline-block; } .katex .llap, .katex .rlap, .katex .clap { width: 0; position: relative; } .katex .llap > .inner, .katex .rlap > .inner, .katex .clap > .inner { position: absolute; } .katex .llap > .fix, .katex .rlap > .fix, .katex .clap > .fix { display: inline-block; } .katex .llap > .inner { right: 0; } .katex .rlap > .inner, .katex .clap > .inner { left: 0; } .katex .clap > .inner > span { margin-left: -50%; margin-right: 50%; } .katex .rule { display: inline-block; border: solid 0; position: relative; } .katex .overline .overline-line, .katex .underline .underline-line, .katex .hline { display: inline-block; width: 100%; border-bottom-style: solid; } .katex .hdashline { display: inline-block; width: 100%; border-bottom-style: dashed; } .katex .sqrt > .root { /* These values are taken from the definition of `\r@@t`, `\mkern 5mu` and `\mkern -10mu`. */ margin-left: 0.27777778em; margin-right: -0.55555556em; } .katex .sizing.reset-size1.size1, .katex .fontsize-ensurer.reset-size1.size1 { font-size: 1em; } .katex .sizing.reset-size1.size2, .katex .fontsize-ensurer.reset-size1.size2 { font-size: 1.2em; } .katex .sizing.reset-size1.size3, .katex .fontsize-ensurer.reset-size1.size3 { font-size: 1.4em; } .katex .sizing.reset-size1.size4, .katex .fontsize-ensurer.reset-size1.size4 { font-size: 1.6em; } .katex .sizing.reset-size1.size5, .katex .fontsize-ensurer.reset-size1.size5 { font-size: 1.8em; } .katex .sizing.reset-size1.size6, .katex .fontsize-ensurer.reset-size1.size6 { font-size: 2em; } .katex .sizing.reset-size1.size7, .katex .fontsize-ensurer.reset-size1.size7 { font-size: 2.4em; } .katex .sizing.reset-size1.size8, .katex .fontsize-ensurer.reset-size1.size8 { font-size: 2.88em; } .katex .sizing.reset-size1.size9, .katex .fontsize-ensurer.reset-size1.size9 { font-size: 3.456em; } .katex .sizing.reset-size1.size10, .katex .fontsize-ensurer.reset-size1.size10 { font-size: 4.148em; } .katex .sizing.reset-size1.size11, .katex .fontsize-ensurer.reset-size1.size11 { font-size: 4.976em; } .katex .sizing.reset-size2.size1, .katex .fontsize-ensurer.reset-size2.size1 { font-size: 0.83333333em; } .katex .sizing.reset-size2.size2, .katex .fontsize-ensurer.reset-size2.size2 { font-size: 1em; } .katex .sizing.reset-size2.size3, .katex .fontsize-ensurer.reset-size2.size3 { font-size: 1.16666667em; } .katex .sizing.reset-size2.size4, .katex .fontsize-ensurer.reset-size2.size4 { font-size: 1.33333333em; } .katex .sizing.reset-size2.size5, .katex .fontsize-ensurer.reset-size2.size5 { font-size: 1.5em; } .katex .sizing.reset-size2.size6, .katex .fontsize-ensurer.reset-size2.size6 { font-size: 1.66666667em; } .katex .sizing.reset-size2.size7, .katex .fontsize-ensurer.reset-size2.size7 { font-size: 2em; } .katex .sizing.reset-size2.size8, .katex .fontsize-ensurer.reset-size2.size8 { font-size: 2.4em; } .katex .sizing.reset-size2.size9, .katex .fontsize-ensurer.reset-size2.size9 { font-size: 2.88em; } .katex .sizing.reset-size2.size10, .katex .fontsize-ensurer.reset-size2.size10 { font-size: 3.45666667em; } .katex .sizing.reset-size2.size11, .katex .fontsize-ensurer.reset-size2.size11 { font-size: 4.14666667em; } .katex .sizing.reset-size3.size1, .katex .fontsize-ensurer.reset-size3.size1 { font-size: 0.71428571em; } .katex .sizing.reset-size3.size2, .katex .fontsize-ensurer.reset-size3.size2 { font-size: 0.85714286em; } .katex .sizing.reset-size3.size3, .katex .fontsize-ensurer.reset-size3.size3 { font-size: 1em; } .katex .sizing.reset-size3.size4, .katex .fontsize-ensurer.reset-size3.size4 { font-size: 1.14285714em; } .katex .sizing.reset-size3.size5, .katex .fontsize-ensurer.reset-size3.size5 { font-size: 1.28571429em; } .katex .sizing.reset-size3.size6, .katex .fontsize-ensurer.reset-size3.size6 { font-size: 1.42857143em; } .katex .sizing.reset-size3.size7, .katex .fontsize-ensurer.reset-size3.size7 { font-size: 1.71428571em; } .katex .sizing.reset-size3.size8, .katex .fontsize-ensurer.reset-size3.size8 { font-size: 2.05714286em; } .katex .sizing.reset-size3.size9, .katex .fontsize-ensurer.reset-size3.size9 { font-size: 2.46857143em; } .katex .sizing.reset-size3.size10, .katex .fontsize-ensurer.reset-size3.size10 { font-size: 2.96285714em; } .katex .sizing.reset-size3.size11, .katex .fontsize-ensurer.reset-size3.size11 { font-size: 3.55428571em; } .katex .sizing.reset-size4.size1, .katex .fontsize-ensurer.reset-size4.size1 { font-size: 0.625em; } .katex .sizing.reset-size4.size2, .katex .fontsize-ensurer.reset-size4.size2 { font-size: 0.75em; } .katex .sizing.reset-size4.size3, .katex .fontsize-ensurer.reset-size4.size3 { font-size: 0.875em; } .katex .sizing.reset-size4.size4, .katex .fontsize-ensurer.reset-size4.size4 { font-size: 1em; } .katex .sizing.reset-size4.size5, .katex .fontsize-ensurer.reset-size4.size5 { font-size: 1.125em; } .katex .sizing.reset-size4.size6, .katex .fontsize-ensurer.reset-size4.size6 { font-size: 1.25em; } .katex .sizing.reset-size4.size7, .katex .fontsize-ensurer.reset-size4.size7 { font-size: 1.5em; } .katex .sizing.reset-size4.size8, .katex .fontsize-ensurer.reset-size4.size8 { font-size: 1.8em; } .katex .sizing.reset-size4.size9, .katex .fontsize-ensurer.reset-size4.size9 { font-size: 2.16em; } .katex .sizing.reset-size4.size10, .katex .fontsize-ensurer.reset-size4.size10 { font-size: 2.5925em; } .katex .sizing.reset-size4.size11, .katex .fontsize-ensurer.reset-size4.size11 { font-size: 3.11em; } .katex .sizing.reset-size5.size1, .katex .fontsize-ensurer.reset-size5.size1 { font-size: 0.55555556em; } .katex .sizing.reset-size5.size2, .katex .fontsize-ensurer.reset-size5.size2 { font-size: 0.66666667em; } .katex .sizing.reset-size5.size3, .katex .fontsize-ensurer.reset-size5.size3 { font-size: 0.77777778em; } .katex .sizing.reset-size5.size4, .katex .fontsize-ensurer.reset-size5.size4 { font-size: 0.88888889em; } .katex .sizing.reset-size5.size5, .katex .fontsize-ensurer.reset-size5.size5 { font-size: 1em; } .katex .sizing.reset-size5.size6, .katex .fontsize-ensurer.reset-size5.size6 { font-size: 1.11111111em; } .katex .sizing.reset-size5.size7, .katex .fontsize-ensurer.reset-size5.size7 { font-size: 1.33333333em; } .katex .sizing.reset-size5.size8, .katex .fontsize-ensurer.reset-size5.size8 { font-size: 1.6em; } .katex .sizing.reset-size5.size9, .katex .fontsize-ensurer.reset-size5.size9 { font-size: 1.92em; } .katex .sizing.reset-size5.size10, .katex .fontsize-ensurer.reset-size5.size10 { font-size: 2.30444444em; } .katex .sizing.reset-size5.size11, .katex .fontsize-ensurer.reset-size5.size11 { font-size: 2.76444444em; } .katex .sizing.reset-size6.size1, .katex .fontsize-ensurer.reset-size6.size1 { font-size: 0.5em; } .katex .sizing.reset-size6.size2, .katex .fontsize-ensurer.reset-size6.size2 { font-size: 0.6em; } .katex .sizing.reset-size6.size3, .katex .fontsize-ensurer.reset-size6.size3 { font-size: 0.7em; } .katex .sizing.reset-size6.size4, .katex .fontsize-ensurer.reset-size6.size4 { font-size: 0.8em; } .katex .sizing.reset-size6.size5, .katex .fontsize-ensurer.reset-size6.size5 { font-size: 0.9em; } .katex .sizing.reset-size6.size6, .katex .fontsize-ensurer.reset-size6.size6 { font-size: 1em; } .katex .sizing.reset-size6.size7, .katex .fontsize-ensurer.reset-size6.size7 { font-size: 1.2em; } .katex .sizing.reset-size6.size8, .katex .fontsize-ensurer.reset-size6.size8 { font-size: 1.44em; } .katex .sizing.reset-size6.size9, .katex .fontsize-ensurer.reset-size6.size9 { font-size: 1.728em; } .katex .sizing.reset-size6.size10, .katex .fontsize-ensurer.reset-size6.size10 { font-size: 2.074em; } .katex .sizing.reset-size6.size11, .katex .fontsize-ensurer.reset-size6.size11 { font-size: 2.488em; } .katex .sizing.reset-size7.size1, .katex .fontsize-ensurer.reset-size7.size1 { font-size: 0.41666667em; } .katex .sizing.reset-size7.size2, .katex .fontsize-ensurer.reset-size7.size2 { font-size: 0.5em; } .katex .sizing.reset-size7.size3, .katex .fontsize-ensurer.reset-size7.size3 { font-size: 0.58333333em; } .katex .sizing.reset-size7.size4, .katex .fontsize-ensurer.reset-size7.size4 { font-size: 0.66666667em; } .katex .sizing.reset-size7.size5, .katex .fontsize-ensurer.reset-size7.size5 { font-size: 0.75em; } .katex .sizing.reset-size7.size6, .katex .fontsize-ensurer.reset-size7.size6 { font-size: 0.83333333em; } .katex .sizing.reset-size7.size7, .katex .fontsize-ensurer.reset-size7.size7 { font-size: 1em; } .katex .sizing.reset-size7.size8, .katex .fontsize-ensurer.reset-size7.size8 { font-size: 1.2em; } .katex .sizing.reset-size7.size9, .katex .fontsize-ensurer.reset-size7.size9 { font-size: 1.44em; } .katex .sizing.reset-size7.size10, .katex .fontsize-ensurer.reset-size7.size10 { font-size: 1.72833333em; } .katex .sizing.reset-size7.size11, .katex .fontsize-ensurer.reset-size7.size11 { font-size: 2.07333333em; } .katex .sizing.reset-size8.size1, .katex .fontsize-ensurer.reset-size8.size1 { font-size: 0.34722222em; } .katex .sizing.reset-size8.size2, .katex .fontsize-ensurer.reset-size8.size2 { font-size: 0.41666667em; } .katex .sizing.reset-size8.size3, .katex .fontsize-ensurer.reset-size8.size3 { font-size: 0.48611111em; } .katex .sizing.reset-size8.size4, .katex .fontsize-ensurer.reset-size8.size4 { font-size: 0.55555556em; } .katex .sizing.reset-size8.size5, .katex .fontsize-ensurer.reset-size8.size5 { font-size: 0.625em; } .katex .sizing.reset-size8.size6, .katex .fontsize-ensurer.reset-size8.size6 { font-size: 0.69444444em; } .katex .sizing.reset-size8.size7, .katex .fontsize-ensurer.reset-size8.size7 { font-size: 0.83333333em; } .katex .sizing.reset-size8.size8, .katex .fontsize-ensurer.reset-size8.size8 { font-size: 1em; } .katex .sizing.reset-size8.size9, .katex .fontsize-ensurer.reset-size8.size9 { font-size: 1.2em; } .katex .sizing.reset-size8.size10, .katex .fontsize-ensurer.reset-size8.size10 { font-size: 1.44027778em; } .katex .sizing.reset-size8.size11, .katex .fontsize-ensurer.reset-size8.size11 { font-size: 1.72777778em; } .katex .sizing.reset-size9.size1, .katex .fontsize-ensurer.reset-size9.size1 { font-size: 0.28935185em; } .katex .sizing.reset-size9.size2, .katex .fontsize-ensurer.reset-size9.size2 { font-size: 0.34722222em; } .katex .sizing.reset-size9.size3, .katex .fontsize-ensurer.reset-size9.size3 { font-size: 0.40509259em; } .katex .sizing.reset-size9.size4, .katex .fontsize-ensurer.reset-size9.size4 { font-size: 0.46296296em; } .katex .sizing.reset-size9.size5, .katex .fontsize-ensurer.reset-size9.size5 { font-size: 0.52083333em; } .katex .sizing.reset-size9.size6, .katex .fontsize-ensurer.reset-size9.size6 { font-size: 0.5787037em; } .katex .sizing.reset-size9.size7, .katex .fontsize-ensurer.reset-size9.size7 { font-size: 0.69444444em; } .katex .sizing.reset-size9.size8, .katex .fontsize-ensurer.reset-size9.size8 { font-size: 0.83333333em; } .katex .sizing.reset-size9.size9, .katex .fontsize-ensurer.reset-size9.size9 { font-size: 1em; } .katex .sizing.reset-size9.size10, .katex .fontsize-ensurer.reset-size9.size10 { font-size: 1.20023148em; } .katex .sizing.reset-size9.size11, .katex .fontsize-ensurer.reset-size9.size11 { font-size: 1.43981481em; } .katex .sizing.reset-size10.size1, .katex .fontsize-ensurer.reset-size10.size1 { font-size: 0.24108004em; } .katex .sizing.reset-size10.size2, .katex .fontsize-ensurer.reset-size10.size2 { font-size: 0.28929605em; } .katex .sizing.reset-size10.size3, .katex .fontsize-ensurer.reset-size10.size3 { font-size: 0.33751205em; } .katex .sizing.reset-size10.size4, .katex .fontsize-ensurer.reset-size10.size4 { font-size: 0.38572806em; } .katex .sizing.reset-size10.size5, .katex .fontsize-ensurer.reset-size10.size5 { font-size: 0.43394407em; } .katex .sizing.reset-size10.size6, .katex .fontsize-ensurer.reset-size10.size6 { font-size: 0.48216008em; } .katex .sizing.reset-size10.size7, .katex .fontsize-ensurer.reset-size10.size7 { font-size: 0.57859209em; } .katex .sizing.reset-size10.size8, .katex .fontsize-ensurer.reset-size10.size8 { font-size: 0.69431051em; } .katex .sizing.reset-size10.size9, .katex .fontsize-ensurer.reset-size10.size9 { font-size: 0.83317261em; } .katex .sizing.reset-size10.size10, .katex .fontsize-ensurer.reset-size10.size10 { font-size: 1em; } .katex .sizing.reset-size10.size11, .katex .fontsize-ensurer.reset-size10.size11 { font-size: 1.19961427em; } .katex .sizing.reset-size11.size1, .katex .fontsize-ensurer.reset-size11.size1 { font-size: 0.20096463em; } .katex .sizing.reset-size11.size2, .katex .fontsize-ensurer.reset-size11.size2 { font-size: 0.24115756em; } .katex .sizing.reset-size11.size3, .katex .fontsize-ensurer.reset-size11.size3 { font-size: 0.28135048em; } .katex .sizing.reset-size11.size4, .katex .fontsize-ensurer.reset-size11.size4 { font-size: 0.32154341em; } .katex .sizing.reset-size11.size5, .katex .fontsize-ensurer.reset-size11.size5 { font-size: 0.36173633em; } .katex .sizing.reset-size11.size6, .katex .fontsize-ensurer.reset-size11.size6 { font-size: 0.40192926em; } .katex .sizing.reset-size11.size7, .katex .fontsize-ensurer.reset-size11.size7 { font-size: 0.48231511em; } .katex .sizing.reset-size11.size8, .katex .fontsize-ensurer.reset-size11.size8 { font-size: 0.57877814em; } .katex .sizing.reset-size11.size9, .katex .fontsize-ensurer.reset-size11.size9 { font-size: 0.69453376em; } .katex .sizing.reset-size11.size10, .katex .fontsize-ensurer.reset-size11.size10 { font-size: 0.83360129em; } .katex .sizing.reset-size11.size11, .katex .fontsize-ensurer.reset-size11.size11 { font-size: 1em; } .katex .delimsizing.size1 { font-family: KaTeX_Size1; } .katex .delimsizing.size2 { font-family: KaTeX_Size2; } .katex .delimsizing.size3 { font-family: KaTeX_Size3; } .katex .delimsizing.size4 { font-family: KaTeX_Size4; } .katex .delimsizing.mult .delim-size1 > span { font-family: KaTeX_Size1; } .katex .delimsizing.mult .delim-size4 > span { font-family: KaTeX_Size4; } .katex .nulldelimiter { display: inline-block; width: 0.12em; } .katex .delimcenter { position: relative; } .katex .op-symbol { position: relative; } .katex .op-symbol.small-op { font-family: KaTeX_Size1; } .katex .op-symbol.large-op { font-family: KaTeX_Size2; } .katex .op-limits > .vlist-t { text-align: center; } .katex .accent > .vlist-t { text-align: center; } .katex .accent .accent-body { position: relative; } .katex .accent .accent-body:not(.accent-full) { width: 0; } .katex .overlay { display: block; } .katex .mtable .vertical-separator { display: inline-block; min-width: 1px; } .katex .mtable .arraycolsep { display: inline-block; } .katex .mtable .col-align-c > .vlist-t { text-align: center; } .katex .mtable .col-align-l > .vlist-t { text-align: left; } .katex .mtable .col-align-r > .vlist-t { text-align: right; } .katex .svg-align { text-align: left; } .katex svg { display: block; position: absolute; width: 100%; height: inherit; fill: currentColor; stroke: currentColor; fill-rule: nonzero; fill-opacity: 1; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0; stroke-opacity: 1; } .katex svg path { stroke: none; } .katex img { border-style: none; min-width: 0; min-height: 0; max-width: none; max-height: none; } .katex .stretchy { width: 100%; display: block; position: relative; overflow: hidden; } .katex .stretchy::before, .katex .stretchy::after { content: ""; } .katex .hide-tail { width: 100%; position: relative; overflow: hidden; } .katex .halfarrow-left { position: absolute; left: 0; width: 50.2%; overflow: hidden; } .katex .halfarrow-right { position: absolute; right: 0; width: 50.2%; overflow: hidden; } .katex .brace-left { position: absolute; left: 0; width: 25.1%; overflow: hidden; } .katex .brace-center { position: absolute; left: 25%; width: 50%; overflow: hidden; } .katex .brace-right { position: absolute; right: 0; width: 25.1%; overflow: hidden; } .katex .x-arrow-pad { padding: 0 0.5em; } .katex .cd-arrow-pad { padding: 0 0.55556em 0 0.27778em; } .katex .x-arrow, .katex .mover, .katex .munder { text-align: center; } .katex .boxpad { padding: 0 0.3em; } .katex .fbox, .katex .fcolorbox { box-sizing: border-box; border: 0.04em solid; } .katex .cancel-pad { padding: 0 0.2em; } .katex .cancel-lap { margin-left: -0.2em; margin-right: -0.2em; } .katex .sout { border-bottom-style: solid; border-bottom-width: 0.08em; } .katex .angl { box-sizing: border-box; border-top: 0.049em solid; border-right: 0.049em solid; margin-right: 0.03889em; } .katex .anglpad { padding: 0 0.03889em; } .katex .eqn-num::before { counter-increment: katexEqnNo; content: "(" counter(katexEqnNo) ")"; } .katex .mml-eqn-num::before { counter-increment: mmlEqnNo; content: "(" counter(mmlEqnNo) ")"; } .katex .mtr-glue { width: 50%; } .katex .cd-vert-arrow { display: inline-block; position: relative; } .katex .cd-label-left { display: inline-block; position: absolute; right: calc(50% + 0.3em); text-align: left; } .katex .cd-label-right { display: inline-block; position: absolute; left: calc(50% + 0.3em); text-align: right; } .katex-display { display: block; margin: 1em 0; text-align: center; } .katex-display > .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > .katex-html { display: block; position: relative; } .katex-display > .katex > .katex-html > .tag { position: absolute; right: 0; } .katex-display.leqno > .katex > .katex-html > .tag { left: 0; right: auto; } .katex-display.fleqn > .katex { text-align: left; padding-left: 2em; } body { counter-reset: katexEqnNo mmlEqnNo; } ================================================ FILE: app/src/assets/globals.less ================================================ @tailwind base; @tailwind components; @tailwind utilities; @import 'normalize.css'; @layer base { :root { --background: 0 0% 100%; --background-light: 0 0% 100%; --background-dark: 0 0% 0%; --background-hover: 5 0 98%; --background-active: 5 0 96%; --background-container: 0, 0%, 97%, 0.9; --background-container-hover: 0, 0%, 97%, 0.95; --foreground: 240 10% 3.9%; --card: 0 0% 100%; --card-hover: 0 0% 97%; --card-active: 0 0% 94.5%; --card-foreground: 240 10% 3.9%; --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; --primary: 240 5.9% 10%; --primary-foreground: 0 0% 98%; --secondary: 240 5.9% 97.5%; --secondary-foreground: 240 5.9% 10%; --muted: 240 4.8% 95.9%; --muted-foreground: 240 3.8% 46.1%; --accent: 240 4.8% 95.9%; --accent-secondary: 240 4.8% 95.9%; --accent-foreground: 240 5.9% 10%; --destructive: 0 67.22% 50.59%; --destructive-foreground: 0 0% 98%; --success: 120 100% 50%; --success-foreground: 0 0% 98%; --failure: 0 67.22% 50.59%; --failure-foreground: 0 0% 98%; --border: 240 5.9% 90%; --border-hover: 240 5.9% 85%; --border-active: 240 5.9% 80%; --input: 240 5.9% 90%; --input-unread: 240 5.9% 50%; --ring: 240 5% 64.9%; --text: 0 0% 0%; --text-light: 0 0% 100%; --text-dark: 0 0% 100%; --text-secondary: 0 0% 35%; --text-unread: 0 0% 50%; --text-secondary-dark: 0 0% 80%; --selection: 212 100% 41%; --selection-foreground: 0 0% 98%; --radius: 0.5rem; --shadow: #00000005; --gold: 40 100% 50%; --gold-foreground: 35 100% 40%; --link: 210 100% 63%; --error: 20 80% 50%; --anim-bar: no-repeat linear-gradient(#333 0 0); } .dark { --background: 0 0% 0%; --background-hover: 0 0% 7.8%; --background-active: 0 0% 13.7%; --background-container: 0, 0%, 5%, 0.9; --background-container-hover: 0, 0%, 5%, 0.95; --foreground: 210 40% 98%; --card: 240 10% 3.9%; --card-hover: 240 11% 12.5%; --card-active: 240 9.5% 19%; --card-foreground: 0 0% 98%; --popover: 240 10% 3.9%; --popover-foreground: 0 0% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 0 0 10%; --secondary-foreground: 0 0% 98%; --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; --accent: 240 3.7% 15.9%; --accent-secondary: 240 5% 15.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 240 3.7% 12.9%; --border-hover: 240 3.7% 17.9%; --border-active: 240 3.7% 25.9%; --input: 240 3.7% 15.9%; --input-unread: 240 3.7% 50%; --ring: 240 4.9% 83.9%; --text: 0 0% 100%; --text-secondary: 0 0% 80%; --text-unread: 0 0% 50%; --shadow: #ffffff05; --anim-bar: no-repeat linear-gradient(#ccc 0 0); } [type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { @apply border-border; } } * { --tw-ring-color: none !important; } .ui-input:-webkit-autofill, .ui-input:-moz-autofill, .ui-input:-ms-autofill, .ui-input:-o-autofill, .ui-input:autofill { background: hsl(var(--background)) !important; color: hsl(var(--foreground)) !important; -webkit-text-fill-color: hsl(var(--foreground)) !important; transition: background-color 5000s ease-in-out 0s; caret-color: hsl(var(--foreground)) !important; } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: app/src/assets/main.less ================================================ @import "ui"; @font-family: "HarmonyOS Sans","Inter Variable",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; @font-family-normal: "HarmonyOS Sans","Inter Variable",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; @font-family-code: "JetBrains Mono Variable","HarmonyOS Sans",monospace,"Inter Variable",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; @line-height: 1.5; @font-weight: 400; @color-scheme: light dark; @font-synthesis: none; @text-rendering: optimizeLegibility; @-webkit-font-smoothing: antialiased; @-moz-osx-font-smoothing: grayscale; @-webkit-text-size-adjust: 100%; html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; touch-action: pan-y; -webkit-overflow-scrolling: touch; } :root { font-family: @font-family; line-height: @line-height; font-weight: @font-weight; color-scheme: @color-scheme; font-synthesis: @font-synthesis; text-rendering: @text-rendering; -webkit-font-smoothing: @-webkit-font-smoothing; -moz-osx-font-smoothing: @-moz-osx-font-smoothing; -webkit-text-size-adjust: @-webkit-text-size-adjust; --font-family: @font-family; --font-family-normal: @font-family-normal; --font-family-code: @font-family-code; --navbar-height: 3.375rem; } .font-code { font-family: @font-family-code; } * { outline: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; -webkit-overflow-scrolling: touch; } body { scrollbar-width: none; &::-webkit-scrollbar { display: none; } } .grow { flex-grow: 1; } strong { font-weight: bold; } .jetbrains-mono { font-family: @font-family-code; } .hover\:bg-accent[aria-pressed="false"] { color: hsl(var(--text-secondary)); &:hover { color: hsl(var(--text-secondary)); background: hsl(var(--accent-secondary)); } } .hover\:bg-accent[aria-pressed="true"] { background: hsl(var(--accent)); color: hsl(var(--text)); border: 1px solid hsl(var(--border)); } .icon-tooltip { display: flex; flex-direction: row; align-items: center; &.gold { color: hsl(var(--gold)); } } .flex-dialog { border-radius: var(--radius) !important; max-height: calc(95% - 2rem) !important; overflow-x: hidden; overflow-y: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; touch-action: pan-y; outline: none; @media (max-width: 520px) { & { margin-top: 1rem; margin-bottom: 1rem; transform: translate(var(--tw-translate-x), calc(var(--tw-translate-y) - 1rem)) !important; } } .link { color: hsl(var(--text-secondary)); text-decoration: none; transition: color 0.2s ease-in-out; user-select: none; cursor: pointer; margin: 0.5rem auto; &:hover { color: hsl(var(--text)); } } .content-h-60 { height: 60vh; } .content-w-980 { width: 80vw; max-width: 980px; } &.full-screen { width: 100vw !important; height: 100% !important; max-width: 100vw !important; max-height: 100% !important; border-radius: 0 !important; border: 1px solid hsl(var(--border)) !important; .content-h-60 { height: calc(100vh - 6rem); } .content-w-980 { max-width: none; width: calc(100vw - 2rem); } .content-h-fit { height: fit-content; } .editor-preview, .editor-input { min-height: calc(100vh - 8.5rem) !important; } } } .announcement-dialog { max-width: min(90vw, 720px) !important; } .share-dialog { max-width: min(90vw, 720px) !important; } .record-dialog { max-width: min(90vw, 1024px) !important; } .pre-quota-dialog { max-width: min(90vw, 720px) !important; } .key-dialog { max-width: min(90vw, 1024px) !important; } .fixed-dialog { border-radius: var(--radius) !important; max-height: calc(95% - 2rem) !important; min-height: 60vh; overflow-x: hidden; overflow-y: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; touch-action: pan-y; outline: 0; @media (max-width: 660px) { width: 100vw !important; height: 100% !important; max-width: 100vw !important; max-height: 100% !important; border-radius: 0 !important; } } .cent { font-weight: normal !important; } .error-boundary { position: relative; display: flex; flex-direction: column; align-items: center; width: 100%; height: max-content; min-height: calc(100% - var(--navbar-height)); overflow: hidden; background: hsla(var(--background-container)); padding: 2.5rem 5rem; @media (max-width: 720px) { & { padding: 2.5rem 1rem; } } .error-provider { text-align: center; margin: 0.5rem auto; p { margin: 0.35rem; } } .error-tips { text-align: center; padding: 1rem 2rem; color: hsl(var(--text-secondary)); } } .tips-icon { width: 1rem; height: 1rem; cursor: pointer; margin-left: 0.2rem; scale: 0.9; color: hsl(var(--text-secondary)); outline: none !important; } .chat-logo { border-radius: var(--radius); user-select: none; } .broadcast-markdown { * { font-size: 0.9rem; color: hsl(var(--text)); } br { display: none; } } ================================================ FILE: app/src/assets/markdown/all.less ================================================ @import "highlight.less"; @import "style.less"; @import "theme.less"; .file-instance { display: flex; flex-direction: column; margin: 2px 0 !important; margin-bottom: 4px !important; padding: 0.5rem !important; border-radius: var(--radius); .file-content { background: none !important; display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; } img { max-width: 320px; max-height: 240px; object-fit: cover; border-radius: var(--radius); margin: 0.25rem 0; @media (max-width: 768px) { max-width: 100%; max-height: 100%; } } .download-action { background: none !important; border-radius: 0 !important; } @media (max-width: 668px) { & { white-space: pre-wrap; } } svg { width: 0.85rem; height: 0.85rem; color: hsl(var(--text)); flex-shrink: 0; } .name { content: attr(file); display: block; color: hsl(var(--text)); margin-right: 6px; font-family: var(--font-family) !important; } } .markdown-body { pre code { color: #c9d1d9; } pre.file-block div { background: none !important; } pre.file-block, pre.file-block > div { padding: 0; margin: 0; background: none !important; box-shadow: none !important; &:before { content: none !important; } } pre.file-block > div.file-instance { background: hsla(var(--background-container)) !important; } .markdown-syntax { position: relative; .markdown-syntax-header { position: absolute; display: flex; flex-direction: row; align-items: center; justify-content: center; z-index: 1; top: -34px; right: 0; user-select: none; cursor: pointer; transition: 0.25s ease; &:hover { p, svg { color: hsl(var(--text-dark)); } } p { color: hsl(var(--text-secondary-dark)); font-size: 12px; line-height: 1; margin: 0 0 0 6px; padding: 0; transition: 0.25s ease; } svg { color: hsl(var(--text-secondary-dark)); transition: 0.25s ease; } } } } .virtual-prompt { text-align: center; border-radius: var(--radius); border: 1px solid hsl(var(--border)); padding: 0.5rem; margin-bottom: 0.5rem !important; } .virtual-action { svg { transform: translateY(1px); } } p:has(.virtual-action) { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; justify-content: end; margin-top: 1.25rem !important; br { display: none; } @media (max-width: 768px) { grid-template-columns: 1fr 1fr; } @media (max-width: 480px) { grid-template-columns: 1fr; } } ================================================ FILE: app/src/assets/markdown/highlight.less ================================================ /*! Theme: GitHub Description: Light theme as seen on github.com Author: github.com Maintainer: @Hirse Updated: 2021-05-15 Outdated base version: https://github.com/primer/github-syntax-light Current colors taken from GitHub's CSS License: MIT */ pre code.hljs { display: block; overflow-x: auto; padding: 1em } code.hljs { padding: 3px 5px } .hljs { color: #24292e; background: #fff } .hljs-doctag, .hljs-keyword, .hljs-meta .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_ { color: #d73a49 } .hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_ { color: #6f42c1 } .hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5 } .hljs-meta .hljs-string, .hljs-regexp, .hljs-string { color: #032f62 } .hljs-built_in, .hljs-symbol { color: #e36209 } .hljs-code, .hljs-comment, .hljs-formula { color: #6a737d } .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #22863a } .hljs-subst { color: #24292e } .hljs-section { color: #005cc5; font-weight: 700 } .hljs-bullet { color: #735c0f } .hljs-emphasis { color: #24292e; font-style: italic } .hljs-strong { color: #24292e; font-weight: 700 } .hljs-addition { color: #22863a; background-color: #f0fff4 } .hljs-deletion { color: #b31d28; background-color: #ffeef0 } ================================================ FILE: app/src/assets/markdown/style.less ================================================ /* This is a theme distributed by `starry-night`. * It’s based on what GitHub uses on their site. * See for more info. */ :root { --color-prettylights-syntax-comment: #6e7781; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-entity: #8250df; --color-prettylights-syntax-storage-modifier-import: #24292f; --color-prettylights-syntax-entity-tag: #116329; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: #0a3069; --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; --color-prettylights-syntax-invalid-illegal-bg: #82071e; --color-prettylights-syntax-carriage-return-text: #f6f8fa; --color-prettylights-syntax-carriage-return-bg: #cf222e; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-list: #3b2300; --color-prettylights-syntax-markup-heading: #0550ae; --color-prettylights-syntax-markup-italic: #24292f; --color-prettylights-syntax-markup-bold: #24292f; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-markup-changed-text: #953800; --color-prettylights-syntax-markup-changed-bg: #ffd8b5; --color-prettylights-syntax-markup-ignored-text: #eaeef2; --color-prettylights-syntax-markup-ignored-bg: #0550ae; --color-prettylights-syntax-meta-diff-range: #8250df; --color-prettylights-syntax-brackethighlighter-angle: #57606a; --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; --color-prettylights-syntax-constant-other-reference-link: #0a3069; } .dark { :root { --color-prettylights-syntax-comment: #8b949e; --color-prettylights-syntax-constant: #79c0ff; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-storage-modifier-import: #c9d1d9; --color-prettylights-syntax-entity-tag: #7ee787; --color-prettylights-syntax-keyword: #ff7b72; --color-prettylights-syntax-string: #a5d6ff; --color-prettylights-syntax-variable: #ffa657; --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; --color-prettylights-syntax-invalid-illegal-bg: #8e1519; --color-prettylights-syntax-carriage-return-text: #f0f6fc; --color-prettylights-syntax-carriage-return-bg: #b62324; --color-prettylights-syntax-string-regexp: #7ee787; --color-prettylights-syntax-markup-list: #f2cc60; --color-prettylights-syntax-markup-heading: #1f6feb; --color-prettylights-syntax-markup-italic: #c9d1d9; --color-prettylights-syntax-markup-bold: #c9d1d9; --color-prettylights-syntax-markup-deleted-text: #ffdcd7; --color-prettylights-syntax-markup-deleted-bg: #67060c; --color-prettylights-syntax-markup-inserted-text: #aff5b4; --color-prettylights-syntax-markup-inserted-bg: #033a16; --color-prettylights-syntax-markup-changed-text: #ffdfb6; --color-prettylights-syntax-markup-changed-bg: #5a1e02; --color-prettylights-syntax-markup-ignored-text: #c9d1d9; --color-prettylights-syntax-markup-ignored-bg: #1158c7; --color-prettylights-syntax-meta-diff-range: #d2a8ff; --color-prettylights-syntax-brackethighlighter-angle: #8b949e; --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; } } .pl-c { color: var(--color-prettylights-syntax-comment); } .pl-c1, .pl-s .pl-v { color: var(--color-prettylights-syntax-constant); } .pl-e, .pl-en { color: var(--color-prettylights-syntax-entity); } .pl-smi, .pl-s .pl-s1 { color: var(--color-prettylights-syntax-storage-modifier-import); } .pl-ent { color: var(--color-prettylights-syntax-entity-tag); } .pl-k { color: var(--color-prettylights-syntax-keyword); } .pl-s, .pl-pds, .pl-s .pl-pse .pl-s1, .pl-sr, .pl-sr .pl-cce, .pl-sr .pl-sre, .pl-sr .pl-sra { color: var(--color-prettylights-syntax-string); } .pl-v, .pl-smw { color: var(--color-prettylights-syntax-variable); } .pl-bu { color: var(--color-prettylights-syntax-brackethighlighter-unmatched); } .pl-ii { color: var(--color-prettylights-syntax-invalid-illegal-text); background-color: var(--color-prettylights-syntax-invalid-illegal-bg); } .pl-c2 { color: var(--color-prettylights-syntax-carriage-return-text); background-color: var(--color-prettylights-syntax-carriage-return-bg); } .pl-sr .pl-cce { font-weight: bold; color: var(--color-prettylights-syntax-string-regexp); } .pl-ml { color: var(--color-prettylights-syntax-markup-list); } .pl-mh, .pl-mh .pl-en, .pl-ms { font-weight: bold; color: var(--color-prettylights-syntax-markup-heading); } .pl-mi { font-style: italic; color: var(--color-prettylights-syntax-markup-italic); } .pl-mb { font-weight: bold; color: var(--color-prettylights-syntax-markup-bold); } .pl-md { color: var(--color-prettylights-syntax-markup-deleted-text); background-color: var(--color-prettylights-syntax-markup-deleted-bg); } .pl-mi1 { color: var(--color-prettylights-syntax-markup-inserted-text); background-color: var(--color-prettylights-syntax-markup-inserted-bg); } .pl-mc { color: var(--color-prettylights-syntax-markup-changed-text); background-color: var(--color-prettylights-syntax-markup-changed-bg); } .pl-mi2 { color: var(--color-prettylights-syntax-markup-ignored-text); background-color: var(--color-prettylights-syntax-markup-ignored-bg); } .pl-mdr { font-weight: bold; color: var(--color-prettylights-syntax-meta-diff-range); } .pl-ba { color: var(--color-prettylights-syntax-brackethighlighter-angle); } .pl-sg { color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); } .pl-corl { text-decoration: underline; color: var(--color-prettylights-syntax-constant-other-reference-link); } ================================================ FILE: app/src/assets/markdown/theme.less ================================================ .markdown-body { color-scheme: light; --color-prettylights-syntax-comment: #6e7781; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-entity: #8250df; --color-prettylights-syntax-storage-modifier-import: #24292f; --color-prettylights-syntax-entity-tag: #116329; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: #0a3069; --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; --color-prettylights-syntax-invalid-illegal-bg: #82071e; --color-prettylights-syntax-carriage-return-text: #f6f8fa; --color-prettylights-syntax-carriage-return-bg: #cf222e; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-list: #3b2300; --color-prettylights-syntax-markup-heading: #0550ae; --color-prettylights-syntax-markup-italic: #24292f; --color-prettylights-syntax-markup-bold: #24292f; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-markup-changed-text: #953800; --color-prettylights-syntax-markup-changed-bg: #ffd8b5; --color-prettylights-syntax-markup-ignored-text: #eaeef2; --color-prettylights-syntax-markup-ignored-bg: #0550ae; --color-prettylights-syntax-meta-diff-range: #8250df; --color-prettylights-syntax-brackethighlighter-angle: #57606a; --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; --color-prettylights-syntax-constant-other-reference-link: #0a3069; --color-fg-default: #24292f; --color-fg-muted: #57606a; --color-fg-subtle: #6e7781; --color-canvas-default: #ffffff; --color-canvas-subtle: #f6f8fa; --color-border-default: #d0d7de; --color-border-muted: hsla(210,18%,87%,1); --color-neutral-muted: rgba(175,184,193,0.2); --color-accent-fg: #0969da; --color-accent-emphasis: #0969da; --color-attention-subtle: #fff8c5; --color-danger-fg: #cf222e; } .dark { .markdown-body { color-scheme: dark; --color-prettylights-syntax-comment: #8b949e; --color-prettylights-syntax-constant: #79c0ff; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-storage-modifier-import: #c9d1d9; --color-prettylights-syntax-entity-tag: #7ee787; --color-prettylights-syntax-keyword: #ff7b72; --color-prettylights-syntax-string: #a5d6ff; --color-prettylights-syntax-variable: #ffa657; --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; --color-prettylights-syntax-invalid-illegal-bg: #8e1519; --color-prettylights-syntax-carriage-return-text: #f0f6fc; --color-prettylights-syntax-carriage-return-bg: #b62324; --color-prettylights-syntax-string-regexp: #7ee787; --color-prettylights-syntax-markup-list: #f2cc60; --color-prettylights-syntax-markup-heading: #1f6feb; --color-prettylights-syntax-markup-italic: #c9d1d9; --color-prettylights-syntax-markup-bold: #c9d1d9; --color-prettylights-syntax-markup-deleted-text: #ffdcd7; --color-prettylights-syntax-markup-deleted-bg: #67060c; --color-prettylights-syntax-markup-inserted-text: #aff5b4; --color-prettylights-syntax-markup-inserted-bg: #033a16; --color-prettylights-syntax-markup-changed-text: #ffdfb6; --color-prettylights-syntax-markup-changed-bg: #5a1e02; --color-prettylights-syntax-markup-ignored-text: #c9d1d9; --color-prettylights-syntax-markup-ignored-bg: #1158c7; --color-prettylights-syntax-meta-diff-range: #d2a8ff; --color-prettylights-syntax-brackethighlighter-angle: #8b949e; --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; --color-fg-default: #c9d1d9; --color-fg-muted: #8b949e; --color-fg-subtle: #6e7681; --color-canvas-default: #0d1117; --color-canvas-subtle: #161b22; --color-border-default: #30363d; --color-border-muted: #21262d; --color-neutral-muted: rgba(110,118,129,0.4); --color-accent-fg: #58a6ff; --color-accent-emphasis: #1f6feb; --color-attention-subtle: rgba(187,128,9,0.15); --color-danger-fg: #f85149; } } .markdown-body { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; color: var(--color-fg-default); background-color: var(--color-canvas-default); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .octicon { display: inline-block; fill: currentColor; vertical-align: text-bottom; } .markdown-body h1:hover .anchor .octicon-link:before, .markdown-body h2:hover .anchor .octicon-link:before, .markdown-body h3:hover .anchor .octicon-link:before, .markdown-body h4:hover .anchor .octicon-link:before, .markdown-body h5:hover .anchor .octicon-link:before, .markdown-body h6:hover .anchor .octicon-link:before { width: 16px; height: 16px; content: ' '; display: inline-block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); mask-image: url("data:image/svg+xml,"); } .markdown-body details, .markdown-body figcaption, .markdown-body figure { display: block; } .markdown-body summary { display: list-item; } .markdown-body [hidden] { display: none !important; } .markdown-body a { background-color: transparent; color: var(--color-accent-fg); text-decoration: none; } .markdown-body abbr[title] { border-bottom: none; text-decoration: underline dotted; } .markdown-body b, .markdown-body strong { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dfn { font-style: italic; } .markdown-body h1 { margin: .67em 0; font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 2em; border-bottom: 1px solid var(--color-border-muted); } .markdown-body mark { background-color: var(--color-attention-subtle); color: var(--color-fg-default); } .markdown-body small { font-size: 90%; } .markdown-body sub, .markdown-body sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } .markdown-body sub { bottom: -0.25em; } .markdown-body sup { top: -0.5em; } .markdown-body img { border-style: none; max-width: 100%; box-sizing: content-box; background-color: var(--color-canvas-default); border-radius: 4px; max-height: 50vh; margin: 0.5rem auto; &.broken { display: none; } } .markdown-body code, .markdown-body kbd, .markdown-body pre, .markdown-body samp { font-family: monospace; font-size: 1em; } .markdown-body figure { margin: 1em 40px; } .markdown-body hr { box-sizing: content-box; overflow: hidden; background: transparent; border-bottom: 1px solid var(--color-border-muted); height: .25em; padding: 0; margin: 24px 0; background-color: var(--color-border-default); border: 0; } .markdown-body input { font: inherit; margin: 0; overflow: visible; font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body [type=button], .markdown-body [type=reset], .markdown-body [type=submit] { -webkit-appearance: button; } .markdown-body [type=checkbox], .markdown-body [type=radio] { box-sizing: border-box; padding: 0; } .markdown-body [type=number]::-webkit-inner-spin-button, .markdown-body [type=number]::-webkit-outer-spin-button { height: auto; } .markdown-body [type=search]::-webkit-search-cancel-button, .markdown-body [type=search]::-webkit-search-decoration { -webkit-appearance: none; } .markdown-body ::-webkit-input-placeholder { color: inherit; opacity: .54; } .markdown-body ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } .markdown-body a:hover { text-decoration: underline; } .markdown-body ::placeholder { color: var(--color-fg-subtle); opacity: 1; } .markdown-body hr::before { display: table; content: ""; } .markdown-body hr::after { display: table; clear: both; content: ""; } .markdown-body table { border-spacing: 0; border-collapse: collapse; display: block; width: 100%; overflow: auto; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body details summary { cursor: pointer; } .markdown-body details:not([open])>*:not(summary) { display: none !important; } .markdown-body a:focus, .markdown-body [role=button]:focus, .markdown-body input[type=radio]:focus, .markdown-body input[type=checkbox]:focus { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:focus:not(:focus-visible), .markdown-body [role=button]:focus:not(:focus-visible), .markdown-body input[type=radio]:focus:not(:focus-visible), .markdown-body input[type=checkbox]:focus:not(:focus-visible) { outline: solid 1px transparent; } .markdown-body a:focus-visible, .markdown-body [role=button]:focus-visible, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus-visible { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:not([class]):focus, .markdown-body a:not([class]):focus-visible, .markdown-body input[type=radio]:focus, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus, .markdown-body input[type=checkbox]:focus-visible { outline-offset: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; line-height: 10px; color: var(--color-fg-default); vertical-align: middle; background-color: var(--color-canvas-subtle); border: solid 1px var(--color-neutral-muted); border-bottom-color: var(--color-neutral-muted); border-radius: 6px; box-shadow: inset 0 -1px 0 var(--color-neutral-muted); } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25; } .markdown-body h2 { font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 1.5em; border-bottom: 1px solid var(--color-border-muted); } .markdown-body h3 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1.25em; } .markdown-body h4 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1em; } .markdown-body h5 { font-weight: var(--base-text-weight-semibold, 600); font-size: .875em; } .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); font-size: .85em; color: var(--color-fg-muted); } .markdown-body p { margin-top: 0; margin-bottom: 10px; } .markdown-body blockquote { margin: 0; padding: 0 1em; color: var(--color-fg-muted); border-left: .25em solid var(--color-border-default); } .markdown-body ul, .markdown-body ol { margin-top: 0; margin-bottom: 0; padding-left: 2em; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ul ul ol, .markdown-body ul ol ol, .markdown-body ol ul ol, .markdown-body ol ol ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body tt, .markdown-body code, .markdown-body samp { font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; font-size: 12px; word-wrap: normal; } .markdown-body .octicon { display: inline-block; overflow: visible !important; vertical-align: text-bottom; fill: currentColor; } .markdown-body input::-webkit-outer-spin-button, .markdown-body input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; appearance: none; } .markdown-body::before { display: table; content: ""; } .markdown-body::after { display: table; clear: both; content: ""; } .markdown-body>*:first-child { margin-top: 0 !important; } .markdown-body>*:last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .absent { color: var(--color-danger-fg); } .markdown-body .anchor { float: left; padding-right: 4px; margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre, .markdown-body details { margin-top: 0; margin-bottom: 16px; } .markdown-body blockquote>:first-child { margin-top: 0; } .markdown-body blockquote>:last-child { margin-bottom: 0; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: var(--color-fg-default); vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { padding: 0 .2em; font-size: inherit; } .markdown-body summary h1, .markdown-body summary h2, .markdown-body summary h3, .markdown-body summary h4, .markdown-body summary h5, .markdown-body summary h6 { display: inline-block; } .markdown-body summary h1 .anchor, .markdown-body summary h2 .anchor, .markdown-body summary h3 .anchor, .markdown-body summary h4 .anchor, .markdown-body summary h5 .anchor, .markdown-body summary h6 .anchor { margin-left: -40px; } .markdown-body summary h1, .markdown-body summary h2 { padding-bottom: 0; border-bottom: 0; } .markdown-body ul.no-list, .markdown-body ol.no-list { padding: 0; list-style-type: none; } .markdown-body ol[type=a] { list-style-type: lower-alpha; } .markdown-body ol[type=A] { list-style-type: upper-alpha; } .markdown-body ol[type=i] { list-style-type: lower-roman; } .markdown-body ol[type=I] { list-style-type: upper-roman; } .markdown-body ol[type="1"] { list-style-type: decimal; } .markdown-body div>ol:not([type]) { list-style-type: decimal; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li>p { margin-top: 16px; } .markdown-body li+li { margin-top: .25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dl dd { padding: 0 16px; margin-bottom: 16px; } .markdown-body table th { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid var(--color-border-default); } .markdown-body table tr { background-color: var(--color-canvas-default); border-top: 1px solid var(--color-border-muted); } .markdown-body table tr:nth-child(2n) { background-color: var(--color-canvas-subtle); } .markdown-body table img { background-color: transparent; } .markdown-body img[align=right] { padding-left: 20px; } .markdown-body img[align=left] { padding-right: 20px; } .markdown-body .emoji { max-width: none; vertical-align: text-top; background-color: transparent; } .markdown-body span.frame { display: block; overflow: hidden; } .markdown-body span.frame>span { display: block; float: left; width: auto; padding: 7px; margin: 13px 0 0; overflow: hidden; border: 1px solid var(--color-border-default); } .markdown-body span.frame span img { display: block; float: left; } .markdown-body span.frame span span { display: block; padding: 5px 0 0; clear: both; color: var(--color-fg-default); } .markdown-body span.align-center { display: block; overflow: hidden; clear: both; } .markdown-body span.align-center>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: center; } .markdown-body span.align-center span img { margin: 0 auto; text-align: center; } .markdown-body span.align-right { display: block; overflow: hidden; clear: both; } .markdown-body span.align-right>span { display: block; margin: 13px 0 0; overflow: hidden; text-align: right; } .markdown-body span.align-right span img { margin: 0; text-align: right; } .markdown-body span.float-left { display: block; float: left; margin-right: 13px; overflow: hidden; } .markdown-body span.float-left span { margin: 13px 0 0; } .markdown-body span.float-right { display: block; float: right; margin-left: 13px; overflow: hidden; } .markdown-body span.float-right>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: right; } .markdown-body code, .markdown-body tt { padding: .2em .4em; margin: 0; font-size: 85%; white-space: break-spaces; background-color: var(--color-neutral-muted); border-radius: 6px; } .markdown-body code br, .markdown-body tt br { display: none; } .markdown-body del code { text-decoration: inherit; } .markdown-body samp { font-size: 85%; } .markdown-body pre code { font-size: 100%; } .markdown-body pre>code { padding: 0; margin: 0; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: var(--color-canvas-subtle); border-radius: 6px; } .markdown-body pre code, .markdown-body pre tt { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .markdown-body .csv-data td, .markdown-body .csv-data th { padding: 5px; overflow: hidden; font-size: 12px; line-height: 1; text-align: left; white-space: nowrap; } .markdown-body .csv-data .blob-num { padding: 10px 8px 9px; text-align: right; background: var(--color-canvas-default); border: 0; } .markdown-body .csv-data tr { border-top: 0; } .markdown-body .csv-data th { font-weight: var(--base-text-weight-semibold, 600); background: var(--color-canvas-subtle); border-top: 0; } .markdown-body [data-footnote-ref]::before { content: "["; } .markdown-body [data-footnote-ref]::after { content: "]"; } .markdown-body .footnotes { font-size: 12px; color: var(--color-fg-muted); border-top: 1px solid var(--color-border-default); } .markdown-body .footnotes ol { padding-left: 16px; } .markdown-body .footnotes ol ul { display: inline-block; padding-left: 16px; margin-top: 16px; } .markdown-body .footnotes li { position: relative; } .markdown-body .footnotes li:target::before { position: absolute; top: -8px; right: -8px; bottom: -8px; left: -24px; pointer-events: none; content: ""; border: 2px solid var(--color-accent-emphasis); border-radius: 6px; } .markdown-body .footnotes li:target { color: var(--color-fg-default); } .markdown-body .footnotes .data-footnote-backref g-emoji { font-family: monospace; } .markdown-body .pl-c { color: var(--color-prettylights-syntax-comment); } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: var(--color-prettylights-syntax-constant); } .markdown-body .pl-e, .markdown-body .pl-en { color: var(--color-prettylights-syntax-entity); } .markdown-body .pl-smi, .markdown-body .pl-s .pl-s1 { color: var(--color-prettylights-syntax-storage-modifier-import); } .markdown-body .pl-ent { color: var(--color-prettylights-syntax-entity-tag); } .markdown-body .pl-k { color: var(--color-prettylights-syntax-keyword); } .markdown-body .pl-s, .markdown-body .pl-pds, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sre, .markdown-body .pl-sr .pl-sra { color: var(--color-prettylights-syntax-string); } .markdown-body .pl-v, .markdown-body .pl-smw { color: var(--color-prettylights-syntax-variable); } .markdown-body .pl-bu { color: var(--color-prettylights-syntax-brackethighlighter-unmatched); } .markdown-body .pl-ii { color: var(--color-prettylights-syntax-invalid-illegal-text); background-color: var(--color-prettylights-syntax-invalid-illegal-bg); } .markdown-body .pl-c2 { color: var(--color-prettylights-syntax-carriage-return-text); background-color: var(--color-prettylights-syntax-carriage-return-bg); } .markdown-body .pl-sr .pl-cce { font-weight: bold; color: var(--color-prettylights-syntax-string-regexp); } .markdown-body .pl-ml { color: var(--color-prettylights-syntax-markup-list); } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { font-weight: bold; color: var(--color-prettylights-syntax-markup-heading); } .markdown-body .pl-mi { font-style: italic; color: var(--color-prettylights-syntax-markup-italic); } .markdown-body .pl-mb { font-weight: bold; color: var(--color-prettylights-syntax-markup-bold); } .markdown-body .pl-md { color: var(--color-prettylights-syntax-markup-deleted-text); background-color: var(--color-prettylights-syntax-markup-deleted-bg); } .markdown-body .pl-mi1 { color: var(--color-prettylights-syntax-markup-inserted-text); background-color: var(--color-prettylights-syntax-markup-inserted-bg); } .markdown-body .pl-mc { color: var(--color-prettylights-syntax-markup-changed-text); background-color: var(--color-prettylights-syntax-markup-changed-bg); } .markdown-body .pl-mi2 { color: var(--color-prettylights-syntax-markup-ignored-text); background-color: var(--color-prettylights-syntax-markup-ignored-bg); } .markdown-body .pl-mdr { font-weight: bold; color: var(--color-prettylights-syntax-meta-diff-range); } .markdown-body .pl-ba { color: var(--color-prettylights-syntax-brackethighlighter-angle); } .markdown-body .pl-sg { color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); } .markdown-body .pl-corl { text-decoration: underline; color: var(--color-prettylights-syntax-constant-other-reference-link); } .markdown-body g-emoji { display: inline-block; min-width: 1ch; font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; font-size: 1em; font-style: normal !important; font-weight: var(--base-text-weight-normal, 400); line-height: 1; vertical-align: -0.075em; } .markdown-body g-emoji img { width: 1em; height: 1em; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item label { font-weight: var(--base-text-weight-normal, 400); } .markdown-body .task-list-item.enabled label { cursor: pointer; } .markdown-body .task-list-item+.task-list-item { margin-top: 4px; } .markdown-body .task-list-item .handle { display: none; } .markdown-body .task-list-item-checkbox { margin: 0 .2em .25em -1.4em; vertical-align: middle; } .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em .25em .2em; } .markdown-body .contains-task-list { position: relative; } .markdown-body .contains-task-list:hover .task-list-item-convert-container, .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { display: block; width: auto; height: 24px; overflow: visible; clip: auto; } .markdown-body ::-webkit-calendar-picker-indicator { filter: invert(50%); } ================================================ FILE: app/src/assets/pages/api.less ================================================ .api-dialog { display: flex; flex-direction: column; align-items: center; width: 100%; height: max-content; padding: 24px 0 12px; gap: 24px; } .api-wrapper { display: flex; flex-direction: row; gap: 6px; width: 100%; input { text-align: center; font-size: 16px; cursor: pointer; flex-grow: 1; } button { flex-shrink: 0; } } ================================================ FILE: app/src/assets/pages/article.less ================================================ .article-page { position: relative; display: flex; width: 100%; min-height: calc(100% - var(--navbar-height)); height: max-content; } .article-container { display: flex; flex-direction: column; padding: 12px 16px; gap: 6px; width: 100%; height: 100%; } .article-wrapper { width: calc(96vw - 32px); height: 100%; margin: 1rem auto; padding: 1rem; max-width: 840px; .article-title { display: flex; flex-direction: row; user-select: none; align-items: center; } .article-action { @media (max-width: 768px) { flex-direction: column; } } .article-content { display: flex; flex-direction: column; margin: 1rem 0; & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } } } ================================================ FILE: app/src/assets/pages/auth.less ================================================ .auth { width: 100%; height: 100%; overflow: hidden; } .auth-container { display: flex; flex-direction: column; align-items: center; justify-items: center; padding: 2.5rem 2rem; height: fit-content; user-select: none; .logo { width: 4rem; height: 4rem; border-radius: var(--radius); } & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } } .auth-card { width: 80vw; max-width: 360px; min-width: 280px; margin: 1rem 0; } .auth-wrapper { display: flex; flex-direction: column; padding: 1.5rem 0; & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } } .addition-wrapper { display: flex; flex-direction: column; border-radius: var(--radius); border: 1px solid hsla(var(--border)); padding: 1.25rem; align-items: center; transform: translateY(-1rem); font-size: 0.875rem; text-align: center; a { text-decoration: underline; text-underline-offset: 0.25rem; text-underline: 2px solid hsl(var(--text-secondary)); color: hsl(var(--text-secondary)); transition: 0.2s ease-in-out; cursor: pointer; &:hover { color: hsl(var(--text)); text-underline-color: hsl(var(--text)); } } .row { margin-bottom: 0.5rem; .link { margin-left: 0.1rem; } &:last-child { margin-bottom: 0; } } } ================================================ FILE: app/src/assets/pages/chat.less ================================================ @keyframes FlexInAnimationFromBottom { 0% { opacity: .2; margin-top: 20px; margin-bottom: 0; } 100% { opacity: 1; margin-top: 0; margin-bottom: 20px; } } .scroll-action { position: absolute; z-index: 12; opacity: 0; right: 36px; bottom: 12rem; transition: .25s; pointer-events: none; button { border-color: rgba(0,0,0,0) !important; } &.active { opacity: 0.8; pointer-events: all; } @media (max-width: 668px) { bottom: 8.5rem; } } .message { display: flex; gap: 6px; flex-direction: column; max-width: 100%; pre { scrollbar-width: thin; &::-webkit-scrollbar { height: 6px; } } &:last-child { animation: FlexInAnimationFromBottom 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s 1 normal forwards running; .bing { animation: fadein 0.2s ease-in-out; @keyframes fadein { from { opacity: 0.5; } to { opacity: 1; } } } } .content-wrapper { display: flex; flex-direction: row; max-width: 100%; .message-toolbar { display: flex; flex-direction: column; padding: 0 4px; user-select: none; height: max-content; margin-top: auto; gap: 4px; svg { cursor: pointer; color: hsl(var(--text-secondary)); transition: 0.25s; &:hover { color: hsl(var(--text)); } } } } .message-quota { display: flex; flex-direction: row; align-items: center; user-select: none; gap: 4px; cursor: pointer; border: 1px solid hsl(var(--input)); border-radius: var(--radius); transition: 0.2s linear; padding: 4px 8px; width: max-content; height: max-content; white-space: nowrap; margin-left: 3rem; transition-property: border-color, color, background-color, width; &.subscription { svg, span { color: hsl(var(--gold)); } } .quota { font-size: 14px; color: hsl(var(--text-secondary)); transition: .25s; } .icon { color: hsl(var(--text-secondary)); } &:hover { border-color: hsl(var(--border-hover)); } } .message-content { display: flex; flex: 1 1 auto; min-width: 0; flex-direction: column; max-width: 100%; padding: 8px 16px; border-radius: var(--radius); transition: 0.25s linear; } .message-avatar-wrapper { .message-avatar { display: flex; width: 2.25rem; height: 2.25rem; border-radius: var(--radius); text-align: center; font-size: 0.785rem; } flex-shrink: 0; margin-left: 0.5rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); width: 2.25rem; height: 2.25rem; user-select: none; } &.user { align-items: flex-end; } @media (max-width: 768px) { .content-wrapper { flex-direction: column !important; align-items: flex-start; .message-avatar-wrapper { margin-left: 0; margin-right: 0; margin-bottom: 0.5rem; } } .message-toolbar { padding: 0 !important; } .message-quota { margin-left: 0; } &.user { .content-wrapper { align-items: flex-end; } } } &.user { .content-wrapper { flex-direction: row-reverse; } } &.assistant, &.system { align-items: flex-start; .message-avatar-wrapper { margin-right: 0.5rem; margin-left: 0; } } } .markdown-body { max-width: 100%; padding: 4px 0; background: none !important; color: hsl(var(--text)); .prompt-row { border: 1px solid hsl(var(--border)); border-radius: var(--radius); margin: 0.25rem 0; white-space: nowrap; .grow { min-width: 0.75rem; } .value { display: flex; flex-direction: row; align-items: center; font-family: var(--font-family); margin: 0 !important; } svg { transform: translateY(1px); } } ol, ul, menu { list-style: inherit; } pre, code { font-family: var(--font-family-code) !important; background: rgb(40, 44, 52); } pre { // width: calc(100% - 8px); box-shadow: #0005 0 2px 2px; border-radius: var(--radius); &:before { content: ""; display: block; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAACCCAYAAADVN8idAAAgAElEQVR4nO2de5QU5Zn/v1VdVX2/zQwMzDCDgCBKOIx4myXLRlnYGDlhzWWDSTxkhXBQo2iS34kmavb3C5qo5+yqqBs5xNG4ZpVskjXk6BrhqAkbdoyXgSUoiqgMzDjAzPS1+lLX3x/TYNU7F6C7untm+vn8Ne/bVdVvP+8777fe2/MABEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQExKu2BtN03SyHGVhxdS61jk+77xWr3dWk9c7Y4okTakThbqAIIa8POcTeF4EAM0w1KxhZtKamhxUtcETinKiN5s92p3Nfngok31vx/HB7mr/FmLisaItMGv2NPfclqnCrKYGoXVqWJxWF+TrAj4u5JE4n+jiRZMzoWmmmlPMTDpjJgdTxuDxhNrX2691HzmuffhBX/7gjj3pD6v9W4iJx9TFwXqxWWrlG6UmforYiIhQb4ZcEcPPBzjJ5eZd4AHA0GGYip7nZSPNJfU44tqAcUI9ZhxTetUepfv4W6mBav+W08FxRUvZ0P3F3jjehHBByM+3RyNLLw6H29vCwQubPJ6ZhY/aS3x0JwD05nKH9yRSXW8kEp2dsfiu/UnZKPG5xCRiQYuHb5/vvfyieZ4lbXO8FzU1uE62vwtLfHQXAPT064f3Hsq++cZ7ud2vHci+uv9IjtofYWP6VfWfEud7F2Gu9wJMEacVsteW+NgOAMAJtQ8Hs2+rB7J7P35h4C8lPtNxaloI2+tDkRUNDSuvqG9YPsfvnY/SRe9M6TwkywdeGRjcuaO///nOgWS8Qt9LjCP+ap6v/m8X+1de3ua78twmaT5KF70zpev9XuXAq3syL+54S97+2nsZan81SMN8v9tzaXApvziwBDOkky9epQrf6RgSxqPKYeOt9O7cn1O7+g/I+TJ/52mpSSG8aXbL51ZNa/zCeX7/QlRO/Eaj811Z3re979h/PvLBkf+qclmICvCtlfUrP78k8JX5LdJCVE78RqPrwBFl3+92p3/56PMDz1e5LEQFaPrClEvFvw4uN2d65qD8wnc6OozDuUPmrtTve5478Wa1ClEzQtgW8bu/1ty8dnXT9DWFrDEFkOcAURIhCiJEQYDL5YLocoEXePDgwPHcKeOZpgnTMGHAhKEZUHUduq5D1TSomgo1r+IM5qE6TcD4Ze/HT//70Z6OPYnqvyURzrHoHK/7a8tCG1ZfHvrHQtaYAsgBkCQXRJGDKLggCIDg4uHiOfCFtsdxQ/9DpsnBNE0YhgndMKHpBjQNUDUdqmpCUXScwX9bl2lyxrZXk08+80pi696PstT+JhnN1zdf7Voe/nwhOaYAGpwJSRTBSQK4ocYH3sXD5eIB3gWe42AU2h9vcjBMEzB06LoBQzcATYepaTAVDYqqgjdPKxUdnAld3xl/4eiW3udK/7Vnx6QXwraI372uteWmVY2NX8ZpxM/r8cDjluCRJEhu0dFyKHkVOUVBLq8gm8ud7vLO7ceO/erxw0ceIUGc2Cw6x+ted2Xk1lVLAqtxGvHzelzwuAV43DzcEu9oOfKKgVzeQC6vIZvTT3d51/bd6W2Pvxh/kARx4tNyy4zV3NLQ3xWSowqg4HGD94jg3SJcDvd/el6FkVdh5FRouTGbVAcAmLuSLx156Og2RwsxBpNaCH98/rnrvz6jeS3GEECf1wuf1wOf112yMc4U0zSRyeaRyeaQyWbHurTzF0d7On7wzvtbK1IwwlHuWdN4w9eXh9ZjDAH0eUX4vTx8XlfF2p9hmshmdchZDZnsmKLY9fTO5JY7nzq2pSIFIxyl5ZvTV3JXRr9YSI4ogJzPA7fXDc4nga9g+zMzCvLZPMzMqIOCIUF8MfabIz/7uOxT9pNSCNfNbP7MrbNn3RYSXFGMIIKiICLg98Lv98LFO/vmfbbohgFZziItZ6Fq6kiXdCY1LfbgBx/d9/jhnj9UunzE2bNuRXTZLV+quyPk46MYQQRFgUfALyLgF+CqbvODrgPpjIa0rELVRpzA70pmjNiDvx68p2NH7OVKl484e6ZfVf8p1zUN63mfK4CRBFBwQQp44fJ7wFe5ARq6AV3OQUlnAW3El7IOyEZa3XZiazl3m04qIVwQ8vPfnzt709K6umUYQQDdbhFBfwB+n8fx73YCOZNDSk4jnx9ZEHcNDr78k4Mf3EVHL8YnC1o8/G3X1N/7Nwt9yzGCALolF0IBAX6fUIXSnR45oyGZ1pBXRuyQuv74v9md923rv52OXoxfWu+cuQFt/ksxggAKkgQh6IXL765CyU6PLuehpWRoijbSxx3YI/+5++7DZZmdmDRC+I3WpiU/nDvnXoHnl7KfSYKAUCg4bgWQRc7kkEymoGjDG4RqGH/YdPDQD37e3bu7CkUjRmHNsujSO6+tv18SuOEzECKHSFAatwLIImc0xFMKVHX4/6iimZ13Pz3wvadeju2qQtGIUZj+2boLhOumbeQEiGBEkBdFSCHfuBVAFl3OQ0lmYKjDBgQdpmaoesfxB3tfGjzg5HdOCiG8f8G8Gwq7QW2dEM8B4VAIoaDfse+qJMmUjFgiOdJHndt6P37qe/vf+2mly0QM5761jRsLu0Fto0COMxEJeRAOTgwBZEmkNMQT+ZF2nHY9+0qq4/Yn+h6pfKkIFstuUJsAGpwJTzgIMeSrUslKQ01mkEukRtpx2mHsiP/Oyd2lE14If31J2wMXR8LtYETQ5/UiGglCcLkc+Z5qoak6YsnUSJtqOt+IJzq/9Pqeb1ejXMQQv7qj5eGLz/N8GowI+rwi6sIiBKEyGxDKhaaZGEzkR9pU0/X6u/k//cM93TdXo1zEEK2bZm3E+d5FYESQ83ngi/gBYWL3f9B0ZOLySJtqOvBOdm/3XR9uduJrJqwQtteHIg9ccP5jBVdoNhGsi4QRDEzMt6DRSKUzGIwn2OzO3lzu8Lfffud68k5TWS6b54s8cP20Jwqu0GwiWB+REAw4u/282qTSKgbiCpvd1dOvH/7OY33XkXeaytIw3+/23dL0w4IrNJsIeqJBuILeKpWsPOipLHKxFJvdwR1X++TNvT8q1TvNhBTCFVPrWh9euOBxL88vt+a7RQnRaAhuaXJ1QifJKyoG40koir1DyhrGzpv37V9Hzr0rw4q2wKyHvjXt5z43Z1uPlkQXGqISJIfPAI4XFMVAf0yBotpHh5m8uWvjo33X7tyTpvZXAaYuDtZ7vtu8CW7+BtsHkgBfXQicNDGn4k+HqWjIDCYBdjNN3vhp7p977irFufeEE8KrpjXM+deFC57igCXWfL/Pg/popGJnsaqFaZoYiMUhM1MFJrD7xn3717zQ13+oSkWrCT53UWjuo7c0PsMDF1nz/T4BDVHPKW8vkxXT5NAfy0HOaGz+mzdu7v3qf72ZPlilotUEjZeEp7hva74XzChQ9HngqQ+f8vYyWeFNE7mBFFSm/+NMbM3e33PH8dcTJ4p57oQSwhVT61q3Llr4DCuCoWAA0XCw2KJMSGKJFJKptC3PBHav37vvqzQyLA8r2gKztnxn+n+wIhgKCqgLT4wdeU4xmMgjmRouhusf6P0ijQzLw9TFwXrPD1ruByuCIS+kSG31f1osjXwqY8vjTGzN/uTI94sZGZYqhBWbA2qvD0UeXrjgcVYEI6FgzYkgAETDQURC9t/NAUseXrjg8fb6UKRKxZq0XDbPF3noW9N+zopgNCTVnAgCQF3YjUhIsuVxnHnR5m9Ne/qyeT5qfw7TMN/v9ny3eRMYEXSHAzUnggAgRANwhwO2PJPDes93mzc1zK/8OZGKCeEDF5z/GLsmGA2HEA4FRrtl0hMOBRANh2x5Xp5f/sAF5z9WpSJNWh64ftoT7JpgNOxGODQ516PPhEhIRJR5CfC5uaX/cv20J6pUpEmL75amH7JrglIkACE8uTYFng1C2AcpwvT/bv4G/8amH1a6LBURwl9f0vaAJVAugKGR4EQ9H+gkoaB/2MiwyeOZ+etL2h6oUpEmHb+6o+VhS6BcAEMjwYl6PtBJwkFh2MiwucE18z/uaH24SkWadLRumrXREigXwJAITtTzgU4ihnzDR4ZTxWmtm2ZtrGQ5yi6E9y+YdwN7TjAUDNT0SJAlHAogFLTZo/3iSLj9/gXzbhjtHuLMuG9t40b2nGA4KNT0SJAlEhIRsr8UXHjJee5P33vdtJuqVabJQvP1zVez5wTFkJ9E0IIQ9sEdtNljLc73LpqxoenqSpWhrEL4jdamJazHGL/PU5NrgqcjGh7mQq59ddP0Nd9obVoy2j3E2KxZFl3Keozx+4Rh04HE0Joh40LuwmuuCK5dsyw6zOUhcWZM/2zdBazHGNHngRShmTAWIRqAaO//1vIrIp+f/tm6Cyrx/WXbNbog5Oe3X7L4VavvULcooXFq3aQ/IlEspmmi78Sg7Zyhahh/+PvX31pGjrrPjgUtHv4//9+MP1l9h0qiC9Oneif9EYliMU0OHx/P2s4ZKprZ+YV/OvppctR99rQ8e8FjnIANpzIkAcHG+kl/RKJYeNNE6ljMds7Q1IwtR645cP3p7h23u0a/P3f2JtaBdjQaIhEcA47jUBexb54Ref4z3587e1OVijRhue2a+ntZB9oNUYlEcAw4zkRD1L5eKAlc+23X1N9bpSJNWFrvnLmh4ED7FL66EIngGBgcB1+dvf/jBF5svXPmhlFucYyyCOG6mc2fKYRSOkVdJDxpPcY4iVsSURcJ2/KW1tUtWzez+TNVKtKEY92K6LJCKKVT1Ecmr8cYJ5EkHvURuxj+zULf8rUrostGuYVgmH5V/afYUEqeaHDSeoxxEk4S4Inals7Wos1/6fSr6j9Vzu8tS89w6+xZt8GyLujzeied79ByEgz44PPafA223zr7nNuqVZ6Jxi1fqrsDlnVBn1ecdL5Dy0kwIMLntTl7vvDWIZsSZ4Drmob1sIgg5/NMOt+h5cQV9IJj1gvF1VPWl/M7HRfCH59/7vpCZPmhL+CAaA0eGC2VKHOkIiQI0R+ff25ZG8Nk4J41jTcUIssDGJruqwuTCJ4tdWG3bQNByMdH717TWPYpqolOyzenryxElgcwFErJR5tjzhpfxG+fRvbzgZZvTl9Zru9zVAjbIn7312c0r4VlNBgOhSZ8KKVqIIgu9rB9+9dnNK9tC0+Q6JxVYNE5XvfXl4fWwzIajIQ8Ez6UUjUQBA4R++7aC69dHtqw6Bwvtb8x4K6MfhHWKdFwcOKHUqoGgmvIdp+wtmDbsuCoEK5rbbkJFhGUBIEOzZdAKOiHJNjWFdrXzWyhs12jsO7KyK2wiKAocnRovgTCQQGiaHuJuLBgY2IEWm6Zsdqa5kWRzguWgBjygRftszmsjZ3CMSFsi/jdqxobv2zNC4VoSrRUQozjgVWNjV+mUeFwFp3jda9aErD9k0SC0miXE2cIa8NVSwKraVQ4MtzS0N/BMhqUSARLhrHh2oKNHccxIfxas31K1O0W2QPiRBH4fV64RVtn1P61oelnwsLXloU2wDIadEsu9oA4UQR+nwC3ZN8489UrwrRWzdB8fbPNC4ogSXDR+2rJuPxuCMxu23J4nHFMCAseZE4R9JMLNacI2t0P4StN06+tUlHGLQUPMqcIBUgEnYK1JWtrAmA9yAi0S9QxBPvy2lrX8shVTn+HI0J40+yWz1nTokCjQSfx+7wQhU/myjmAZ21ey3xrZb1tN5ko8DQadBC/T4AofNJVcJzJ38jYvJZp+sKUS20ZgotGgw7i8rshWDYcmRxczVdPuWiMW84aR4Rw1bTGL8AyLRrw09uQ0zA2bS/YnADw+SWBr8AyLRrw03EJp2FseuGqIZsTAMS/Di6HdW0wQP2f0/B2m67llgY/6+jzS31Ae30ocp7fv9Ca5ychdBzWpuf5/QspgC/wV/N89fNbJFv7C/hpNOg0AWaEPb9FWkgBfIcC7pozPXOseS4/zYY5DWtTfqZnjpMBfEsWwhUNDSvBeJFx8eTKymlcPD/M20zB9jXN3y72rwTjRcZFzc9xXC4M8zazYrF/VbXKM17wXBpcCsaLDE8N0HF4Fz/M20zB9s48v9QHXFHfYPPp6PPS21C58DG71q+or1s+yqU1w+Vtviutab+XOqFy4ffaR4Ws7WsRfnHAFibNTSdLygZrW9b2pVBSr7Eg5Ofn+L3zrXlsZ004B/uSMcfvn78g5K/Znn9Bi4c/t0li2h958SgXXsa25zZJ8xe0eGq2/QEAZkgzrUnOR2dXy8Uw2zK2L4WSGnF7NLIUlmlRr8dDYZbKCMdx8HrswXsLdVCTtM/3Xg7LtKjX46L2V0Z4joPXY58evWyoDmoSNiKC4HGDp/ZXNniOg+CxD7ScikpRkhBeHA7b4r153PQ2VG5YG7N1UEtcNM9jmxrxuGmTTLlhbXwxUwe1hDjfuwiW9UHeQ7uVyw1j47WFOij9uaXc3BYOXmhNeyQSwnLD2pitg1qibY7XdpbI467tWbpKwNp4EVMHNcVc7wXWJO8mISw3w2zM1EHRzy3l5iaP59QcLc8BEjWEssPa2FoHtUZTg+vUb+c4E24KvFt23BJvC8/UbKmDmmOKOO3knwZnwkX9X9lxuUV7eCZLHZRC0T3Hiql1rda0SNHnK4ab+Ydj66IWWNEWmGVNSyJNi1YKye57dFhd1AJTFwfrrWlJpP6vUrC2ZuuiGIoWwjk+7zxYNspYXYAR5YWxdXuhLmqK2dPcc8GEXCIqAxuaqVAXNYXYLLXCen5QohexSuESmXXCobooiaKFsNXrtb0FigI1hErB2pqti1qgZarAtD86NlEpWFuzdVEL8I1SkzXNUf9XMUzR3v7YuiiGooWwyeudYU27KAp9xWBtzdZFLdDUINjeAqkfqhysrZvqxZqbmueniI22DHoRqxyMrYfVRREULYRTJGmKNS2SEFYM1tZsXdQCU8P2RXKB3FpVDNbWUyO8IxsWJhQRwbYuRW7VKscwWzN1UdQzi72xThTqbA8SqCFUCp7x5crWRS1QF+Rtv9nF0xphpWBtHQ3WXvszQy6bw3EXCWHFYG3N1kUxFF17AUEM2R9EHVGl4JmOiK2LWiDg4+ztj4SwYrC2DjJ1UQsYft4eeZynGbGKwdh6WF0U88hib/TynC1sOkcdUcVgbc3WRS3gkZj2R66tKgZra7YuagFOctl8fZFrtcrBW88RYnhdFPXMYm8UeN62h5U6osrB2pqti1pAdLHtzxztUsJhWFsLAldz7Y932ftOg9pfxTCY/o+ti2KgiW1iQmJSx0MQhEMULYSaYajWtGlSx1QpWFuzdVELaJrJtD+akagUrK3ZuqgFDB2GNc1T+6sYPNP/sXVR1DOLvTFrmBlr2jRICCsFa+usYWRGuXTSklOY9kcvYhWDtTVbF7WAqeh5a9qg9lcxDOalg8sb+VEuPWOKFsK0piataYOEsGKwtk5rWnKUSyct6YxJ7a9KsLZOMXVRC/CykbZlGHqVSlKDsLbO6OmRLzxzihbCQVUbtKYNo+TRKXGGsLZm66IWGEwZtt+skxBWDNbWsVTttT8uqcetaV2n/q9SsLZm66IYihbCE4pywppWdXojqhSsrU/k7XVRCxyPa33WtEYdUcVgbX08bvSNcunkJa4NWJMGtb+KMczWTF0UQ9FC2JvNHrWmdRLCisHaujdnr4taoHdA7bamNa1aJak9WFuzdVELGCfUY7YMjfq/isHYelhdFEHRQtidzX5oTavUE1UM1tZsXdQCR45rTPujjqhSsLbuZuqiFjCOKb3WtEn9X8Vgbc3WRTEULYSHMtn3AHSeTKtaze2grhqqYmsInYW6qCk+6MsfBNB1Mq2qtEZYKRhbd304VBc1hdqjdAPoOJk2FRLCSsHYuqNQFyVRtBDuOD5o+3JVISGsFHlVsaXZuqgFduxJ20YhikIjwkrB2pqti1rg+Fsp27qUolL/VylYW7N1UQwleZbpzeUOn/zbMAElT42h3OQYG1vroNbo6ddP/XYTQF6hDQvlJq8YsI4HrXVQc5xQT20S4k0OOvV/ZUfPqzbnBdxx1ZGNWiUJ4Z5EqsuazinKaJcSDqEwNmbroJbYeyj7pjWdy5MQlptc3j4FyNZBTXEw+7Y1aZAQlh3Wxub79joolpKE8I1EotOazuVJCMsNa2O2DmqJN97L7bam2U6acB72ZYOtg1pCPZDdC8s6oZEjISw3jI07CnVQMiUJYWcsvguWDTPZXI5cXZUR0zSRzeWsWZ2FOqhJXjuQfRWWDTPZnE6ursqIYZrI5mzrg12FOqhJPn5h4C/WtJbLU/srJ8aQja2wdVAsJQnh/qRsHJLlA9a8TLZkt2/EKGSyNhHEIVk+sD8p1+x84P4jOeP9XsXW/rJZ2jRTLljbvt+rHNh/JFez7Q8AcFSxrZGaGZoVKxc6qy2M7Uuh5DBMrwwM7rSm2c6acA72JYO1fS3y6p7Mi9a0nKXp0XLB2pa1fS1ivJW2TQ3naSBQNljbmm+mHZuWL1kId/T3Pw/L9Ggmm4VOfkcdRzcMZLJZa1bnjhP9z1erPOOFHW/J22GZHs1kdZCTI+fR9SHbWugq2L6myf05tQvW84SZHLlbKwOGbsDM2AZZHdnXk44tC5UshJ0Dyfi7srzPmifL2dEuJ4okzdj0XVne1zmYLNnZ7ETntfcy8QNHFFv7S2doVOg0aWbK70C3uu+19zI13/76D8h543DukDVPl2lWzGmG2fSj3KH+AxnHht+ORKjf3nfsP2EZFbKdNlE6sixbk52/7Tv2q2qVZbzxu93pX8IyKkzLtHvPadKyfTS4/X9Sv6xWWcYb5q7U72EZFSpp6v+chrFph/7fyd87+XxHhPCRD478l4lPogSrmgo5Q29FTiFnsjb/jiZgPPrBkZeqWKRxxaPPDzxvmpyl/RmQaVToGHJGg6p9Mt1nmpzxr88P1Py0/El6njvxJmfik39QTYcu01qhU+hy3uZomzOh9zzX7+j5VUeEEAB+2fvx09Z0Si45ViJRIJWyBwDf1tv7VJWKMm7Z9mrySWs6mSYhdArWlqytCUDfGX8BllGhlpLHuJo4GxhbdhRs7SiOCeG/H+3pgGV6NJ+nUaETyJks61u085mjvU9WqTjjlmdeSWyFZXo0r+g0KnQAOaMhb/ct2vXMK/Gt1SrPeOXolt7nrGlN0WhU6AC6nIfGODRnbe0EjgnhnoSc337Mvm6VTKacenzNkkzaR9bbjx371Z4E/Yex7P0om9++O73NmhdP0ZmuUmFt+Nvd6Wf2fpSj9jcC5q7kS7CuFSYzY1xNnAmMDTsKNnYcx4QQAB4/fOQRWEaFiqYhSVMERZNIyVDssbc6CzYmRuDxF+MPggnNlEjRqLBYEillWMiljhdjm6tVnvHOkYeO2l7EDFWFSmJYNGoyA4OJNMHa2CkcFcI9CTn/C2aKNJFIQlPpYNfZoqk6komkNavzF0d7Omg0ODp7P8rmn96Z3AKLGMaTOWgaub06WzTNRDxh64S6nt6Z3EKjwbExX4z9BpZRYS6Rouj1xaDpQ7b7hI6CbcuCo0IIAD945/2tSU2LnUwbAGI0RXrWxJIpWI/lJjUt9oN33qe1mdNw51PHtiQzxqn2Z5ocBhN0nOJsGUzkbeGWErIZu/OpY1uqVqAJwpGfffw8ZOPUegZvcsjEaVbsbMnEZVu4JchG+sjPPi7bTmXHhRAAHvzgo/vAeJtJpWmK4ExJpTPDvMgUbEqcAQ/+evAe2LzNqEilSQzPlGRaG+ZF5qHfDNxTrfJMNNRtJ7aC8Tajp+hs4Zmip7LDvMgUbFo2yiKEjx/u+cOuwcGXrXmD8QTyFMX+tOQVFYPxhC1v1+Dgy48f7vlDlYo04ejYEXv5j/syNj+sA3EFCgXuPS15xcBg3D77+cf/ze7s2BF7eZRbCIaPXxj4C/bIf4Z1ijSWgqnQevXpMBUNuZh9ShR75D87FWViNMoihADwk4Mf3KUahq3zjsWSFKZpDEzTRCxmWxeEahh/+MnBD+6qUpEmLPc9O3C7opm2WI39MQWmdbqFsGGaHAZi9l2iimZ23vds/+1VKtKEpfvuw1tMzbC9+WcGk+Cp/xsV3jSRGbT3f6ZmqN13Hy77lHzZhHB/UjY2HTz0A1jPFqoKBmI1755wVAZi8WFnBn908P3baznUUrHsP5Iz7n564HuwTJEqqo7+GJ1tHY3+WA6KfWNb193/NvC9/UdrPNRSkWhPHN8My6gQiobcAO2XGI3cQAqwj5o79I7jD1biu8smhADw8+7e3dt6P34KFjGUMznEEtQYWGKJFOuAoPPZ3t4nn+r+uGYj0JfKUy/Hdj37SqoDFjGUMxoGE7TxkWUwkWcdEHQ9+0qq46lXYjUb+LlUPv794NvGjvjvYBFDNZODFiOvWyxaLA2VWRc0dsR/1/vS4IHR7nGSsgohAHxv/3s/fSOe6IRFDJOpNBJJagwnSSTTSKZs9uh8PZ740237D9IuvRK5/Ym+R15/N/8nWMQwmdIQT9J69UniSRVJ+3nLrtffzf/p9if66MxqiRzd0vsc3snuhUUM86kMtARtHjyJlsggb3cj2cG9ndlbDg8yo1F2IQSAL72+59u9uZwtmnA8maLD9gCSKRlx5nhJby53+Muv7/k/VSrSpOMf7um+uadfZ9qfQoftMXRoPp60rwv29OuH/+Ge7purVKRJR/ddH27mjqt91rx8Ik2H7TF0aD6fsA+KuONq3+EfflRRxw0VEUIA+Pbb71yfNQzbTr5YIlnTI8NEMo2Y/dA8srqx89v737m+SkWatHznsb7rMnnTNs0XS+RremQYT6qIMWcsM3lz13ce67uuSkWatMibe3+EvPFTa54ST9f0yFBLZKDEmf4/b/xU3tzzo0qXpWJC2DmQjN+8b/86E9htzY8nUzW5ZhhLpCGUJnwAAAm3SURBVIaNBE1g981/2b+OAu46z2vvZeIbH+271jQ5W/iWeFKpyTXDwUR+2EjQNLk3Nz7Sdy0F3HWe/gNyPvfPPXdxJmzn4fKJdE2uGWqx9PCRoImtuX/uucvJgLtnSsWEEAB2HB/svnHf/jWsGCZTafQPxmriaIVpmugfjLFrgjCB3Tfu27dmx/HB7ioVbdKzc0+6+8bNvV9lxTCZ0nBiMF8TRytMk8OJAYVdE4Rpcm/esLl39c69aWp/ZeL4W6mB7P09dwwTw1QGSn9tHK3gTRNKf5JdEwRnYmv2/iN3HH8rNVCNchX9n1+KaK2YWtf68MIFj3t5frk13y1KiEZDcEti0c8ezwwdlk9CUexv4lnd2HnzX/avIxGsDCvaArMe+ta0n/vc3FJrviS60BCVIEkVfT+sGHnFwEBMYY9IIJM3d218pO9aEsHKMHVxsN7z3eZNcPM32D6QBPjqQuAkoUolKy+mog2dE2QcC/A545HMv/T831JEkONKe4mtihACQHt9KPLABec/1uTxzATQbv2sLhJGMOAr6fnjjVQ6M8xjDIDO3lzu8Lf3v3M9TYdWlsvm+SL/cv20J5obXDMBXGj9rD4iIRiYXC9jqbSKgfiwsFRdPf364e881ncdTYdWlob5frfvlqYfYoo4DcBa62eeaBCuoLdKJSsPeirLeowBgA7uuNonb+75UanToRNWCE/y60vaHrg4Em4HI4Y+rxfRUBCC6HLke6qFpuqIJVOs71CgcESCdodWl/+4o/XhS85zfxqMGPq8LtSF3RCEiT1dqmkmBhN51ncoUDgiQbtDq0vrplkbcb53ERgx5Hwe+CJ+QJjY/R80HZm4zPoOBQpHJJzaHTrhhRAA7l8w74bVTdPXgBFDAIiGQwgF/Y59VyVJpuRhu0ILdD7b2/sknRMcH9x73bSbrrkiuBaMGHIAImE3wsGJOVWVSCmIJ1SM8J/a9ewrqQ46Jzg+mLGh6Wp+ReTzYMTQ4Ex4wkGIoYk5O6YmR9gVOkSHsSP+OyfPCU4KIQSAb7Q2Lblr7pwfizz/GfYzSRAQCgXg902M6QI5k0UymWaD6gIY8h36o4Pv304eY8YXa5ZFl955bf39ksANexkTRQ6RoAS/b2IIopzREB8eVBfAkO/Qu/9t4HvkMWZ8Mf2zdRcI103byAkQwQgiL4qQQj64/O4qle7s0OU8lBGC6gLoMDVD1TuOP+i0x5hJI4QAsCDk578/d/ampXV1yzDC6NDtFhH0+8etIMqZLFKpDOsv9CSdfxyM7bz34KF/It+h45MFLR7+tmvq7/2bhb7lYEaHAOCWXAgFhHEriHJGQzKtIa+MGAi264//m91537P9t5Pv0PFL650zN6DNfykYMQQAQRIgBP3jVhB1OQ8tJUMbOcpGB/bIfy6XA+1JJYQnWTez+TO3zp51W0hwRTGCIIqCiIDfC7/fCxdf3R1+umEgLWchyzLUkSNRdyY1LfbAhx/9pOOjHnoLnwCsXRFdduuX6u4I+fgoRhBEUeAR8IsI+AS4qryEo+tAOqMgLetQtRH1rSshm7GHfjNwD4VSmhhMv6r+U65rGtbzPlcAIwgiBBekgBcuvwe8q7r9n6Eb0OUclHQWGLn/64BspNVtJ7aWM5TSpBTCk/z4/HPXf31G81qMIIYn8Xm98Hnd8Hk9JRvjTDFNE5lsDplsfqRNMFY6n+7p3XrH2wc7xrqIGJ/cvaZxw7XLQxswghiexOd1we8V4PW6wFeo/RmmiWxWh5wdFkCXpevpncktFFl+YtLyzekruSujXywkhwsihjbVuL1ucD6pYu0PBqBn88hn8yNtgjlJBwCYL8Z+U87I8ieZ1EIIAG0Rv3tda8tNqxobv4wxBBEAvB4PPG4JHkmC5HZ2+3sur0JRFOTyCrK504by6fztsWPbOg4f+emehFx7bksmEYvO8brXXRm5ddWSwGqMIYgA4PW44HEL8Lh5uB0+i5hXDOTyBnJ5DdncmOIHAF2/3Z1+puPF2Oa9H+Wo/U1wWm6ZsZpbGvq7QnJEQQQAweMG7xHBu0W4HO7/9LwKI6/CyKnQxm5SQwK4K/nSkYeObnO0EGMw6YXwJG1hv/trM5rXfqVp+rXckEecMUURGDqgL0oCREGAy+WC6HKB53nwPAeO504ZzzRNmIYJwzBhGAZUXYeu61A1DaqiQVUVnMGiSqcJGNt6e5965mjvkySAk4tF53jdX70ivH715aF/5DiTx2lEERhaUxRFDqLggiAAgouHi+eG2h/HgeOG/odMk4NpDrU/3TCh6QY0DVA1HapqQlH0kXZ+snSZJmdsezX55DOvxLeSAE4+Zmxoutq1PHKVycGFMQQRGNpxKokiOEkAN9T4wLt4uFw8wLvAcyaMQv/HmyYMkwMMHbpuwNANQNNhahpMRYOiquBP73WpgzOh6zvjL1QyasRJakYIrdw0u+Vzq6Y1fuE8v38hzkAQy0znu7K877d9x3716AdHXqpyWYgKcOPK+pWrlgS+Mr9FWogzEMQy03WgW923/X9Sv/zX5wfKPgVFVJ/mq6dcxC0Nfpaf6ZmD0whiBejAR7lD+n8nf9/zXP+bp7+8PNSkEJ6kvT4UWdHQsPKK+rrlc/z++aicKHYekuUDrwwM7txxov958gpTm1w2zxdZsdi/6vI235XnNknzUTlR7Hq/Vznw6p7MizvekreTV5japGG+3+25NLiUXxxYghnSzEJ2uYVxaL/DUeWw+WZ6d/b15K5qOMlmqWkhtLIg5Ofbo5GlF4fD7W3h4IUF121A6eLYCQzFCNyTSHW9kUh0dsbiu+gIBGFlQYuHv2y+9/KL53mWLJrjvajgug0oXRy7gKEYgXsPZd98473c7tcOZF/df4SOQBB2pl9V/ylxvncR5novKLhuA0oXxg5gKEag+X72bfVAdm85d38WCwnhGKyYWtc6x+ed1+r1zmryemdMkaQpdaJQFxDEkJfnfALPiwCgGYaaNYxMWtOSg6o2eCKvnOjNZY92Z7MfHspk3yNn2EQxrGgLzJo9zT23Zaowq6lebJ0a4adFg0Jd0MeFPBLnEwRuqP1ppppTzEwqYyZjKW3weNzo6x1Qu7uPax9+2Jc/uGNP+sNq/xZi4jF1cbBebJZa+UapiZ8iNiIi1JshV8Tw8wFOcrl511D0IUOHweWNPDJ6mkvqccS1AeOEesw4pvSqPUp3tSJCnA2VOjFAEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAQxzvj/snGtbrdYI/0AAAAASUVORK5CYII=); height: 32px; width: 100%; background-size: 40px; background-repeat: no-repeat; margin-bottom: 0; border-top-left-radius: 5px; border-top-right-radius: 5px; } } .code-block { position: relative; overflow-x: visible !important; &:before { content: attr(lang); font-size: 12px; position: absolute; top: -34px; right: 0; line-height: 1; z-index: 1; } } .code-inline { margin: 0 2px; } pre, pre div { background: #1a1a1a !important; } img { background: none !important; } * { // text wrap word-wrap: anywhere; word-break: break-word; } } .bing { display: inline-flex; flex-direction: row; align-items: center; vertical-align: center; gap: 6px; color: #2f7eee; background: #e8f2ff; border-radius: 12px; padding: 6px 12px; font-size: 16px; margin: 6px 0; width: max-content; user-select: none; word-wrap: anywhere; max-width: 100%; svg { width: 20px; height: 20px; flex-shrink: 0; } } ================================================ FILE: app/src/assets/pages/generation.less ================================================ .generation-page { position: relative; display: flex; width: 100%; height: calc(100% - var(--navbar-height)); .login-action { display: flex; flex-direction: column; justify-content: center; align-items: center; margin: auto; transform: translateY(-28px); .tip { display: flex; flex-direction: row; align-items: center; margin-bottom: max(6vh, 24px); user-select: none; } .text { font-size: 1rem; } } } .generation-container { display: flex; flex-direction: column; padding: 12px 16px; gap: 6px; width: 100%; height: 100%; .action { flex-shrink: 0; } .generation-wrapper { display: flex; flex-direction: column; align-items: center; flex-grow: 1; padding: 15vh 0; .box { display: flex; flex-direction: column; align-items: center; width: 80%; max-width: 680px; height: max-content; margin: 6px 0; gap: 12px; .message-box { width: 100%; height: max-content; min-height: 120px; border-radius: var(--radius); border: 1px solid hsl(var(--border)); color: hsl(var(--text-secondary)); padding: 0.6rem 1rem; font-size: 10px; overflow: hidden; text-overflow: ellipsis; } .quota-box { display: flex; flex-direction: row; align-items: center; width: max-content; height: max-content; border: 1px solid hsl(var(--border)); border-radius: var(--radius); user-select: none; padding: 4px 12px; transition: .2s; cursor: pointer; &:hover { border: 1px solid hsl(var(--border-hover)); } } .hash-box { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1rem; flex-wrap: wrap; width: max-content; height: max-content; padding: 1rem 1.5rem; .download-box { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.5rem; min-width: 10rem; width: max-content; height: max-content; padding: 1rem 1.6rem; text-decoration: none; cursor: pointer; user-select: none; border-radius: var(--radius); border: 1px solid hsl(var(--border)); transition: .2s; color: hsl(var(--text)); font-size: 1rem; font-weight: 500; background: hsla(var(--background-container)); &:hover { border: 1px solid hsl(var(--border-hover)); } } } } .product { display: flex; flex-direction: row; align-items: center; text-align: center; font-size: min(2rem, 7vw); gap: 12px; user-select: none; white-space: nowrap; img { width: min(3rem, 14vw); height: min(3rem, 14vw); } } } } .model-box { width: 80%; margin: 0 auto; max-width: 680px; } .generate-box { display: flex; flex-direction: row; width: 80%; margin: 2rem auto; max-width: 680px; .input { flex-grow: 1; text-align: center; font-size: 1.25rem; height: 46px; border-radius: var(--radius); border: 1px solid hsl(var(--border-hover)); letter-spacing: 1px; margin-right: 8px; } .action { width: 46px; height: 46px; } } ================================================ FILE: app/src/assets/pages/home.less ================================================ .main { position: relative; display: inline-flex; flex-direction: row; width: 100%; height: calc(100% - var(--navbar-height)); overflow: hidden; @media (orientation: portrait) { flex-direction: column-reverse; .home-page { height: calc(100% - 9rem); width: 100%; } .toolbar { flex-direction: row; height: max-content; width: 100%; border-right: none; border-top: 1px solid hsl(var(--border)); & > * { margin-bottom: 0; margin-right: auto; &:last-child { margin-right: 0.5rem; } &:first-child { margin-left: 0.5rem; } } } .sidebar.open { width: 100% !important; } .sidebar .sidebar-menu { display: none; } } } .model-market { display: flex; flex-direction: column; width: 100%; padding: 0 1.5rem; height: 100%; overflow: auto; scrollbar-width: thin; .market-wrapper { width: 100%; height: 100%; margin: 0 auto; max-width: 1400px; } @media (max-width: 768px) { padding: 0 1rem; } .title { font-size: 24px; } & > * { flex-shrink: 0; } .search-bar-wrapper { margin: 1rem auto; width: calc(100% - 1rem); .search-bar { position: relative; margin-bottom: 0.5rem; } .search-icon { position: absolute; top: 50%; left: 1rem; transform: translateY(-50%); width: 1rem; height: 1rem; } .clear-icon { position: absolute; top: 50%; right: 1rem; transform: translateY(-50%); width: 1rem; height: 1rem; opacity: 0; transition: 0.25s; cursor: pointer; &.active { opacity: .8; &:hover { opacity: 1; } } } .input-box { padding: 0 2.5rem; } } .model-list { display: grid; grid-template-columns: repeat(3, 1fr); justify-content: center; width: 100%; @media (max-width: 1024px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 768px) { grid-template-columns: 1fr; } } .model-item { position: relative; display: flex; flex-direction: column; user-select: none; padding: 0.5rem 0.5rem; margin: 0.5rem; border: 1px solid hsl(var(--border-hover)); transition: 0.25s; transition-property: border-color, padding, background, box-shadow; cursor: pointer; animation: fadein 0.25s forwards ease-in-out; opacity: 0; width: calc(100% - 1rem); min-width: 0; svg { flex-shrink: 0; } @keyframes fadein { from { opacity: 0; transform: translateY(2.5rem); } to { opacity: 1; transform: translateY(0); } } .model-name { color: hsl(var(--text)); font-weight: medium; } .model-id { color: hsl(var(--text-secondary)); font-size: 14px; margin-top: 0.25rem; background: hsl(var(--muted)); } .model-description { color: hsl(var(--text-secondary)); margin-bottom: 0.25rem; } &:hover { background: hsla(var(--background-hover)); border-color: hsl(var(--border-active)); .grip-icon { opacity: 1; } } &.active { border-color: hsl(var(--border-active)); box-shadow: 0 0 0 1px hsl(var(--text-secondary)); .grip-icon { opacity: 1; } } .grip-icon { opacity: 0.6; transition: 0.25s; } .model-avatar { border-radius: 50%; width: 3rem; height: 3rem; @media (max-width: 768px) { width: 2.5rem; height: 2.5rem; } } .model-action { button { background: none !important; } @keyframes rotate { 0% { transform: rotate(0deg); } 25% { transform: rotate(25deg); } 50% { transform: rotate(0deg); } 75% { transform: rotate(-25deg); } 100% { transform: rotate(0deg); } } svg { animation: rotate 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); } } .market-tip { transform: translateY(1px); } .model-name { display: flex; flex-direction: row; align-items: center; .badge { transform: translateY(-2px); } &.pro { p { // gold color gradient background: linear-gradient(to right, hsl(45, 100%, 70%) 0%, hsl(46, 100%, 58%) 50%, hsl(46, 100%, 50%) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .badge { color: rgb(164, 128, 0) !important; background: rgb(255, 231, 145) !important; } } } .model-tag { display: flex; flex-direction: row; flex-wrap: wrap; margin-top: 0.25rem; transform: translateY(0.25rem); } } .market-header { position: relative; width: 100%; height: max-content; margin-top: 1.5rem; .header-bar { width: 0.75rem; margin: 0 1rem; aspect-ratio: .75; background: var(--anim-bar) 0% 50%, var(--anim-bar) 50% 50%, var(--anim-bar) 100% 50%; animation: l7 1s infinite linear alternate; @keyframes l7 { 0% {background-size: 20% 50% ,20% 50% ,20% 50% } 20% {background-size: 20% 20% ,20% 50% ,20% 50% } 40% {background-size: 20% 100%,20% 20% ,20% 50% } 60% {background-size: 20% 50% ,20% 100%,20% 20% } 80% {background-size: 20% 50% ,20% 50% ,20% 100%} 100%{background-size: 20% 50% ,20% 50% ,20% 50% } } &.reverse { background: var(--anim-bar) 100% 50%, var(--anim-bar) 50% 50%, var(--anim-bar) 0% 50%; } } .close-action { position: absolute; } } .market-footer { margin-top: 3rem; margin-bottom: 2.5rem; user-select: none; padding: 0 1rem; a { display: flex; flex-direction: row; align-items: center; justify-content: center; font-size: 14px; color: hsl(var(--text-secondary)); transition: 0.25s; width: max-content; max-width: 100%; margin: 0 auto; text-align: center; svg { flex-shrink: 0; } &:hover { color: hsl(var(--text)); } } } } .conversation-name { color: hsl(var(--text)); font-weight: bold !important; word-wrap: anywhere; } .chat-action { @keyframes up { 0% { opacity: 0; transform: translateY(100%); } 100% { opacity: 1; transform: translateY(0); } } animation: up 0.2s ease-in-out; } .web { color: hsl(var(--input-unread)); transition: .25s linear; &.enable { color: hsl(var(--text)); } } .toolbar { position: relative; display: flex; flex-shrink: 0; flex-direction: column; height: 100%; margin: 0; width: max-content; padding: 0.5rem; background: hsl(var(--background)); border-right: 1px solid hsl(var(--border)); transition: 0.25s; &.stacked { @media (orientation: landscape) { width: 0; padding-left: 0; padding-right: 0; border-right: 0; } @media (orientation: portrait) { height: 0; padding-top: 0; padding-bottom: 0; border-top: 0; } .toolbar-text { display: none; } button { width: 0; flex-shrink: 1; } } .bar-kit { position: absolute; opacity: 0; transition: 0.25s ease-in-out; cursor: pointer; z-index: 64; border-radius: 0.25rem; background: hsl(var(--background)); width: max-content; height: max-content; border: 1px solid hsl(var(--border-active)); @media (orientation: portrait) { top: 0; left: 50%; transform: translate(-50%, -100%); padding: 0 0.75rem; border-bottom-left-radius: 0; border-bottom-right-radius: 0; svg { rotate: 0deg; } &.stacked svg { rotate: 180deg; } } @media (orientation: landscape) { top: 50%; right: 0; transform: translate(100%, -50%); padding: 0.75rem 0; border-top-left-radius: 0; border-bottom-left-radius: 0; svg { rotate: 90deg; } &.stacked svg { rotate: -90deg; } } } &:hover, &:focus-within, &:focus, &:active { .bar-kit { opacity: 1; pointer-events: all; } } & > * { margin-bottom: 0.75rem; &:last-child { margin-bottom: 0; } } } .sidebar { display: flex; flex-shrink: 0; flex-direction: column; width: 0; height: 100%; padding: 0; margin: 0; background: hsl(var(--background)); transition: 0.2s ease-in-out; transition-property: width, background, box-shadow, border-right, opacity; border-right: 0; pointer-events: none; opacity: 0; overflow-x: hidden; &.open { width: 260px; border-right: 1px solid hsl(var(--border)); pointer-events: auto; opacity: 1; } &.hidden { width: 0; border-right: 0; } .sidebar-content { display: flex; flex-direction: column; width: 100%; height: 100%; padding: 4px; } .sidebar-menu { height: max-content; width: 100%; .sidebar-wrapper { display: flex; flex-direction: row; align-items: center; justify-content: center; height: max-content; width: calc(100% - 0.5rem); margin: 0.25rem; img { width: 2.5rem; height: 2.5rem; padding: 0.2rem; border-radius: .5rem; transform: translateY(0.05rem); flex-shrink: 0; } .username { margin: 0 auto 0 8px; color: hsl(var(--text)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; font-family: var(--font-family-normal); } svg { color: hsl(var(--text-secondary)); } } } .conversation-list { position: relative; display: flex; flex-direction: column; gap: 6px; width: 100%; height: 100%; padding: 6px 0; overflow-x: hidden; overflow-y: auto; touch-action: pan-y; user-select: none; scrollbar-width: none; -webkit-overflow-scrolling: touch; transition: 0.2s ease-in-out; .empty { color: hsl(var(--text-secondary)); font-size: 14px; margin: auto; user-select: none; } .conversation { display: flex; flex-direction: row; vertical-align: center; align-items: center; width: calc(100% - 12px); height: max-content; cursor: pointer; margin: 0 6px; padding: 10px 12px; border-radius: var(--radius); border: 1px solid hsl(var(--border)); transition: 0.2s ease-in-out; background: hsl(var(--card)); .more { color: hsl(var(--text-secondary)); scale: 0; opacity: 1; transition: 0.2s; transition-property: color, opacity; border: 1px solid var(--border); outline: none; height: 0; width: 0; &:hover { color: hsl(var(--text)); } } &:hover { border-color: hsl(var(--border-active)); .id { color: hsl(var(--text)); } } &.active { background: hsl(var(--card-hover)); border-color: hsl(var(--border-active)); .id { color: hsl(var(--text)); } } } .id { color: hsl(var(--text-unread)); } svg { flex-shrink: 0; } .title { flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 16px; user-select: none; margin: 0 4px; color: hsl(var(--text)); } &::-webkit-scrollbar { width: 6px; } } .sidebar-action { display: flex; flex-direction: row; flex-wrap: nowrap; margin-bottom: 0.25rem; .refresh-action { &.active { svg { animation: RotateAnimation 0.5s linear infinite; @keyframes RotateAnimation { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } } } } } .login-action { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; width: calc(100% - 1rem); margin: auto auto 0.5rem; svg { transform: translateY(1px); } } @media (max-width: 768px) { &.open { width: max(30vw, 180px); } } @media (max-width: 468px) { // sidebar collapsed &.open { width: calc(100% - 3.5rem) !important; } &.open ~ .chat-container { width: 0; } } } .chat-container { flex: 1 1 auto; min-width: 0; height: 100%; transition: width 0.2s ease-in-out; .chat-wrapper { display: inline-flex; flex-direction: column; width: 100%; height: 100%; } .tooltip { user-select: none; strong { font-weight: 600 !important; } } .chat-product { position: relative; display: flex; flex-direction: column; flex-grow: 1; width: 100%; overflow: hidden; justify-content: center; align-items: center; button { margin: 0.5rem 0; white-space: nowrap; } .space-footer { position: absolute; display: flex; flex-direction: column; align-items: center; bottom: 6px; user-select: none; color: hsl(var(--text-secondary)); padding: 1rem; z-index: 10; * { font-size: 14px; } p { white-space: pre-wrap; text-align: center; &:first-child { margin-bottom: 4px; } } a { color: hsl(var(--text)); text-decoration: none; transition: 0.25s; cursor: pointer; } } } .chat-content { overscroll-behavior: none; flex: 1 1; width: 100%; overflow: hidden; .chat-messages-wrapper { width: 100%; height: 100%; display: flex; flex-direction: column; padding: 18px 24px; } .message { margin-bottom: 0.75rem; &:last-child { margin-bottom: 0; } } } .chat-input { flex-shrink: 0; width: 100%; overflow: hidden; .input-wrapper { position: relative; display: flex; flex-direction: row; align-items: center; flex-wrap: nowrap; width: 100%; height: min-content; .chat-box { position: relative; flex-grow: 1; } .input-box { resize: none; width: 100%; color: hsl(var(--text)); white-space: pre-wrap; padding-right: 3.25rem; &.align { text-align: center; } &::placeholder { color: hsl(var(--text-secondary)); opacity: 1; transition: opacity 0.3s ease, visibility 0.3s ease; } &:active::placeholder, &:focus::placeholder { opacity: 0; visibility: hidden; } @-moz-document url-prefix() { &::-moz-placeholder { opacity: 1; transition: opacity 0.3s ease, visibility 0.3s ease; visibility: visible; } &:active::-moz-placeholder, &:focus::-moz-placeholder { opacity: 0; visibility: hidden; } } } .action-button { position: absolute; right: 0.75rem; bottom: 0.75rem; } } .input-options { margin: 12px auto -8px; display: flex; flex-direction: row; align-items: center; justify-content: center; flex-wrap: wrap; gap: 4px; height: min-content; } } } .share-wrapper { display: flex; flex-direction: row; gap: 6px; width: 100%; input { text-align: center; font-size: 16px; cursor: pointer; flex-grow: 1; } button { flex-shrink: 0; } } .contact-image { margin-top: 0.75rem; max-width: min(60vw, 420px); max-height: 50vh; border-radius: var(--radius); } .version { display: flex; flex-direction: row; align-items: center; user-select: none; font-size: 14px; color: hsl(var(--text-secondary)); transform: translateY(4px); width: max-content; margin: 0 auto; .app { margin-right: 2px; padding: 2px; width: 24px; height: 24px; color: hsl(var(--text-secondary)); cursor: pointer; transition: 0.25s; rotate: 0; &:hover { color: hsl(var(--text)); rotate: 30deg; } } p { font: var(--font-family-normal); transform: translateY(-1px); } } .tag-item { flex-shrink: 0; margin-right: 0.25rem; padding: 0.15rem 0.5rem; border: 1px solid hsl(var(--text-secondary) / 0.25); background: hsl(var(--muted)); border-radius: 4px; font-size: 12px; margin-bottom: 0.25rem; transition: 0.2s; &.clickable { cursor: pointer; } &:hover { background: hsl(var(--muted-foreground) / 0.25); color: hsl(var(--text)); } &.pro { color: hsl(var(--gold)) !important; } &:last-child { margin-right: 0; } } .conversation-id { color: hsl(var(--text)); &:before { content: "#"; color: hsl(var(--text-secondary)); font-size: 12px; } } ================================================ FILE: app/src/assets/pages/navbar.less ================================================ .navbar { display: flex; flex-direction: row; align-content: center; vertical-align: center; user-select: none; height: var(--navbar-height); padding: 0.5rem 0.5rem; background: hsl(var(--background)); border-bottom: 1px solid hsl(var(--border)); .items { width: 100%; height: 100%; margin: 0; padding-right: 0.5rem; display: flex; flex-direction: row; align-content: center; vertical-align: center; } .logo { border-radius: var(--radius); cursor: pointer; } button { white-space: nowrap; } } .avatar { outline: 0; user-select: none; img { cursor: pointer; } } div[data-radix-popper-content-wrapper=""] { user-select: none; div.relative { cursor: pointer; } .username { color: hsl(var(--text)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 120px; &:before { content: "@"; font-size: 12px; margin-right: 1px; color: hsl(var(--text-secondary)); } } .action-button { width: calc(100% - 4px); cursor: pointer; margin: 8px 2px 2px; height: max-content !important; } } ================================================ FILE: app/src/assets/pages/notify.less ================================================ .notify-container { width: 100%; height: 100%; overflow: hidden; background: hsla(var(--background-container)); } ================================================ FILE: app/src/assets/pages/package.less ================================================ .package-wrapper { display: flex; flex-direction: column; .package { display: flex; flex-direction: column; gap: 8px; .package-title { display: flex; flex-direction: row; align-items: center; gap: 4px; font-size: 0.85rem; color: hsl(var(--text)); user-select: none; svg { transform: translateY(1px); } } .package-content { font-size: 0.8rem; color: hsl(var(--text-secondary)); } } } ================================================ FILE: app/src/assets/pages/preset.less ================================================ .mask-container { height: max-content; overflow-x: hidden; overflow-y: auto; scrollbar-width: thin; padding: 1rem 0.5rem 0.5rem; } .mask-drawer-viewport { width: 80vw; @media (max-width: 768px) { margin-left: 0.5rem; margin-right: 0.5rem; width: calc(100% - 1rem) !important; } } .mask-picker-dialog { width: max-content !important; height: max-content !important; padding: 0 !important; .picker { --epr-category-navigation-button-size: 28px; --epr-search-input-bg-color: hsl(var(--background)); --epr-search-border-color: hsl(var(--border-hover)); --epr-bg-color: hsl(var(--background)); --epr-category-label-bg-color: hsl(var(--background)); img { padding: 0.5rem; } .epr-icn-search { width: 1rem; height: 1rem; transform: translateY(-0.55rem); } .epr-body { scrollbar-width: thin; -ms-overflow-style: none; &::-webkit-scrollbar { width: 6px; } } .epr-search-container { input { border: 1px solid hsl(var(--border)); } } .epr-cat-btn { &:focus:before { border: none; } } * { font-family: var(--font-family) !important; font-size: 0.85rem !important; } } } .mask-editor-container { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; padding: 2rem 0; .mask-conversation-list { position: relative; display: flex; flex-direction: column; width: 100%; height: max-content; padding: 0.5rem 1.5rem; margin: 1rem 0; border-radius: var(--radius); border: 1px solid hsl(var(--border)); color: hsl(var(--text)); .mask-conversation-title { margin-top: 0.5rem; margin-bottom: 1rem; } .mask-conversation-wrapper { display: flex; flex-direction: column; margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } .mask-conversation { display: flex; flex-direction: row; padding: 0.25rem; } .mask-conversation:hover ~ .mask-actions { .mask-action { border-color: hsl(var(--border-hover)); } } .mask-actions { display: flex; flex-direction: row; align-items: center; justify-content: center; margin-top: 0.5rem; .mask-action { display: flex; align-items: center; justify-content: center; cursor: pointer; border: 1px solid hsl(var(--border)); width: 36px; height: 24px; border-radius: 12px; margin: 0 0.25rem; transition: 0.2s; svg { height: 16px; width: 16px; padding: 1px; } &.disabled { cursor: not-allowed; } &:hover:not(.disabled) { border-color: hsl(var(--border-hover)); background-color: hsl(var(--background-hover)); } } } } .mask-editor-row { display: flex; flex-direction: column; width: 100%; border-radius: var(--radius); border: 1px solid hsl(var(--border)); } .mask-editor-column { display: flex; flex-direction: row; width: 100%; height: max-content; padding: 0.5rem; color: hsl(var(--text)); align-items: center; border-bottom: 1px solid hsl(var(--border)); &:last-child { border-bottom: none; } & > p { margin-left: 1rem; user-select: none; white-space: nowrap; } & > * { margin-right: 0.5rem; &:last-child { margin-right: 0; } } } } .mask-wrapper { display: flex; flex-direction: column; .mask-header { display: flex; flex-direction: row; width: 100%; } .mask-col { display: flex; flex-direction: column; margin: 0.5rem 0; width: 100%; .mask-col-title { color: hsl(var(--text)); margin: 0.5rem auto 0.5rem 0.5rem; } } } .mask-list { display: flex; flex-direction: column; user-select: none; border: 1px solid hsl(var(--border)); border-radius: var(--radius); overflow: hidden; &::-webkit-scrollbar { width: 0.25rem; } .mask-item { &:last-child { border-bottom: none; } } } .mask-item { display: flex; flex-direction: row; align-items: center; flex-shrink: 0; cursor: pointer; border-bottom: 1px solid hsl(var(--border)); padding: 1rem 0; transition: 0.2s ease-in-out; .mask-avatar { width: 2.25rem; height: 2.25rem; padding: 0.5rem; margin-right: 0.75rem; margin-left: 1rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); transition: 0.2s ease-in-out; } .mask-content { display: flex; flex-direction: column; .mask-name { color: hsl(var(--text)); margin-right: 0.5rem; } .mask-info { font-size: 12px; color: hsl(var(--text-secondary)); white-space: nowrap; max-width: max-content; } } &:hover { background-color: hsl(var(--background-hover)); .mask-avatar { border-color: hsl(var(--border-active)); } } } ================================================ FILE: app/src/assets/pages/quota.less ================================================ .buy-interface { display: flex; flex-direction: row; flex-wrap: wrap; flex-basis: auto; flex-shrink: 0; width: 100%; height: max-content; } .buy-action { width: 100%; margin-top: 1.5rem; .buy-button { width: 100%; transition: .25s; } } .quota-dialog { max-width: min(90vw, 680px) !important; } .amount-container { display: flex; flex-direction: column; gap: 24px; width: 100%; align-items: center; } .other-wrapper { display: flex; flex-direction: row; align-items: center; justify-content: center; width: 100%; gap: 12px; user-select: none; @media (max-width: 460px) { & { flex-direction: column; } } .amount-number { color: hsl(var(--text)); transform: translateY(-2px); white-space: nowrap; } .amount-input-box { position: relative; width: max-content; max-width: 100%; height: max-content; margin: 0 auto; .amount-input { color: hsl(var(--text)); font-size: 16px; text-align: center; } svg { color: hsl(var(--text)); position: absolute; top: 50%; left: 12px; user-select: none; transform: translateY(-50%); } } } .line { background: hsl(var(--border)); width: 1px; min-height: 0; height: 100%; @media (max-width: 980px) { & { display: none; } } } .amount-wrapper { display: inline-grid; gap: 12px; justify-content: center; width: 100%; grid-template-columns: 1fr 1fr 1fr 1fr; @media (max-width: 760px) { grid-template-columns: 1fr 1fr; } @media (max-width: 380px) { grid-template-columns: 1fr; } .amount { position: relative; display: flex; padding: 1rem 0.5rem; flex-direction: column; align-items: center; justify-content: center; border: 1px solid hsl(var(--border)); border-radius: var(--radius); user-select: none; cursor: pointer; gap: 4px; min-width: 6rem; width: 100%; transition: .1s linear; text-align: center; &.active { border-color: hsl(var(--text-secondary)); .amount-desc, .other { color: hsl(var(--text)); } } .amount-title { display: flex; flex-direction: row; gap: 2px; color: hsl(var(--text)); font-size: 16px; align-items: center; svg { transform: translateY(1px); width: 14px; height: 14px; } } .amount-desc { color: hsl(var(--text-secondary)); transition: .1s linear; } .other { font-size: 12px; color: hsl(var(--text-secondary)); transition: .1s linear; &:after { content: '...'; font-size: 10px; } } } } .grow { flex-grow: 1; } .product-item { display: flex; flex-direction: column; padding: 4px 2px; width: 100%; gap: 4px; .row { display: flex; flex-direction: row; align-items: center; gap: 4px; .column { display: flex; flex-direction: row; align-items: center; gap: 4px; svg { flex-shrink: 0; } } .info { margin-top: 6px; } } .title { color: hsl(var(--text)); } .desc { color: hsl(var(--text-secondary)); } } .quota-tip { color: hsl(var(--text)); text-align: center; align-items: center; a { display: flex; flex-direction: row; align-items: center; } } ================================================ FILE: app/src/assets/pages/record.less ================================================ .record-wrapper > * { max-width: 100%; } .record-area { display: block !important; } .stats-boxes { .stats-box { display: flex; flex-direction: row; flex-grow: 1; height: max-content; padding: 0.75rem 1.5rem; border-radius: var(--radius); background: hsl(var(--muted) / 0.25); box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow); user-select: none; width: 100%; margin-right: 1.5rem; margin-left: auto; &:last-child { margin-right: auto; } & > * { flex-shrink: 0; } .box-wrapper { flex-grow: 1; .box-title { font-size: 1rem; margin-bottom: 0.5rem; } .box-value { font-size: 1.5rem; font-weight: normal; } } .box-icon { width: max-content; height: max-content; transform: translate(0.25rem, 0.25rem); border-radius: 0.25rem; svg { width: 2rem; height: 2rem; stroke-width: 1; } } } } ================================================ FILE: app/src/assets/pages/settings.less ================================================ .settings-dialog { max-width: min(90vw, 660px) !important; } .settings-container { display: flex; flex-direction: column; align-items: center; text-align: center; height: 100%; .info-box { display: flex; flex-direction: column; align-items: center; user-select: none; color: hsl(var(--text)); & > * { margin-bottom: 0.5rem; &:last-child { margin-bottom: 0; } } } } .settings-wrapper { width: 100%; height: max-content; margin: 1.5rem 0.5rem; .settings-segment { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } } .settings-segment { display: flex; flex-direction: column; width: 100%; color: hsl(var(--text)); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0.75rem 0; .item { display: flex; flex-direction: row; align-items: center; user-select: none; padding: 0 1rem; .value { transition: .1s; } .slider-value { min-width: 2rem; } input { text-align: center; max-width: 4rem; max-height: 1.75rem; &.large-value { max-width: 6rem; } } button:not(.tips-trigger):not(.set-action) { margin: 0.25rem 1rem; } .select { margin: 0 !important; padding: 0.25rem 0.75rem !important; span { font-size: 0.8rem !important; margin: 0 0.5rem; } } .name { display: flex; flex-direction: row; text-align: start; align-items: center; .tips-trigger { transform: translateY(1px); } } } & > .item { margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid hsl(var(--border)); &:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; } } } ================================================ FILE: app/src/assets/pages/share-manager.less ================================================ .share-table { width: 100%; height: max-content; } ================================================ FILE: app/src/assets/pages/sharing.less ================================================ ================================================ FILE: app/src/assets/pages/subscription.less ================================================ .sub-dialog { width: max-content !important; max-width: min(90vw, 1200px) !important; } .sub-wrapper { display: flex; flex-direction: column; align-items: center; } .sub-row { display: flex; flex-direction: column; padding: 0.5rem 1rem; margin-bottom: 8px; align-items: center; width: 100%; .sub-column { display: flex; flex-direction: row; align-items: center; margin-bottom: 4px; width: 100%; svg { flex-shrink: 0; transform: translateY(1px); } .sub-value { display: flex; flex-direction: row; align-items: center; gap: 2px; p { font-weight: bolder; } } &:first-child { margin-bottom: 8px; } &:last-child { margin-bottom: 0; } } } .plan-wrapper { margin-top: 0.5rem; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; justify-content: center; width: 100%; @media (max-width: 870px) { grid-template-columns: 1fr; } &.disable { grid-template-columns: 1fr; } } .plan { display: flex; flex-direction: column; gap: 6px; padding: 1rem; color: hsl(var(--text)); width: 100%; &.standard { border-color: hsl(var(--text-secondary)); } .title { text-align: center; font-size: 16px; margin: 4px; } .award { display: flex; flex-direction: column; color: hsl(var(--gold)); justify-content: center; align-items: center; user-select: none; font-size: 0.75rem; line-height: 0.8rem; margin-bottom: 1rem; padding: 0.5rem; border-radius: var(--radius); border: 1px solid hsl(var(--gold)); svg { flex-shrink: 0; transform: translateY(1px); } div { word-break: break-word; white-space: break-spaces; text-align: center; } } .price-wrapper { position: relative; width: max-content; margin: 0 auto; .price { font-size: 18px; font-weight: bold; margin: 2px auto; .tax { color: hsl(var(--text-secondary)); } } .annotate { position: absolute; font-size: 14px; margin-top: 0.25rem; font-weight: normal; color: hsl(var(--text-secondary)); right: 0; bottom: 0; transform: translateX(calc(100% + 0.1rem)); } } .desc { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.5rem 0; .api-tip { display: flex; flex-direction: column; align-items: center; } div { svg { flex-shrink: 0; } } } .action { margin-top: auto !important; } } .upgrade-wrapper { margin: 24px auto 8px; .price { font-size: 14px; margin-top: 12px; text-align: center; transform: translateY(12px); .tax { color: hsl(var(--text-secondary)); } } } @media (max-width: 460px) { .plan { min-width: 0 !important; } } ================================================ FILE: app/src/assets/ui.less ================================================ @import "fonts/all"; .gold-text { color: hsl(var(--gold)) !important; } .spinner { position: absolute; top: 0; left: 0; height: 2px; background: hsl(var(--text)); transition: 0.25s linear; transition-property: width, opacity; z-index: 1024; user-select: none; border-top-right-radius: 2px; border-bottom-right-radius: 2px; } .select-group { display: flex; flex-direction: row; flex-wrap: wrap; padding: 6px 8px; border-radius: 4px; user-select: none; justify-content: center; &.mobile { text-align: center; & span { margin: 0 auto; } } .select-group-item { display: flex; flex-direction: row; align-items: center; padding: 0.35rem 0.8rem; margin: 0.25rem; border: 1px solid hsl(var(--border)); border-radius: 4px; transition: .2s; cursor: pointer; font-size: 16px; background: hsl(var(--background)); color: hsl(var(--text)); &:hover { background: hsl(var(--accent-secondary)); } &.active { border-color: hsl(var(--border-hover)); background: hsl(var(--accent)); } } } .select-element.badge { user-select: none; transition: .2s; padding-left: 0.45rem; padding-right: 0.45rem; &.badge-default { background: hsl(var(--primary)) !important; &:hover { background: hsl(var(--primary)) !important; } } &.badge-gold { color: rgb(164, 128, 0) !important; background: rgb(255, 231, 145) !important; } } .no-scrollbar { scrollbar-width: none; -ms-overflow-style: none; &::-webkit-scrollbar { display: none; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: transparent; } } .thin-scrollbar { scrollbar-width: thin; -ms-overflow-style: none; &::-webkit-scrollbar { width: 6px; } // update the scrollbar style &::-webkit-scrollbar-thumb { background: hsl(var(--border)); border-radius: 6px; } &::-webkit-scrollbar-thumb:hover { background: hsl(var(--border-hover)); } &::-webkit-scrollbar-track { background: hsl(var(--background)); } } .horizontal-scrollbar { --radix-scroll-area-thumb-height: 6px; } input[type="number"] { -webkit-appearance: textfield; margin: 0; &::-webkit-inner-spin-button, &::-webkit-outer-spin-button { -webkit-appearance: none; } &::after { content: '>'; position: absolute; right: 5px; top: 2px; transform: rotate(-45deg); } &::before { content: '<'; position: absolute; right: 5px; top: 20px; transform: rotate(135deg); } } .selection { ::selection { color: hsl(var(--selection-foreground)); background: hsl(var(--selection)); } ::-moz-selection { color: hsl(var(--selection-foreground)); background: hsl(var(--selection)); } } .paragraph { display: flex; flex-direction: column; margin: 0.5rem 0; padding: 0.5rem; border-bottom: 1px solid hsl(var(--border)); color: hsl(var(--text)); transition: .25s; cursor: pointer; &.collapsable { .paragraph-content { max-height: var(--max-height); will-change: max-height; overflow: hidden; transition: .5s; } &.collapsed { padding-bottom: 1rem; .paragraph-content { max-height: 0; } } } .paragraph-content { & > * { margin-bottom: 1rem; &:last-child { margin-bottom: 0; } } } .paragraph-header { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; transform: translateY(-0.25rem); } .paragraph-title { position: relative; display: flex; flex-direction: row; font-size: 1.05rem; user-select: none; line-height: 1.1rem; color: hsl(var(--text-secondary)); transition: .25s; text-decoration: none !important; &:before { content: ''; margin-right: 0.75rem; height: 1.25rem; width: 2px; border-radius: 1px; background: hsl(var(--text-secondary)); transition: .25s; } } .paragraph-item { display: flex; flex-direction: row; white-space: nowrap; align-items: center; font-size: 0.9rem; &.row-layout { align-items: flex-start !important; flex-direction: column !important; & > * { margin-right: 0 !important; margin-bottom: 0.75rem; &:last-child { margin-bottom: 0; } } } label { font-size: 0.9rem; font-weight: normal; } & > * { margin-right: 1rem; &:last-child { margin-right: 0; } } } .paragraph-description { display: flex; flex-direction: row; align-items: center; color: hsl(var(--text-secondary)); width: 100%; height: max-content; font-size: 0.9rem; margin: 0.75rem 0 1.25rem; svg { margin-right: 0.5rem; flex-shrink: 0; } } .paragraph-space { width: 100%; height: 0.25rem; } .paragraph-content { transition: 1.5s ease-in-out; } .paragraph-footer { display: flex; flex-direction: row; margin-top: 1rem; & > * { margin-right: 0.5rem; &:last-child { margin-right: 0; } } } &:hover { border-color: hsl(var(--border-hover)); .paragraph-title { color: hsl(var(--text)); &:before { background: hsl(var(--text)); } } } &.config-paragraph { .paragraph-content { input, button { margin-left: auto; } } } } .number-input { transition: 0.25s; transition-property: border-color; } .avatar { display: flex; border-radius: var(--radius); text-align: center; p { margin: auto; color: hsl(var(--text-dark)); } } .text-secondary { color: hsl(var(--text-secondary)) !important; } .chart-tooltip, .recharts-tooltip-wrapper > .rounded-tremor-default.text-tremor-default.border { @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } animation: fadeIn 0.5s; position: absolute; z-index: 64; } .border-input:focus { border-color: hsl(var(--border)); } .animate-fade-in { @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } animation: fadeIn 0.5s forwards; } .text-common { color: hsl(var(--text)) !important; } .text-md { font-size: 0.95rem; } .error-border { border-color: hsl(var(--destructive)) !important; } .button-wrapper.is-loading { .loading-hidden { display: none; } } ================================================ FILE: app/src/components/Avatar.tsx ================================================ import { deeptrainApiEndpoint, useDeeptrain } from "@/conf/env.ts"; import { ImgHTMLAttributes, useMemo, useState, useEffect } from "react"; import { cn } from "@/components/ui/lib/utils.ts"; import { getUserInfo, UserInfo, initialUserInfo } from "@/api/auth.ts"; import md5 from "crypto-js/md5"; import { getConfig } from "@/admin/api/system"; import { useSelector, useDispatch } from "react-redux"; import { setAvatar } from "@/store/avatar"; export interface AvatarProps extends ImgHTMLAttributes { username: string; } async function checkGravatar( gravatar_endpoint: string, email: string, ): Promise { if (!email || email === "root@example.com") { return false; } const trimmedEmail = email.trim().toLowerCase(); const hash = md5(trimmedEmail).toString(); const uri = `${gravatar_endpoint}/avatar/${hash}?d=404`; try { const response = await fetch(uri); if (response.ok) { return true; } console.info("[avatar] gravatar not found:", trimmedEmail); return false; } catch (error) { console.error("[avatar] request failed:", error); return false; } } function Avatar({ username, ...props }: AvatarProps) { const dispatch = useDispatch(); const cachedAvatarBlob = useSelector( (state: any) => state.avatar.avatars[username], ); const [userInfo, setUserInfo] = useState(initialUserInfo); const [hasAvatar, setHasAvatar] = useState(false); const [gravatar_endpoint, setGravatarEndpoint] = useState(""); useEffect(() => { getUserInfo().then((info) => setUserInfo(info?.data ?? initialUserInfo)); }, []); useEffect(() => { getConfig().then((config) => { if ( config.data.general.gravatar === undefined || config.data.general.gravatar === "" ) { setGravatarEndpoint(""); return; } setGravatarEndpoint(config.data.general.gravatar); }); }, []); useEffect(() => { if (cachedAvatarBlob !== null) { setHasAvatar(true); return; } checkGravatar(gravatar_endpoint, userInfo.email).then((hasAvatar) => { setHasAvatar(hasAvatar); if (hasAvatar) { const avatarUrl = getGravatarUrl(userInfo.email); fetch(avatarUrl) .then((response) => response.blob()) .then((blob) => { dispatch(setAvatar({ username, blob })); }); } }); }, [gravatar_endpoint, userInfo]); const code = useMemo( () => (username?.length > 0 ? username[0].toUpperCase() : "A"), [username], ); const background = useMemo(() => { const colors = [ "bg-gradient-to-br from-red-500 to-orange-500", "bg-gradient-to-br from-yellow-500 to-green-500", "bg-gradient-to-br from-green-500 to-teal-500", "bg-gradient-to-br from-indigo-500 to-purple-500", "bg-gradient-to-br from-purple-500 to-pink-500", "bg-gradient-to-br from-sky-500 to-blue-500", "bg-gradient-to-br from-pink-500 to-rose-500", ]; const index = code.charCodeAt(0) % colors.length; return colors[index]; }, [username]); const getGravatarUrl = (email: string | undefined) => { if (!email) return ""; const trimmedEmail = email.trim().toLowerCase(); const hash = md5(trimmedEmail).toString(); if (!gravatar_endpoint) return ""; return `${gravatar_endpoint}/avatar/${hash}?d=identicon`; }; const avatarSrc = useDeeptrain && username.length > 0 ? `${deeptrainApiEndpoint}/avatar/${username}` : hasAvatar && cachedAvatarBlob ? URL.createObjectURL(cachedAvatarBlob) : hasAvatar && userInfo.email ? getGravatarUrl(userInfo.email) : ""; return avatarSrc ? ( ) : (

{code}

); } export default Avatar; ================================================ FILE: app/src/components/EditorProvider.tsx ================================================ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "./ui/dialog.tsx"; import { Maximize, Image, MenuSquare, PanelRight, Eraser } from "lucide-react"; import { useTranslation } from "react-i18next"; import "@/assets/common/editor.less"; import { Textarea } from "./ui/textarea.tsx"; import Markdown from "./Markdown.tsx"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { Toggle } from "./ui/toggle.tsx"; import { mobile } from "@/utils/device.ts"; import { Button } from "./ui/button.tsx"; import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx"; import { cn } from "@/components/ui/lib/utils.ts"; type RichEditorProps = { value: string; onChange: (value: string) => void; maxLength?: number; formatter?: (value: string) => string; isInvalid?: (value: string) => boolean; title?: string; open?: boolean; setOpen?: (open: boolean) => void; children?: React.ReactNode; submittable?: boolean; onSubmit?: (value: string) => void; closeOnSubmit?: boolean; }; function RichEditor({ value, onChange, maxLength, formatter, submittable, isInvalid, onSubmit, setOpen, closeOnSubmit, }: RichEditorProps) { const { t } = useTranslation(); const input = useRef(null); const [openPreview, setOpenPreview] = useState(!mobile); const [openInput, setOpenInput] = useState(true); const formattedValue = useMemo(() => { return formatter ? formatter(value) : value; }, [value, formatter]); const invalid = useMemo(() => { return isInvalid ? isInvalid(value) : false; }, [value, isInvalid]); const handler = () => { if (!input.current) return; const target = input.current as HTMLElement; const preview = target.parentElement?.querySelector( ".editor-preview", ) as HTMLElement | null; if (!preview) { setTimeout(handler, 100); return; } const listener = () => { preview.style.height = `${target.clientHeight}px`; }; target.addEventListener("transitionstart", listener); setInterval(listener, 250); target.addEventListener("scroll", () => { preview.scrollTop = target.scrollTop; }); preview.style.height = `${target.clientHeight}px`; if (openInput) target.focus(); }; useEffect(handler, [input]); return (
{ setOpenPreview(false); setOpenInput(true); }} > { setOpenPreview(true); setOpenInput(true); }} > { setOpenPreview(true); setOpenInput(false); }} >
{openInput && (