diff --git a/Dockerfile b/Dockerfile index 679463a82..d01ab3f0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,17 +20,6 @@ COPY ./web/classic ./classic COPY ./VERSION /build/VERSION RUN cd classic && VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build -FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder-mt - -WORKDIR /build/web -COPY web/package.json web/bun.lock ./ -COPY web/default/package.json ./default/package.json -COPY web/classic/package.json ./classic/package.json -RUN bun install --frozen-lockfile -COPY ./web/mt ./classic -COPY ./VERSION /build/VERSION -RUN cd classic && VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build - FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2 ENV GO111MODULE=on CGO_ENABLED=0 @@ -47,7 +36,6 @@ RUN go mod download COPY . . COPY --from=builder /build/web/default/dist ./web/default/dist COPY --from=builder-classic /build/web/classic/dist ./web/classic/dist -COPY --from=builder-mt /build/web/classic/dist ./web/mt/dist RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a diff --git a/common/constants.go b/common/constants.go index 4f866ca90..ec2cbf1db 100644 --- a/common/constants.go +++ b/common/constants.go @@ -30,9 +30,9 @@ func GetTheme() string { } // SetTheme updates the frontend theme atomically. -// Only "default", "classic", and "mt" are accepted; other values are silently ignored. +// Only "default" and "classic" are accepted; other values are silently ignored. func SetTheme(t string) { - if t == "default" || t == "classic" || t == "mt" { + if t == "default" || t == "classic" { themeValue.Store(t) } } diff --git a/common/embed-file-system.go b/common/embed-file-system.go index 6e0d32453..187292161 100644 --- a/common/embed-file-system.go +++ b/common/embed-file-system.go @@ -48,15 +48,12 @@ func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem { type themeAwareFileSystem struct { defaultFS static.ServeFileSystem classicFS static.ServeFileSystem - mtFS static.ServeFileSystem } func (t *themeAwareFileSystem) Exists(prefix string, path string) bool { switch GetTheme() { case "classic": return t.classicFS.Exists(prefix, path) - case "mt": - return t.mtFS.Exists(prefix, path) default: return t.defaultFS.Exists(prefix, path) } @@ -66,13 +63,11 @@ func (t *themeAwareFileSystem) Open(name string) (http.File, error) { switch GetTheme() { case "classic": return t.classicFS.Open(name) - case "mt": - return t.mtFS.Open(name) default: return t.defaultFS.Open(name) } } -func NewThemeAwareFS(defaultFS, classicFS, mtFS static.ServeFileSystem) static.ServeFileSystem { - return &themeAwareFileSystem{defaultFS: defaultFS, classicFS: classicFS, mtFS: mtFS} +func NewThemeAwareFS(defaultFS, classicFS static.ServeFileSystem) static.ServeFileSystem { + return &themeAwareFileSystem{defaultFS: defaultFS, classicFS: classicFS} } diff --git a/controller/option.go b/controller/option.go index 5fc973b88..8f9f7c364 100644 --- a/controller/option.go +++ b/controller/option.go @@ -204,10 +204,10 @@ func UpdateOption(c *gin.Context) { return } case "theme.frontend": - if option.Value != "default" && option.Value != "classic" && option.Value != "mt" { + if option.Value != "default" && option.Value != "classic" { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无效的主题值,可选值:default(新版前端)、classic(经典前端)、mt(MT 前端)", + "message": "无效的主题值,可选值:default(新版前端)、classic(经典前端)", }) return } diff --git a/main.go b/main.go index bfff19a55..2470e8656 100644 --- a/main.go +++ b/main.go @@ -47,12 +47,6 @@ var classicBuildFS embed.FS //go:embed web/classic/dist/index.html var classicIndexPage []byte -//go:embed web/mt/dist -var mtBuildFS embed.FS - -//go:embed web/mt/dist/index.html -var mtIndexPage []byte - func main() { startTime := time.Now() @@ -201,8 +195,6 @@ func main() { DefaultIndexPage: indexPage, ClassicBuildFS: classicBuildFS, ClassicIndexPage: classicIndexPage, - MtBuildFS: mtBuildFS, - MtIndexPage: mtIndexPage, }) var port = os.Getenv("PORT") if port == "" { diff --git a/router/web-router.go b/router/web-router.go index c0be289cb..288990c0c 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -19,15 +19,12 @@ type ThemeAssets struct { DefaultIndexPage []byte ClassicBuildFS embed.FS ClassicIndexPage []byte - MtBuildFS embed.FS - MtIndexPage []byte } func SetWebRouter(router *gin.Engine, assets ThemeAssets) { defaultFS := common.EmbedFolder(assets.DefaultBuildFS, "web/default/dist") classicFS := common.EmbedFolder(assets.ClassicBuildFS, "web/classic/dist") - mtFS := common.EmbedFolder(assets.MtBuildFS, "web/mt/dist") - themeFS := common.NewThemeAwareFS(defaultFS, classicFS, mtFS) + themeFS := common.NewThemeAwareFS(defaultFS, classicFS) router.Use(gzip.Gzip(gzip.DefaultCompression)) router.Use(middleware.GlobalWebRateLimit()) @@ -43,8 +40,6 @@ func SetWebRouter(router *gin.Engine, assets ThemeAssets) { switch common.GetTheme() { case "classic": c.Data(http.StatusOK, "text/html; charset=utf-8", assets.ClassicIndexPage) - case "mt": - c.Data(http.StatusOK, "text/html; charset=utf-8", assets.MtIndexPage) default: c.Data(http.StatusOK, "text/html; charset=utf-8", assets.DefaultIndexPage) } diff --git a/web/classic/src/components/layout/Footer.jsx b/web/classic/src/components/layout/Footer.jsx index 159530c55..84e094aa0 100644 --- a/web/classic/src/components/layout/Footer.jsx +++ b/web/classic/src/components/layout/Footer.jsx @@ -56,14 +56,14 @@ const FooterBar = () => { /> -
+

{t('关于我们')}

