mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:13:59 +08:00
fix(web): auth form state management (#37003)
This commit is contained in:
parent
02e1a60cde
commit
5c7f05bd10
@ -11,7 +11,7 @@ When('I sign in as the default E2E admin', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
|
||||
await page.getByLabel('Email address').fill(adminCredentials.email)
|
||||
await page.getByLabel('Password').fill(adminCredentials.password)
|
||||
await page.getByLabel('Password', { exact: true }).fill(adminCredentials.password)
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
})
|
||||
|
||||
|
||||
@ -4992,22 +4992,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/signin/components/mail-and-code-auth.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/signin/components/mail-and-password-auth.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/signin/invite-settings/page.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { sendEMailLoginCode } from '@/service/common'
|
||||
|
||||
@ -20,8 +21,9 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||
const [email, setEmail] = useState(emailFromLink)
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const locale = useLocale()
|
||||
const setCountdownLeftTime = useSetLocalStorage<string>(COUNT_DOWN_KEY, { raw: true })
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
@ -34,10 +36,10 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
||||
toast.error(t('error.emailInValid', { ns: 'login' }))
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
setLoading(true)
|
||||
const ret = await sendEMailLoginCode(email, locale)
|
||||
if (ret.result === 'success') {
|
||||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
||||
setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`)
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('email', encodeURIComponent(email))
|
||||
params.set('token', encodeURIComponent(ret.data))
|
||||
@ -48,27 +50,31 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
||||
console.error(error)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
handleGetEMailVerificationCode()
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input type="text" className="hidden" />
|
||||
<div className="mb-2">
|
||||
<label htmlFor="email" className="my-2 system-md-semibold text-text-secondary">{t('email', { ns: 'login' })}</label>
|
||||
<div className="mt-1">
|
||||
<Input id="email" type="email" disabled={isInvite} value={email} placeholder={t('emailPlaceholder', { ns: 'login' }) as string} onChange={e => setEmail(e.target.value)} />
|
||||
</div>
|
||||
<Form
|
||||
onFormSubmit={() => {
|
||||
void handleGetEMailVerificationCode()
|
||||
}}
|
||||
>
|
||||
<FieldRoot name="email" disabled={isInvite} className="mb-2">
|
||||
<FieldLabel className="my-2 py-0 system-md-semibold text-text-secondary">{t('email', { ns: 'login' })}</FieldLabel>
|
||||
<FieldControl
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
spellCheck={false}
|
||||
disabled={isInvite}
|
||||
value={email}
|
||||
placeholder={t('emailPlaceholder', { ns: 'login' }) as string}
|
||||
onValueChange={setEmail}
|
||||
/>
|
||||
<div className="mt-3">
|
||||
<Button type="submit" loading={loading} disabled={loading || !email} variant="primary" className="w-full">{t('signup.verifyMail', { ns: 'login' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FieldRoot>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import type { ResponseError } from '@/service/fetch'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { emailRegex } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import Link from '@/next/link'
|
||||
@ -20,10 +19,24 @@ import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
type MailAndPasswordAuthProps = {
|
||||
isInvite: boolean
|
||||
isEmailSetup: boolean
|
||||
allowRegistration: boolean
|
||||
}
|
||||
|
||||
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration: _allowRegistration }: MailAndPasswordAuthProps) {
|
||||
type LoginRequestBody = {
|
||||
email: string
|
||||
password: string
|
||||
language: string
|
||||
remember_me: boolean
|
||||
invite_token?: string
|
||||
}
|
||||
|
||||
function hasErrorCode(error: unknown, code: string) {
|
||||
return typeof error === 'object'
|
||||
&& error !== null
|
||||
&& 'code' in error
|
||||
&& error.code === code
|
||||
}
|
||||
|
||||
export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndPasswordAuthProps) {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
@ -52,7 +65,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const loginData: Record<string, any> = {
|
||||
const loginData: LoginRequestBody = {
|
||||
email,
|
||||
password: encryptPassword(password),
|
||||
language: locale,
|
||||
@ -88,7 +101,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if ((error as ResponseError).code === 'authentication_failed') {
|
||||
if (hasErrorCode(error, 'authentication_failed')) {
|
||||
toast.error(t('error.invalidEmailOrPassword', { ns: 'login' }))
|
||||
}
|
||||
}
|
||||
@ -98,28 +111,29 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={noop}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="my-2 system-md-semibold text-text-secondary">
|
||||
<Form
|
||||
onFormSubmit={() => {
|
||||
void handleEmailPasswordLogin()
|
||||
}}
|
||||
>
|
||||
<FieldRoot name="email" disabled={isInvite} className="mb-3">
|
||||
<FieldLabel className="my-2 py-0 system-md-semibold text-text-secondary">
|
||||
{t('email', { ns: 'login' })}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
disabled={isInvite}
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FieldLabel>
|
||||
<FieldControl
|
||||
value={email}
|
||||
onValueChange={setEmail}
|
||||
disabled={isInvite}
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
spellCheck={false}
|
||||
placeholder={t('emailPlaceholder', { ns: 'login' }) || ''}
|
||||
/>
|
||||
</FieldRoot>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="my-2 flex items-center justify-between">
|
||||
<span className="system-md-semibold text-text-secondary">{t('password', { ns: 'login' })}</span>
|
||||
<FieldRoot name="password" className="mb-3">
|
||||
<div className="my-2 flex items-center justify-between">
|
||||
<FieldLabel className="py-0 system-md-semibold text-text-secondary">{t('password', { ns: 'login' })}</FieldLabel>
|
||||
<Link
|
||||
href={`/reset-password?${searchParams.toString()}`}
|
||||
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
|
||||
@ -128,44 +142,45 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
>
|
||||
{t('forget', { ns: 'login' })}
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
<div className="relative mt-1">
|
||||
<Input
|
||||
id="password"
|
||||
<FieldControl
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter')
|
||||
handleEmailPasswordLogin()
|
||||
}}
|
||||
onValueChange={setPassword}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
spellCheck={false}
|
||||
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}
|
||||
tabIndex={2}
|
||||
className="pr-10"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label={t(showPassword ? 'hidePassword' : 'showPassword', { ns: 'login' })}
|
||||
aria-pressed={showPassword}
|
||||
className="mr-1 size-8 p-0 text-text-tertiary hover:text-text-secondary"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
{showPassword
|
||||
? <span className="i-ri-eye-off-line size-4" aria-hidden="true" />
|
||||
: <span className="i-ri-eye-line size-4" aria-hidden="true" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FieldRoot>
|
||||
|
||||
<div className="mb-2">
|
||||
<Button
|
||||
tabIndex={2}
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
variant="primary"
|
||||
onClick={handleEmailPasswordLogin}
|
||||
disabled={isLoading || !email || !password}
|
||||
className="w-full"
|
||||
>
|
||||
{t('signBtn', { ns: 'login' })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,8 +2,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
@ -20,7 +19,9 @@ import SSOAuth from './components/sso-auth'
|
||||
import Split from './split'
|
||||
import { resolvePostLoginRedirect } from './utils/post-login-redirect'
|
||||
|
||||
const NormalForm = () => {
|
||||
type AuthType = 'code' | 'password'
|
||||
|
||||
function NormalForm() {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
@ -30,55 +31,58 @@ const NormalForm = () => {
|
||||
const { isPending: isCheckLoading, data: userResp, error: probeError } = useQuery({
|
||||
...userProfileQueryOptions(),
|
||||
throwOnError: err => !isLegacyBase401(err),
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
const isLoggedIn = !!userResp && !probeError
|
||||
const message = decodeURIComponent(searchParams.get('message') || '')
|
||||
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
|
||||
const [isInitCheckLoading, setInitCheckLoading] = useState(true)
|
||||
const [isRedirecting, setIsRedirecting] = useState(false)
|
||||
const isLoading = isCheckLoading || isInitCheckLoading || isRedirecting
|
||||
const inviteToken = decodeURIComponent(searchParams.get('invite_token') || '')
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
|
||||
const [showORLine, setShowORLine] = useState(false)
|
||||
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
|
||||
const [workspaceName, setWorkSpaceName] = useState('')
|
||||
const [selectedAuthType, setSelectedAuthType] = useState<AuthType | null>(null)
|
||||
|
||||
const isInviteLink = Boolean(invite_token && invite_token !== 'null')
|
||||
const isInviteLink = Boolean(inviteToken && inviteToken !== 'null')
|
||||
const { data: invitationCheckResp, isPending: isInviteCheckLoading, isError: isInviteCheckError } = useQuery({
|
||||
queryKey: ['signin', 'invite-check', inviteToken],
|
||||
queryFn: () => invitationCheck({
|
||||
url: '/activate/check',
|
||||
params: {
|
||||
token: inviteToken,
|
||||
},
|
||||
}),
|
||||
enabled: isInviteLink,
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const init = useCallback(async () => {
|
||||
try {
|
||||
if (isLoggedIn) {
|
||||
setIsRedirecting(true)
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/')
|
||||
return
|
||||
}
|
||||
const workspaceName = invitationCheckResp?.data?.workspace_name || ''
|
||||
const hasSocialLogin = systemFeatures.enable_social_oauth_login
|
||||
const hasSsoLogin = Boolean(systemFeatures.sso_enforced_for_signin)
|
||||
const hasEmailCodeLogin = systemFeatures.enable_email_code_login
|
||||
const hasEmailPasswordLogin = systemFeatures.enable_email_password_login
|
||||
const hasEmailLogin = hasEmailCodeLogin || hasEmailPasswordLogin
|
||||
const defaultAuthType: AuthType = hasEmailPasswordLogin ? 'password' : 'code'
|
||||
const authType = selectedAuthType === 'password' && hasEmailPasswordLogin
|
||||
? 'password'
|
||||
: selectedAuthType === 'code' && hasEmailCodeLogin
|
||||
? 'code'
|
||||
: defaultAuthType
|
||||
const showORLine = (hasSocialLogin || hasSsoLogin) && hasEmailLogin
|
||||
const noLoginMethodsConfigured = !hasSocialLogin && !hasEmailCodeLogin && !hasEmailPasswordLogin && !hasSsoLogin
|
||||
const allMethodsAreDisabled = noLoginMethodsConfigured || isInviteCheckError
|
||||
const isLoading = isCheckLoading || isLoggedIn || (isInviteLink && isInviteCheckLoading)
|
||||
|
||||
if (message) {
|
||||
toast.error(message)
|
||||
}
|
||||
setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
|
||||
setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
|
||||
updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
|
||||
if (isInviteLink) {
|
||||
const checkRes = await invitationCheck({
|
||||
url: '/activate/check',
|
||||
params: {
|
||||
token: invite_token,
|
||||
},
|
||||
})
|
||||
setWorkSpaceName(checkRes?.data?.workspace_name || '')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setAllMethodsAreDisabled(true)
|
||||
}
|
||||
finally { setInitCheckLoading(false) }
|
||||
}, [isLoggedIn, message, router, searchParams, invite_token, isInviteLink, systemFeatures])
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [init])
|
||||
if (!isLoggedIn)
|
||||
return
|
||||
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
router.replace(redirectUrl || '/')
|
||||
}, [isLoggedIn, router, searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
if (message)
|
||||
toast.error(message)
|
||||
}, [message])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={
|
||||
@ -169,8 +173,8 @@ const NormalForm = () => {
|
||||
)}
|
||||
<div className="relative">
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
{systemFeatures.enable_social_oauth_login && <SocialAuth />}
|
||||
{systemFeatures.sso_enforced_for_signin && (
|
||||
{hasSocialLogin && <SocialAuth />}
|
||||
{hasSsoLogin && (
|
||||
<div className="w-full">
|
||||
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
|
||||
</div>
|
||||
@ -187,23 +191,23 @@ const NormalForm = () => {
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && (
|
||||
hasEmailLogin && (
|
||||
<>
|
||||
{systemFeatures.enable_email_code_login && authType === 'code' && (
|
||||
{hasEmailCodeLogin && authType === 'code' && (
|
||||
<>
|
||||
<MailAndCodeAuth isInvite={isInviteLink} />
|
||||
{systemFeatures.enable_email_password_login && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('password') }}>
|
||||
{hasEmailPasswordLogin && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { setSelectedAuthType('password') }}>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('usePassword', { ns: 'login' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{systemFeatures.enable_email_password_login && authType === 'password' && (
|
||||
{hasEmailPasswordLogin && authType === 'password' && (
|
||||
<>
|
||||
<MailAndPasswordAuth isInvite={isInviteLink} isEmailSetup={systemFeatures.is_email_setup} allowRegistration={systemFeatures.is_allow_register} />
|
||||
{systemFeatures.enable_email_code_login && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('code') }}>
|
||||
<MailAndPasswordAuth isInvite={isInviteLink} isEmailSetup={systemFeatures.is_email_setup} />
|
||||
{hasEmailCodeLogin && (
|
||||
<div className="cursor-pointer py-1 text-center" onClick={() => { setSelectedAuthType('code') }}>
|
||||
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('useVerificationCode', { ns: 'login' })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
"forgotPasswordDesc": "Please enter your email address to reset your password. We will send you an email with instructions on how to reset your password.",
|
||||
"go": "Go to Dify",
|
||||
"goToInit": "If you have not initialized the account, please go to the initialization page",
|
||||
"hidePassword": "Hide password",
|
||||
"installBtn": "Set up",
|
||||
"interfaceLanguage": "Interface Language",
|
||||
"invalid": "The link has expired",
|
||||
@ -92,6 +93,7 @@
|
||||
"setAdminAccount": "Setting up an admin account",
|
||||
"setAdminAccountDesc": "Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.",
|
||||
"setYourAccount": "Set Your Account",
|
||||
"showPassword": "Show password",
|
||||
"signBtn": "Sign in",
|
||||
"signup.createAccount": "Create your account",
|
||||
"signup.haveAccount": "Already have an account? ",
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
"forgotPasswordDesc": "请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件,包含如何重置密码的说明。",
|
||||
"go": "跳转至 Dify",
|
||||
"goToInit": "如果您还没有初始化账户,请前往初始化页面",
|
||||
"hidePassword": "隐藏密码",
|
||||
"installBtn": "设置",
|
||||
"interfaceLanguage": "界面语言",
|
||||
"invalid": "链接已失效",
|
||||
@ -92,6 +93,7 @@
|
||||
"setAdminAccount": "设置管理员账户",
|
||||
"setAdminAccountDesc": "管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。",
|
||||
"setYourAccount": "设置您的账户",
|
||||
"showPassword": "显示密码",
|
||||
"signBtn": "登录",
|
||||
"signup.createAccount": "创建您的账户",
|
||||
"signup.haveAccount": "已有账户?",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user