Explorar o código

fix: azure-openai key validate (#164)

zxhlyh hai 1 ano
pai
achega
1c5f63de7e

+ 55 - 14
web/app/components/header/account-setting/provider-page/azure-provider/index.tsx

@@ -1,10 +1,17 @@
 import type { Provider, ProviderAzureToken } from '@/models/common'
+import { ProviderName } from '@/models/common'
 import { useTranslation } from 'react-i18next'
 import Link from 'next/link'
 import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
-import ProviderInput, { ProviderValidateTokenInput} from '../provider-input'
-import { useState } from 'react'
-import { ValidatedStatus } from '../provider-input/useValidateToken'
+import { useState, useEffect } from 'react'
+import ProviderInput from '../provider-input'
+import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
+import { 
+  ValidatedErrorIcon, 
+  ValidatedSuccessIcon,
+  ValidatingTip,
+  ValidatedErrorOnAzureOpenaiTip
+} from '../provider-input/Validate'
 
 interface IAzureProviderProps {
   provider: Provider
@@ -17,19 +24,51 @@ const AzureProvider = ({
   onValidatedStatus
 }: IAzureProviderProps) => {
   const { t } = useTranslation()
-  const [token, setToken] = useState(provider.token as ProviderAzureToken || {})
-  const handleFocus = () => {
-    if (token === provider.token) {
-      token.openai_api_key = ''
+  const [token, setToken] = useState<ProviderAzureToken>(provider.provider_name === ProviderName.AZURE_OPENAI ? {...provider.token}: {})
+  const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name)
+  const handleFocus = (type: keyof ProviderAzureToken) => {
+    if (token[type] === (provider?.token as ProviderAzureToken)[type]) {
+      token[type] = ''
       setToken({...token})
       onTokenChange({...token})
+      setValidatedStatus(undefined)
     }
   }
-  const handleChange = (type: keyof ProviderAzureToken, v: string) => {
+  const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => {
     token[type] = v
     setToken({...token})
     onTokenChange({...token})
+    validate({...token}, {
+      beforeValidating: () => {
+        if (!token.openai_api_base || !token.openai_api_key) {
+          setValidatedStatus(undefined)
+          return false
+        }
+        return true
+      }
+    })
   }
+  const getValidatedIcon = () => {
+    if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) {
+      return <ValidatedErrorIcon />
+    }
+    if (validatedStatus === ValidatedStatus.Success) {
+      return <ValidatedSuccessIcon />
+    }
+  }
+  const getValidatedTip = () => {
+    if (validating) {
+      return <ValidatingTip />
+    }
+    if (validatedStatus === ValidatedStatus.Error) {
+      return <ValidatedErrorOnAzureOpenaiTip />
+    }
+  }
+  useEffect(() => {
+    if (typeof onValidatedStatus === 'function') {
+      onValidatedStatus(validatedStatus)
+    }
+  }, [validatedStatus])
 
   return (
     <div className='px-4 py-3'>
@@ -38,17 +77,19 @@ const AzureProvider = ({
         name={t('common.provider.azure.apiBase')}
         placeholder={t('common.provider.azure.apiBasePlaceholder')}
         value={token.openai_api_base}
-        onChange={(v) => handleChange('openai_api_base', v)}
+        onChange={(v) => handleChange('openai_api_base', v, validate)}
+        onFocus={() => handleFocus('openai_api_base')}
+        validatedIcon={getValidatedIcon()}
       />
-      <ProviderValidateTokenInput 
+      <ProviderInput 
         className='mb-4'
         name={t('common.provider.azure.apiKey')}
         placeholder={t('common.provider.azure.apiKeyPlaceholder')}
         value={token.openai_api_key}
-        onChange={v => handleChange('openai_api_key', v)}
-        onFocus={handleFocus}
-        onValidatedStatus={onValidatedStatus}
-        providerName={provider.provider_name}
+        onChange={(v) => handleChange('openai_api_key', v, validate)}
+        onFocus={() => handleFocus('openai_api_key')}
+        validatedIcon={getValidatedIcon()}
+        validatedTip={getValidatedTip()}
       />
       <Link className="flex items-center text-xs cursor-pointer text-primary-600" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
         {t('common.provider.azure.helpTip')}

+ 1 - 1
web/app/components/header/account-setting/provider-page/index.tsx

@@ -67,7 +67,7 @@ const ProviderPage = () => {
   const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0]
 
   return (
-    <div>
+    <div className='pb-7'>
       {
         providerHosted && !IS_CE_EDITION && (
           <>

+ 71 - 199
web/app/components/header/account-setting/provider-page/openai-provider/index.tsx

@@ -1,222 +1,94 @@
-import { ChangeEvent, useEffect, useRef, useState } from 'react'
-import { useContext } from 'use-context-selector'
+import type { Provider } from '@/models/common'
+import { useState, useEffect } from 'react'
 import { useTranslation } from 'react-i18next'
-import { debounce } from 'lodash-es'
+import ProviderInput from '../provider-input'
 import Link from 'next/link'
-import useSWR from 'swr'
-import { ArrowTopRightOnSquareIcon, PencilIcon } from '@heroicons/react/24/outline'
-import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid'
-import Button from '@/app/components/base/button'
-import s from './index.module.css'
-import classNames from 'classnames'
-import { fetchTenantInfo, validateProviderKey, updateProviderAIKey } from '@/service/common'
-import { ToastContext } from '@/app/components/base/toast'
-import Indicator from '../../../indicator'
-import I18n from '@/context/i18n'
+import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
+import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken'
+import { 
+  ValidatedErrorIcon, 
+  ValidatedSuccessIcon,
+  ValidatingTip,
+  ValidatedExceedOnOpenaiTip,
+  ValidatedErrorOnOpenaiTip
+} from '../provider-input/Validate'
 
-type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
-
-type TInputWithStatusProps = {
-  value: string
-  onChange: (v: string) => void
-  onValidating: (validating: boolean) => void
-  verifiedStatus: IStatusType
-  onVerified: (verified: IStatusType) => void
-}
-const InputWithStatus = ({
-  value,
-  onChange,
-  onValidating,
-  verifiedStatus,
-  onVerified
-}: TInputWithStatusProps) => {
-  const { t } = useTranslation()
-  const validateKey = useRef(debounce(async (token: string) => {
-    if (!token) return
-    onValidating(true)
-    try {
-      const res = await validateProviderKey({ url: '/workspaces/current/providers/openai/token-validate', body: { token } })
-      onVerified(res.result === 'success' ? 'verified' : 'error')
-    } catch (e: any) {
-      if (e.status === 400) {
-        e.json().then(({ code }: any) => {
-          if (code === 'provider_request_failed') {
-            onVerified('error-api-key-exceed-bill')
-          }
-        })
-      } else {
-        onVerified('error')
-      }
-    } finally {
-      onValidating(false)
-    }
-  }, 500))
- 
-  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const inputValue = e.target.value
-    onChange(inputValue)
-    if (!inputValue) {
-      onVerified('normal')
-    }
-    validateKey.current(inputValue)
-  }
-  return (
-    <div className={classNames('flex items-center h-9 px-3 bg-white border border-gray-300 rounded-lg', s.input)}>
-      <input
-        value={value}
-        placeholder={t('common.provider.enterYourKey') || ''}
-        className='w-full h-9 mr-2 appearance-none outline-none bg-transparent text-xs'
-        onChange={handleChange} 
-      />
-      {
-        verifiedStatus === 'error' && <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' />
-      }
-      {
-        verifiedStatus === 'verified' && <CheckCircleIcon className='w-4 h-4 text-[#039855]' />
-      }
-    </div>
-  )
+interface IOpenaiProviderProps {
+  provider: Provider
+  onValidatedStatus: (status?: ValidatedStatus) => void
+  onTokenChange: (token: string) => void
 }
 
-const OpenaiProvider = () => {
+const OpenaiProvider = ({
+  provider,
+  onValidatedStatus,
+  onTokenChange
+}: IOpenaiProviderProps) => {
   const { t } = useTranslation()
-  const { locale } = useContext(I18n)
-  const { data: userInfo, mutate } = useSWR({ url: '/info' }, fetchTenantInfo)
-  const [inputValue, setInputValue] = useState<string>('')
-  const [validating, setValidating] = useState(false)
-  const [editStatus, setEditStatus] = useState<IStatusType>('normal')
-  const [loading, setLoading] = useState(false)
-  const [editing, setEditing] = useState(false)
-  const [invalidStatus, setInvalidStatus] = useState(false)
-  const { notify } = useContext(ToastContext)
-  const provider = userInfo?.providers?.find(({ provider }) => provider === 'openai')
- 
-  const handleReset = () => {
-    setInputValue('')
-    setValidating(false)
-    setEditStatus('normal')
-    setLoading(false)
-    setEditing(false)
+  const [token, setToken] = useState(provider.token as string || '')
+  const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name)
+  const handleFocus = () => {
+    if (token === provider.token) {
+      setToken('')
+      onTokenChange('')
+      setValidatedStatus(undefined)
+    }
   }
-  const handleSave = async () => {
-    if (editStatus === 'verified') {
-      try {
-        setLoading(true)
-        await updateProviderAIKey({ url: '/workspaces/current/providers/openai/token', body: { token: inputValue ?? '' } })
-        notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
-      } catch (e) {
-        notify({ type: 'error', message: t('common.provider.saveFailed') })
-      } finally {
-        setLoading(false)
-        handleReset()
-        mutate()
+  const handleChange = (v: string) => {
+    setToken(v)
+    onTokenChange(v)
+    validate(v, {
+      beforeValidating: () => {
+        if (!v) {
+          setValidatedStatus(undefined)
+          return false
+        }
+        return true
       }
-    }
+    })
   }
   useEffect(() => {
-    if (provider && !provider.token_is_valid && provider.token_is_set) {
-      setInvalidStatus(true)
+    if (typeof onValidatedStatus === 'function') {
+      onValidatedStatus(validatedStatus)
     }
-  }, [userInfo])
+  }, [validatedStatus])
 
-  const showInvalidStatus = invalidStatus && !editing
-  const renderErrorMessage = () => {
+  const getValidatedIcon = () => {
+    if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) {
+      return <ValidatedErrorIcon />
+    }
+    if (validatedStatus === ValidatedStatus.Success) {
+      return <ValidatedSuccessIcon />
+    }
+  }
+  const getValidatedTip = () => {
     if (validating) {
-      return (
-        <div className={`mt-2 text-primary-600 text-xs font-normal`}>
-          {t('common.provider.validating')}
-        </div>
-      )
+      return <ValidatingTip />
     }
-    if (editStatus === 'error-api-key-exceed-bill') {
-      return (
-        <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
-          {t('common.provider.apiKeyExceedBill')}&nbsp;
-          <Link 
-            className='underline'
-            href="https://platform.openai.com/account/api-keys" 
-            target={'_blank'}>
-            {locale === 'en' ? 'this link' : '这篇文档'}
-          </Link>
-        </div>
-      )
+    if (validatedStatus === ValidatedStatus.Exceed) {
+      return <ValidatedExceedOnOpenaiTip />
     }
-    if (showInvalidStatus || editStatus === 'error') {
-      return (
-        <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
-          {t('common.provider.invalidKey')}
-        </div>
-      )
+    if (validatedStatus === ValidatedStatus.Error) {
+      return <ValidatedErrorOnOpenaiTip />
     }
-    return null
   }
 
   return (
     <div className='px-4 pt-3 pb-4'>
-        <div className='flex items-center mb-2 h-6'>
-          <div className='grow text-[13px] text-gray-800 font-medium'>
-            {t('common.provider.apiKey')} 
-          </div>
-          {
-            provider && !editing && (
-              <div 
-                className='
-                  flex items-center h-6 px-2 rounded-md border border-gray-200
-                  text-xs font-medium text-gray-700 cursor-pointer
-                '
-                onClick={() => setEditing(true)}
-              >
-                <PencilIcon className='mr-1 w-3 h-3 text-gray-500' />
-                {t('common.operation.edit')}
-              </div>
-            )
-          }
-          {
-            (inputValue || editing) && (
-              <>
-                <Button 
-                  className={classNames('mr-1', s.button)} 
-                  loading={loading}
-                  onClick={handleReset}
-                >
-                  {t('common.operation.cancel')}
-                </Button>
-                <Button 
-                  type='primary' 
-                  className={classNames(s.button)} 
-                  loading={loading} 
-                  onClick={handleSave}>
-                  {t('common.operation.save')}
-                </Button>
-              </>
-            )
-          }
-        </div>
-        {
-          (!provider || (provider && editing)) && (
-            <InputWithStatus
-              value={inputValue}
-              onChange={v => setInputValue(v)}
-              verifiedStatus={editStatus}
-              onVerified={v => setEditStatus(v)}
-              onValidating={v => setValidating(v)}
-            />
-          )
-        }
-        {
-          (provider && !editing) && (
-            <div className={classNames('flex justify-between items-center bg-white px-3 h-9 rounded-lg text-gray-800 text-xs font-medium', s.input)}>
-              sk-0C...skuA
-              <Indicator color={(provider.token_is_set && provider.token_is_valid) ? 'green' : 'orange'} />
-            </div>
-          )
-        }
-        {renderErrorMessage()}
-        <Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
-          {t('appOverview.welcome.getKeyTip')}
-          <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
-        </Link>
-      </div>
+      <ProviderInput 
+        value={token}
+        name={t('common.provider.apiKey')}
+        placeholder={t('common.provider.enterYourKey')}
+        onChange={handleChange}
+        onFocus={handleFocus}
+        validatedIcon={getValidatedIcon()}
+        validatedTip={getValidatedTip()}
+      />
+      <Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
+        {t('appOverview.welcome.getKeyTip')}
+        <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
+      </Link>
+    </div>
   )
 }
 

+ 0 - 52
web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx

@@ -1,52 +0,0 @@
-import type { Provider } from '@/models/common'
-import { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { ProviderValidateTokenInput } from '../provider-input'
-import Link from 'next/link'
-import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
-import { ValidatedStatus } from '../provider-input/useValidateToken'
-
-interface IOpenaiProviderProps {
-  provider: Provider
-  onValidatedStatus: (status?: ValidatedStatus) => void
-  onTokenChange: (token: string) => void
-}
-
-const OpenaiProvider = ({
-  provider,
-  onValidatedStatus,
-  onTokenChange
-}: IOpenaiProviderProps) => {
-  const { t } = useTranslation()
-  const [token, setToken] = useState(provider.token as string || '')
-  const handleFocus = () => {
-    if (token === provider.token) {
-      setToken('')
-      onTokenChange('')
-    }
-  }
-  const handleChange = (v: string) => {
-    setToken(v)
-    onTokenChange(v)
-  }
-
-  return (
-    <div className='px-4 pt-3 pb-4'>
-      <ProviderValidateTokenInput 
-        value={token}
-        name={t('common.provider.apiKey')}
-        placeholder={t('common.provider.enterYourKey')}
-        onChange={handleChange}
-        onFocus={handleFocus}
-        onValidatedStatus={onValidatedStatus}
-        providerName={provider.provider_name}
-      />
-      <Link className="inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
-        {t('appOverview.welcome.getKeyTip')}
-        <ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
-      </Link>
-    </div>
-  )
-}
-
-export default OpenaiProvider

+ 59 - 0
web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx

@@ -0,0 +1,59 @@
+import Link from 'next/link'
+import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import I18n from '@/context/i18n'
+
+export const ValidatedErrorIcon = () => {
+  return <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' />
+}
+
+export const ValidatedSuccessIcon = () => {
+  return <CheckCircleIcon className='w-4 h-4 text-[#039855]' />
+}
+
+export const ValidatingTip = () => {
+  const { t } = useTranslation()
+  return (
+    <div className={`mt-2 text-primary-600 text-xs font-normal`}>
+      {t('common.provider.validating')}
+    </div>
+  )
+}
+
+export const ValidatedExceedOnOpenaiTip = () => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+
+  return (
+    <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
+      {t('common.provider.apiKeyExceedBill')}&nbsp;
+      <Link 
+        className='underline'
+        href="https://platform.openai.com/account/api-keys" 
+        target={'_blank'}>
+        {locale === 'en' ? 'this link' : '这篇文档'}
+      </Link>
+    </div>
+  )
+}
+
+export const ValidatedErrorOnOpenaiTip = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
+      {t('common.provider.invalidKey')}
+    </div>
+  )
+}
+
+export const ValidatedErrorOnAzureOpenaiTip = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
+      {t('common.provider.invalidApiKey')}
+    </div>
+  )
+}

+ 8 - 95
web/app/components/header/account-setting/provider-page/provider-input/index.tsx

@@ -1,10 +1,5 @@
-import { ChangeEvent, useEffect } from 'react'
-import Link from 'next/link'
-import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid'
-import { useTranslation } from 'react-i18next'
-import { useContext } from 'use-context-selector'
-import I18n from '@/context/i18n'
-import useValidateToken, { ValidatedStatus } from './useValidateToken'
+import { ChangeEvent } from 'react'
+import { ReactElement } from 'react-markdown/lib/react-markdown'
 
 interface IProviderInputProps {
   value?: string
@@ -13,6 +8,8 @@ interface IProviderInputProps {
   className?: string
   onChange: (v: string) => void
   onFocus?: () => void
+  validatedIcon?: ReactElement
+  validatedTip?: ReactElement
 }
 
 const ProviderInput = ({
@@ -22,6 +19,8 @@ const ProviderInput = ({
   className,
   onChange,
   onFocus,
+  validatedIcon,
+  validatedTip
 }: IProviderInputProps) => {
 
   const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
@@ -47,95 +46,9 @@ const ProviderInput = ({
           onChange={handleChange}
           onFocus={onFocus}
         />
+        {validatedIcon}
       </div>
-    </div>
-  )
-}
-
-type TproviderInputProps = IProviderInputProps 
-  & { 
-      onValidatedStatus?: (status?: ValidatedStatus) => void
-      providerName: string
-    }
-export const ProviderValidateTokenInput = ({
-  value,
-  name,
-  placeholder,
-  className,
-  onChange,
-  onFocus,
-  onValidatedStatus,
-  providerName
-}: TproviderInputProps) => {
-  const { t } = useTranslation()
-  const { locale } = useContext(I18n)
-  const [ validating, validatedStatus, validate ] = useValidateToken(providerName)
-
-  useEffect(() => {
-    if (typeof onValidatedStatus === 'function') {
-      onValidatedStatus(validatedStatus)
-    }
-  }, [validatedStatus])
-
-  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
-    const inputValue = e.target.value
-    onChange(inputValue)
-
-    validate(inputValue)
-  }
-
-  return (
-    <div className={className}>
-      <div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div>
-      <div className='
-        flex items-center px-3 bg-white rounded-lg
-        shadow-[0_1px_2px_rgba(16,24,40,0.05)]
-      '>
-        <input 
-          className='
-            w-full py-[9px]
-            text-xs font-medium text-gray-700 leading-[18px]
-            appearance-none outline-none bg-transparent 
-          ' 
-          value={value}
-          placeholder={placeholder}
-          onChange={handleChange}
-          onFocus={onFocus}
-        />
-        {
-          validatedStatus === ValidatedStatus.Error && <ExclamationCircleIcon className='w-4 h-4 text-[#D92D20]' />
-        }
-        {
-          validatedStatus === ValidatedStatus.Success && <CheckCircleIcon className='w-4 h-4 text-[#039855]' />
-        }
-      </div>
-      {
-        validating && (
-          <div className={`mt-2 text-primary-600 text-xs font-normal`}>
-            {t('common.provider.validating')}
-          </div>
-        )
-      }
-      {
-        validatedStatus === ValidatedStatus.Exceed && !validating && (
-          <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
-            {t('common.provider.apiKeyExceedBill')}&nbsp;
-            <Link 
-              className='underline'
-              href="https://platform.openai.com/account/api-keys" 
-              target={'_blank'}>
-              {locale === 'en' ? 'this link' : '这篇文档'}
-            </Link>
-          </div>
-        )
-      }
-      {
-        validatedStatus === ValidatedStatus.Error && !validating && (
-          <div className={`mt-2 text-[#D92D20] text-xs font-normal`}>
-            {t('common.provider.invalidKey')}
-          </div>
-        )
-      }
+      {validatedTip}
     </div>
   )
 }

+ 21 - 8
web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts

@@ -1,4 +1,4 @@
-import { useState, useCallback } from 'react'
+import { useState, useCallback, SetStateAction, Dispatch } from 'react'
 import debounce from 'lodash-es/debounce'
 import { DebouncedFunc } from 'lodash-es'
 import { validateProviderKey } from '@/service/common'
@@ -8,14 +8,24 @@ export enum ValidatedStatus {
   Error = 'error',
   Exceed = 'exceed'
 }
+export type SetValidatedStatus = Dispatch<SetStateAction<ValidatedStatus | undefined>>
+export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void>
+type ValidateTokenReturn = [
+  boolean, 
+  ValidatedStatus | undefined, 
+  SetValidatedStatus,
+  ValidateFn
+]
+export type ValidateFnConfig = {
+  beforeValidating: (token: any) => boolean
+}
 
-const useValidateToken = (providerName: string): [boolean, ValidatedStatus | undefined, DebouncedFunc<(token: string) => Promise<void>>] => {
+const useValidateToken = (providerName: string): ValidateTokenReturn => {
   const [validating, setValidating] = useState(false)
   const [validatedStatus, setValidatedStatus] = useState<ValidatedStatus | undefined>()
-  const validate = useCallback(debounce(async (token: string) => {
-    if (!token) {
-      setValidatedStatus(undefined)
-      return
+  const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => {
+    if (!config.beforeValidating(token)) {
+      return false
     }
     setValidating(true)
     try {
@@ -24,8 +34,10 @@ const useValidateToken = (providerName: string): [boolean, ValidatedStatus | und
     } catch (e: any) {
       if (e.status === 400) {
         e.json().then(({ code }: any) => {
-          if (code === 'provider_request_failed') {
+          if (code === 'provider_request_failed' && providerName === 'openai') {
             setValidatedStatus(ValidatedStatus.Exceed)
+          } else {
+            setValidatedStatus(ValidatedStatus.Error)
           }
         })
       } else {
@@ -39,7 +51,8 @@ const useValidateToken = (providerName: string): [boolean, ValidatedStatus | und
   return [
     validating,
     validatedStatus,
-    validate,
+    setValidatedStatus,
+    validate
   ]
 }
 

+ 18 - 7
web/app/components/header/account-setting/provider-page/provider-item/index.tsx

@@ -5,7 +5,8 @@ import { useContext } from 'use-context-selector'
 import Indicator from '../../../indicator'
 import { useTranslation } from 'react-i18next'
 import type { Provider, ProviderAzureToken } from '@/models/common'
-import OpenaiProvider from '../openai-provider/provider'
+import { ProviderName } from '@/models/common'
+import OpenaiProvider from '../openai-provider'
 import AzureProvider from '../azure-provider'
 import { ValidatedStatus } from '../provider-input/useValidateToken'
 import { updateProviderAIKey } from '@/service/common'
@@ -38,13 +39,23 @@ const ProviderItem = ({
     )
   const id = `${provider.provider_name}-${provider.provider_type}`
   const isOpen = id === activeId
-  const providerKey = provider.provider_name === 'azure_openai' ? (provider.token as ProviderAzureToken)?.openai_api_key  : provider.token
   const comingSoon = false
   const isValid = provider.is_valid
 
+  const providerTokenHasSetted = () => {
+    if (provider.provider_name === ProviderName.AZURE_OPENAI) {
+      return provider.token && provider.token.openai_api_base && provider.token.openai_api_key ? {
+        openai_api_base: provider.token.openai_api_base,
+        openai_api_key: provider.token.openai_api_key
+      }: undefined
+    }
+    if (provider.provider_name === ProviderName.OPENAI) {
+      return provider.token
+    }
+  }
   const handleUpdateToken = async () => {
     if (loading) return
-    if (validatedStatus === ValidatedStatus.Success || !token) {
+    if (validatedStatus === ValidatedStatus.Success) {
       try {
         setLoading(true)
         await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })
@@ -65,7 +76,7 @@ const ProviderItem = ({
         <div className={cn(s[`icon-${icon}`], 'mr-3 w-6 h-6 rounded-md')} />
         <div className='grow text-sm font-medium text-gray-800'>{name}</div>
         {
-          providerKey && !comingSoon && !isOpen && (
+          providerTokenHasSetted() && !comingSoon && !isOpen && (
             <div className='flex items-center mr-4'>
               {!isValid && <div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>}
               <Indicator color={!isValid ? 'red' : 'green'} className='ml-2' />
@@ -78,7 +89,7 @@ const ProviderItem = ({
               px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
               text-xs font-medium text-gray-700 flex items-center
             ' onClick={() => onActive(id)}>
-              {providerKey ? t('common.provider.editKey') : t('common.provider.addKey')}
+              {providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')}
             </div>
           )
         }
@@ -114,7 +125,7 @@ const ProviderItem = ({
         }
       </div>
       {
-        provider.provider_name === 'openai' && isOpen && (
+        provider.provider_name === ProviderName.OPENAI && isOpen && (
           <OpenaiProvider 
             provider={provider} 
             onValidatedStatus={v => setValidatedStatus(v)} 
@@ -123,7 +134,7 @@ const ProviderItem = ({
         )
       }
       {
-        provider.provider_name === 'azure_openai' && isOpen && (
+        provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && (
           <AzureProvider 
             provider={provider} 
             onValidatedStatus={v => setValidatedStatus(v)} 

+ 20 - 9
web/models/common.ts

@@ -54,18 +54,29 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l
   role: 'owner' | 'admin' | 'normal'
 }
 
+export enum ProviderName {
+  OPENAI = 'openai',
+  AZURE_OPENAI = 'azure_openai'
+}
 export type ProviderAzureToken = {
-  openai_api_base: string
-  openai_api_key: string
+  openai_api_base?: string
+  openai_api_key?: string
 }
-export type Provider = {
-  provider_name: string
-  provider_type: string
-  is_valid: boolean
-  is_enabled: boolean
-  last_used: string
-  token?: string | ProviderAzureToken
+export type ProviderTokenType = {
+  [ProviderName.OPENAI]: string
+  [ProviderName.AZURE_OPENAI]: ProviderAzureToken
 }
+export type Provider = {
+  [Name in ProviderName]: {
+    provider_name: Name
+  } & {
+    provider_type: 'custom' | 'system'
+    is_valid: boolean
+    is_enabled: boolean
+    last_used: string
+    token?: ProviderTokenType[Name]
+  }
+}[ProviderName]
 
 export type ProviderHosted = Provider & {
   quota_type: string