{ {t('关于项目')} {t('联系我们')} - - {t('功能特性')} -
@@ -95,7 +87,7 @@ const FooterBar = () => {

{ {t('快速开始')} - {t('安装指南')} - - {
- -
-

- {t('相关项目')} -

-
- - One API - - - Midjourney-Proxy - - - new-api-key-tool - -
-
- -
-

- {t('友情链接')} -

-
- - new-api-horizon - - - CoAI - - - GPT-Load - -
-
)} @@ -194,20 +114,6 @@ const FooterBar = () => { © {currentYear} {systemName}. {t('版权所有')} - -
- - {t('设计与开发由')}{' '} - - - New API - -
), @@ -227,19 +133,6 @@ const FooterBar = () => { className='custom-footer na-cb6feafeb3990c78 text-sm !text-semi-color-text-1' dangerouslySetInnerHTML={{ __html: footer }} > -
- - {t('设计与开发由')}{' '} - - - New API - -
) : ( diff --git a/web/classic/src/helpers/utils.jsx b/web/classic/src/helpers/utils.jsx index 19cd74941..b0bcaf1ae 100644 --- a/web/classic/src/helpers/utils.jsx +++ b/web/classic/src/helpers/utils.jsx @@ -48,7 +48,7 @@ export function isRoot() { export function getSystemName() { let system_name = localStorage.getItem('system_name'); - if (!system_name) return 'New API'; + if (!system_name) return 'ModelsToken'; return system_name; } diff --git a/web/classic/src/index.jsx b/web/classic/src/index.jsx index 7bb45c194..e5b08d424 100644 --- a/web/classic/src/index.jsx +++ b/web/classic/src/index.jsx @@ -39,7 +39,7 @@ import en_GB from '@douyinfe/semi-ui/lib/es/locale/source/en_GB'; // Welcome message (Do not remove this without permission from the original developer) if (typeof window !== 'undefined') { console.log( - '%cWE ❤ NEWAPI%c Github: https://github.com/QuantumNous/new-api', + '%cWE ❤ ModelsToken%c https://modelstoken.com', 'color: #10b981; font-weight: bold; font-size: 24px;', 'color: inherit; font-size: 14px;', ); diff --git a/web/classic/src/pages/About/index.jsx b/web/classic/src/pages/About/index.jsx index 79474fdc5..7860aa301 100644 --- a/web/classic/src/pages/About/index.jsx +++ b/web/classic/src/pages/About/index.jsx @@ -62,63 +62,11 @@ const About = () => { const customDescription = (

{t('可在设置页面设置关于内容,支持 HTML & Markdown')}

- {t('New API项目仓库地址:')} - - https://github.com/QuantumNous/new-api -

- - NewAPI - {' '} - {t('© {{currentYear}}', { currentYear })}{' '} - - QuantumNous - {' '} - {t('| 基于')}{' '} - - One API v0.5.4 - {' '} - © 2023{' '} - - JustSong - + ModelsToken © {currentYear} modelstoken.com

{t('本项目根据')} - - {t('MIT许可证')} - - {t('授权,需在遵守')} { > {t('AGPL v3.0协议')} - {t('的前提下使用。')} + {t('授权使用。')}

); diff --git a/web/mt/.eslintrc.cjs b/web/mt/.eslintrc.cjs deleted file mode 100644 index b1afd96f5..000000000 --- a/web/mt/.eslintrc.cjs +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2021: true, node: true }, - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', - ecmaFeatures: { jsx: true }, - }, - plugins: ['header', 'react-hooks'], - overrides: [ - { - files: ['**/*.{js,jsx}'], - rules: { - 'header/header': [ - 2, - 'block', - [ - '', - 'Copyright (C) 2025 QuantumNous', - '', - 'This program is free software: you can redistribute it and/or modify', - 'it under the terms of the GNU Affero General Public License as', - 'published by the Free Software Foundation, either version 3 of the', - 'License, or (at your option) any later version.', - '', - 'This program is distributed in the hope that it will be useful,', - 'but WITHOUT ANY WARRANTY; without even the implied warranty of', - 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the', - 'GNU Affero General Public License for more details.', - '', - 'You should have received a copy of the GNU Affero General Public License', - 'along with this program. If not, see .', - '', - 'For commercial licensing, please contact support@quantumnous.com', - '', - ], - ], - 'no-multiple-empty-lines': ['error', { max: 1 }], - }, - }, - ], -}; diff --git a/web/mt/.gitignore b/web/mt/.gitignore deleted file mode 100644 index 2b5bba767..000000000 --- a/web/mt/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.idea -package-lock.json -yarn.lock \ No newline at end of file diff --git a/web/mt/.prettierrc.mjs b/web/mt/.prettierrc.mjs deleted file mode 100644 index 5140bc3e9..000000000 --- a/web/mt/.prettierrc.mjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@so1ve/prettier-config'); diff --git a/web/mt/dist/index.html b/web/mt/dist/index.html deleted file mode 100644 index 9b16e4b32..000000000 --- a/web/mt/dist/index.html +++ /dev/null @@ -1 +0,0 @@ -Classic Theme
\ No newline at end of file diff --git a/web/mt/i18next.config.js b/web/mt/i18next.config.js deleted file mode 100644 index fc4767ee6..000000000 --- a/web/mt/i18next.config.js +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -import { defineConfig } from 'i18next-cli'; - -/** @type {import('i18next-cli').I18nextToolkitConfig} */ -export default defineConfig({ - locales: ['zh-CN', 'zh-TW', 'en', 'fr', 'ru', 'ja', 'vi'], - extract: { - input: ['src/**/*.{js,jsx,ts,tsx}'], - ignore: ['src/i18n/**/*'], - output: 'src/i18n/locales/{{language}}.json', - ignoredAttributes: [ - 'accept', - 'align', - 'aria-label', - 'autoComplete', - 'className', - 'clipRule', - 'color', - 'crossOrigin', - 'data-index', - 'data-name', - 'data-testid', - 'data-type', - 'defaultActiveKey', - 'direction', - 'editorType', - 'field', - 'fill', - 'fillRule', - 'height', - 'hoverStyle', - 'htmlType', - 'id', - 'itemKey', - 'key', - 'keyPrefix', - 'layout', - 'margin', - 'maxHeight', - 'mode', - 'name', - 'overflow', - 'placement', - 'position', - 'rel', - 'role', - 'rowKey', - 'searchPosition', - 'selectedStyle', - 'shape', - 'size', - 'style', - 'theme', - 'trigger', - 'uploadTrigger', - 'validateStatus', - 'value', - 'viewBox', - 'width', - ], - sort: true, - disablePlurals: false, - removeUnusedKeys: false, - nsSeparator: false, - keySeparator: false, - mergeNamespaces: true, - }, -}); diff --git a/web/mt/index.html b/web/mt/index.html deleted file mode 100644 index baed8b4e8..000000000 --- a/web/mt/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - ModelsToken - - - - - - -
- - diff --git a/web/mt/jsconfig.json b/web/mt/jsconfig.json deleted file mode 100644 index 170a7cb4c..000000000 --- a/web/mt/jsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "./", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src/**/*"] -} diff --git a/web/mt/package.json b/web/mt/package.json deleted file mode 100644 index 8a840d67e..000000000 --- a/web/mt/package.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "react-template", - "version": "0.1.0", - "private": true, - "type": "module", - "dependencies": { - "@douyinfe/semi-illustrations": "^2.69.1", - "@douyinfe/semi-icons": "^2.63.1", - "@douyinfe/semi-ui": "^2.69.1", - "@lobehub/icons": "catalog:", - "@visactor/react-vchart": "~1.8.8", - "@visactor/vchart": "~1.8.8", - "@visactor/vchart-semi-theme": "~1.8.8", - "axios": "catalog:", - "clsx": "catalog:", - "dayjs": "catalog:", - "history": "^5.3.0", - "highlight.js": "^11.11.1", - "i18next": "^23.16.8", - "i18next-browser-languagedetector": "^7.2.0", - "katex": "^0.16.22", - "lucide-react": "^0.511.0", - "marked": "^4.1.1", - "mermaid": "^11.6.0", - "qrcode.react": "catalog:", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-dropzone": "^14.2.3", - "react-fireworks": "^1.0.4", - "react-i18next": "^13.0.0", - "react-icons": "catalog:", - "react-markdown": "catalog:", - "react-router-dom": "^6.3.0", - "react-telegram-login": "^1.1.2", - "react-toastify": "^9.0.8", - "react-turnstile": "^1.0.5", - "rehype-highlight": "^7.0.2", - "rehype-katex": "^7.0.1", - "remark-breaks": "^4.0.0", - "remark-gfm": "catalog:", - "remark-math": "^6.0.0", - "sse.js": "catalog:", - "unist-util-visit": "^5.0.0", - "use-debounce": "^10.0.4" - }, - "scripts": { - "dev": "rsbuild dev", - "build": "rsbuild build", - "lint": "prettier . --check", - "lint:fix": "prettier . --write", - "eslint": "bunx eslint \"**/*.{js,jsx}\" --cache", - "eslint:fix": "bunx eslint \"**/*.{js,jsx}\" --fix --cache", - "preview": "rsbuild preview", - "i18n:extract": "bunx i18next-cli extract", - "i18n:status": "bunx i18next-cli status", - "i18n:sync": "bunx i18next-cli sync", - "i18n:lint": "bunx i18next-cli lint" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@rsbuild/core": "^2.0.7", - "@rsbuild/plugin-react": "^2.0.0", - "@so1ve/prettier-config": "^3.1.0", - "autoprefixer": "^10.4.21", - "eslint": "8.57.0", - "eslint-plugin-header": "^3.1.1", - "eslint-plugin-react-hooks": "^5.2.0", - "i18next-cli": "^1.10.3", - "postcss": "^8.5.3", - "prop-types": "^15.8.1", - "prettier": "catalog:", - "tailwindcss": "^3", - "typescript": "4.4.2" - }, - "prettier": { - "singleQuote": true, - "jsxSingleQuote": true - }, - "proxy": "http://localhost:3000" -} diff --git a/web/mt/postcss.config.js b/web/mt/postcss.config.js deleted file mode 100644 index 5731ce76e..000000000 --- a/web/mt/postcss.config.js +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/web/mt/public/azure_model_name.png b/web/mt/public/azure_model_name.png deleted file mode 100644 index 36828db21..000000000 Binary files a/web/mt/public/azure_model_name.png and /dev/null differ diff --git a/web/mt/public/cover-4.webp b/web/mt/public/cover-4.webp deleted file mode 100644 index 0e9ecbf0d..000000000 Binary files a/web/mt/public/cover-4.webp and /dev/null differ diff --git a/web/mt/public/favicon.ico b/web/mt/public/favicon.ico deleted file mode 100644 index 93fe777da..000000000 Binary files a/web/mt/public/favicon.ico and /dev/null differ diff --git a/web/mt/public/logo.png b/web/mt/public/logo.png deleted file mode 100644 index 3a4226703..000000000 Binary files a/web/mt/public/logo.png and /dev/null differ diff --git a/web/mt/public/pay-apple.png b/web/mt/public/pay-apple.png deleted file mode 100644 index b040f6e89..000000000 Binary files a/web/mt/public/pay-apple.png and /dev/null differ diff --git a/web/mt/public/pay-card.png b/web/mt/public/pay-card.png deleted file mode 100644 index 98c414618..000000000 Binary files a/web/mt/public/pay-card.png and /dev/null differ diff --git a/web/mt/public/pay-google.png b/web/mt/public/pay-google.png deleted file mode 100644 index bd31bb92f..000000000 Binary files a/web/mt/public/pay-google.png and /dev/null differ diff --git a/web/mt/public/ratio.png b/web/mt/public/ratio.png deleted file mode 100644 index 9c7e02c86..000000000 Binary files a/web/mt/public/ratio.png and /dev/null differ diff --git a/web/mt/public/robots.txt b/web/mt/public/robots.txt deleted file mode 100644 index e9e57dc4d..000000000 --- a/web/mt/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/web/mt/public/waffo-logo-dark.svg b/web/mt/public/waffo-logo-dark.svg deleted file mode 100644 index 18b5df03c..000000000 --- a/web/mt/public/waffo-logo-dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/mt/public/waffo-logo-light.svg b/web/mt/public/waffo-logo-light.svg deleted file mode 100644 index a7bdce05a..000000000 --- a/web/mt/public/waffo-logo-light.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/mt/rsbuild.config.ts b/web/mt/rsbuild.config.ts deleted file mode 100644 index 0a37a0bb7..000000000 --- a/web/mt/rsbuild.config.ts +++ /dev/null @@ -1,106 +0,0 @@ -import path from 'path' -import { createRequire } from 'module' -import { fileURLToPath } from 'url' -import { defineConfig, loadEnv } from '@rsbuild/core' -import { pluginReact } from '@rsbuild/plugin-react' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const require = createRequire(import.meta.url) -const semiUiDir = path.resolve( - path.dirname(require.resolve('@douyinfe/semi-ui')), - '../..', -) - -export default defineConfig(({ envMode }) => { - const env = loadEnv({ mode: envMode, prefixes: ['VITE_'] }) - const clientServerUrl = - process.env.VITE_REACT_APP_SERVER_URL || - env.rawPublicVars.VITE_REACT_APP_SERVER_URL || - '' - const proxyServerUrl = - clientServerUrl || - 'http://localhost:3000' - const isProd = envMode === 'production' - const devProxy = Object.fromEntries( - (['/api', '/mj', '/pg'] as const).map((key) => [ - key, - { target: proxyServerUrl, changeOrigin: true }, - ]), - ) as Record - - return { - plugins: [pluginReact()], - source: { - entry: { - index: './src/index.jsx', - }, - define: { - 'import.meta.env.VITE_REACT_APP_SERVER_URL': JSON.stringify( - clientServerUrl, - ), - }, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@douyinfe/semi-ui/dist/css/semi.css': path.resolve( - semiUiDir, - 'dist/css/semi.css', - ), - }, - }, - html: { - template: './index.html', - }, - server: { - host: '0.0.0.0', - strictPort: true, - proxy: devProxy, - }, - output: { - minify: isProd, - target: 'web', - distPath: { - root: 'dist', - }, - }, - performance: { - removeConsole: isProd ? ['log'] : false, - buildCache: { - cacheDigest: [process.env.VITE_REACT_APP_VERSION], - }, - }, - tools: { - rspack: { - module: { - rules: [ - { - test: /src[\\/].*\.js$/, - type: 'javascript/auto', - use: [ - { - loader: 'builtin:swc-loader', - options: { - jsc: { - parser: { - syntax: 'ecmascript', - jsx: true, - }, - transform: { - react: { - runtime: 'automatic', - development: !isProd, - refresh: !isProd, - }, - }, - }, - }, - }, - ], - }, - ], - }, - }, - }, - } -}) diff --git a/web/mt/src/App.jsx b/web/mt/src/App.jsx deleted file mode 100644 index 55470b2de..000000000 --- a/web/mt/src/App.jsx +++ /dev/null @@ -1,386 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { lazy, Suspense, useContext, useMemo } from 'react'; -import { Route, Routes, useLocation, useParams } from 'react-router-dom'; -import Loading from './components/common/ui/Loading'; -import User from './pages/User'; -import { AuthRedirect, PrivateRoute, AdminRoute } from './helpers'; -import RegisterForm from './components/auth/RegisterForm'; -import LoginForm from './components/auth/LoginForm'; -import NotFound from './pages/NotFound'; -import Forbidden from './pages/Forbidden'; -import Setting from './pages/Setting'; -import { StatusContext } from './context/Status'; - -import PasswordResetForm from './components/auth/PasswordResetForm'; -import PasswordResetConfirm from './components/auth/PasswordResetConfirm'; -import Channel from './pages/Channel'; -import Token from './pages/Token'; -import Redemption from './pages/Redemption'; -import TopUp from './pages/TopUp'; -import Log from './pages/Log'; -import Chat from './pages/Chat'; -import Chat2Link from './pages/Chat2Link'; -import Midjourney from './pages/Midjourney'; -import Pricing from './pages/Pricing'; -import Task from './pages/Task'; -import ModelPage from './pages/Model'; -import ModelDeploymentPage from './pages/ModelDeployment'; -import Playground from './pages/Playground'; -import Subscription from './pages/Subscription'; -import OAuth2Callback from './components/auth/OAuth2Callback'; -import PersonalSetting from './components/settings/PersonalSetting'; -import Setup from './pages/Setup'; -import SetupCheck from './components/layout/SetupCheck'; - -const Home = lazy(() => import('./pages/Home')); -const Dashboard = lazy(() => import('./pages/Dashboard')); -const About = lazy(() => import('./pages/About')); -const UserAgreement = lazy(() => import('./pages/UserAgreement')); -const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); - -function DynamicOAuth2Callback() { - const { provider } = useParams(); - return ; -} - -function App() { - const location = useLocation(); - const [statusState] = useContext(StatusContext); - - // 获取模型广场权限配置 - const pricingRequireAuth = useMemo(() => { - const headerNavModulesConfig = statusState?.status?.HeaderNavModules; - if (headerNavModulesConfig) { - try { - const modules = JSON.parse(headerNavModulesConfig); - - // 处理向后兼容性:如果pricing是boolean,默认不需要登录 - if (typeof modules.pricing === 'boolean') { - return false; // 默认不需要登录鉴权 - } - - // 如果是对象格式,使用requireAuth配置 - return modules.pricing?.requireAuth === true; - } catch (error) { - console.error('解析顶栏模块配置失败:', error); - return false; // 默认不需要登录 - } - } - return false; // 默认不需要登录 - }, [statusState?.status?.HeaderNavModules]); - - return ( - - - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - - - } - /> - } key={location.pathname}> - - - - - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - }> - - - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - - } key={location.pathname}> - - - - } - /> - - } key={location.pathname}> - - - - } - /> - - } key={location.pathname}> - - - - } - /> - - - - } - /> - - } key={location.pathname}> - - - - } - /> - - } key={location.pathname}> - - - - } - /> - - } key={location.pathname}> - - - - } - /> - - } - key={location.pathname} - > - - - - ) : ( - } key={location.pathname}> - - - ) - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - } key={location.pathname}> - - - } - /> - {/* 方便使用chat2link直接跳转聊天... */} - - } key={location.pathname}> - - - - } - /> - } /> - - - ); -} - -export default App; diff --git a/web/mt/src/components/auth/LoginForm.jsx b/web/mt/src/components/auth/LoginForm.jsx deleted file mode 100644 index d8980f72c..000000000 --- a/web/mt/src/components/auth/LoginForm.jsx +++ /dev/null @@ -1,983 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; -import { UserContext } from '../../context/User'; -import { StatusContext } from '../../context/Status'; -import { - API, - getLogo, - showError, - showInfo, - showSuccess, - updateAPI, - getSystemName, - getOAuthProviderIcon, - setUserData, - onGitHubOAuthClicked, - onDiscordOAuthClicked, - onOIDCClicked, - onLinuxDOOAuthClicked, - onCustomOAuthClicked, - prepareCredentialRequestOptions, - buildAssertionResult, - isPasskeySupported, -} from '../../helpers'; -import Turnstile from 'react-turnstile'; -import { - Button, - Card, - Checkbox, - Divider, - Form, - Icon, - Modal, -} from '@douyinfe/semi-ui'; -import Title from '@douyinfe/semi-ui/lib/es/typography/title'; -import Text from '@douyinfe/semi-ui/lib/es/typography/text'; -import TelegramLoginButton from 'react-telegram-login'; - -import { - IconGithubLogo, - IconMail, - IconLock, - IconKey, -} from '@douyinfe/semi-icons'; -import OIDCIcon from '../common/logo/OIDCIcon'; -import WeChatIcon from '../common/logo/WeChatIcon'; -import LinuxDoIcon from '../common/logo/LinuxDoIcon'; -import TwoFAVerification from './TwoFAVerification'; -import { useTranslation } from 'react-i18next'; -import { SiDiscord } from 'react-icons/si'; - -const LoginForm = () => { - let navigate = useNavigate(); - const { t } = useTranslation(); - const githubButtonTextKeyByState = { - idle: '使用 GitHub 继续', - redirecting: '正在跳转 GitHub...', - timeout: '请求超时,请刷新页面后重新发起 GitHub 登录', - }; - const [inputs, setInputs] = useState({ - username: '', - password: '', - wechat_verification_code: '', - }); - const { username, password } = inputs; - const [searchParams, setSearchParams] = useSearchParams(); - const [submitted, setSubmitted] = useState(false); - const [userState, userDispatch] = useContext(UserContext); - const [statusState] = useContext(StatusContext); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); - const [showEmailLogin, setShowEmailLogin] = useState(false); - const [wechatLoading, setWechatLoading] = useState(false); - const [githubLoading, setGithubLoading] = useState(false); - const [discordLoading, setDiscordLoading] = useState(false); - const [oidcLoading, setOidcLoading] = useState(false); - const [linuxdoLoading, setLinuxdoLoading] = useState(false); - const [emailLoginLoading, setEmailLoginLoading] = useState(false); - const [loginLoading, setLoginLoading] = useState(false); - const [resetPasswordLoading, setResetPasswordLoading] = useState(false); - const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = - useState(false); - const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); - const [showTwoFA, setShowTwoFA] = useState(false); - const [passkeySupported, setPasskeySupported] = useState(false); - const [passkeyLoading, setPasskeyLoading] = useState(false); - const [agreedToTerms, setAgreedToTerms] = useState(false); - const [hasUserAgreement, setHasUserAgreement] = useState(false); - const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); - const [githubButtonState, setGithubButtonState] = useState('idle'); - const [githubButtonDisabled, setGithubButtonDisabled] = useState(false); - const githubTimeoutRef = useRef(null); - const githubButtonText = t(githubButtonTextKeyByState[githubButtonState]); - const [customOAuthLoading, setCustomOAuthLoading] = useState({}); - - const logo = getLogo(); - const systemName = getSystemName(); - - let affCode = new URLSearchParams(window.location.search).get('aff'); - if (affCode) { - localStorage.setItem('aff', affCode); - } - - const status = useMemo(() => { - if (statusState?.status) return statusState.status; - const savedStatus = localStorage.getItem('status'); - if (!savedStatus) return {}; - try { - return JSON.parse(savedStatus) || {}; - } catch (err) { - return {}; - } - }, [statusState?.status]); - const hasCustomOAuthProviders = - (status.custom_oauth_providers || []).length > 0; - const hasOAuthLoginOptions = Boolean( - status.github_oauth || - status.discord_oauth || - status.oidc_enabled || - status.wechat_login || - status.linuxdo_oauth || - status.telegram_oauth || - hasCustomOAuthProviders, - ); - - useEffect(() => { - if (status?.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - - // 从 status 获取用户协议和隐私政策的启用状态 - setHasUserAgreement(status?.user_agreement_enabled || false); - setHasPrivacyPolicy(status?.privacy_policy_enabled || false); - }, [status]); - - useEffect(() => { - isPasskeySupported() - .then(setPasskeySupported) - .catch(() => setPasskeySupported(false)); - - return () => { - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current); - } - }; - }, []); - - useEffect(() => { - if (searchParams.get('expired')) { - showError(t('未登录或登录已过期,请重新登录')); - } - }, []); - - const onWeChatLoginClicked = () => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - setWechatLoading(true); - setShowWeChatLoginModal(true); - setWechatLoading(false); - }; - - const onSubmitWeChatVerificationCode = async () => { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setWechatCodeSubmitLoading(true); - try { - const res = await API.get( - `/api/oauth/wechat?code=${inputs.wechat_verification_code}`, - ); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - setUserData(data); - updateAPI(); - navigate('/'); - showSuccess('登录成功!'); - setShowWeChatLoginModal(false); - } else { - showError(message); - } - } catch (error) { - showError('登录失败,请重试'); - } finally { - setWechatCodeSubmitLoading(false); - } - }; - - function handleChange(name, value) { - setInputs((inputs) => ({ ...inputs, [name]: value })); - } - - async function handleSubmit(e) { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setSubmitted(true); - setLoginLoading(true); - try { - if (username && password) { - const res = await API.post( - `/api/user/login?turnstile=${turnstileToken}`, - { - username, - password, - }, - ); - const { success, message, data } = res.data; - if (success) { - // 检查是否需要2FA验证 - if (data && data.require_2fa) { - setShowTwoFA(true); - setLoginLoading(false); - return; - } - - userDispatch({ type: 'login', payload: data }); - setUserData(data); - updateAPI(); - showSuccess('登录成功!'); - if (username === 'root' && password === '123456') { - Modal.error({ - title: '您正在使用默认密码!', - content: '请立刻修改默认密码!', - centered: true, - }); - } - navigate('/console'); - } else { - showError(message); - } - } else { - showError('请输入用户名和密码!'); - } - } catch (error) { - showError('登录失败,请重试'); - } finally { - setLoginLoading(false); - } - } - - // 添加Telegram登录处理函数 - const onTelegramLoginClicked = async (response) => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - const fields = [ - 'id', - 'first_name', - 'last_name', - 'username', - 'photo_url', - 'auth_date', - 'hash', - 'lang', - ]; - const params = {}; - fields.forEach((field) => { - if (response[field]) { - params[field] = response[field]; - } - }); - try { - const res = await API.get(`/api/oauth/telegram/login`, { params }); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - showSuccess('登录成功!'); - setUserData(data); - updateAPI(); - navigate('/'); - } else { - showError(message); - } - } catch (error) { - showError('登录失败,请重试'); - } - }; - - // 包装的GitHub登录点击处理 - const handleGitHubClick = () => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - if (githubButtonDisabled) { - return; - } - setGithubLoading(true); - setGithubButtonDisabled(true); - setGithubButtonState('redirecting'); - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current); - } - githubTimeoutRef.current = setTimeout(() => { - setGithubLoading(false); - setGithubButtonState('timeout'); - setGithubButtonDisabled(true); - }, 20000); - try { - onGitHubOAuthClicked(status.github_client_id, { shouldLogout: true }); - } finally { - // 由于重定向,这里不会执行到,但为了完整性添加 - setTimeout(() => setGithubLoading(false), 3000); - } - }; - - // 包装的Discord登录点击处理 - const handleDiscordClick = () => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - setDiscordLoading(true); - try { - onDiscordOAuthClicked(status.discord_client_id, { shouldLogout: true }); - } finally { - // 由于重定向,这里不会执行到,但为了完整性添加 - setTimeout(() => setDiscordLoading(false), 3000); - } - }; - - // 包装的OIDC登录点击处理 - const handleOIDCClick = () => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - setOidcLoading(true); - try { - onOIDCClicked( - status.oidc_authorization_endpoint, - status.oidc_client_id, - false, - { shouldLogout: true }, - ); - } finally { - // 由于重定向,这里不会执行到,但为了完整性添加 - setTimeout(() => setOidcLoading(false), 3000); - } - }; - - // 包装的LinuxDO登录点击处理 - const handleLinuxDOClick = () => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - setLinuxdoLoading(true); - try { - onLinuxDOOAuthClicked(status.linuxdo_client_id, { shouldLogout: true }); - } finally { - // 由于重定向,这里不会执行到,但为了完整性添加 - setTimeout(() => setLinuxdoLoading(false), 3000); - } - }; - - // 包装的自定义OAuth登录点击处理 - const handleCustomOAuthClick = (provider) => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: true })); - try { - onCustomOAuthClicked(provider, { shouldLogout: true }); - } finally { - // 由于重定向,这里不会执行到,但为了完整性添加 - setTimeout(() => { - setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: false })); - }, 3000); - } - }; - - // 包装的邮箱登录选项点击处理 - const handleEmailLoginClick = () => { - setEmailLoginLoading(true); - setShowEmailLogin(true); - setEmailLoginLoading(false); - }; - - const handlePasskeyLogin = async () => { - if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { - showInfo(t('请先阅读并同意用户协议和隐私政策')); - return; - } - if (!passkeySupported) { - showInfo('当前环境无法使用 Passkey 登录'); - return; - } - if (!window.PublicKeyCredential) { - showInfo('当前浏览器不支持 Passkey'); - return; - } - - setPasskeyLoading(true); - try { - const beginRes = await API.post('/api/user/passkey/login/begin'); - const { success, message, data } = beginRes.data; - if (!success) { - showError(message || '无法发起 Passkey 登录'); - return; - } - - const publicKeyOptions = prepareCredentialRequestOptions( - data?.options || data?.publicKey || data, - ); - const assertion = await navigator.credentials.get({ - publicKey: publicKeyOptions, - }); - const payload = buildAssertionResult(assertion); - if (!payload) { - showError('Passkey 验证失败,请重试'); - return; - } - - const finishRes = await API.post( - '/api/user/passkey/login/finish', - payload, - ); - const finish = finishRes.data; - if (finish.success) { - userDispatch({ type: 'login', payload: finish.data }); - setUserData(finish.data); - updateAPI(); - showSuccess('登录成功!'); - navigate('/console'); - } else { - showError(finish.message || 'Passkey 登录失败,请重试'); - } - } catch (error) { - if (error?.name === 'AbortError') { - showInfo('已取消 Passkey 登录'); - } else { - showError('Passkey 登录失败,请重试'); - } - } finally { - setPasskeyLoading(false); - } - }; - - // 包装的重置密码点击处理 - const handleResetPasswordClick = () => { - setResetPasswordLoading(true); - navigate('/reset'); - setResetPasswordLoading(false); - }; - - // 包装的其他登录选项点击处理 - const handleOtherLoginOptionsClick = () => { - setOtherLoginOptionsLoading(true); - setShowEmailLogin(false); - setOtherLoginOptionsLoading(false); - }; - - // 2FA验证成功处理 - const handle2FASuccess = (data) => { - userDispatch({ type: 'login', payload: data }); - setUserData(data); - updateAPI(); - showSuccess('登录成功!'); - navigate('/console'); - }; - - // 返回登录页面 - const handleBackToLogin = () => { - setShowTwoFA(false); - setInputs({ username: '', password: '', wechat_verification_code: '' }); - }; - - const renderOAuthOptions = () => { - return ( -
-
-
- Logo - - {systemName} - -
- - -
- - {t('登 录')} - -
-
-
- {status.wechat_login && ( - - )} - - {status.github_oauth && ( - - )} - - {status.discord_oauth && ( - - )} - - {status.oidc_enabled && ( - - )} - - {status.linuxdo_oauth && ( - - )} - - {status.custom_oauth_providers && - status.custom_oauth_providers.map((provider) => ( - - ))} - - {status.telegram_oauth && ( -
- -
- )} - - {status.passkey_login && passkeySupported && ( - - )} - - - {t('或')} - - - -
- - {(hasUserAgreement || hasPrivacyPolicy) && ( -
- setAgreedToTerms(e.target.checked)} - > - - {t('我已阅读并同意')} - {hasUserAgreement && ( - <> - - {t('用户协议')} - - - )} - {hasUserAgreement && hasPrivacyPolicy && t('和')} - {hasPrivacyPolicy && ( - <> - - {t('隐私政策')} - - - )} - - -
- )} - - {!status.self_use_mode_enabled && ( -
- - {t('没有账户?')}{' '} - - {t('注册')} - - -
- )} -
-
-
-
- ); - }; - - const renderEmailLoginForm = () => { - return ( -
-
-
- Logo - {systemName} -
- - -
- - {t('登 录')} - -
-
- {status.passkey_login && passkeySupported && ( - - )} -
- handleChange('username', value)} - prefix={} - /> - - handleChange('password', value)} - prefix={} - /> - - {(hasUserAgreement || hasPrivacyPolicy) && ( -
- setAgreedToTerms(e.target.checked)} - > - - {t('我已阅读并同意')} - {hasUserAgreement && ( - <> - - {t('用户协议')} - - - )} - {hasUserAgreement && hasPrivacyPolicy && t('和')} - {hasPrivacyPolicy && ( - <> - - {t('隐私政策')} - - - )} - - -
- )} - -
- - - -
- - - {hasOAuthLoginOptions && ( - <> - - {t('或')} - - -
- -
- - )} - - {!status.self_use_mode_enabled && ( -
- - {t('没有账户?')}{' '} - - {t('注册')} - - -
- )} -
-
-
-
- ); - }; - - // 微信登录模态框 - const renderWeChatLoginModal = () => { - return ( - setShowWeChatLoginModal(false)} - okText={t('登录')} - centered={true} - okButtonProps={{ - loading: wechatCodeSubmitLoading, - }} - > -
- 微信二维码 -
- -
-

