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 = () => {
/>
-
+
@@ -95,7 +87,7 @@ const FooterBar = () => {
-
-
-
-
)}
@@ -194,20 +114,6 @@ const FooterBar = () => {
© {currentYear} {systemName}. {t('版权所有')}
-
-
),
@@ -227,19 +133,6 @@ const FooterBar = () => {
className='custom-footer na-cb6feafeb3990c78 text-sm !text-semi-color-text-1'
dangerouslySetInnerHTML={{ __html: footer }}
>
-
) : (
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 = (
);
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
-
-
-
-
-
- You need to enable JavaScript to run this app.
-
-
-
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 (
-
-
-
-
-
- {systemName}
-
-
-
-
-
-
- {t('登 录')}
-
-
-
-
- {status.wechat_login && (
-
} style={{ color: '#07C160' }} />
- }
- onClick={onWeChatLoginClicked}
- loading={wechatLoading}
- >
-
{t('使用 微信 继续')}
-
- )}
-
- {status.github_oauth && (
-
}
- onClick={handleGitHubClick}
- loading={githubLoading}
- disabled={githubButtonDisabled}
- >
-
{githubButtonText}
-
- )}
-
- {status.discord_oauth && (
-
- }
- onClick={handleDiscordClick}
- loading={discordLoading}
- >
-
{t('使用 Discord 继续')}
-
- )}
-
- {status.oidc_enabled && (
-
}
- onClick={handleOIDCClick}
- loading={oidcLoading}
- >
-
{t('使用 OIDC 继续')}
-
- )}
-
- {status.linuxdo_oauth && (
-
- }
- onClick={handleLinuxDOClick}
- loading={linuxdoLoading}
- >
-
{t('使用 LinuxDO 继续')}
-
- )}
-
- {status.custom_oauth_providers &&
- status.custom_oauth_providers.map((provider) => (
-
handleCustomOAuthClick(provider)}
- loading={customOAuthLoading[provider.slug]}
- >
-
- {t('使用 {{name}} 继续', { name: provider.name })}
-
-
- ))}
-
- {status.telegram_oauth && (
-
-
-
- )}
-
- {status.passkey_login && passkeySupported && (
-
}
- onClick={handlePasskeyLogin}
- loading={passkeyLoading}
- >
-
{t('使用 Passkey 登录')}
-
- )}
-
-
- {t('或')}
-
-
-
}
- onClick={handleEmailLoginClick}
- loading={emailLoginLoading}
- >
-
{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 (
-
-
-
-
-
{systemName}
-
-
-
-
-
- {t('登 录')}
-
-
-
- {status.passkey_login && passkeySupported && (
-
}
- onClick={handlePasskeyLogin}
- loading={passkeyLoading}
- >
-
{t('使用 Passkey 登录')}
-
- )}
-
handleChange('username', value)}
- prefix={ }
- />
-
- handleChange('password', value)}
- prefix={ }
- />
-
- {(hasUserAgreement || hasPrivacyPolicy) && (
-
-
setAgreedToTerms(e.target.checked)}
- >
-
- {t('我已阅读并同意')}
- {hasUserAgreement && (
- <>
-
- {t('用户协议')}
-
- >
- )}
- {hasUserAgreement && hasPrivacyPolicy && t('和')}
- {hasPrivacyPolicy && (
- <>
-
- {t('隐私政策')}
-
- >
- )}
-
-
-
- )}
-
-
-
- {t('继续')}
-
-
-
- {t('忘记密码?')}
-
-
-
-
- {hasOAuthLoginOptions && (
- <>
-
- {t('或')}
-
-
-
-
- {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 (
-
- {/* 背景模糊晕染球 */}
-
-
-
-
-
-
-
-
- {systemName}
-
-
-
-
-
-
- {t('密码重置确认')}
-
-
-
- {!isValidResetLink && (
-
- )}
-
}
- placeholder={email ? '' : t('等待获取邮箱信息...')}
- />
-
- {newPassword && (
-
}
- suffix={
-
}
- type='tertiary'
- theme='borderless'
- onClick={async () => {
- await copy(newPassword);
- showNotice(
- `${t('密码已复制到剪贴板:')} ${newPassword}`,
- );
- }}
- >
- {t('复制')}
-
- }
- />
- )}
-
-
-
- {newPassword ? t('密码重置完成') : t('确认重置密码')}
-
-
-
-
-
-
-
- {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 (
-
- {/* 背景模糊晕染球 */}
-
-
-
-
-
-
-
-
- {systemName}
-
-
-
-
-
-
- {t('密码重置')}
-
-
-
-
}
- />
-
-
-
- {disableButton
- ? `${t('重试')} (${countdown})`
- : 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 (
-
-
-
-
-
- {systemName}
-
-
-
-
-
-
- {t('注 册')}
-
-
-
-
- {status.wechat_login && (
-
} style={{ color: '#07C160' }} />
- }
- onClick={onWeChatLoginClicked}
- loading={wechatLoading}
- >
-
{t('使用 微信 继续')}
-
- )}
-
- {status.github_oauth && (
-
}
- onClick={handleGitHubClick}
- loading={githubLoading}
- disabled={githubButtonDisabled}
- >
-
{githubButtonText}
-
- )}
-
- {status.discord_oauth && (
-
- }
- onClick={handleDiscordClick}
- loading={discordLoading}
- >
-
{t('使用 Discord 继续')}
-
- )}
-
- {status.oidc_enabled && (
-
}
- onClick={handleOIDCClick}
- loading={oidcLoading}
- >
-
{t('使用 OIDC 继续')}
-
- )}
-
- {status.linuxdo_oauth && (
-
- }
- onClick={handleLinuxDOClick}
- loading={linuxdoLoading}
- >
-
{t('使用 LinuxDO 继续')}
-
- )}
-
- {status.custom_oauth_providers &&
- status.custom_oauth_providers.map((provider) => (
-
handleCustomOAuthClick(provider)}
- loading={customOAuthLoading[provider.slug]}
- >
-
- {t('使用 {{name}} 继续', { name: provider.name })}
-
-
- ))}
-
- {status.telegram_oauth && (
-
-
-
- )}
-
-
- {t('或')}
-
-
-
}
- onClick={handleEmailRegisterClick}
- loading={emailRegisterLoading}
- >
-
{t('使用 用户名 注册')}
-
-
-
-
-
- {t('已有账户?')}{' '}
-
- {t('登录')}
-
-
-
-
-
-
-
- );
- };
-
- const renderEmailRegisterForm = () => {
- return (
-
-
-
-
-
- {systemName}
-
-
-
-
-
-
- {t('注 册')}
-
-
-
-
handleChange('username', value)}
- prefix={ }
- />
-
- handleChange('password', value)}
- prefix={ }
- />
-
- handleChange('password2', value)}
- prefix={ }
- />
-
- {showEmailVerification && (
- <>
- handleChange('email', value)}
- prefix={ }
- suffix={
-
- {disableButton
- ? `${t('重新发送')} (${countdown})`
- : t('获取验证码')}
-
- }
- />
-
- handleChange('verification_code', value)
- }
- prefix={ }
- />
- >
- )}
-
- {(hasUserAgreement || hasPrivacyPolicy) && (
-
-
setAgreedToTerms(e.target.checked)}
- >
-
- {t('我已阅读并同意')}
- {hasUserAgreement && (
- <>
-
- {t('用户协议')}
-
- >
- )}
- {hasUserAgreement && hasPrivacyPolicy && t('和')}
- {hasPrivacyPolicy && (
- <>
-
- {t('隐私政策')}
-
- >
- )}
-
-
-
- )}
-
-
-
- {t('注册')}
-
-
-
-
- {hasOAuthRegisterOptions && (
- <>
-
- {t('或')}
-
-
-
-
- {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 (
-
-
- 请输入认证器应用显示的验证码完成登录
-
-
-
-
-
- 验证并登录
-
-
-
-
-
-
- {
- setUseBackupCode(!useBackupCode);
- setVerificationCode('');
- }}
- style={{ marginRight: 16, color: '#1890ff', padding: 0 }}
- >
- {useBackupCode ? '使用认证器验证码' : '使用备用码'}
-
-
- {onBack && (
-
- 返回登录
-
- )}
-
-
-
-
- 提示:
-
- • 验证码每30秒更新一次
-
- • 如果无法获取验证码,请使用备用码
- • 每个备用码只能使用一次
-
-
-
- );
- }
-
- return (
-
-
-
-
两步验证
-
- 请输入认证器应用显示的验证码完成登录
-
-
-
-
-
-
- 验证并登录
-
-
-
-
-
-
- {
- setUseBackupCode(!useBackupCode);
- setVerificationCode('');
- }}
- style={{ marginRight: 16, color: '#1890ff', padding: 0 }}
- >
- {useBackupCode ? '使用认证器验证码' : '使用备用码'}
-
-
- {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 (
-
- );
- }
-
- // 如果是 HTML 内容,直接渲染
- if (isHtmlContent(content)) {
- return (
-
- );
- }
-
- // 其他内容统一使用 Markdown 渲染器
- return (
-
- );
-};
-
-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('页面渲染出错,请刷新页面重试')}
- />
- window.location.reload()}
- >
- {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 (
- <>
- {/* 查看密钥按钮 */}
-
- {t('查看密钥')}
-
-
- {/* 安全验证模态框 */}
-
-
- {/* 密钥显示模态框 */}
- setShowKeyModal(false)}
- footer={
- setShowKeyModal(false)}>
- {t('完成')}
-
- }
- 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 (
-
- );
-}
-
-export function PreCode(props) {
- const ref = useRef(null);
- const [mermaidCode, setMermaidCode] = useState('');
- const [htmlCode, setHtmlCode] = useState('');
- const { t } = useTranslation();
-
- const renderArtifacts = useDebouncedCallback(() => {
- if (!ref.current) return;
- const mermaidDom = ref.current.querySelector('code.language-mermaid');
- if (mermaidDom) {
- setMermaidCode(mermaidDom.innerText);
- }
- const htmlDom = ref.current.querySelector('code.language-html');
- const refText = ref.current.querySelector('code')?.innerText;
- if (htmlDom) {
- setHtmlCode(htmlDom.innerText);
- } else if (
- refText?.startsWith(' {
- if (ref.current) {
- const codeElements = ref.current.querySelectorAll('code');
- const wrapLanguages = [
- '',
- 'md',
- 'markdown',
- 'text',
- 'txt',
- 'plaintext',
- 'tex',
- 'latex',
- ];
- codeElements.forEach((codeElement) => {
- let languageClass = codeElement.className.match(/language-(\w+)/);
- let name = languageClass ? languageClass[1] : '';
- if (wrapLanguages.includes(name)) {
- codeElement.style.whiteSpace = 'pre-wrap';
- }
- });
- setTimeout(renderArtifacts, 1);
- }
- }, []);
-
- return (
- <>
-
-
-
- }
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- if (ref.current) {
- const codeElement = ref.current.querySelector('code');
- const code = codeElement?.textContent ?? '';
- copy(code).then((success) => {
- if (success) {
- Toast.success(t('代码已复制到剪贴板'));
- } else {
- Toast.error(t('复制失败,请手动复制'));
- }
- });
- }
- }}
- style={{
- padding: '4px',
- backgroundColor: 'var(--semi-color-bg-2)',
- borderRadius: '4px',
- cursor: 'pointer',
- border: '1px solid var(--semi-color-border)',
- boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
- }}
- />
-
-
- {props.children}
-
- {mermaidCode.length > 0 && (
-
- )}
- {htmlCode.length > 0 && (
-
- )}
- >
- );
-}
-
-function CustomCode(props) {
- const ref = useRef(null);
- const [collapsed, setCollapsed] = useState(true);
- const [showToggle, setShowToggle] = useState(false);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (ref.current) {
- const codeHeight = ref.current.scrollHeight;
- setShowToggle(codeHeight > 400);
- ref.current.scrollTop = ref.current.scrollHeight;
- }
- }, [props.children]);
-
- const toggleCollapsed = () => {
- setCollapsed((collapsed) => !collapsed);
- };
-
- const renderShowMoreButton = () => {
- if (showToggle && collapsed) {
- return (
-
-
- {t('显示更多')}
-
-
- );
- }
- return null;
- };
-
- return (
-
-
- {props.children}
-
- {renderShowMoreButton()}
-
- );
-}
-
-function escapeBrackets(text) {
- const pattern =
- /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
- return text.replace(
- pattern,
- (match, codeBlock, squareBracket, roundBracket) => {
- if (codeBlock) {
- return codeBlock;
- } else if (squareBracket) {
- return `$$${squareBracket}$$`;
- } else if (roundBracket) {
- return `$${roundBracket}$`;
- }
- return match;
- },
- );
-}
-
-function tryWrapHtmlCode(text) {
- // 尝试包装HTML代码
- if (text.includes('```')) {
- return text;
- }
- return text
- .replace(
- /([`]*?)(\w*?)([\n\r]*?)()/g,
- (match, quoteStart, lang, newLine, doctype) => {
- return !quoteStart ? '\n```html\n' + doctype : match;
- },
- )
- .replace(
- /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
- (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
- return !quoteEnd ? bodyEnd + space + htmlEnd + '\n```\n' : match;
- },
- );
-}
-
-function _MarkdownContent(props) {
- const {
- content,
- className,
- animated = false,
- previousContentLength = 0,
- } = props;
-
- const escapedContent = useMemo(() => {
- return tryWrapHtmlCode(escapeBrackets(content));
- }, [content]);
-
- // 判断是否为用户消息
- const isUserMessage = className && className.includes('user-message');
-
- const rehypePluginsBase = useMemo(() => {
- const base = [
- RehypeKatex,
- [
- RehypeHighlight,
- {
- detect: false,
- ignoreMissing: true,
- },
- ],
- ];
- if (animated) {
- base.push([rehypeSplitWordsIntoSpans, { previousContentLength }]);
- }
- return base;
- }, [animated, previousContentLength]);
-
- return (
- (
-
- ),
- a: (aProps) => {
- const href = aProps.href || '';
- if (/\.(aac|mp3|opus|wav)$/.test(href)) {
- return (
-
-
-
- );
- }
- if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
- return (
-
-
-
- );
- }
- const isInternal = /^\/#/i.test(href);
- const target = isInternal ? '_self' : (aProps.target ?? '_blank');
- return (
- {
- e.target.style.textDecoration = 'underline';
- }}
- onMouseLeave={(e) => {
- e.target.style.textDecoration = 'none';
- }}
- />
- );
- },
- h1: (props) => (
-
- ),
- h2: (props) => (
-
- ),
- h3: (props) => (
-
- ),
- h4: (props) => (
-
- ),
- h5: (props) => (
-
- ),
- h6: (props) => (
-
- ),
- blockquote: (props) => (
-
- ),
- ul: (props) => (
-
- ),
- ol: (props) => (
-
- ),
- li: (props) => (
-
- ),
- table: (props) => (
-
- ),
- th: (props) => (
-
- ),
- td: (props) => (
-
- ),
- }}
- >
- {escapedContent}
-
- );
-}
-
-export const MarkdownContent = React.memo(_MarkdownContent);
-
-export function MarkdownRenderer(props) {
- const {
- content,
- loading,
- fontSize = 14,
- fontFamily = 'inherit',
- className,
- style,
- animated = false,
- previousContentLength = 0,
- ...otherProps
- } = props;
-
- return (
-
- {loading ? (
-
- ) : (
-
- )}
-
- );
-}
-
-export default MarkdownRenderer;
diff --git a/web/mt/src/components/common/markdown/markdown.css b/web/mt/src/components/common/markdown/markdown.css
deleted file mode 100644
index e1e9e9cb4..000000000
--- a/web/mt/src/components/common/markdown/markdown.css
+++ /dev/null
@@ -1,449 +0,0 @@
-/* 基础markdown样式 */
-.markdown-body {
- font-family: inherit;
- line-height: 1.6;
- color: var(--semi-color-text-0);
- overflow-wrap: break-word;
- word-wrap: break-word;
- word-break: break-word;
-}
-
-/* 用户消息样式 - 白色字体适配蓝色背景 */
-.user-message {
- color: white !important;
-}
-
-.user-message .markdown-body {
- color: white !important;
-}
-
-.user-message h1,
-.user-message h2,
-.user-message h3,
-.user-message h4,
-.user-message h5,
-.user-message h6 {
- color: white !important;
-}
-
-.user-message p {
- color: white !important;
-}
-
-.user-message span {
- color: white !important;
-}
-
-.user-message div {
- color: white !important;
-}
-
-.user-message li {
- color: white !important;
-}
-
-.user-message td,
-.user-message th {
- color: white !important;
-}
-
-.user-message blockquote {
- color: white !important;
- border-left-color: rgba(255, 255, 255, 0.5) !important;
- background-color: rgba(255, 255, 255, 0.1) !important;
-}
-
-.user-message code:not(pre code) {
- color: #000 !important;
- background-color: rgba(255, 255, 255, 0.9) !important;
-}
-
-.user-message a {
- color: #87ceeb !important;
- /* 浅蓝色链接 */
-}
-
-.user-message a:hover {
- color: #b0e0e6 !important;
- /* hover时更浅的蓝色 */
-}
-
-/* 表格在用户消息中的样式 */
-.user-message table {
- border-color: rgba(255, 255, 255, 0.3) !important;
-}
-
-.user-message th {
- background-color: rgba(255, 255, 255, 0.2) !important;
- border-color: rgba(255, 255, 255, 0.3) !important;
-}
-
-.user-message td {
- border-color: rgba(255, 255, 255, 0.3) !important;
-}
-
-/* 加载动画 */
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
-}
-
-/* 代码高亮主题 - 适配Semi Design */
-.hljs {
- display: block;
- overflow-x: auto;
- padding: 0;
- background: transparent;
- color: var(--semi-color-text-0);
-}
-
-.hljs-comment,
-.hljs-quote {
- color: var(--semi-color-text-2);
- font-style: italic;
-}
-
-.hljs-keyword,
-.hljs-selector-tag,
-.hljs-subst {
- color: var(--semi-color-primary);
- font-weight: bold;
-}
-
-.hljs-number,
-.hljs-literal,
-.hljs-variable,
-.hljs-template-variable,
-.hljs-tag .hljs-attr {
- color: var(--semi-color-warning);
-}
-
-.hljs-string,
-.hljs-doctag {
- color: var(--semi-color-success);
-}
-
-.hljs-title,
-.hljs-section,
-.hljs-selector-id {
- color: var(--semi-color-primary);
- font-weight: bold;
-}
-
-.hljs-subst {
- font-weight: normal;
-}
-
-.hljs-type,
-.hljs-class .hljs-title {
- color: var(--semi-color-info);
- font-weight: bold;
-}
-
-.hljs-tag,
-.hljs-name,
-.hljs-attribute {
- color: var(--semi-color-primary);
- font-weight: normal;
-}
-
-.hljs-regexp,
-.hljs-link {
- color: var(--semi-color-tertiary);
-}
-
-.hljs-symbol,
-.hljs-bullet {
- color: var(--semi-color-warning);
-}
-
-.hljs-built_in,
-.hljs-builtin-name {
- color: var(--semi-color-info);
-}
-
-.hljs-meta {
- color: var(--semi-color-text-2);
-}
-
-.hljs-deletion {
- background: var(--semi-color-danger-light-default);
-}
-
-.hljs-addition {
- background: var(--semi-color-success-light-default);
-}
-
-.hljs-emphasis {
- font-style: italic;
-}
-
-.hljs-strong {
- font-weight: bold;
-}
-
-/* Mermaid容器样式 */
-.mermaid-container {
- transition: all 0.2s ease;
-}
-
-.mermaid-container:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- transform: translateY(-1px);
-}
-
-/* 代码块样式增强 */
-pre {
- position: relative;
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
- transition: all 0.2s ease;
-}
-
-pre:hover {
- border-color: var(--semi-color-primary) !important;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-pre:hover .copy-code-button {
- opacity: 1 !important;
-}
-
-.copy-code-button {
- opacity: 0;
- transition: opacity 0.2s ease;
- z-index: 10;
- pointer-events: auto;
-}
-
-.copy-code-button:hover {
- opacity: 1 !important;
-}
-
-.copy-code-button button {
- pointer-events: auto !important;
- cursor: pointer !important;
-}
-
-/* 确保按钮可点击 */
-.copy-code-button .semi-button {
- pointer-events: auto !important;
- cursor: pointer !important;
- transition: all 0.2s ease;
-}
-
-.copy-code-button .semi-button:hover {
- background-color: var(--semi-color-fill-1) !important;
- border-color: var(--semi-color-primary) !important;
- transform: scale(1.05);
-}
-
-/* 表格响应式 */
-@media (max-width: 768px) {
- .markdown-body table {
- font-size: 12px;
- }
-
- .markdown-body th,
- .markdown-body td {
- padding: 6px 8px;
- }
-}
-
-/* 数学公式样式 */
-.katex {
- font-size: 1em;
-}
-
-.katex-display {
- margin: 1em 0;
- text-align: center;
-}
-
-/* 链接hover效果 */
-.markdown-body a {
- transition: all 0.2s ease;
-}
-
-/* 引用块样式增强 */
-.markdown-body blockquote {
- position: relative;
-}
-
-.markdown-body blockquote::before {
- content: '"';
- position: absolute;
- left: -8px;
- top: -8px;
- font-size: 24px;
- color: var(--semi-color-primary);
- opacity: 0.3;
-}
-
-/* 列表样式增强 */
-.markdown-body ul li::marker {
- color: var(--semi-color-primary);
-}
-
-.markdown-body ol li::marker {
- color: var(--semi-color-primary);
- font-weight: bold;
-}
-
-/* 分隔线样式 */
-.markdown-body hr {
- border: none;
- height: 1px;
- background: linear-gradient(
- to right,
- transparent,
- var(--semi-color-border),
- transparent
- );
- margin: 24px 0;
-}
-
-/* 图片样式 */
-.markdown-body img {
- max-width: 100%;
- height: auto;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- margin: 12px 0;
-}
-
-/* 内联代码样式 */
-.markdown-body code:not(pre code) {
- background-color: var(--semi-color-fill-1);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.9em;
- color: var(--semi-color-primary);
- border: 1px solid var(--semi-color-border);
-}
-
-/* 标题锚点样式 */
-.markdown-body h1:hover,
-.markdown-body h2:hover,
-.markdown-body h3:hover,
-.markdown-body h4:hover,
-.markdown-body h5:hover,
-.markdown-body h6:hover {
- position: relative;
-}
-
-/* 任务列表样式 */
-.markdown-body input[type='checkbox'] {
- margin-right: 8px;
- transform: scale(1.1);
-}
-
-.markdown-body li.task-list-item {
- list-style: none;
- margin-left: -20px;
-}
-
-/* 键盘按键样式 */
-.markdown-body kbd {
- background-color: var(--semi-color-fill-0);
- border: 1px solid var(--semi-color-border);
- border-radius: 3px;
- box-shadow: 0 1px 0 var(--semi-color-border);
- color: var(--semi-color-text-0);
- display: inline-block;
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
- font-size: 0.85em;
- font-weight: 700;
- line-height: 1;
- padding: 2px 4px;
- white-space: nowrap;
-}
-
-/* 详情折叠样式 */
-.markdown-body details {
- border: 1px solid var(--semi-color-border);
- border-radius: 6px;
- padding: 12px;
- margin: 12px 0;
-}
-
-.markdown-body summary {
- cursor: pointer;
- font-weight: bold;
- color: var(--semi-color-primary);
- margin-bottom: 8px;
-}
-
-.markdown-body summary:hover {
- color: var(--semi-color-primary-hover);
-}
-
-/* 脚注样式 */
-.markdown-body .footnote-ref {
- color: var(--semi-color-primary);
- text-decoration: none;
- font-weight: bold;
-}
-
-.markdown-body .footnote-ref:hover {
- text-decoration: underline;
-}
-
-/* 警告块样式 */
-.markdown-body .warning {
- background-color: var(--semi-color-warning-light-default);
- border-left: 4px solid var(--semi-color-warning);
- padding: 12px 16px;
- margin: 12px 0;
- border-radius: 0 6px 6px 0;
-}
-
-.markdown-body .info {
- background-color: var(--semi-color-info-light-default);
- border-left: 4px solid var(--semi-color-info);
- padding: 12px 16px;
- margin: 12px 0;
- border-radius: 0 6px 6px 0;
-}
-
-.markdown-body .success {
- background-color: var(--semi-color-success-light-default);
- border-left: 4px solid var(--semi-color-success);
- padding: 12px 16px;
- margin: 12px 0;
- border-radius: 0 6px 6px 0;
-}
-
-.markdown-body .danger {
- background-color: var(--semi-color-danger-light-default);
- border-left: 4px solid var(--semi-color-danger);
- padding: 12px 16px;
- margin: 12px 0;
- border-radius: 0 6px 6px 0;
-}
-
-@keyframes fade-in {
- 0% {
- opacity: 0;
- transform: translateY(6px) scale(0.98);
- filter: blur(3px);
- }
- 60% {
- opacity: 0.85;
- filter: blur(0.5px);
- }
- 100% {
- opacity: 1;
- transform: translateY(0) scale(1);
- filter: blur(0);
- }
-}
-
-.animate-fade-in {
- animation: fade-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
- will-change: opacity, transform;
-}
diff --git a/web/mt/src/components/common/modals/RiskAcknowledgementModal.jsx b/web/mt/src/components/common/modals/RiskAcknowledgementModal.jsx
deleted file mode 100644
index e3cfad97d..000000000
--- a/web/mt/src/components/common/modals/RiskAcknowledgementModal.jsx
+++ /dev/null
@@ -1,298 +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, { useCallback, useEffect, useMemo, useState } from 'react';
-import {
- Modal,
- Button,
- Typography,
- Checkbox,
- Input,
- Space,
-} from '@douyinfe/semi-ui';
-import { IconAlertTriangle } from '@douyinfe/semi-icons';
-import { useIsMobile } from '../../../hooks/common/useIsMobile';
-import MarkdownRenderer from '../markdown/MarkdownRenderer';
-
-const { Text } = Typography;
-
-const RiskMarkdownBlock = React.memo(function RiskMarkdownBlock({
- markdownContent,
-}) {
- if (!markdownContent) {
- return null;
- }
-
- return (
-
-
-
- );
-});
-
-const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
- visible,
- title,
- markdownContent = '',
- detailTitle = '',
- detailItems = [],
- checklist = [],
- inputPrompt = '',
- requiredText = '',
- requiredTextParts = [],
- inputPlaceholder = '',
- mismatchText = '',
- cancelText = '',
- confirmText = '',
- onCancel,
- onConfirm,
-}) {
- const isMobile = useIsMobile();
- const [checkedItems, setCheckedItems] = useState([]);
- const [typedText, setTypedText] = useState('');
- const [typedTextParts, setTypedTextParts] = useState([]);
-
- const normalizedRequiredTextParts = useMemo(() => {
- let inputIndex = 0;
- return requiredTextParts.map((part) => {
- if (part.type === 'input') {
- const normalizedPart = { ...part, inputIndex };
- inputIndex += 1;
- return normalizedPart;
- }
- return part;
- });
- }, [requiredTextParts]);
-
- const requiredTextInputCount = useMemo(
- () =>
- normalizedRequiredTextParts.filter((part) => part.type === 'input')
- .length,
- [normalizedRequiredTextParts],
- );
- const hasSegmentedRequiredText = requiredTextInputCount > 0;
- const requiredTextToDisplay = hasSegmentedRequiredText
- ? normalizedRequiredTextParts.map((part) => part.text).join('')
- : requiredText;
-
- useEffect(() => {
- if (!visible) return;
- setCheckedItems(Array(checklist.length).fill(false));
- setTypedText('');
- setTypedTextParts(Array(requiredTextInputCount).fill(''));
- }, [visible, checklist.length, requiredTextInputCount]);
-
- const allChecked = useMemo(() => {
- if (checklist.length === 0) return true;
- return (
- checkedItems.length === checklist.length && checkedItems.every(Boolean)
- );
- }, [checkedItems, checklist.length]);
-
- const typedMatched = useMemo(() => {
- if (hasSegmentedRequiredText) {
- return normalizedRequiredTextParts.every((part) => {
- if (part.type === 'static') return true;
- return (
- typedTextParts[part.inputIndex ?? 0]?.trim() === part.text.trim()
- );
- });
- }
- if (!requiredText) return true;
- return typedText.trim() === requiredText.trim();
- }, [
- hasSegmentedRequiredText,
- normalizedRequiredTextParts,
- requiredText,
- typedText,
- typedTextParts,
- ]);
-
- const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
- const hasTypedRequiredText = hasSegmentedRequiredText
- ? typedTextParts.some((part) => part.trim() !== '')
- : typedText.length > 0;
- const canConfirm = allChecked && typedMatched;
-
- const handleChecklistChange = useCallback((index, checked) => {
- setCheckedItems((previous) => {
- const next = [...previous];
- next[index] = checked;
- return next;
- });
- }, []);
-
- const handleTextPartChange = useCallback((index, value) => {
- setTypedTextParts((previous) => {
- const next = [...previous];
- next[index] = value;
- return next;
- });
- }, []);
-
- return (
-
-
- {title}
-
- }
- width={isMobile ? '100%' : 860}
- centered
- maskClosable={false}
- closeOnEsc={false}
- onCancel={onCancel}
- bodyStyle={{
- maxHeight: isMobile ? '70vh' : '72vh',
- overflowY: 'auto',
- padding: isMobile ? '12px 16px' : '18px 22px',
- }}
- footer={
-
- {cancelText}
-
- {confirmText}
-
-
- }
- >
-
-
-
- {detailItems.length > 0 ? (
-
- {detailTitle ?
{detailTitle} : null}
-
- {detailText}
-
-
- ) : null}
-
- {checklist.length > 0 ? (
-
- {checklist.map((item, index) => (
- {
- handleChecklistChange(index, event.target.checked);
- }}
- >
- {item}
-
- ))}
-
- ) : null}
-
- {requiredTextToDisplay ? (
-
- {inputPrompt ?
{inputPrompt} : null}
-
- {requiredTextToDisplay}
-
- {hasSegmentedRequiredText ? (
-
- {normalizedRequiredTextParts.map((part, index) =>
- part.type === 'static' ? (
-
- {part.text}
-
- ) : (
-
- handleTextPartChange(part.inputIndex ?? 0, value)
- }
- placeholder={
- part.placeholder || part.text || inputPlaceholder
- }
- autoFocus={visible && part.inputIndex === 0}
- onCopy={(event) => event.preventDefault()}
- onCut={(event) => event.preventDefault()}
- onPaste={(event) => event.preventDefault()}
- onDrop={(event) => event.preventDefault()}
- style={{ width: isMobile ? '100%' : 260 }}
- />
- ),
- )}
-
- ) : (
-
event.preventDefault()}
- onCut={(event) => event.preventDefault()}
- onPaste={(event) => event.preventDefault()}
- onDrop={(event) => event.preventDefault()}
- />
- )}
- {!typedMatched && hasTypedRequiredText ? (
-
- {mismatchText}
-
- ) : null}
-
- ) : null}
-
-
- );
-});
-
-export default RiskAcknowledgementModal;
diff --git a/web/mt/src/components/common/modals/SecureVerificationModal.jsx b/web/mt/src/components/common/modals/SecureVerificationModal.jsx
deleted file mode 100644
index 7cdf76e2c..000000000
--- a/web/mt/src/components/common/modals/SecureVerificationModal.jsx
+++ /dev/null
@@ -1,322 +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 { useTranslation } from 'react-i18next';
-import {
- Modal,
- Button,
- Input,
- Typography,
- Tabs,
- TabPane,
- Space,
- Spin,
-} from '@douyinfe/semi-ui';
-
-/**
- * 通用安全验证模态框组件
- * 配合 useSecureVerification Hook 使用
- * @param {Object} props
- * @param {boolean} props.visible - 是否显示模态框
- * @param {Object} props.verificationMethods - 可用的验证方式
- * @param {Object} props.verificationState - 当前验证状态
- * @param {Function} props.onVerify - 验证回调
- * @param {Function} props.onCancel - 取消回调
- * @param {Function} props.onCodeChange - 验证码变化回调
- * @param {Function} props.onMethodSwitch - 验证方式切换回调
- * @param {string} props.title - 模态框标题
- * @param {string} props.description - 验证描述文本
- */
-const SecureVerificationModal = ({
- visible,
- verificationMethods,
- verificationState,
- onVerify,
- onCancel,
- onCodeChange,
- onMethodSwitch,
- title,
- description,
-}) => {
- const { t } = useTranslation();
- const [isAnimating, setIsAnimating] = useState(false);
- const [verifySuccess, setVerifySuccess] = useState(false);
-
- const { has2FA, hasPasskey, passkeySupported } = verificationMethods;
- const { method, loading, code } = verificationState;
-
- useEffect(() => {
- if (visible) {
- setIsAnimating(true);
- setVerifySuccess(false);
- } else {
- setIsAnimating(false);
- }
- }, [visible]);
-
- const handleKeyDown = (e) => {
- if (e.key === 'Enter' && code.trim() && !loading && method === '2fa') {
- onVerify(method, code);
- }
- if (e.key === 'Escape' && !loading) {
- onCancel();
- }
- };
-
- // 如果用户没有启用任何验证方式
- if (visible && !has2FA && !hasPasskey) {
- return (
- {t('确定')}}
- width={500}
- style={{ maxWidth: '90vw' }}
- >
-
-
-
- {t('需要安全验证')}
-
-
- {t('您需要先启用两步验证或 Passkey 才能查看敏感信息。')}
-
-
-
- {t('请前往个人设置 → 安全设置进行配置。')}
-
-
-
- );
- }
-
- return (
-
-
- {/* 描述信息 */}
- {description && (
-
- {description}
-
- )}
-
- {/* 验证方式选择 */}
-
- {has2FA && (
-
-
-
-
-
-
- }
- style={{ width: '100%' }}
- />
-
-
-
- {t('从认证器应用中获取验证码,或使用备用码')}
-
-
-
-
- {t('取消')}
-
- onVerify(method, code)}
- >
- {t('验证')}
-
-
-
-
- )}
-
- {hasPasskey && passkeySupported && (
-
-
-
-
-
- {t('使用 Passkey 验证')}
-
-
- {t('点击验证按钮,使用您的生物特征或安全密钥')}
-
-
-
-
-
- {t('取消')}
-
- onVerify(method)}
- >
- {t('验证 Passkey')}
-
-
-
-
- )}
-
-
-
- );
-};
-
-export default SecureVerificationModal;
diff --git a/web/mt/src/components/common/modals/TwoFactorAuthModal.jsx b/web/mt/src/components/common/modals/TwoFactorAuthModal.jsx
deleted file mode 100644
index 4ffad914c..000000000
--- a/web/mt/src/components/common/modals/TwoFactorAuthModal.jsx
+++ /dev/null
@@ -1,148 +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 { useTranslation } from 'react-i18next';
-import { Modal, Button, Input, Typography } from '@douyinfe/semi-ui';
-
-/**
- * 可复用的两步验证模态框组件
- * @param {Object} props
- * @param {boolean} props.visible - 是否显示模态框
- * @param {string} props.code - 验证码值
- * @param {boolean} props.loading - 是否正在验证
- * @param {Function} props.onCodeChange - 验证码变化回调
- * @param {Function} props.onVerify - 验证回调
- * @param {Function} props.onCancel - 取消回调
- * @param {string} props.title - 模态框标题
- * @param {string} props.description - 验证描述文本
- * @param {string} props.placeholder - 输入框占位文本
- */
-const TwoFactorAuthModal = ({
- visible,
- code,
- loading,
- onCodeChange,
- onVerify,
- onCancel,
- title,
- description,
- placeholder,
-}) => {
- const { t } = useTranslation();
-
- const handleKeyDown = (e) => {
- if (e.key === 'Enter' && code && !loading) {
- onVerify();
- }
- };
-
- return (
-
-
- {title || t('安全验证')}
-
- }
- visible={visible}
- onCancel={onCancel}
- footer={
- <>
- {t('取消')}
-
- {t('验证')}
-
- >
- }
- width={500}
- style={{ maxWidth: '90vw' }}
- >
-
- {/* 安全提示 */}
-
-
-
-
-
-
-
- {t('安全验证')}
-
-
- {description || t('为了保护账户安全,请验证您的两步验证码。')}
-
-
-
-
-
- {/* 验证码输入 */}
-
-
- {t('验证身份')}
-
-
-
- {t(
- '支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。',
- )}
-
-
-
-
- );
-};
-
-export default TwoFactorAuthModal;
diff --git a/web/mt/src/components/common/ui/CardPro.jsx b/web/mt/src/components/common/ui/CardPro.jsx
deleted file mode 100644
index 164f70ec3..000000000
--- a/web/mt/src/components/common/ui/CardPro.jsx
+++ /dev/null
@@ -1,200 +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 { Card, Divider, Typography, Button } from '@douyinfe/semi-ui';
-import PropTypes from 'prop-types';
-import { useIsMobile } from '../../../hooks/common/useIsMobile';
-import { IconEyeOpened, IconEyeClosed } from '@douyinfe/semi-icons';
-
-const { Text } = Typography;
-
-/**
- * CardPro 高级卡片组件
- *
- * 布局分为6个区域:
- * 1. 统计信息区域 (statsArea)
- * 2. 描述信息区域 (descriptionArea)
- * 3. 类型切换/标签区域 (tabsArea)
- * 4. 操作按钮区域 (actionsArea)
- * 5. 搜索表单区域 (searchArea)
- * 6. 分页区域 (paginationArea) - 固定在卡片底部
- *
- * 支持三种布局类型:
- * - type1: 操作型 (如TokensTable) - 描述信息 + 操作按钮 + 搜索表单
- * - type2: 查询型 (如LogsTable) - 统计信息 + 搜索表单
- * - type3: 复杂型 (如ChannelsTable) - 描述信息 + 类型切换 + 操作按钮 + 搜索表单
- */
-const CardPro = ({
- type = 'type1',
- className = '',
- children,
- // 各个区域的内容
- statsArea,
- descriptionArea,
- tabsArea,
- actionsArea,
- searchArea,
- paginationArea, // 新增分页区域
- // 卡片属性
- shadows = '',
- bordered = true,
- // 自定义样式
- style,
- // 国际化函数
- t = (key) => key,
- ...props
-}) => {
- const isMobile = useIsMobile();
- const [showMobileActions, setShowMobileActions] = useState(false);
-
- const toggleMobileActions = () => {
- setShowMobileActions(!showMobileActions);
- };
-
- const hasMobileHideableContent = actionsArea || searchArea;
-
- const renderHeader = () => {
- const hasContent =
- statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
- if (!hasContent) return null;
-
- return (
-
- {/* 统计信息区域 - 用于type2 */}
- {type === 'type2' && statsArea && <>{statsArea}>}
-
- {/* 描述信息区域 - 用于type1和type3 */}
- {(type === 'type1' || type === 'type3') && descriptionArea && (
- <>{descriptionArea}>
- )}
-
- {/* 第一个分隔线 - 在描述信息或统计信息后面 */}
- {((type === 'type1' || type === 'type3') && descriptionArea) ||
- (type === 'type2' && statsArea) ? (
-
- ) : null}
-
- {/* 类型切换/标签区域 - 主要用于type3 */}
- {type === 'type3' && tabsArea && <>{tabsArea}>}
-
- {/* 移动端操作切换按钮 */}
- {isMobile && hasMobileHideableContent && (
- <>
-
- : }
- type='tertiary'
- size='small'
- theme='outline'
- block
- >
- {showMobileActions ? t('隐藏操作项') : t('显示操作项')}
-
-
- >
- )}
-
- {/* 操作按钮和搜索表单的容器 */}
-
- {/* 操作按钮区域 - 用于type1和type3 */}
- {(type === 'type1' || type === 'type3') &&
- actionsArea &&
- (Array.isArray(actionsArea) ? (
- actionsArea.map((area, idx) => (
-
- {idx !== 0 && }
- {area}
-
- ))
- ) : (
-
{actionsArea}
- ))}
-
- {/* 当同时存在操作区和搜索区时,插入分隔线 */}
- {actionsArea && searchArea &&
}
-
- {/* 搜索表单区域 - 所有类型都可能有 */}
- {searchArea &&
{searchArea}
}
-
-
- );
- };
-
- const headerContent = renderHeader();
-
- // 渲染分页区域
- const renderFooter = () => {
- if (!paginationArea) return null;
-
- return (
-
- {paginationArea}
-
- );
- };
-
- const footerContent = renderFooter();
-
- return (
-
- {children}
-
- );
-};
-
-CardPro.propTypes = {
- // 布局类型
- type: PropTypes.oneOf(['type1', 'type2', 'type3']),
- // 样式相关
- className: PropTypes.string,
- style: PropTypes.object,
- shadows: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
- bordered: PropTypes.bool,
- // 内容区域
- statsArea: PropTypes.node,
- descriptionArea: PropTypes.node,
- tabsArea: PropTypes.node,
- actionsArea: PropTypes.oneOfType([
- PropTypes.node,
- PropTypes.arrayOf(PropTypes.node),
- ]),
- searchArea: PropTypes.node,
- paginationArea: PropTypes.node,
- // 表格内容
- children: PropTypes.node,
- // 国际化函数
- t: PropTypes.func,
-};
-
-export default CardPro;
diff --git a/web/mt/src/components/common/ui/CardTable.jsx b/web/mt/src/components/common/ui/CardTable.jsx
deleted file mode 100644
index e5faaf104..000000000
--- a/web/mt/src/components/common/ui/CardTable.jsx
+++ /dev/null
@@ -1,242 +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, useEffect, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
- Table,
- Card,
- Skeleton,
- Pagination,
- Empty,
- Button,
- Collapsible,
-} from '@douyinfe/semi-ui';
-import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
-import PropTypes from 'prop-types';
-import { useIsMobile } from '../../../hooks/common/useIsMobile';
-import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
-
-/**
- * CardTable 响应式表格组件
- *
- * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
- * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
- */
-const CardTable = ({
- columns = [],
- dataSource = [],
- loading = false,
- rowKey = 'key',
- hidePagination = false,
- ...tableProps
-}) => {
- const isMobile = useIsMobile();
- const { t } = useTranslation();
-
- const showSkeleton = useMinimumLoadingTime(loading);
-
- const getRowKey = (record, index) => {
- if (typeof rowKey === 'function') return rowKey(record);
- return record[rowKey] !== undefined ? record[rowKey] : index;
- };
-
- if (!isMobile) {
- const finalTableProps = hidePagination
- ? { ...tableProps, pagination: false }
- : tableProps;
-
- return (
-
- );
- }
-
- if (showSkeleton) {
- const visibleCols = columns.filter((col) => {
- if (tableProps?.visibleColumns && col.key) {
- return tableProps.visibleColumns[col.key];
- }
- return true;
- });
-
- const renderSkeletonCard = (key) => {
- const placeholder = (
-
- {visibleCols.map((col, idx) => {
- if (!col.title) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
- );
- })}
-
- );
-
- return (
-
-
-
- );
- };
-
- return (
-
- {[1, 2, 3].map((i) => renderSkeletonCard(i))}
-
- );
- }
-
- const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
-
- const MobileRowCard = ({ record, index }) => {
- const [showDetails, setShowDetails] = useState(false);
- const rowKeyVal = getRowKey(record, index);
-
- const hasDetails =
- tableProps.expandedRowRender &&
- (!tableProps.rowExpandable || tableProps.rowExpandable(record));
-
- return (
-
- {columns.map((col, colIdx) => {
- if (
- tableProps?.visibleColumns &&
- !tableProps.visibleColumns[col.key]
- ) {
- return null;
- }
-
- const title = col.title;
- const cellContent = col.render
- ? col.render(record[col.dataIndex], record, index)
- : record[col.dataIndex];
-
- if (!title) {
- return (
-
- {cellContent}
-
- );
- }
-
- return (
-
-
- {title}
-
-
- {cellContent !== undefined && cellContent !== null
- ? cellContent
- : '-'}
-
-
- );
- })}
-
- {hasDetails && (
- <>
- : }
- onClick={(e) => {
- e.stopPropagation();
- setShowDetails(!showDetails);
- }}
- >
- {showDetails ? t('收起') : t('详情')}
-
-
-
- {tableProps.expandedRowRender(record, index)}
-
-
- >
- )}
-
- );
- };
-
- if (isEmpty) {
- if (tableProps.empty) return tableProps.empty;
- return (
-
-
-
- );
- }
-
- return (
-
- {dataSource.map((record, index) => (
-
- ))}
- {!hidePagination && tableProps.pagination && dataSource.length > 0 && (
-
- )}
-
- );
-};
-
-CardTable.propTypes = {
- columns: PropTypes.array.isRequired,
- dataSource: PropTypes.array,
- loading: PropTypes.bool,
- rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
- hidePagination: PropTypes.bool,
-};
-
-export default CardTable;
diff --git a/web/mt/src/components/common/ui/ChannelKeyDisplay.jsx b/web/mt/src/components/common/ui/ChannelKeyDisplay.jsx
deleted file mode 100644
index b9d6ad175..000000000
--- a/web/mt/src/components/common/ui/ChannelKeyDisplay.jsx
+++ /dev/null
@@ -1,280 +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 { useTranslation } from 'react-i18next';
-import { Card, Button, Typography, Tag } from '@douyinfe/semi-ui';
-import { copy, showSuccess } from '../../../helpers';
-
-/**
- * 解析密钥数据,支持多种格式
- * @param {string} keyData - 密钥数据
- * @param {Function} t - 翻译函数
- * @returns {Array} 解析后的密钥数组
- */
-const parseChannelKeys = (keyData, t) => {
- if (!keyData) return [];
-
- const trimmed = keyData.trim();
-
- // 检查是否是JSON数组格式(如Vertex AI)
- if (trimmed.startsWith('[')) {
- try {
- const parsed = JSON.parse(trimmed);
- if (Array.isArray(parsed)) {
- return parsed.map((item, index) => ({
- id: index,
- content:
- typeof item === 'string' ? item : JSON.stringify(item, null, 2),
- type: typeof item === 'string' ? 'text' : 'json',
- label: `${t('密钥')} ${index + 1}`,
- }));
- }
- } catch (e) {
- // 如果解析失败,按普通文本处理
- console.warn('Failed to parse JSON keys:', e);
- }
- }
-
- // 检查是否是多行密钥(按换行符分割)
- const lines = trimmed.split('\n').filter((line) => line.trim());
- if (lines.length > 1) {
- return lines.map((line, index) => ({
- id: index,
- content: line.trim(),
- type: 'text',
- label: `${t('密钥')} ${index + 1}`,
- }));
- }
-
- // 单个密钥
- return [
- {
- id: 0,
- content: trimmed,
- type: trimmed.startsWith('{') ? 'json' : 'text',
- label: t('密钥'),
- },
- ];
-};
-
-/**
- * 可复用的密钥显示组件
- * @param {Object} props
- * @param {string} props.keyData - 密钥数据
- * @param {boolean} props.showSuccessIcon - 是否显示成功图标
- * @param {string} props.successText - 成功文本
- * @param {boolean} props.showWarning - 是否显示安全警告
- * @param {string} props.warningText - 警告文本
- */
-const ChannelKeyDisplay = ({
- keyData,
- showSuccessIcon = true,
- successText,
- showWarning = true,
- warningText,
-}) => {
- const { t } = useTranslation();
-
- const parsedKeys = parseChannelKeys(keyData, t);
- const isMultipleKeys = parsedKeys.length > 1;
-
- const handleCopyAll = () => {
- copy(keyData);
- showSuccess(t('所有密钥已复制到剪贴板'));
- };
-
- const handleCopyKey = (content) => {
- copy(content);
- showSuccess(t('密钥已复制到剪贴板'));
- };
-
- return (
-
- {/* 成功状态 */}
- {showSuccessIcon && (
-
-
-
-
-
- {successText || t('验证成功')}
-
-
- )}
-
- {/* 密钥内容 */}
-
-
-
- {isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')}
-
- {isMultipleKeys && (
-
-
- {t('共 {{count}} 个密钥', { count: parsedKeys.length })}
-
-
- {t('复制全部')}
-
-
- )}
-
-
-
- {parsedKeys.map((keyItem) => (
-
-
-
-
- {keyItem.label}
-
-
- {keyItem.type === 'json' && (
-
- {t('JSON')}
-
- )}
-
-
-
-
- }
- onClick={() => handleCopyKey(keyItem.content)}
- >
- {t('复制')}
-
-
-
-
-
-
- {keyItem.content}
-
-
-
- {keyItem.type === 'json' && (
-
- {t('JSON格式密钥,请确保格式正确')}
-
- )}
-
-
- ))}
-
-
- {isMultipleKeys && (
-
-
-
-
-
- {t(
- '检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。',
- )}
-
-
- )}
-
-
- {/* 安全警告 */}
- {showWarning && (
-
-
-
-
-
-
-
- {t('安全提醒')}
-
-
- {warningText ||
- t(
- '请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。',
- )}
-
-
-
-
- )}
-
- );
-};
-
-export default ChannelKeyDisplay;
diff --git a/web/mt/src/components/common/ui/CompactModeToggle.jsx b/web/mt/src/components/common/ui/CompactModeToggle.jsx
deleted file mode 100644
index 796236e0d..000000000
--- a/web/mt/src/components/common/ui/CompactModeToggle.jsx
+++ /dev/null
@@ -1,68 +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 { Button } from '@douyinfe/semi-ui';
-import PropTypes from 'prop-types';
-import { useIsMobile } from '../../../hooks/common/useIsMobile';
-
-/**
- * 紧凑模式切换按钮组件
- * 用于在自适应列表和紧凑列表之间切换
- * 在移动端时自动隐藏,因为移动端使用"显示操作项"按钮来控制内容显示
- */
-const CompactModeToggle = ({
- compactMode,
- setCompactMode,
- t,
- size = 'small',
- type = 'tertiary',
- className = '',
- ...props
-}) => {
- const isMobile = useIsMobile();
-
- // 在移动端隐藏紧凑列表切换按钮
- if (isMobile) {
- return null;
- }
-
- return (
- setCompactMode(!compactMode)}
- {...props}
- >
- {compactMode ? t('自适应列表') : t('紧凑列表')}
-
- );
-};
-
-CompactModeToggle.propTypes = {
- compactMode: PropTypes.bool.isRequired,
- setCompactMode: PropTypes.func.isRequired,
- t: PropTypes.func.isRequired,
- size: PropTypes.string,
- type: PropTypes.string,
- className: PropTypes.string,
-};
-
-export default CompactModeToggle;
diff --git a/web/mt/src/components/common/ui/JSONEditor.jsx b/web/mt/src/components/common/ui/JSONEditor.jsx
deleted file mode 100644
index 3ecebad7b..000000000
--- a/web/mt/src/components/common/ui/JSONEditor.jsx
+++ /dev/null
@@ -1,718 +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, useEffect, useCallback, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
- Button,
- Form,
- Typography,
- Banner,
- Tabs,
- TabPane,
- Card,
- Input,
- InputNumber,
- Switch,
- TextArea,
- Row,
- Col,
- Divider,
- Tooltip,
-} from '@douyinfe/semi-ui';
-import { IconPlus, IconDelete, IconAlertTriangle } from '@douyinfe/semi-icons';
-
-const { Text } = Typography;
-
-// 唯一 ID 生成器,确保在组件生命周期内稳定且递增
-const generateUniqueId = (() => {
- let counter = 0;
- return () => `kv_${counter++}`;
-})();
-
-const JSONEditor = ({
- value = '',
- onChange,
- field,
- label,
- placeholder,
- extraText,
- extraFooter,
- showClear = true,
- template,
- templateLabel,
- editorType = 'keyValue',
- rules = [],
- formApi = null,
- renderStringValueSuffix,
- ...props
-}) => {
- const { t } = useTranslation();
-
- // 将对象转换为键值对数组(包含唯一ID)
- const objectToKeyValueArray = useCallback((obj, prevPairs = []) => {
- if (!obj || typeof obj !== 'object') return [];
-
- const entries = Object.entries(obj);
- return entries.map(([key, value], index) => {
- // 如果上一次转换后同位置的键一致,则沿用其 id,保持 React key 稳定
- const prev = prevPairs[index];
- const shouldReuseId = prev && prev.key === key;
- return {
- id: shouldReuseId ? prev.id : generateUniqueId(),
- key,
- value,
- };
- });
- }, []);
-
- // 将键值对数组转换为对象(重复键时后面的会覆盖前面的)
- const keyValueArrayToObject = useCallback((arr) => {
- const result = {};
- arr.forEach((item) => {
- if (item.key) {
- result[item.key] = item.value;
- }
- });
- return result;
- }, []);
-
- // 初始化键值对数组
- const [keyValuePairs, setKeyValuePairs] = useState(() => {
- if (typeof value === 'string' && value.trim()) {
- try {
- const parsed = JSON.parse(value);
- return objectToKeyValueArray(parsed);
- } catch (error) {
- return [];
- }
- }
- if (typeof value === 'object' && value !== null) {
- return objectToKeyValueArray(value);
- }
- return [];
- });
-
- // 手动模式下的本地文本缓冲
- const [manualText, setManualText] = useState(() => {
- if (typeof value === 'string') return value;
- if (value && typeof value === 'object')
- return JSON.stringify(value, null, 2);
- return '';
- });
-
- // 根据键数量决定默认编辑模式
- const [editMode, setEditMode] = useState(() => {
- if (typeof value === 'string' && value.trim()) {
- try {
- const parsed = JSON.parse(value);
- const keyCount = Object.keys(parsed).length;
- return keyCount > 10 ? 'manual' : 'visual';
- } catch (error) {
- return 'manual';
- }
- }
- return 'visual';
- });
-
- const [jsonError, setJsonError] = useState('');
-
- // 计算重复的键
- const duplicateKeys = useMemo(() => {
- const keyCount = {};
- const duplicates = new Set();
-
- keyValuePairs.forEach((pair) => {
- if (pair.key) {
- keyCount[pair.key] = (keyCount[pair.key] || 0) + 1;
- if (keyCount[pair.key] > 1) {
- duplicates.add(pair.key);
- }
- }
- });
-
- return duplicates;
- }, [keyValuePairs]);
-
- // 数据同步 - 当value变化时更新键值对数组
- useEffect(() => {
- try {
- let parsed = {};
- if (typeof value === 'string' && value.trim()) {
- parsed = JSON.parse(value);
- } else if (typeof value === 'object' && value !== null) {
- parsed = value;
- }
-
- // 只在外部值真正改变时更新,避免循环更新
- const currentObj = keyValueArrayToObject(keyValuePairs);
- if (JSON.stringify(parsed) !== JSON.stringify(currentObj)) {
- setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
- }
- setJsonError('');
- } catch (error) {
- console.log('JSON解析失败:', error.message);
- setJsonError(error.message);
- }
- }, [value]);
-
- // 外部 value 变化时,若不在手动模式,则同步手动文本
- useEffect(() => {
- if (editMode !== 'manual') {
- if (typeof value === 'string') setManualText(value);
- else if (value && typeof value === 'object')
- setManualText(JSON.stringify(value, null, 2));
- else setManualText('');
- }
- }, [value, editMode]);
-
- // 处理可视化编辑的数据变化
- const handleVisualChange = useCallback(
- (newPairs) => {
- setKeyValuePairs(newPairs);
- const jsonObject = keyValueArrayToObject(newPairs);
- const jsonString =
- Object.keys(jsonObject).length === 0
- ? ''
- : JSON.stringify(jsonObject, null, 2);
-
- setJsonError('');
-
- // 通过formApi设置值
- if (formApi && field) {
- formApi.setValue(field, jsonString);
- }
-
- onChange?.(jsonString);
- },
- [onChange, formApi, field, keyValueArrayToObject],
- );
-
- // 处理手动编辑的数据变化
- const handleManualChange = useCallback(
- (newValue) => {
- setManualText(newValue);
- if (newValue && newValue.trim()) {
- try {
- const parsed = JSON.parse(newValue);
- setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
- setJsonError('');
- onChange?.(newValue);
- } catch (error) {
- setJsonError(error.message);
- }
- } else {
- setKeyValuePairs([]);
- setJsonError('');
- onChange?.('');
- }
- },
- [onChange, objectToKeyValueArray, keyValuePairs],
- );
-
- // 切换编辑模式
- const toggleEditMode = useCallback(() => {
- if (editMode === 'visual') {
- const jsonObject = keyValueArrayToObject(keyValuePairs);
- setManualText(
- Object.keys(jsonObject).length === 0
- ? ''
- : JSON.stringify(jsonObject, null, 2),
- );
- setEditMode('manual');
- } else {
- try {
- let parsed = {};
- if (manualText && manualText.trim()) {
- parsed = JSON.parse(manualText);
- } else if (typeof value === 'string' && value.trim()) {
- parsed = JSON.parse(value);
- } else if (typeof value === 'object' && value !== null) {
- parsed = value;
- }
- setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs));
- setJsonError('');
- setEditMode('visual');
- } catch (error) {
- setJsonError(error.message);
- return;
- }
- }
- }, [
- editMode,
- value,
- manualText,
- keyValuePairs,
- keyValueArrayToObject,
- objectToKeyValueArray,
- ]);
-
- // 添加键值对
- const addKeyValue = useCallback(() => {
- const newPairs = [...keyValuePairs];
- const existingKeys = newPairs.map((p) => p.key);
- let counter = 1;
- let newKey = `field_${counter}`;
- while (existingKeys.includes(newKey)) {
- counter += 1;
- newKey = `field_${counter}`;
- }
- newPairs.push({
- id: generateUniqueId(),
- key: newKey,
- value: '',
- });
- handleVisualChange(newPairs);
- }, [keyValuePairs, handleVisualChange]);
-
- // 删除键值对
- const removeKeyValue = useCallback(
- (id) => {
- const newPairs = keyValuePairs.filter((pair) => pair.id !== id);
- handleVisualChange(newPairs);
- },
- [keyValuePairs, handleVisualChange],
- );
-
- // 更新键名
- const updateKey = useCallback(
- (id, newKey) => {
- const newPairs = keyValuePairs.map((pair) =>
- pair.id === id ? { ...pair, key: newKey } : pair,
- );
- handleVisualChange(newPairs);
- },
- [keyValuePairs, handleVisualChange],
- );
-
- // 更新值
- const updateValue = useCallback(
- (id, newValue) => {
- const newPairs = keyValuePairs.map((pair) =>
- pair.id === id ? { ...pair, value: newValue } : pair,
- );
- handleVisualChange(newPairs);
- },
- [keyValuePairs, handleVisualChange],
- );
-
- // 填入模板
- const fillTemplate = useCallback(() => {
- if (template) {
- const templateString = JSON.stringify(template, null, 2);
-
- if (formApi && field) {
- formApi.setValue(field, templateString);
- }
-
- setManualText(templateString);
- setKeyValuePairs(objectToKeyValueArray(template, keyValuePairs));
- onChange?.(templateString);
- setJsonError('');
- }
- }, [
- template,
- onChange,
- formApi,
- field,
- objectToKeyValueArray,
- keyValuePairs,
- ]);
-
- // 渲染值输入控件(支持嵌套)
- const renderValueInput = (pairId, pairKey, value) => {
- const valueType = typeof value;
-
- if (valueType === 'boolean') {
- return (
-
- updateValue(pairId, newValue)}
- />
-
- {value ? t('true') : t('false')}
-
-
- );
- }
-
- if (valueType === 'number') {
- return (
- updateValue(pairId, newValue)}
- style={{ width: '100%' }}
- placeholder={t('输入数字')}
- />
- );
- }
-
- if (valueType === 'object' && value !== null) {
- // 简化嵌套对象的处理,使用TextArea
- return (
-