- {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')} -

-
- -
- - handleChange('wechat_verification_code', value) - } - /> - -
- ); - }; - - // 2FA验证弹窗 - const render2FAModal = () => { - return ( - -
- - - -
- 两步验证 - - } - visible={showTwoFA} - onCancel={handleBackToLogin} - footer={null} - width={450} - centered - > - -
- ); - }; - - return ( -
- {/* 背景模糊晕染球 */} -
-
-
- {showEmailLogin || - !hasOAuthLoginOptions - ? renderEmailLoginForm() - : renderOAuthOptions()} - {renderWeChatLoginModal()} - {render2FAModal()} - - {turnstileEnabled && ( -
- { - setTurnstileToken(token); - }} - /> -
- )} -
-
- ); -}; - -export default LoginForm; diff --git a/web/mt/src/components/auth/OAuth2Callback.jsx b/web/mt/src/components/auth/OAuth2Callback.jsx deleted file mode 100644 index c8dc3b41b..000000000 --- a/web/mt/src/components/auth/OAuth2Callback.jsx +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useContext, useEffect, useRef } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { - API, - showError, - showSuccess, - updateAPI, - setUserData, -} from '../../helpers'; -import { UserContext } from '../../context/User'; -import Loading from '../common/ui/Loading'; - -const OAuth2Callback = (props) => { - const { t } = useTranslation(); - const [searchParams] = useSearchParams(); - const [, userDispatch] = useContext(UserContext); - const navigate = useNavigate(); - - // 防止 React 18 Strict Mode 下重复执行 - const hasExecuted = useRef(false); - - // 最大重试次数 - const MAX_RETRIES = 3; - - const sendCode = async (code, state, retry = 0) => { - try { - const { data: resData } = await API.get( - `/api/oauth/${props.type}?code=${code}&state=${state}`, - ); - - const { success, message, data } = resData; - - if (!success) { - // 业务错误不重试,直接显示错误 - showError(message || t('授权失败')); - return; - } - - if (data?.action === 'bind') { - showSuccess(t('绑定成功!')); - navigate('/console/personal'); - } else { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - setUserData(data); - updateAPI(); - showSuccess(t('登录成功!')); - navigate('/console/token'); - } - } catch (error) { - // 网络错误等可重试 - if (retry < MAX_RETRIES) { - // 递增的退避等待 - await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000)); - return sendCode(code, state, retry + 1); - } - - // 重试次数耗尽,提示错误并返回设置页面 - showError(error.message || t('授权失败')); - navigate('/console/personal'); - } - }; - - useEffect(() => { - // 防止 React 18 Strict Mode 下重复执行 - if (hasExecuted.current) { - return; - } - hasExecuted.current = true; - - const code = searchParams.get('code'); - const state = searchParams.get('state'); - - // 参数缺失直接返回 - if (!code) { - showError(t('未获取到授权码')); - navigate('/console/personal'); - return; - } - - sendCode(code, state); - }, []); - - return ; -}; - -export default OAuth2Callback; diff --git a/web/mt/src/components/auth/PasswordResetConfirm.jsx b/web/mt/src/components/auth/PasswordResetConfirm.jsx deleted file mode 100644 index d7a6689fa..000000000 --- a/web/mt/src/components/auth/PasswordResetConfirm.jsx +++ /dev/null @@ -1,220 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useEffect, useState } from 'react'; -import { - API, - copy, - showError, - showNotice, - getLogo, - getSystemName, -} from '../../helpers'; -import { useSearchParams, Link } from 'react-router-dom'; -import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui'; -import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons'; -import { useTranslation } from 'react-i18next'; - -const { Text, Title } = Typography; - -const PasswordResetConfirm = () => { - const { t } = useTranslation(); - const [inputs, setInputs] = useState({ - email: '', - token: '', - }); - const { email, token } = inputs; - const isValidResetLink = email && token; - - const [loading, setLoading] = useState(false); - const [disableButton, setDisableButton] = useState(false); - const [countdown, setCountdown] = useState(30); - const [newPassword, setNewPassword] = useState(''); - const [searchParams, setSearchParams] = useSearchParams(); - const [formApi, setFormApi] = useState(null); - - const logo = getLogo(); - const systemName = getSystemName(); - - useEffect(() => { - let token = searchParams.get('token'); - let email = searchParams.get('email'); - setInputs({ - token: token || '', - email: email || '', - }); - if (formApi) { - formApi.setValues({ - email: email || '', - newPassword: newPassword || '', - }); - } - }, [searchParams, newPassword, formApi]); - - useEffect(() => { - let countdownInterval = null; - if (disableButton && countdown > 0) { - countdownInterval = setInterval(() => { - setCountdown(countdown - 1); - }, 1000); - } else if (countdown === 0) { - setDisableButton(false); - setCountdown(30); - } - return () => clearInterval(countdownInterval); - }, [disableButton, countdown]); - - async function handleSubmit(e) { - if (!email || !token) { - showError(t('无效的重置链接,请重新发起密码重置请求')); - return; - } - setDisableButton(true); - setLoading(true); - const res = await API.post(`/api/user/reset`, { - email, - token, - }); - const { success, message } = res.data; - if (success) { - let password = res.data.data; - setNewPassword(password); - await copy(password); - showNotice(`${t('密码已重置并已复制到剪贴板:')} ${password}`); - } else { - showError(message); - } - setLoading(false); - } - - return ( -
- {/* 背景模糊晕染球 */} -
-
-
-
-
-
- Logo - - {systemName} - -
- - -
- - {t('密码重置确认')} - -
-
- {!isValidResetLink && ( - - )} -
setFormApi(api)} - initValues={{ - email: email || '', - newPassword: newPassword || '', - }} - className='space-y-4' - > - } - placeholder={email ? '' : t('等待获取邮箱信息...')} - /> - - {newPassword && ( - } - suffix={ - - } - /> - )} - -
- -
- - -
- - - {t('返回登录')} - - -
-
-
-
-
-
-
- ); -}; - -export default PasswordResetConfirm; diff --git a/web/mt/src/components/auth/PasswordResetForm.jsx b/web/mt/src/components/auth/PasswordResetForm.jsx deleted file mode 100644 index 3b8a2c4a6..000000000 --- a/web/mt/src/components/auth/PasswordResetForm.jsx +++ /dev/null @@ -1,193 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useEffect, useState } from 'react'; -import { - API, - getLogo, - showError, - showInfo, - showSuccess, - getSystemName, -} from '../../helpers'; -import Turnstile from 'react-turnstile'; -import { Button, Card, Form, Typography } from '@douyinfe/semi-ui'; -import { IconMail } from '@douyinfe/semi-icons'; -import { Link } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -const { Text, Title } = Typography; - -const PasswordResetForm = () => { - const { t } = useTranslation(); - const [inputs, setInputs] = useState({ - email: '', - }); - const { email } = inputs; - - const [loading, setLoading] = useState(false); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - const [disableButton, setDisableButton] = useState(false); - const [countdown, setCountdown] = useState(30); - - const logo = getLogo(); - const systemName = getSystemName(); - - useEffect(() => { - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - } - }, []); - - useEffect(() => { - let countdownInterval = null; - if (disableButton && countdown > 0) { - countdownInterval = setInterval(() => { - setCountdown(countdown - 1); - }, 1000); - } else if (countdown === 0) { - setDisableButton(false); - setCountdown(30); - } - return () => clearInterval(countdownInterval); - }, [disableButton, countdown]); - - function handleChange(value) { - setInputs((inputs) => ({ ...inputs, email: value })); - } - - async function handleSubmit(e) { - if (!email) { - showError(t('请输入邮箱地址')); - return; - } - if (turnstileEnabled && turnstileToken === '') { - showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!')); - return; - } - setDisableButton(true); - setLoading(true); - const res = await API.get( - `/api/reset_password?email=${email}&turnstile=${turnstileToken}`, - ); - const { success, message } = res.data; - if (success) { - showSuccess(t('重置邮件发送成功,请检查邮箱!')); - setInputs({ ...inputs, email: '' }); - } else { - showError(message); - } - setLoading(false); - } - - return ( -
- {/* 背景模糊晕染球 */} -
-
-
-
-
-
- Logo - - {systemName} - -
- - -
- - {t('密码重置')} - -
-
-
- } - /> - -
- -
- - -
- - {t('想起来了?')}{' '} - - {t('登录')} - - -
-
-
- - {turnstileEnabled && ( -
- { - setTurnstileToken(token); - }} - /> -
- )} -
-
-
-
- ); -}; - -export default PasswordResetForm; diff --git a/web/mt/src/components/auth/RegisterForm.jsx b/web/mt/src/components/auth/RegisterForm.jsx deleted file mode 100644 index c974886c9..000000000 --- a/web/mt/src/components/auth/RegisterForm.jsx +++ /dev/null @@ -1,805 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { - API, - getLogo, - showError, - showInfo, - showSuccess, - updateAPI, - getSystemName, - getOAuthProviderIcon, - setUserData, - onDiscordOAuthClicked, - onCustomOAuthClicked, -} from '../../helpers'; -import Turnstile from 'react-turnstile'; -import { - Button, - Card, - Checkbox, - Divider, - Form, - Icon, - Modal, -} from '@douyinfe/semi-ui'; -import Title from '@douyinfe/semi-ui/lib/es/typography/title'; -import Text from '@douyinfe/semi-ui/lib/es/typography/text'; -import { - IconGithubLogo, - IconMail, - IconUser, - IconLock, - IconKey, -} from '@douyinfe/semi-icons'; -import { - onGitHubOAuthClicked, - onLinuxDOOAuthClicked, - onOIDCClicked, -} from '../../helpers'; -import OIDCIcon from '../common/logo/OIDCIcon'; -import LinuxDoIcon from '../common/logo/LinuxDoIcon'; -import WeChatIcon from '../common/logo/WeChatIcon'; -import TelegramLoginButton from 'react-telegram-login/src'; -import { UserContext } from '../../context/User'; -import { StatusContext } from '../../context/Status'; -import { useTranslation } from 'react-i18next'; -import { SiDiscord } from 'react-icons/si'; - -const RegisterForm = () => { - let navigate = useNavigate(); - const { t } = useTranslation(); - const githubButtonTextKeyByState = { - idle: '使用 GitHub 继续', - redirecting: '正在跳转 GitHub...', - timeout: '请求超时,请刷新页面后重新发起 GitHub 登录', - }; - const [inputs, setInputs] = useState({ - username: '', - password: '', - password2: '', - email: '', - verification_code: '', - wechat_verification_code: '', - }); - const { username, password, password2 } = inputs; - const [userState, userDispatch] = useContext(UserContext); - const [statusState] = useContext(StatusContext); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); - const [showEmailRegister, setShowEmailRegister] = useState(false); - const [wechatLoading, setWechatLoading] = useState(false); - const [githubLoading, setGithubLoading] = useState(false); - const [discordLoading, setDiscordLoading] = useState(false); - const [oidcLoading, setOidcLoading] = useState(false); - const [linuxdoLoading, setLinuxdoLoading] = useState(false); - const [emailRegisterLoading, setEmailRegisterLoading] = useState(false); - const [registerLoading, setRegisterLoading] = useState(false); - const [verificationCodeLoading, setVerificationCodeLoading] = useState(false); - const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = - useState(false); - const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); - const [customOAuthLoading, setCustomOAuthLoading] = useState({}); - const [disableButton, setDisableButton] = useState(false); - const [countdown, setCountdown] = useState(30); - const [agreedToTerms, setAgreedToTerms] = useState(false); - const [hasUserAgreement, setHasUserAgreement] = useState(false); - const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); - const [githubButtonState, setGithubButtonState] = useState('idle'); - const [githubButtonDisabled, setGithubButtonDisabled] = useState(false); - const githubTimeoutRef = useRef(null); - const githubButtonText = t(githubButtonTextKeyByState[githubButtonState]); - - const logo = getLogo(); - const systemName = getSystemName(); - - let affCode = new URLSearchParams(window.location.search).get('aff'); - if (affCode) { - localStorage.setItem('aff', affCode); - } - - const status = useMemo(() => { - if (statusState?.status) return statusState.status; - const savedStatus = localStorage.getItem('status'); - if (!savedStatus) return {}; - try { - return JSON.parse(savedStatus) || {}; - } catch (err) { - return {}; - } - }, [statusState?.status]); - const hasCustomOAuthProviders = - (status.custom_oauth_providers || []).length > 0; - const hasOAuthRegisterOptions = Boolean( - status.github_oauth || - status.discord_oauth || - status.oidc_enabled || - status.wechat_login || - status.linuxdo_oauth || - status.telegram_oauth || - hasCustomOAuthProviders, - ); - - const [showEmailVerification, setShowEmailVerification] = useState(false); - - useEffect(() => { - setShowEmailVerification(!!status?.email_verification); - if (status?.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - - // 从 status 获取用户协议和隐私政策的启用状态 - setHasUserAgreement(status?.user_agreement_enabled || false); - setHasPrivacyPolicy(status?.privacy_policy_enabled || false); - }, [status]); - - useEffect(() => { - let countdownInterval = null; - if (disableButton && countdown > 0) { - countdownInterval = setInterval(() => { - setCountdown(countdown - 1); - }, 1000); - } else if (countdown === 0) { - setDisableButton(false); - setCountdown(30); - } - return () => clearInterval(countdownInterval); // Clean up on unmount - }, [disableButton, countdown]); - - useEffect(() => { - return () => { - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current); - } - }; - }, []); - - const onWeChatLoginClicked = () => { - setWechatLoading(true); - setShowWeChatLoginModal(true); - setWechatLoading(false); - }; - - const onSubmitWeChatVerificationCode = async () => { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setWechatCodeSubmitLoading(true); - try { - const res = await API.get( - `/api/oauth/wechat?code=${inputs.wechat_verification_code}`, - ); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - setUserData(data); - updateAPI(); - navigate('/'); - showSuccess('登录成功!'); - setShowWeChatLoginModal(false); - } else { - showError(message); - } - } catch (error) { - showError('登录失败,请重试'); - } finally { - setWechatCodeSubmitLoading(false); - } - }; - - function handleChange(name, value) { - setInputs((inputs) => ({ ...inputs, [name]: value })); - } - - async function handleSubmit(e) { - if (password.length < 8) { - showInfo('密码长度不得小于 8 位!'); - return; - } - if (password !== password2) { - showInfo('两次输入的密码不一致'); - return; - } - if (username && password) { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setRegisterLoading(true); - try { - if (!affCode) { - affCode = localStorage.getItem('aff'); - } - inputs.aff_code = affCode; - const res = await API.post( - `/api/user/register?turnstile=${turnstileToken}`, - inputs, - ); - const { success, message } = res.data; - if (success) { - navigate('/login'); - showSuccess('注册成功!'); - } else { - showError(message); - } - } catch (error) { - showError('注册失败,请重试'); - } finally { - setRegisterLoading(false); - } - } - } - - const sendVerificationCode = async () => { - if (inputs.email === '') return; - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setVerificationCodeLoading(true); - try { - const res = await API.get( - `/api/verification?email=${encodeURIComponent(inputs.email)}&turnstile=${turnstileToken}`, - ); - const { success, message } = res.data; - if (success) { - showSuccess('验证码发送成功,请检查你的邮箱!'); - setDisableButton(true); // 发送成功后禁用按钮,开始倒计时 - } else { - showError(message); - } - } catch (error) { - showError('发送验证码失败,请重试'); - } finally { - setVerificationCodeLoading(false); - } - }; - - const handleGitHubClick = () => { - if (githubButtonDisabled) { - return; - } - setGithubLoading(true); - setGithubButtonDisabled(true); - setGithubButtonState('redirecting'); - if (githubTimeoutRef.current) { - clearTimeout(githubTimeoutRef.current); - } - githubTimeoutRef.current = setTimeout(() => { - setGithubLoading(false); - setGithubButtonState('timeout'); - setGithubButtonDisabled(true); - }, 20000); - try { - onGitHubOAuthClicked(status.github_client_id, { shouldLogout: true }); - } finally { - setTimeout(() => setGithubLoading(false), 3000); - } - }; - - const handleDiscordClick = () => { - setDiscordLoading(true); - try { - onDiscordOAuthClicked(status.discord_client_id, { shouldLogout: true }); - } finally { - setTimeout(() => setDiscordLoading(false), 3000); - } - }; - - const handleOIDCClick = () => { - setOidcLoading(true); - try { - onOIDCClicked( - status.oidc_authorization_endpoint, - status.oidc_client_id, - false, - { shouldLogout: true }, - ); - } finally { - setTimeout(() => setOidcLoading(false), 3000); - } - }; - - const handleLinuxDOClick = () => { - setLinuxdoLoading(true); - try { - onLinuxDOOAuthClicked(status.linuxdo_client_id, { shouldLogout: true }); - } finally { - setTimeout(() => setLinuxdoLoading(false), 3000); - } - }; - - const handleCustomOAuthClick = (provider) => { - setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: true })); - try { - onCustomOAuthClicked(provider, { shouldLogout: true }); - } finally { - setTimeout(() => { - setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: false })); - }, 3000); - } - }; - - const handleEmailRegisterClick = () => { - setEmailRegisterLoading(true); - setShowEmailRegister(true); - setEmailRegisterLoading(false); - }; - - const handleOtherRegisterOptionsClick = () => { - setOtherRegisterOptionsLoading(true); - setShowEmailRegister(false); - setOtherRegisterOptionsLoading(false); - }; - - const onTelegramLoginClicked = async (response) => { - const fields = [ - 'id', - 'first_name', - 'last_name', - 'username', - 'photo_url', - 'auth_date', - 'hash', - 'lang', - ]; - const params = {}; - fields.forEach((field) => { - if (response[field]) { - params[field] = response[field]; - } - }); - try { - const res = await API.get(`/api/oauth/telegram/login`, { params }); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - showSuccess('登录成功!'); - setUserData(data); - updateAPI(); - navigate('/'); - } else { - showError(message); - } - } catch (error) { - showError('登录失败,请重试'); - } - }; - - const renderOAuthOptions = () => { - return ( -
-
-
- Logo - - {systemName} - -
- - -
- - {t('注 册')} - -
-
-
- {status.wechat_login && ( - - )} - - {status.github_oauth && ( - - )} - - {status.discord_oauth && ( - - )} - - {status.oidc_enabled && ( - - )} - - {status.linuxdo_oauth && ( - - )} - - {status.custom_oauth_providers && - status.custom_oauth_providers.map((provider) => ( - - ))} - - {status.telegram_oauth && ( -
- -
- )} - - - {t('或')} - - - -
- -
- - {t('已有账户?')}{' '} - - {t('登录')} - - -
-
-
-
-
- ); - }; - - const renderEmailRegisterForm = () => { - return ( -
-
-
- Logo - - {systemName} - -
- - -
- - {t('注 册')} - -
-
-
- handleChange('username', value)} - prefix={} - /> - - handleChange('password', value)} - prefix={} - /> - - handleChange('password2', value)} - prefix={} - /> - - {showEmailVerification && ( - <> - handleChange('email', value)} - prefix={} - suffix={ - - } - /> - - handleChange('verification_code', value) - } - prefix={} - /> - - )} - - {(hasUserAgreement || hasPrivacyPolicy) && ( -
- setAgreedToTerms(e.target.checked)} - > - - {t('我已阅读并同意')} - {hasUserAgreement && ( - <> - - {t('用户协议')} - - - )} - {hasUserAgreement && hasPrivacyPolicy && t('和')} - {hasPrivacyPolicy && ( - <> - - {t('隐私政策')} - - - )} - - -
- )} - -
- -
- - - {hasOAuthRegisterOptions && ( - <> - - {t('或')} - - -
- -
- - )} - -
- - {t('已有账户?')}{' '} - - {t('登录')} - - -
-
-
-
-
- ); - }; - - const renderWeChatLoginModal = () => { - return ( - setShowWeChatLoginModal(false)} - okText={t('登录')} - centered={true} - okButtonProps={{ - loading: wechatCodeSubmitLoading, - }} - > -
- 微信二维码 -
- -
-

- {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')} -

-
- -
- - handleChange('wechat_verification_code', value) - } - /> - -
- ); - }; - - return ( -
- {/* 背景模糊晕染球 */} -
-
-
- {showEmailRegister || - !hasOAuthRegisterOptions - ? renderEmailRegisterForm() - : renderOAuthOptions()} - {renderWeChatLoginModal()} - - {turnstileEnabled && ( -
- { - setTurnstileToken(token); - }} - /> -
- )} -
-
- ); -}; - -export default RegisterForm; diff --git a/web/mt/src/components/auth/TwoFAVerification.jsx b/web/mt/src/components/auth/TwoFAVerification.jsx deleted file mode 100644 index 51913112f..000000000 --- a/web/mt/src/components/auth/TwoFAVerification.jsx +++ /dev/null @@ -1,244 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ -import { API, showError, showSuccess } from '../../helpers'; -import { - Button, - Card, - Divider, - Form, - Input, - Typography, -} from '@douyinfe/semi-ui'; -import React, { useState } from 'react'; - -const { Title, Text, Paragraph } = Typography; - -const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => { - const [loading, setLoading] = useState(false); - const [useBackupCode, setUseBackupCode] = useState(false); - const [verificationCode, setVerificationCode] = useState(''); - - const handleSubmit = async () => { - if (!verificationCode) { - showError('请输入验证码'); - return; - } - // Validate code format - if (useBackupCode && verificationCode.length !== 8) { - showError('备用码必须是8位'); - return; - } else if (!useBackupCode && !/^\d{6}$/.test(verificationCode)) { - showError('验证码必须是6位数字'); - return; - } - - setLoading(true); - try { - const res = await API.post('/api/user/login/2fa', { - code: verificationCode, - }); - - if (res.data.success) { - showSuccess('登录成功'); - // 保存用户信息到本地存储 - localStorage.setItem('user', JSON.stringify(res.data.data)); - if (onSuccess) { - onSuccess(res.data.data); - } - } else { - showError(res.data.message); - } - } catch (error) { - showError('验证失败,请重试'); - } finally { - setLoading(false); - } - }; - - const handleKeyPress = (e) => { - if (e.key === 'Enter') { - handleSubmit(); - } - }; - - if (isModal) { - return ( -
- - 请输入认证器应用显示的验证码完成登录 - - -
- - - - - - - -
- - - {onBack && ( - - )} -
- -
- - 提示: -
- • 验证码每30秒更新一次 -
- • 如果无法获取验证码,请使用备用码 -
• 每个备用码只能使用一次 -
-
-
- ); - } - - return ( -
- -
- 两步验证 - - 请输入认证器应用显示的验证码完成登录 - -
- -
- - - - - - - -
- - - {onBack && ( - - )} -
- -
- - 提示: -
- • 验证码每30秒更新一次 -
- • 如果无法获取验证码,请使用备用码 -
• 每个备用码只能使用一次 -
-
-
-
- ); -}; - -export default TwoFAVerification; diff --git a/web/mt/src/components/common/DocumentRenderer/index.jsx b/web/mt/src/components/common/DocumentRenderer/index.jsx deleted file mode 100644 index c5503dbad..000000000 --- a/web/mt/src/components/common/DocumentRenderer/index.jsx +++ /dev/null @@ -1,232 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useEffect, useMemo, useState } from 'react'; -import { API, showError } from '../../../helpers'; -import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui'; -const { Title } = Typography; -import { - IllustrationConstruction, - IllustrationConstructionDark, -} from '@douyinfe/semi-illustrations'; -import { useTranslation } from 'react-i18next'; -import MarkdownRenderer from '../markdown/MarkdownRenderer'; - -// Check whether content is a URL. -const isUrl = (content) => { - try { - new URL(content.trim()); - return true; - } catch { - return false; - } -}; - -// Check whether content contains HTML. -const isHtmlContent = (content) => { - if (!content || typeof content !== 'string') return false; - - const htmlTagRegex = /<\/?[a-z][\s\S]*>/i; - return htmlTagRegex.test(content); -}; - -// Parse HTML content and extract inline styles. -const sanitizeHtml = (html) => { - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = html; - - const styles = Array.from(tempDiv.querySelectorAll('style')) - .map((style) => style.innerHTML) - .join('\n'); - - const bodyContent = tempDiv.querySelector('body'); - const content = bodyContent ? bodyContent.innerHTML : html; - - return { content, styles }; -}; - -/** - * 通用文档渲染组件 - * @param {string} apiEndpoint - API 接口地址 - * @param {string} title - 文档标题 - * @param {string} cacheKey - 本地存储缓存键 - * @param {string} emptyMessage - 空内容时的提示消息 - */ -const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { - const { t } = useTranslation(); - const [content, setContent] = useState(''); - const [loading, setLoading] = useState(true); - - const loadContent = async () => { - const cachedContent = localStorage.getItem(cacheKey) || ''; - if (cachedContent) { - setContent(cachedContent); - setLoading(false); - } - - try { - const res = await API.get(apiEndpoint); - const { success, message, data } = res.data; - if (success && data) { - setContent(data); - localStorage.setItem(cacheKey, data); - } else { - if (!cachedContent) { - showError(message || emptyMessage); - setContent(''); - } - } - } catch (error) { - if (!cachedContent) { - showError(emptyMessage); - setContent(''); - } - } finally { - setLoading(false); - } - }; - - const htmlPayload = useMemo(() => { - if (!isHtmlContent(content)) { - return { content: '', styles: '' }; - } - return sanitizeHtml(content); - }, [content]); - - useEffect(() => { - loadContent(); - }, []); - - // 处理HTML样式注入 - useEffect(() => { - const styleId = `document-renderer-styles-${cacheKey}`; - const { styles } = htmlPayload; - - if (styles) { - let styleEl = document.getElementById(styleId); - if (!styleEl) { - styleEl = document.createElement('style'); - styleEl.id = styleId; - styleEl.type = 'text/css'; - document.head.appendChild(styleEl); - } - styleEl.innerHTML = styles; - } else { - const el = document.getElementById(styleId); - if (el) el.remove(); - } - - return () => { - const el = document.getElementById(styleId); - if (el) el.remove(); - }; - }, [cacheKey, htmlPayload]); - - // 显示加载状态 - if (loading) { - return ( -
- -
- ); - } - - // 如果没有内容,显示空状态 - if (!content || content.trim() === '') { - return ( -
- - } - darkModeImage={ - - } - className='p-8' - /> -
- ); - } - - // 如果是 URL,显示链接卡片 - if (isUrl(content)) { - return ( -
- -
- - {title} - -

- {t('管理员设置了外部链接,点击下方按钮访问')} -

- - {t('访问' + title)} - -
-
-
- ); - } - - // 如果是 HTML 内容,直接渲染 - if (isHtmlContent(content)) { - return ( -
-
-
- - {title} - -
-
-
-
- ); - } - - // 其他内容统一使用 Markdown 渲染器 - return ( -
-
-
- - {title} - -
- -
-
-
-
- ); -}; - -export default DocumentRenderer; diff --git a/web/mt/src/components/common/ErrorBoundary.jsx b/web/mt/src/components/common/ErrorBoundary.jsx deleted file mode 100644 index 3827969ac..000000000 --- a/web/mt/src/components/common/ErrorBoundary.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { Empty, Button } from '@douyinfe/semi-ui'; -import { - IllustrationFailure, - IllustrationFailureDark, -} from '@douyinfe/semi-illustrations'; -import { withTranslation } from 'react-i18next'; - -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - return { hasError: true }; - } - - componentDidCatch(error, errorInfo) { - console.error('[ErrorBoundary]', error, errorInfo); - } - - render() { - if (this.state.hasError) { - const { t } = this.props; - return ( -
- - } - darkModeImage={ - - } - description={t('页面渲染出错,请刷新页面重试')} - /> - -
- ); - } - return this.props.children; - } -} - -export default withTranslation()(ErrorBoundary); diff --git a/web/mt/src/components/common/examples/ChannelKeyViewExample.jsx b/web/mt/src/components/common/examples/ChannelKeyViewExample.jsx deleted file mode 100644 index d9990dd3f..000000000 --- a/web/mt/src/components/common/examples/ChannelKeyViewExample.jsx +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Button, Modal } from '@douyinfe/semi-ui'; -import { useSecureVerification } from '../../../hooks/common/useSecureVerification'; -import { createApiCalls } from '../../../services/secureVerification'; -import SecureVerificationModal from '../modals/SecureVerificationModal'; -import ChannelKeyDisplay from '../ui/ChannelKeyDisplay'; - -/** - * 渠道密钥查看组件使用示例 - * 展示如何使用通用安全验证系统 - */ -const ChannelKeyViewExample = ({ channelId }) => { - const { t } = useTranslation(); - const [keyData, setKeyData] = useState(''); - const [showKeyModal, setShowKeyModal] = useState(false); - - // 使用通用安全验证 Hook - const { - isModalVisible, - verificationMethods, - verificationState, - startVerification, - executeVerification, - cancelVerification, - setVerificationCode, - switchVerificationMethod, - } = useSecureVerification({ - onSuccess: (result) => { - // 验证成功后处理结果 - if (result.success && result.data?.key) { - setKeyData(result.data.key); - setShowKeyModal(true); - } - }, - successMessage: t('密钥获取成功'), - }); - - // 开始查看密钥流程 - const handleViewKey = async () => { - const apiCall = createApiCalls.viewChannelKey(channelId); - - await startVerification(apiCall, { - title: t('查看渠道密钥'), - description: t('为了保护账户安全,请验证您的身份。'), - preferredMethod: 'passkey', // 可以指定首选验证方式 - }); - }; - - return ( - <> - {/* 查看密钥按钮 */} - - - {/* 安全验证模态框 */} - - - {/* 密钥显示模态框 */} - setShowKeyModal(false)} - footer={ - - } - width={700} - style={{ maxWidth: '90vw' }} - > - - - - ); -}; - -export default ChannelKeyViewExample; diff --git a/web/mt/src/components/common/logo/LinuxDoIcon.jsx b/web/mt/src/components/common/logo/LinuxDoIcon.jsx deleted file mode 100644 index c237f8955..000000000 --- a/web/mt/src/components/common/logo/LinuxDoIcon.jsx +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React from 'react'; -import { Icon } from '@douyinfe/semi-ui'; - -const LinuxDoIcon = (props) => { - function CustomIcon() { - return ( - - - - - - - - ); - } - - return } />; -}; - -export default LinuxDoIcon; diff --git a/web/mt/src/components/common/logo/OIDCIcon.jsx b/web/mt/src/components/common/logo/OIDCIcon.jsx deleted file mode 100644 index c83a4942d..000000000 --- a/web/mt/src/components/common/logo/OIDCIcon.jsx +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React from 'react'; -import { Icon } from '@douyinfe/semi-ui'; - -const OIDCIcon = (props) => { - function CustomIcon() { - return ( - - - - - ); - } - - return } />; -}; - -export default OIDCIcon; diff --git a/web/mt/src/components/common/logo/WeChatIcon.jsx b/web/mt/src/components/common/logo/WeChatIcon.jsx deleted file mode 100644 index a3fab081a..000000000 --- a/web/mt/src/components/common/logo/WeChatIcon.jsx +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import React from 'react'; -import { Icon } from '@douyinfe/semi-ui'; - -const WeChatIcon = () => { - function CustomIcon() { - return ( - - - - - ); - } - - return ( -
- } /> -
- ); -}; - -export default WeChatIcon; diff --git a/web/mt/src/components/common/markdown/MarkdownRenderer.jsx b/web/mt/src/components/common/markdown/MarkdownRenderer.jsx deleted file mode 100644 index a172b5d60..000000000 --- a/web/mt/src/components/common/markdown/MarkdownRenderer.jsx +++ /dev/null @@ -1,697 +0,0 @@ -/* -Copyright (C) 2025 modelstoken - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact admin@modelstoken.com -*/ - -import ReactMarkdown from 'react-markdown'; -import 'katex/dist/katex.min.css'; -import 'highlight.js/styles/github.css'; -import './markdown.css'; -import RemarkMath from 'remark-math'; -import RemarkBreaks from 'remark-breaks'; -import RehypeKatex from 'rehype-katex'; -import RemarkGfm from 'remark-gfm'; -import RehypeHighlight from 'rehype-highlight'; -import { useRef, useState, useEffect, useMemo } from 'react'; -import mermaid from 'mermaid'; -import React from 'react'; -import { useDebouncedCallback } from 'use-debounce'; -import clsx from 'clsx'; -import { Button, Tooltip, Toast } from '@douyinfe/semi-ui'; -import { copy, rehypeSplitWordsIntoSpans } from '../../../helpers'; -import { IconCopy } from '@douyinfe/semi-icons'; -import { useTranslation } from 'react-i18next'; - -mermaid.initialize({ - startOnLoad: false, - theme: 'default', - securityLevel: 'loose', -}); - -export function Mermaid(props) { - const ref = useRef(null); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - if (props.code && ref.current) { - mermaid - .run({ - nodes: [ref.current], - suppressErrors: true, - }) - .catch((e) => { - setHasError(true); - console.error('[Mermaid] ', e.message); - }); - } - }, [props.code]); - - function viewSvgInNewWindow() { - const svg = ref.current?.querySelector('svg'); - if (!svg) return; - const text = new XMLSerializer().serializeToString(svg); - const blob = new Blob([text], { type: 'image/svg+xml' }); - const url = URL.createObjectURL(blob); - window.open(url, '_blank'); - } - - if (hasError) { - return null; - } - - return ( -
viewSvgInNewWindow()} - > - {props.code} -
- ); -} - -function SandboxedHtmlPreview({ code }) { - const iframeRef = useRef(null); - const [iframeHeight, setIframeHeight] = useState(150); - - useEffect(() => { - const iframe = iframeRef.current; - if (!iframe) return; - - const handleLoad = () => { - try { - const doc = iframe.contentDocument || iframe.contentWindow?.document; - if (doc) { - const height = - doc.documentElement.scrollHeight || doc.body.scrollHeight; - setIframeHeight(Math.min(Math.max(height + 16, 60), 600)); - } - } catch { - // sandbox restrictions may prevent access, that's fine - } - }; - - iframe.addEventListener('load', handleLoad); - return () => iframe.removeEventListener('load', handleLoad); - }, [code]); - - return ( -