Merge remote-tracking branch 'origin/main'

This commit is contained in:
t0ng7u 2026-05-26 01:22:56 +08:00
commit b37b6d80b3
20 changed files with 488 additions and 146 deletions

View File

@ -3,6 +3,7 @@ package common
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"regexp"
"strconv"
@ -20,6 +21,16 @@ var (
maskApiKeyPattern = regexp.MustCompile(`(['"]?)api_key:([^\s'"]+)(['"]?)`)
)
const LocalLogContentLimit = 2048
// LocalLogPreview limits log-only content unless debug logging is enabled.
func LocalLogPreview(content string) string {
if DebugEnabled || len(content) <= LocalLogContentLimit {
return content
}
return fmt.Sprintf("%s... [truncated, original_length=%d, limit=%d]", content[:LocalLogContentLimit], len(content), LocalLogContentLimit)
}
func GetStringIfEmpty(str string, defaultValue string) string {
if str == "" {
return defaultValue

View File

@ -88,7 +88,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
defer func() {
if newAPIError != nil {
logger.LogError(c, fmt.Sprintf("relay error: %s", newAPIError.Error()))
logger.LogError(c, fmt.Sprintf("relay error: %s", common.LocalLogPreview(newAPIError.Error())))
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
switch relayFormat {
case types.RelayFormatOpenAIRealtime:
@ -354,7 +354,7 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
}
func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) {
logger.LogError(c, fmt.Sprintf("channel error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
logger.LogError(c, fmt.Sprintf("channel error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, common.LocalLogPreview(err.Error())))
// 不要使用context获取渠道信息异步处理时可能会出现渠道信息不一致的情况
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
if service.ShouldDisableChannel(err) && channelError.AutoBan {

View File

@ -17,24 +17,24 @@ import (
)
type Log struct {
Id int `json:"id" gorm:"index:idx_created_at_id,priority:1;index:idx_user_id_id,priority:2"`
UserId int `json:"user_id" gorm:"index;index:idx_user_id_id,priority:1"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;index:idx_created_at_type"`
Type int `json:"type" gorm:"index:idx_created_at_type"`
Content string `json:"content"`
Username string `json:"username" gorm:"index;index:index_username_model_name,priority:2;default:''"`
TokenName string `json:"token_name" gorm:"index;default:''"`
ModelName string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
Quota int `json:"quota" gorm:"default:0"`
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
UseTime int `json:"use_time" gorm:"default:0"`
IsStream bool `json:"is_stream"`
ChannelId int `json:"channel" gorm:"index"`
ChannelName string `json:"channel_name" gorm:"->"`
TokenId int `json:"token_id" gorm:"default:0;index"`
Group string `json:"group" gorm:"index"`
Ip string `json:"ip" gorm:"index;default:''"`
Id int `json:"id" gorm:"index:idx_created_at_id,priority:1;index:idx_user_id_id,priority:2"`
UserId int `json:"user_id" gorm:"index;index:idx_user_id_id,priority:1"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;index:idx_created_at_type"`
Type int `json:"type" gorm:"index:idx_created_at_type"`
Content string `json:"content"`
Username string `json:"username" gorm:"index;index:index_username_model_name,priority:2;default:''"`
TokenName string `json:"token_name" gorm:"index;default:''"`
ModelName string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
Quota int `json:"quota" gorm:"default:0"`
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
UseTime int `json:"use_time" gorm:"default:0"`
IsStream bool `json:"is_stream"`
ChannelId int `json:"channel" gorm:"index"`
ChannelName string `json:"channel_name" gorm:"->"`
TokenId int `json:"token_id" gorm:"default:0;index"`
Group string `json:"group" gorm:"index"`
Ip string `json:"ip" gorm:"index;default:''"`
RequestId string `json:"request_id,omitempty" gorm:"type:varchar(64);index:idx_logs_request_id;default:''"`
UpstreamRequestId string `json:"upstream_request_id,omitempty" gorm:"type:varchar(128);index:idx_logs_upstream_request_id;default:''"`
Other string `json:"other"`
@ -145,7 +145,7 @@ func RecordTopupLog(userId int, content string, callerIp string, paymentMethod s
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int,
isStream bool, group string, other map[string]interface{}) {
logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, common.LocalLogPreview(content)))
username := c.GetString("username")
requestId := c.GetString(common.RequestIdKey)
upstreamRequestId := c.GetString(common.UpstreamRequestIdKey)

View File

@ -442,10 +442,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
tools := make([]dto.ToolCallResponse, 0)
fcIdx := 0
if claudeResponse.Index != nil {
fcIdx = *claudeResponse.Index - 1
if fcIdx < 0 {
fcIdx = 0
}
fcIdx = *claudeResponse.Index
}
var choice dto.ChatCompletionsStreamResponseChoice
if claudeResponse.Type == "message_start" {

View File

@ -1410,6 +1410,14 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
response.Id = id
response.Created = createAt
response.Model = info.UpstreamModelName
if response.IsToolCall() {
finishReason = constant.FinishReasonToolCalls
if info.RelayFormat == types.RelayFormatClaude {
for choiceIdx := range response.Choices {
response.Choices[choiceIdx].FinishReason = nil
}
}
}
for choiceIdx := range response.Choices {
choiceKey := response.Choices[choiceIdx].Index
for toolIdx := range response.Choices[choiceIdx].Delta.ToolCalls {
@ -1470,7 +1478,9 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
logger.LogError(c, err.Error())
}
if isStop {
_ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, finishReason))
if info.RelayFormat != types.RelayFormatClaude {
_ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, finishReason))
}
}
return true
})
@ -1480,6 +1490,10 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
}
response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
if info.RelayFormat == types.RelayFormatClaude && info.ClaudeConvertInfo != nil && !info.ClaudeConvertInfo.Done {
response = helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, finishReason)
response.Usage = usage
}
handleErr := handleFinalStream(c, info, response)
if handleErr != nil {
common.SysLog("send final response failed: " + handleErr.Error())

View File

@ -140,9 +140,9 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
usage.(*dto.Usage).PromptTokens = 1
}
quality := "standard"
if request.Quality == "hd" {
quality = "hd"
quality := request.Quality
if quality == "" {
quality = "standard"
}
var logContent []string

View File

@ -17,7 +17,7 @@ func formatNotifyType(channelId int, status int) string {
// disable & notify
func DisableChannel(channelError types.ChannelError, reason string) {
common.SysLog(fmt.Sprintf("通道「%s」#%d发生错误准备禁用原因%s", channelError.ChannelName, channelError.ChannelId, reason))
common.SysLog(fmt.Sprintf("通道「%s」#%d发生错误准备禁用原因%s", channelError.ChannelName, channelError.ChannelId, common.LocalLogPreview(reason)))
// 检查是否启用自动禁用功能
if !channelError.AutoBan {

View File

@ -92,11 +92,13 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai
}
CloseResponseBodyGracefully(resp)
var errResponse dto.GeneralErrorResponse
responseBodyText := string(responseBody)
responseBodyPreview := common.LocalLogPreview(responseBodyText)
buildErrWithBody := func(message string) error {
if message == "" {
return fmt.Errorf("bad response status code %d, body: %s", resp.StatusCode, string(responseBody))
return fmt.Errorf("bad response status code %d, body: %s", resp.StatusCode, responseBodyText)
}
return fmt.Errorf("bad response status code %d, message: %s, body: %s", resp.StatusCode, message, string(responseBody))
return fmt.Errorf("bad response status code %d, message: %s, body: %s", resp.StatusCode, message, responseBodyText)
}
err = common.Unmarshal(responseBody, &errResponse)
@ -104,7 +106,7 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai
if showBodyWhenFail {
newApiErr.Err = buildErrWithBody("")
} else {
logger.LogError(ctx, fmt.Sprintf("bad response status code %d, body: %s", resp.StatusCode, string(responseBody)))
logger.LogError(ctx, fmt.Sprintf("bad response status code %d, body: %s", resp.StatusCode, responseBodyPreview))
newApiErr.Err = fmt.Errorf("bad response status code %d", resp.StatusCode)
}
return

View File

@ -1,9 +1,17 @@
package service
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
@ -55,3 +63,99 @@ func TestResetStatusCode(t *testing.T) {
})
}
}
func TestRelayErrorHandlerTruncatesInvalidJSONBodyInLog(t *testing.T) {
withDebugEnabled(t, false)
body := strings.Repeat("b", common.LocalLogContentLimit+256)
var logBuffer bytes.Buffer
common.LogWriterMu.Lock()
oldWriter := gin.DefaultErrorWriter
gin.DefaultErrorWriter = &logBuffer
common.LogWriterMu.Unlock()
t.Cleanup(func() {
common.LogWriterMu.Lock()
gin.DefaultErrorWriter = oldWriter
common.LogWriterMu.Unlock()
})
resp := &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(strings.NewReader(body)),
}
newAPIError := RelayErrorHandler(context.Background(), resp, false)
require.NotNil(t, newAPIError)
require.Equal(t, "bad response status code 500", newAPIError.Error())
require.Contains(t, logBuffer.String(), "[truncated")
require.Contains(t, logBuffer.String(), fmt.Sprintf("original_length=%d", len(body)))
require.NotContains(t, logBuffer.String(), strings.Repeat("b", common.LocalLogContentLimit+1))
}
func TestRelayErrorHandlerKeepsStructuredErrorMessage(t *testing.T) {
message := strings.Repeat("c", common.LocalLogContentLimit+256)
body := `{"message":"` + message + `"}`
resp := &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(strings.NewReader(body)),
}
newAPIError := RelayErrorHandler(context.Background(), resp, false)
require.NotNil(t, newAPIError)
require.Equal(t, message, newAPIError.Error())
}
func TestRelayErrorHandlerKeepsOpenAIErrorMessage(t *testing.T) {
message := strings.Repeat("d", common.LocalLogContentLimit+256)
body := `{"error":{"message":"` + message + `","type":"server_error","code":"server_error"}}`
resp := &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(strings.NewReader(body)),
}
newAPIError := RelayErrorHandler(context.Background(), resp, false)
require.NotNil(t, newAPIError)
require.Equal(t, message, newAPIError.Error())
}
func TestRelayErrorHandlerKeepsInvalidJSONBodyInDebugLog(t *testing.T) {
withDebugEnabled(t, true)
body := strings.Repeat("e", common.LocalLogContentLimit+256)
var logBuffer bytes.Buffer
common.LogWriterMu.Lock()
oldWriter := gin.DefaultErrorWriter
gin.DefaultErrorWriter = &logBuffer
common.LogWriterMu.Unlock()
t.Cleanup(func() {
common.LogWriterMu.Lock()
gin.DefaultErrorWriter = oldWriter
common.LogWriterMu.Unlock()
})
resp := &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(strings.NewReader(body)),
}
newAPIError := RelayErrorHandler(context.Background(), resp, false)
require.NotNil(t, newAPIError)
require.NotContains(t, logBuffer.String(), "[truncated")
require.Contains(t, logBuffer.String(), body)
}
func withDebugEnabled(t *testing.T, enabled bool) {
t.Helper()
oldDebug := common.DebugEnabled
common.DebugEnabled = enabled
t.Cleanup(func() {
common.DebugEnabled = oldDebug
})
}

View File

@ -163,7 +163,11 @@ const API_DEMOS: ApiDemoConfig[] = [
const CYCLE_INTERVAL = 4500
const TRANSITION_MS = 220
export function HeroTerminalDemo() {
interface HeroTerminalDemoProps {
className?: string
}
export function HeroTerminalDemo(props: HeroTerminalDemoProps) {
const [activeIndex, setActiveIndex] = useState(0)
const [transitioning, setTransitioning] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined)
@ -202,7 +206,7 @@ export function HeroTerminalDemo() {
const accent = ACCENT_CLASSES[demo.accent]
return (
<div className='mx-auto mt-16 w-full max-w-2xl'>
<div className={cn('mx-auto w-full max-w-2xl', props.className)}>
<div
className={cn(
'overflow-hidden rounded-2xl border backdrop-blur-sm',

View File

@ -17,9 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Link } from '@tanstack/react-router'
import { ArrowRight } from 'lucide-react'
import { ArrowRight, BookOpen } from 'lucide-react'
import { CherryStudio } from '@lobehub/icons'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { useStatus } from '@/hooks/use-status'
import { HeroTerminalDemo } from '../hero-terminal-demo'
interface HeroProps {
@ -27,11 +29,59 @@ interface HeroProps {
isAuthenticated?: boolean
}
// Stylized three-dots indicator representing "More"
const MoreIcon = () => (
<svg
className='size-6 shrink-0 text-muted-foreground/60 transition-colors group-hover:text-foreground'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<circle cx='6' cy='12' r='2' fill='currentColor' />
<circle cx='12' cy='12' r='2' fill='currentColor' />
<circle cx='18' cy='12' r='2' fill='currentColor' />
</svg>
)
export function Hero(props: HeroProps) {
const { t } = useTranslation()
const { status } = useStatus()
const docsUrl = (status?.docs_link as string | undefined) || 'https://docs.newapi.pro'
const renderDocsButton = () => {
const isExternal = docsUrl.startsWith('http')
if (isExternal) {
return (
<Button
variant='outline'
className='group border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium inline-flex items-center gap-1.5'
render={
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
/>
}
>
<BookOpen className='size-4 text-muted-foreground/80 group-hover:text-foreground transition-colors duration-200' />
<span>{t('Docs')}</span>
</Button>
)
}
return (
<Button
variant='outline'
className='group border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium inline-flex items-center gap-1.5'
render={<Link to={docsUrl} />}
>
<BookOpen className='size-4 text-muted-foreground/80 group-hover:text-foreground transition-colors duration-200' />
<span>{t('Docs')}</span>
</Button>
)
}
return (
<section className='relative z-10 flex flex-col items-center overflow-hidden px-6 pt-28 pb-16 md:pt-36 md:pb-24'>
<section className='relative z-10 overflow-hidden px-6 pt-24 pb-16 md:pt-32 md:pb-24 lg:pt-36 lg:pb-28'>
{/* Radial gradient background */}
<div
aria-hidden
@ -50,63 +100,146 @@ export function Hero(props: HeroProps) {
className='absolute inset-0 -z-10 bg-[linear-gradient(to_right,var(--border)_1px,transparent_1px),linear-gradient(to_bottom,var(--border)_1px,transparent_1px)] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_30%,black_20%,transparent_100%)] bg-[size:4rem_4rem] opacity-[0.08]'
/>
<div className='flex max-w-3xl flex-col items-center text-center'>
<h1
className='landing-animate-fade-up text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight'
style={{ animationDelay: '0ms' }}
>
{t('Unified API Gateway for')}
<br />
<span className='bg-gradient-to-r from-blue-400 via-violet-400 to-purple-500 bg-clip-text text-transparent'>
{t('All Your AI Models')}
</span>
</h1>
<p
className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-lg text-base leading-relaxed opacity-0 md:text-lg'
style={{ animationDelay: '80ms' }}
>
{t(
'Power AI applications, manage digital assets, connect the Future'
)}
</p>
<div
className='landing-animate-fade-up mt-8 flex items-center gap-3 opacity-0'
style={{ animationDelay: '160ms' }}
>
{props.isAuthenticated ? (
<Button
className='group rounded-lg'
render={<Link to='/dashboard' />}
>
{t('Go to Dashboard')}
<ArrowRight className='ml-1 size-3.5 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
) : (
<>
<Button
className='group rounded-lg'
render={<Link to='/sign-up' />}
>
{t('Get Started')}
<ArrowRight className='ml-1 size-3.5 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
<Button
variant='outline'
className='border-border/50 hover:border-border hover:bg-muted/50 rounded-lg'
render={<Link to='/pricing' />}
>
{t('View Pricing')}
</Button>
</>
)}
</div>
</div>
<div className='mx-auto grid max-w-6xl grid-cols-1 items-start gap-12 lg:grid-cols-12 lg:gap-8'>
{/* Left Column: Title, description, action buttons and application support */}
<div className='flex flex-col items-start text-left lg:col-span-6'>
{/* Top Pill Badge */}
<div
className='landing-animate-fade-up mb-5 inline-flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/5 px-3 py-1.5 text-[11px] font-medium text-blue-600 dark:border-blue-400/20 dark:bg-blue-400/5 dark:text-blue-400 opacity-0 shadow-xs'
style={{ animationDelay: '0ms' }}
>
<span className='relative flex size-1.5'>
<span className='absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75' />
<span className='relative inline-flex size-1.5 rounded-full bg-blue-500 dark:bg-blue-400' />
</span>
<span>{t('AI Application Infrastructure Foundation')}</span>
</div>
<div
className='landing-animate-fade-up w-full opacity-0'
style={{ animationDelay: '300ms' }}
>
<HeroTerminalDemo />
<h1
className='landing-animate-fade-up text-[clamp(2.25rem,4.5vw,3.25rem)] leading-[1.15] font-bold tracking-tight'
style={{ animationDelay: '60ms' }}
>
{t('Unified API Gateway for')}
<br />
<span className='bg-gradient-to-r from-blue-400 via-violet-400 to-purple-500 bg-clip-text text-transparent'>
{t('Vast Range of AI Models')}
</span>
</h1>
<p
className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-xl text-base leading-relaxed opacity-0 md:text-[15px]'
style={{ animationDelay: '120ms' }}
>
{t(
'Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.'
)}
</p>
<div
className='landing-animate-fade-up mt-8 flex flex-wrap items-center gap-3 opacity-0'
style={{ animationDelay: '180ms' }}
>
{props.isAuthenticated ? (
<>
<Button
className='group rounded-lg h-11 px-5 text-sm font-medium'
render={<Link to='/dashboard' />}
>
{t('Go to Dashboard')}
<ArrowRight className='ml-1.5 size-4 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
{renderDocsButton()}
</>
) : (
<>
<Button
className='group rounded-lg h-11 px-5 text-sm font-medium'
render={<Link to='/sign-up' />}
>
{t('Get Started')}
<ArrowRight className='ml-1.5 size-4 transition-transform duration-200 group-hover:translate-x-0.5' />
</Button>
<Button
variant='outline'
className='border-border/50 hover:border-border hover:bg-muted/50 rounded-lg h-11 px-5 text-sm font-medium'
render={<Link to='/pricing' />}
>
{t('View Pricing')}
</Button>
{renderDocsButton()}
</>
)}
</div>
{/* Supported Apps (参考图二样式,进行卡片化和信息扩充设计,增加视觉高度) */}
<div
className='landing-animate-fade-up mt-10 w-full max-w-xl opacity-0'
style={{ animationDelay: '240ms' }}
>
<div className='flex flex-col gap-1 mb-4'>
<span className='text-[10px] font-bold tracking-[0.15em] text-muted-foreground/50 uppercase'>
{t('Supported Applications')}
</span>
<p className='text-xs text-muted-foreground/60 leading-relaxed'>
{t('Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.')}
</p>
</div>
<div className='flex flex-wrap items-center gap-3'>
{/* Cherry Studio */}
<a
href='https://cherry-ai.com'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/80 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02]'
>
<CherryStudio.Color size={24} className='shrink-0' />
<span>Cherry Studio</span>
</a>
{/* CC Switch */}
<a
href='https://ccswitch.io'
target='_blank'
rel='noopener noreferrer'
className='group flex items-center gap-3 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/80 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02]'
>
<img
src='https://ccswitch.io/favicon.png'
alt='CC Switch'
className='size-6 shrink-0 rounded-md object-contain'
onError={(e) => {
// Fallback to a styled text avatar if the remote favicon fails to load in sandbox or local environments
e.currentTarget.style.display = 'none'
const fallback = e.currentTarget.nextSibling as HTMLElement
if (fallback) fallback.style.display = 'flex'
}}
/>
<span
style={{ display: 'none' }}
className='size-6 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-[10px] font-bold text-blue-600 dark:bg-blue-400/10 dark:text-blue-400'
>
CC
</span>
<span>CC Switch</span>
</a>
{/* "更多" */}
<div
className='group flex items-center gap-2.5 rounded-full border border-border/40 bg-muted/15 px-5 py-2.5 text-sm font-medium text-foreground/55 shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:border-border hover:bg-muted/30 hover:text-foreground hover:scale-[1.02] cursor-default'
>
<MoreIcon />
<span>{t('More Apps')}</span>
</div>
</div>
</div>
</div>
{/* Right Column: Hero Terminal API Demo */}
<div
className='landing-animate-fade-up flex justify-center w-full opacity-0 lg:col-span-6'
style={{ animationDelay: '320ms' }}
>
<HeroTerminalDemo className='mt-8 lg:mt-0' />
</div>
</div>
</section>
)

View File

@ -42,7 +42,7 @@ export async function getApiKeys(
// Search API keys by keyword or token (with pagination)
export async function searchApiKeys(
params: SearchApiKeysParams
): Promise<{ success: boolean; message?: string; data?: ApiKey[] }> {
): Promise<GetApiKeysResponse> {
const { keyword = '', token = '', p, size } = params
const queryParams = new URLSearchParams()
if (keyword) queryParams.set('keyword', keyword)

View File

@ -30,6 +30,7 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import { useDebounce } from '@/hooks'
import { Database } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@ -43,6 +44,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import {
DISABLED_ROW_DESKTOP,
@ -207,9 +209,35 @@ export function ApiKeysTable() {
navigate: route.useNavigate(),
pagination: { defaultPage: 1, defaultPageSize: 20 },
globalFilter: { enabled: true, key: 'filter' },
columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }],
columnFilters: [
{ columnId: 'status', searchKey: 'status', type: 'array' },
{ columnId: '_tokenSearch', searchKey: 'token', type: 'string' },
],
})
const tokenFilterFromUrl =
(columnFilters.find((f) => f.id === '_tokenSearch')?.value as string) || ''
const [tokenFilterInput, setTokenFilterInput] = useState(tokenFilterFromUrl)
const debouncedTokenFilter = useDebounce(tokenFilterInput, 500)
useEffect(() => {
setTokenFilterInput(tokenFilterFromUrl)
}, [tokenFilterFromUrl])
useEffect(() => {
if (debouncedTokenFilter !== tokenFilterFromUrl) {
onColumnFiltersChange((prev) => {
const filtered = prev.filter((f) => f.id !== '_tokenSearch')
return debouncedTokenFilter
? [...filtered, { id: '_tokenSearch', value: debouncedTokenFilter }]
: filtered
})
}
}, [debouncedTokenFilter, tokenFilterFromUrl, onColumnFiltersChange])
const tokenFilter = tokenFilterFromUrl
const shouldSearch = Boolean(globalFilter?.trim() || tokenFilter.trim())
// Fetch data with React Query
// eslint-disable-next-line @tanstack/query/exhaustive-deps
const { data, isLoading, isFetching } = useQuery({
@ -218,32 +246,31 @@ export function ApiKeysTable() {
pagination.pageIndex + 1,
pagination.pageSize,
globalFilter,
tokenFilter,
refreshTrigger,
],
queryFn: async () => {
// If there's a global filter, use search
const hasFilter = globalFilter?.trim()
if (hasFilter) {
const result = await searchApiKeys({ keyword: globalFilter })
if (!result.success) {
toast.error(result.message || t(ERROR_MESSAGES.SEARCH_FAILED))
return { items: [], total: 0 }
}
return {
items: result.data || [],
total: result.data?.length || 0,
}
}
// Otherwise use pagination
const result = await getApiKeys({
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
const result = shouldSearch
? await searchApiKeys({
keyword: globalFilter,
token: tokenFilter,
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
: await getApiKeys({
p: pagination.pageIndex + 1,
size: pagination.pageSize,
})
if (!result.success) {
toast.error(result.message || t(ERROR_MESSAGES.LOAD_FAILED))
toast.error(
result.message ||
t(
shouldSearch
? ERROR_MESSAGES.SEARCH_FAILED
: ERROR_MESSAGES.LOAD_FAILED
)
)
return { items: [], total: 0 }
}
@ -272,13 +299,7 @@ export function ApiKeysTable() {
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
globalFilterFn: (row, _columnId, filterValue) => {
const name = String(row.getValue('name')).toLowerCase()
const key = String(row.original.key).toLowerCase()
const searchValue = String(filterValue).toLowerCase()
return name.includes(searchValue) || key.includes(searchValue)
},
globalFilterFn: () => true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
@ -288,10 +309,8 @@ export function ApiKeysTable() {
onPaginationChange,
onGlobalFilterChange,
onColumnFiltersChange,
manualPagination: !globalFilter,
pageCount: globalFilter
? Math.ceil((data?.total || 0) / pagination.pageSize)
: Math.ceil((data?.total || 0) / pagination.pageSize),
manualPagination: true,
pageCount: Math.ceil((data?.total || 0) / pagination.pageSize),
})
const pageCount = table.getPageCount()
@ -311,7 +330,16 @@ export function ApiKeysTable() {
)}
skeletonKeyPrefix='api-keys-skeleton'
toolbarProps={{
searchPlaceholder: t('Filter by name or key...'),
searchPlaceholder: t('Filter by name...'),
additionalSearch: (
<Input
placeholder={t('Filter by API key...')}
aria-label={t('Filter by API key...')}
value={tokenFilterInput}
onChange={(e) => setTokenFilterInput(e.target.value)}
className='w-full sm:w-50 lg:w-60'
/>
),
filters: [
{
columnId: 'status',

View File

@ -106,6 +106,7 @@
"Accept Unpriced Models": "Accept Unpriced Models",
"Accepts a JSON array of model identifiers that support the Imagine API.": "Accepts a JSON array of model identifiers that support the Imagine API.",
"Accepts comma-separated status codes and inclusive ranges.": "Accepts comma-separated status codes and inclusive ranges.",
"Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.": "Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.",
"Access Denied Message": "Access Denied Message",
"Access Forbidden": "Access Forbidden",
"Access Policy (JSON)": "Access Policy (JSON)",
@ -236,6 +237,7 @@
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.",
"Aggregation bucket": "Aggregation bucket",
"AGPL v3.0 License": "AGPL v3.0 License",
"AI Application Infrastructure Foundation": "AI Application Infrastructure Foundation",
"AI model testing environment": "AI model testing environment",
"AI models": "AI models",
"AI models supported": "AI models supported",
@ -263,7 +265,6 @@
"All Types": "All Types",
"All upstream data is trusted": "All upstream data is trusted",
"All Vendors": "All Vendors",
"All Your AI Models": "All Your AI Models",
"All-time": "All-time",
"Allocated Memory": "Allocated Memory",
"Allow accountFilter parameter": "Allow accountFilter parameter",
@ -677,6 +678,7 @@
"Check for updates": "Check for updates",
"Check in daily to receive random quota rewards": "Check in daily to receive random quota rewards",
"Check in now": "Check in now",
"Check out the Quick Start": "Check out the Quick Start",
"Check resolved IPs against IP filters even when accessing by domain": "Check resolved IPs against IP filters even when accessing by domain",
"Check-in failed": "Check-in failed",
"Check-in Rewards": "Check-in Rewards",
@ -1719,8 +1721,9 @@
"Filter by Midjourney task ID": "Filter by Midjourney task ID",
"Filter by model name...": "Filter by model name...",
"Filter by model...": "Filter by model...",
"Filter by API key...": "Filter by API key...",
"Filter by name or ID...": "Filter by name or ID...",
"Filter by name or key...": "Filter by name or key...",
"Filter by name...": "Filter by name...",
"Filter by name, ID, or key...": "Filter by name, ID, or key...",
"Filter by price field": "Filter by price field",
"Filter by ratio type": "Filter by ratio type",
@ -1767,7 +1770,7 @@
"footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "Related Projects",
"footer.defaultCopyright": "All rights reserved.",
"footer.new\u0061pi.projectAttributionSuffix": "All rights reserved. Designed and developed by the project contributors.",
"footer.newapi.projectAttributionSuffix": "All rights reserved. Designed and developed by the project contributors.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment",
"For private deployments, format: https://fastgpt.run/api/openapi": "For private deployments, format: https://fastgpt.run/api/openapi",
"Force a syntactically valid JSON response": "Force a syntactically valid JSON response",
@ -2397,6 +2400,7 @@
"months": "months",
"Moonshot": "Moonshot",
"More": "More",
"More Apps": "More Apps",
"more mapping": "more mapping",
"More templates...": "More templates...",
"More than 999 days left": "More than 999 days left",
@ -2458,6 +2462,7 @@
"Network proxy for this channel (supports socks5 protocol)": "Network proxy for this channel (supports socks5 protocol)",
"Never": "Never",
"Never expires": "Never expires",
"Never used an API Gateway?": "Never used an API Gateway?",
"NEW": "NEW",
"New API": "New API",
"New API &lt;noreply@example.com&gt;": "New API &lt;noreply@example.com&gt;",
@ -3771,12 +3776,14 @@
"Sunset Glow": "Sunset Glow",
"Super Admin": "Super Admin",
"Support for high concurrency with automatic load balancing": "Support for high concurrency with automatic load balancing",
"Supported Applications": "Supported Applications",
"Supported Imagine Models": "Supported Imagine Models",
"Supported modalities": "Supported modalities",
"Supported parameters": "Supported parameters",
"Supported variables": "Supported variables",
"Supports `-thinking`, `-thinking-": "Supports `-thinking`, `-thinking-",
"Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.",
"Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.": "Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.",
"Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.",
"Sustained tokens per second": "Sustained tokens per second",
"Swap Face": "Swap Face",
@ -4300,6 +4307,7 @@
"Vary": "Vary",
"Vary (Strong)": "Vary (Strong)",
"Vary (Subtle)": "Vary (Subtle)",
"Vast Range of AI Models": "Vast Range of AI Models",
"Vendor": "Vendor",
"Vendor deleted successfully": "Vendor deleted successfully",
"Vendor Name *": "Vendor Name *",

View File

@ -106,6 +106,7 @@
"Accept Unpriced Models": "Accepter les modèles non tarifés",
"Accepts a JSON array of model identifiers that support the Imagine API.": "Accepte un tableau JSON d'identifiants de modèles qui prennent en charge l'API Imagine.",
"Accepts comma-separated status codes and inclusive ranges.": "Accepte les codes de statut séparés par des virgules et les plages inclusives.",
"Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.": "Accédez à une vaste sélection de modèles via un protocole API standard et unifié. Propulsez les applications d'IA, gérez les actifs numériques et connectez le futur.",
"Access Denied Message": "Message d'accès refusé",
"Access Forbidden": "Accès interdit",
"Access Policy (JSON)": "Politique d'accès (JSON)",
@ -236,6 +237,7 @@
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "agrège plus de 50 fournisseurs IA derrière une API unifiée. Gérez l'accès, suivez les coûts et évoluez sans effort.",
"Aggregation bucket": "Fenêtre dagrégation",
"AGPL v3.0 License": "Licence AGPL v3.0",
"AI Application Infrastructure Foundation": "Socle d'infrastructure pour applications d'IA",
"AI model testing environment": "Environnement de test de modèle IA",
"AI models": "Modèles d'IA",
"AI models supported": "Modèles d'IA pris en charge",
@ -263,7 +265,6 @@
"All Types": "Tous les types",
"All upstream data is trusted": "Toutes les données en amont sont fiables",
"All Vendors": "Tous les fournisseurs",
"All Your AI Models": "Tous vos modèles IA",
"All-time": "Tous temps",
"Allocated Memory": "Mémoire allouée",
"Allow accountFilter parameter": "Autoriser le paramètre accountFilter",
@ -677,6 +678,7 @@
"Check for updates": "Vérifier les mises à jour",
"Check in daily to receive random quota rewards": "Connectez-vous quotidiennement pour recevoir des récompenses de quota aléatoires",
"Check in now": "Se connecter maintenant",
"Check out the Quick Start": "Consultez le démarrage rapide",
"Check resolved IPs against IP filters even when accessing by domain": "Vérifier les adresses IP résolues par rapport aux filtres IP même lors de l'accès par domaine",
"Check-in failed": "Échec de la connexion",
"Check-in Rewards": "Récompenses de connexion quotidienne",
@ -1719,8 +1721,9 @@
"Filter by Midjourney task ID": "Filtrer par ID de tâche Midjourney",
"Filter by model name...": "Filtrer par nom du modèle...",
"Filter by model...": "Filtrer par modèle...",
"Filter by API key...": "Filtrer par clé API...",
"Filter by name or ID...": "Filtrer par nom ou ID...",
"Filter by name or key...": "Filtrer par nom ou clé...",
"Filter by name...": "Filtrer par nom...",
"Filter by name, ID, or key...": "Filtrer par nom, ID ou clé...",
"Filter by price field": "Filtrer par champ de prix",
"Filter by ratio type": "Filtrer par type de ratio",
@ -1767,7 +1770,7 @@
"footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "Projets liés",
"footer.defaultCopyright": "Tous droits réservés.",
"footer.new\u0061pi.projectAttributionSuffix": "Tous droits réservés. Conçu et développé par les contributeurs du projet.",
"footer.newapi.projectAttributionSuffix": "Tous droits réservés. Conçu et développé par les contributeurs du projet.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Pour les canaux ajoutés après le 10 mai 2025, pas besoin de supprimer \".\" des noms de modèles lors du déploiement",
"For private deployments, format: https://fastgpt.run/api/openapi": "Pour les déploiements privés, format : https://fastgpt.run/api/openapi",
"Force a syntactically valid JSON response": "Imposer une réponse JSON syntaxiquement valide",
@ -2397,6 +2400,7 @@
"months": "mois",
"Moonshot": "Moonshot",
"More": "Plus",
"More Apps": "Plus",
"more mapping": "plus de mappage",
"More templates...": "Autres modèles…",
"More than 999 days left": "Plus de 999 jours restants",
@ -2458,6 +2462,7 @@
"Network proxy for this channel (supports socks5 protocol)": "Proxy réseau pour ce canal (supporte le protocole socks5)",
"Never": "Jamais",
"Never expires": "N'expire jamais",
"Never used an API Gateway?": "Vous n'avez jamais utilisé de passerelle API ?",
"NEW": "NOUVEAU",
"New API": "New API",
"New API &lt;noreply@example.com&gt;": "New API &lt;noreply@example.com&gt;",
@ -3771,12 +3776,14 @@
"Sunset Glow": "Lueur du couchant",
"Super Admin": "Super Administrateur",
"Support for high concurrency with automatic load balancing": "Prise en charge de la haute concurrence avec équilibrage de charge automatique",
"Supported Applications": "Applications prises en charge",
"Supported Imagine Models": "Modèles Imagine pris en charge",
"Supported modalities": "Modalités prises en charge",
"Supported parameters": "Paramètres pris en charge",
"Supported variables": "Variables supportées",
"Supports `-thinking`, `-thinking-": "Prend en charge `-thinking`, `-thinking-",
"Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "Prend en charge le balisage HTML ou l'intégration d'iframe. Entrez le code HTML directement, ou fournissez une URL complète pour l'intégrer automatiquement en tant qu'iframe.",
"Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.": "Prend en charge la configuration en un clic et s'adapte parfaitement à la configuration multi-protocole NewAPI.",
"Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "Prend en charge PNG, JPG, SVG ou WebP. Taille recommandée : 128×128 ou moins.",
"Sustained tokens per second": "Jetons par seconde soutenus",
"Swap Face": "Échanger le visage",
@ -4300,6 +4307,7 @@
"Vary": "Varier",
"Vary (Strong)": "Varier (fort)",
"Vary (Subtle)": "Varier (subtil)",
"Vast Range of AI Models": "Vaste gamme de modèles d'IA",
"Vendor": "Fournisseur",
"Vendor deleted successfully": "Fournisseur supprimé avec succès",
"Vendor Name *": "Nom du fournisseur *",

View File

@ -106,6 +106,7 @@
"Accept Unpriced Models": "価格設定されていないモデルを許可",
"Accepts a JSON array of model identifiers that support the Imagine API.": "Imagine APIをサポートするモデル識別子のJSON配列を受け入れます。",
"Accepts comma-separated status codes and inclusive ranges.": "カンマ区切りのステータスコードと包含範囲を受け入れます。",
"Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.": "標準的で統一されたAPIプロトコルを介して、膨大なモデルにアクセス。AIアプリケーションを強化し、デジタル資産を管理し、未来へと繋げます。",
"Access Denied Message": "アクセス拒否メッセージ",
"Access Forbidden": "アクセス禁止",
"Access Policy (JSON)": "アクセスポリシー (JSON)",
@ -236,6 +237,7 @@
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "50以上のAIプロバイダーを統一APIで集約。アクセス管理、コスト追跡、スケーリングを簡単に。",
"Aggregation bucket": "集計バケット",
"AGPL v3.0 License": "AGPL v3.0ライセンス",
"AI Application Infrastructure Foundation": "AI アプリケーションのインフラ基盤",
"AI model testing environment": "AIモデルテスト環境",
"AI models": "AIモデル",
"AI models supported": "AIモデル対応",
@ -263,7 +265,6 @@
"All Types": "すべてのタイプ",
"All upstream data is trusted": "すべてのアップストリームデータは信頼されています",
"All Vendors": "すべてのベンダー",
"All Your AI Models": "すべてのAIモデル",
"All-time": "全期間",
"Allocated Memory": "割り当て済みメモリ",
"Allow accountFilter parameter": "accountFilter パラメータを許可",
@ -677,6 +678,7 @@
"Check for updates": "更新を確認",
"Check in daily to receive random quota rewards": "毎日チェックインして、ランダムなノルマ報酬を受け取りましょう",
"Check in now": "今すぐチェックイン",
"Check out the Quick Start": "クイックスタートをご確認ください",
"Check resolved IPs against IP filters even when accessing by domain": "ドメインによるアクセスであっても、解決されたIPをIPフィルターと照合してチェックします",
"Check-in failed": "チェックインできませんでした",
"Check-in Rewards": "チェックイン報酬",
@ -1719,8 +1721,9 @@
"Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター",
"Filter by model name...": "モデル名でフィルター...",
"Filter by model...": "モデルでフィルタリング...",
"Filter by API key...": "APIキーでフィルター...",
"Filter by name or ID...": "名前またはIDでフィルター...",
"Filter by name or key...": "名前またはキーでフィルター...",
"Filter by name...": "名前でフィルター...",
"Filter by name, ID, or key...": "名前、ID、またはキーでフィルター...",
"Filter by price field": "価格フィールドでフィルター",
"Filter by ratio type": "倍率タイプで絞り込み",
@ -1767,7 +1770,7 @@
"footer.columns.related.links.oneApi": "1つのAPI",
"footer.columns.related.title": "関連プロジェクト",
"footer.defaultCopyright": "すべての権利を留保します。",
"footer.new\u0061pi.projectAttributionSuffix": "すべての権利を留保します。プロジェクトコントリビューターにより設計・開発されています。",
"footer.newapi.projectAttributionSuffix": "すべての権利を留保します。プロジェクトコントリビューターにより設計・開発されています。",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "2025 年 5 月 10 日以降に追加されたチャネルの場合、デプロイ時にモデル名から「.」を削除する必要はありません",
"For private deployments, format: https://fastgpt.run/api/openapi": "プライベートデプロイメントの場合、形式: https://fastgpt.run/api/openapi",
"Force a syntactically valid JSON response": "構文的に有効な JSON 応答を強制",
@ -2397,6 +2400,7 @@
"months": "ヶ月",
"Moonshot": "Moonshot",
"More": "もっと見る",
"More Apps": "さらに",
"more mapping": "さらにマッピング",
"More templates...": "ほかのテンプレート…",
"More than 999 days left": "999日以上",
@ -2458,6 +2462,7 @@
"Network proxy for this channel (supports socks5 protocol)": "このチャネルのネットワークプロキシ (socks5プロトコルをサポート)",
"Never": "しない",
"Never expires": "無期限",
"Never used an API Gateway?": "APIゲートウェイを一度も使用したことがありませんか",
"NEW": "NEW",
"New API": "新しいAPI",
"New API &lt;noreply@example.com&gt;": "新しいAPI __ PH_0 __",
@ -3771,12 +3776,14 @@
"Sunset Glow": "サンセットグロウ",
"Super Admin": "スーパー管理者",
"Support for high concurrency with automatic load balancing": "自動ロードバランシングによる高並行性のサポート",
"Supported Applications": "サポートされているアプリケーション",
"Supported Imagine Models": "対応Imagineモデル",
"Supported modalities": "サポートされるモダリティ",
"Supported parameters": "対応パラメータ",
"Supported variables": "サポートされる変数",
"Supports `-thinking`, `-thinking-": "「-thinking」、「-thinking-」をサポートします",
"Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "HTMLマークアップまたはiframe埋め込みをサポートします。HTMLコードを直接入力するか、完全なURLを提供してiframeとして自動的に埋め込みます。",
"Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.": "ワンクリック設定をサポートし、NewAPIマルチプロトコル設定に完全に適応します。",
"Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "PNG、JPG、SVG、WebPに対応。推奨サイズ: 128×128以下。",
"Sustained tokens per second": "持続的な毎秒トークン数",
"Swap Face": "顔入れ替え",
@ -4300,6 +4307,7 @@
"Vary": "バリエーション",
"Vary (Strong)": "バリエーション(強)",
"Vary (Subtle)": "バリエーション(微)",
"Vast Range of AI Models": "膨大なAIモデル",
"Vendor": "ベンダー",
"Vendor deleted successfully": "ベンダーが正常に削除されました",
"Vendor Name *": "ベンダー名 *",

View File

@ -106,6 +106,7 @@
"Accept Unpriced Models": "Принимать модели без цены",
"Accepts a JSON array of model identifiers that support the Imagine API.": "Принимает JSON-массив идентификаторов моделей, поддерживающих Imagine API.",
"Accepts comma-separated status codes and inclusive ranges.": "Принимает коды статуса, разделенные запятыми, и включающие диапазоны.",
"Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.": "Получите доступ к огромному выбору моделей через стандартный, единый протокол API. Развивайте приложения ИИ, управляйте цифровыми активами и соединяйте будущее.",
"Access Denied Message": "Сообщение об отказе в доступе",
"Access Forbidden": "Доступ запрещен",
"Access Policy (JSON)": "Политика доступа (JSON)",
@ -236,6 +237,7 @@
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "объединяет 50+ ИИ-провайдеров за единым API. Управляйте доступом, отслеживайте затраты и масштабируйтесь без усилий.",
"Aggregation bucket": "Интервал агрегации",
"AGPL v3.0 License": "Лицензия AGPL v3.0",
"AI Application Infrastructure Foundation": "Инфраструктурная основа для ИИ-приложений",
"AI model testing environment": "Среда тестирования ИИ моделей",
"AI models": "Модели ИИ",
"AI models supported": "Поддерживаемые модели ИИ",
@ -263,7 +265,6 @@
"All Types": "Все типы",
"All upstream data is trusted": "Все вышестоящие данные являются доверенными",
"All Vendors": "Все поставщики",
"All Your AI Models": "Все ваши ИИ-модели",
"All-time": "За всё время",
"Allocated Memory": "Выделенная память",
"Allow accountFilter parameter": "Разрешить параметр accountFilter",
@ -677,6 +678,7 @@
"Check for updates": "Проверить обновления",
"Check in daily to receive random quota rewards": "Регистрируйтесь ежедневно, чтобы получать случайные вознаграждения по квоте",
"Check in now": "Войдите сейчас",
"Check out the Quick Start": "Ознакомьтесь с быстрым стартом",
"Check resolved IPs against IP filters even when accessing by domain": "Проверять разрешенные IP-адреса по IP-фильтрам даже при доступе по домену",
"Check-in failed": "Регистрация не удалась.",
"Check-in Rewards": "Награды за отметку",
@ -1719,8 +1721,9 @@
"Filter by Midjourney task ID": "Фильтр по ID задачи Midjourney",
"Filter by model name...": "Фильтр по имени модели...",
"Filter by model...": "Фильтровать по модели...",
"Filter by API key...": "Фильтр по API-ключу...",
"Filter by name or ID...": "Фильтр по имени или ID...",
"Filter by name or key...": "Фильтровать по имени или ключу...",
"Filter by name...": "Фильтр по имени...",
"Filter by name, ID, or key...": "Фильтровать по имени, ID или ключу...",
"Filter by price field": "Фильтр по полю цены",
"Filter by ratio type": "Фильтровать по типу коэффициента",
@ -1767,7 +1770,7 @@
"footer.columns.related.links.oneApi": "Один API",
"footer.columns.related.title": "Связанные проекты",
"footer.defaultCopyright": "Все права защищены.",
"footer.new\u0061pi.projectAttributionSuffix": "Все права защищены. Разработано участниками проекта.",
"footer.newapi.projectAttributionSuffix": "Все права защищены. Разработано участниками проекта.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Для каналов, добавленных после 10 мая 2025 г., не нужно удалять \".\" из имён моделей при развёртывании",
"For private deployments, format: https://fastgpt.run/api/openapi": "Для частных развертываний, формат: https://fastgpt.run/api/openapi",
"Force a syntactically valid JSON response": "Принудительно возвращать синтаксически корректный JSON",
@ -2397,6 +2400,7 @@
"months": "месяцев",
"Moonshot": "Moonshot",
"More": "Ещё",
"More Apps": "Еще",
"more mapping": "больше сопоставлений",
"More templates...": "Другие шаблоны…",
"More than 999 days left": "Более 999 дней",
@ -2458,6 +2462,7 @@
"Network proxy for this channel (supports socks5 protocol)": "Сетевой прокси для этого канала (поддерживает протокол socks5)",
"Never": "Никогда",
"Never expires": "Никогда не истекает",
"Never used an API Gateway?": "Никогда не пользовались API-шлюзом?",
"NEW": "НОВОЕ",
"New API": "Новый API",
"New API &lt;noreply@example.com&gt;": "Новый API &lt;noreply@example.com&gt;",
@ -3771,12 +3776,14 @@
"Sunset Glow": "Закатное сияние",
"Super Admin": "Суперадмин",
"Support for high concurrency with automatic load balancing": "Поддержка высокой конкурентности с автоматической балансировкой нагрузки",
"Supported Applications": "Поддерживаемые приложения",
"Supported Imagine Models": "Поддерживаемые модели Imagine",
"Supported modalities": "Поддерживаемые модальности",
"Supported parameters": "Поддерживаемые параметры",
"Supported variables": "Поддерживаемые переменные",
"Supports `-thinking`, `-thinking-": "Поддерживает `-thinking`, `-thinking-",
"Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "Поддерживает HTML-разметку или встраивание iframe. Введите HTML-код напрямую или укажите полный URL для автоматического встраивания в виде iframe.",
"Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.": "Поддерживает настройку в один клик и идеально адаптируется к многопротокольной конфигурации NewAPI.",
"Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "Поддерживаются PNG, JPG, SVG или WebP. Рекомендуемый размер: 128×128 или меньше.",
"Sustained tokens per second": "Устойчивая скорость токенов в секунду",
"Swap Face": "Замена лица",
@ -4300,6 +4307,7 @@
"Vary": "Вариация",
"Vary (Strong)": "Вариация (сильная)",
"Vary (Subtle)": "Вариация (лёгкая)",
"Vast Range of AI Models": "Огромный выбор моделей ИИ",
"Vendor": "Поставщик",
"Vendor deleted successfully": "Поставщик успешно удалён",
"Vendor Name *": "Название поставщика *",

View File

@ -106,6 +106,7 @@
"Accept Unpriced Models": "Chấp nhận các Mô hình chưa định giá",
"Accepts a JSON array of model identifiers that support the Imagine API.": "Chấp nhận một mảng JSON gồm các mã định danh mô hình hỗ trợ API Imagine.",
"Accepts comma-separated status codes and inclusive ranges.": "Chấp nhận mã trạng thái phân cách bằng dấu phẩy và phạm vi bao gồm.",
"Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.": "Truy cập số lượng lớn các mô hình thông qua giao thức API chuẩn hóa và thống nhất. Thúc đẩy các ứng dụng AI, quản lý tài sản kỹ thuật số và kết nối tương lai.",
"Access Denied Message": "Thông báo từ chối truy cập",
"Access Forbidden": "Truy cập bị cấm",
"Access Policy (JSON)": "Chính sách truy cập (JSON)",
@ -236,6 +237,7 @@
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "tổng hợp hơn 50 nhà cung cấp AI sau một API thống nhất. Quản lý truy cập, theo dõi chi phí và mở rộng dễ dàng.",
"Aggregation bucket": "Khoảng tổng hợp",
"AGPL v3.0 License": "Giấy phép AGPL v3.0",
"AI Application Infrastructure Foundation": "Nền tảng hạ tầng ứng dụng AI",
"AI model testing environment": "Môi trường thử nghiệm mô hình AI",
"AI models": "mô hình AI",
"AI models supported": "Các mô hình AI được hỗ trợ",
@ -263,7 +265,6 @@
"All Types": "All types",
"All upstream data is trusted": "Tất cả dữ liệu thượng nguồn đều được tin cậy",
"All Vendors": "Tất cả Nhà cung cấp",
"All Your AI Models": "Tất cả mô hình AI của bạn",
"All-time": "Mọi thời điểm",
"Allocated Memory": "Bộ nhớ đã cấp phát",
"Allow accountFilter parameter": "Cho phép tham số accountFilter",
@ -677,6 +678,7 @@
"Check for updates": "Kiểm tra cập nhật",
"Check in daily to receive random quota rewards": "Nhận phòng hàng ngày để nhận phần thưởng theo hạn ngạch ngẫu nhiên",
"Check in now": "Điểm danh ngay",
"Check out the Quick Start": "Xem hướng dẫn bắt đầu nhanh",
"Check resolved IPs against IP filters even when accessing by domain": "Kiểm tra các IP đã phân giải đối chiếu với các bộ lọc IP ngay cả khi truy cập bằng tên miền",
"Check-in failed": "Điểm danh thất bại",
"Check-in Rewards": "Phần thưởng điểm danh",
@ -1719,8 +1721,9 @@
"Filter by Midjourney task ID": "Lọc theo ID nhiệm vụ Midjourney",
"Filter by model name...": "Lọc theo tên mô hình...",
"Filter by model...": "Lọc theo mẫu...",
"Filter by API key...": "Lọc theo khóa API...",
"Filter by name or ID...": "Lọc theo tên hoặc ID...",
"Filter by name or key...": "Lọc theo tên hoặc khóa...",
"Filter by name...": "Lọc theo tên...",
"Filter by name, ID, or key...": "Lọc theo tên, ID hoặc khóa...",
"Filter by price field": "Lọc theo trường giá",
"Filter by ratio type": "Lọc theo loại tỷ lệ",
@ -1767,7 +1770,7 @@
"footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "Các Dự Án Liên Quan",
"footer.defaultCopyright": "Bản quyền được bảo lưu.",
"footer.new\u0061pi.projectAttributionSuffix": "Bản quyền được bảo lưu. Được thiết kế và phát triển bởi các cộng tác viên dự án.",
"footer.newapi.projectAttributionSuffix": "Bản quyền được bảo lưu. Được thiết kế và phát triển bởi các cộng tác viên dự án.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Đối với các kênh được thêm sau ngày 10 tháng 5 năm 2025, không cần loại bỏ \".\" khỏi tên mô hình trong quá trình triển khai",
"For private deployments, format: https://fastgpt.run/api/openapi": "Đối với các triển khai riêng tư, định dạng: https://fastgpt.run/api/openapi",
"Force a syntactically valid JSON response": "Buộc phản hồi JSON hợp lệ về cú pháp",
@ -2397,6 +2400,7 @@
"months": "tháng",
"Moonshot": "Dự án táo bạo",
"More": "Thêm",
"More Apps": "Thêm",
"more mapping": "thêm lập bản đồ",
"More templates...": "Thêm mẫu...",
"More than 999 days left": "Hơn 999 ngày",
@ -2458,6 +2462,7 @@
"Network proxy for this channel (supports socks5 protocol)": "Proxy mạng cho kênh này (hỗ trợ giao thức socks5)",
"Never": "Không bao giờ",
"Never expires": "Không hết hạn",
"Never used an API Gateway?": "Chưa bao giờ sử dụng API Gateway?",
"NEW": "MỚI",
"New API": "API mới",
"New API &lt;noreply@example.com&gt;": "API mới &lt;noreply@example.com&gt;",
@ -3771,12 +3776,14 @@
"Sunset Glow": "Hoàng hôn",
"Super Admin": "Siêu Quản trị viên",
"Support for high concurrency with automatic load balancing": "Hỗ trợ đồng thời cao với cân bằng tải tự động",
"Supported Applications": "Ứng dụng được hỗ trợ",
"Supported Imagine Models": "Mô hình Imagine được hỗ trợ",
"Supported modalities": "Phương thức hỗ trợ",
"Supported parameters": "Tham số hỗ trợ",
"Supported variables": "Biến được hỗ trợ",
"Supports `-thinking`, `-thinking-": "Hỗ trợ `-thinking`, `-thinking-",
"Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "Hỗ trợ đánh dấu HTML hoặc nhúng iframe. Nhập mã HTML trực tiếp, hoặc cung cấp một URL đầy đủ để tự động nhúng nó dưới dạng một iframe.",
"Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.": "Hỗ trợ cấu hình bằng một cú nhấp chuột và thích ứng hoàn hảo với cấu hình đa giao thức NewAPI.",
"Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "Hỗ trợ PNG, JPG, SVG hoặc WebP. Kích thước khuyến nghị: 128×128 hoặc nhỏ hơn.",
"Sustained tokens per second": "Token mỗi giây duy trì",
"Swap Face": "Đổi mặt",
@ -4300,6 +4307,7 @@
"Vary": "Biến thể",
"Vary (Strong)": "Biến thể (mạnh)",
"Vary (Subtle)": "Biến thể (nhẹ)",
"Vast Range of AI Models": "Số lượng lớn mô hình AI",
"Vendor": "Supplier",
"Vendor deleted successfully": "Đã xóa nhà cung cấp thành công",
"Vendor Name *": "Tên nhà cung cấp *",

View File

@ -106,6 +106,7 @@
"Accept Unpriced Models": "接受未定价模型",
"Accepts a JSON array of model identifiers that support the Imagine API.": "接受支持 Imagine API 的模型标识符的 JSON 数组。",
"Accepts comma-separated status codes and inclusive ranges.": "接受逗号分隔的状态码和包含性范围。",
"Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.": "通过统一、标准的接口协议接入海量模型。承载 AI 应用,高效管理数字资产,连接未来。",
"Access Denied Message": "访问被拒绝消息",
"Access Forbidden": "禁止访问",
"Access Policy (JSON)": "访问策略 (JSON)",
@ -236,6 +237,7 @@
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "聚合 50+ AI 提供商于统一 API 之后。轻松管理访问、追踪成本、弹性扩展。",
"Aggregation bucket": "聚合时间桶",
"AGPL v3.0 License": "AGPL v3.0 协议",
"AI Application Infrastructure Foundation": "人工智能应用基座",
"AI model testing environment": "AI模型测试环境",
"AI models": "AI 模型",
"AI models supported": "支持的 AI 模型",
@ -263,7 +265,6 @@
"All Types": "所有类型",
"All upstream data is trusted": "所有上游数据均受信任",
"All Vendors": "所有供应商",
"All Your AI Models": "所有 AI 模型",
"All-time": "全部时间",
"Allocated Memory": "已分配内存",
"Allow accountFilter parameter": "允许 accountFilter 参数",
@ -677,6 +678,7 @@
"Check for updates": "检查更新",
"Check in daily to receive random quota rewards": "每日签到可获得随机额度奖励",
"Check in now": "立即签到",
"Check out the Quick Start": "请查看 新手入门",
"Check resolved IPs against IP filters even when accessing by domain": "即使通过域名访问,也对照 IP 过滤器检查解析的 IP",
"Check-in failed": "签到失败",
"Check-in Rewards": "签到奖励",
@ -1719,8 +1721,9 @@
"Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选",
"Filter by model name...": "按模型名称筛选...",
"Filter by model...": "按模型筛选...",
"Filter by API key...": "按 API 密钥筛选...",
"Filter by name or ID...": "按名称或 ID 筛选...",
"Filter by name or key...": "按名称或密钥筛选...",
"Filter by name...": "按名称筛选...",
"Filter by name, ID, or key...": "按名称、ID 或密钥筛选...",
"Filter by price field": "按价格字段筛选",
"Filter by ratio type": "按倍率类型筛选",
@ -1767,7 +1770,7 @@
"footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "相关项目",
"footer.defaultCopyright": "版权所有。",
"footer.new\u0061pi.projectAttributionSuffix": "版权所有,由项目贡献者设计与开发。",
"footer.newapi.projectAttributionSuffix": "版权所有,由项目贡献者设计与开发。",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "对于 2025 年 5 月 10 日之后添加的渠道,在部署时无需从模型名称中移除 \".\"",
"For private deployments, format: https://fastgpt.run/api/openapi": "对于私有部署格式为https://fastgpt.run/api/openapi",
"Force a syntactically valid JSON response": "强制返回语法合法的 JSON",
@ -2397,6 +2400,7 @@
"months": "个月",
"Moonshot": "Moonshot",
"More": "更多",
"More Apps": "更多",
"more mapping": "更多映射",
"More templates...": "更多模板...",
"More than 999 days left": "剩余超过 999 天",
@ -2458,6 +2462,7 @@
"Network proxy for this channel (supports socks5 protocol)": "此渠道的网络代理(支持 socks5 协议)",
"Never": "永不",
"Never expires": "永不过期",
"Never used an API Gateway?": "从未使用过 API 网关/中转 API",
"NEW": "新",
"New API": "New API",
"New API &lt;noreply@example.com&gt;": "New API &lt;noreply@example.com&gt;",
@ -3771,12 +3776,14 @@
"Sunset Glow": "日落霞光",
"Super Admin": "超级管理员",
"Support for high concurrency with automatic load balancing": "支持高并发和自动负载均衡",
"Supported Applications": "常用应用支持",
"Supported Imagine Models": "支持的 Imagine 模型",
"Supported modalities": "支持的模态",
"Supported parameters": "支持的参数",
"Supported variables": "支持变量",
"Supports `-thinking`, `-thinking-": "支持 `-thinking`、`-thinking-`",
"Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.": "支持 HTML 标记或 iframe 嵌入。直接输入 HTML 代码,或提供完整的 URL 以将其自动嵌入为 iframe。",
"Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.": "支持一键配置并完美适配 NewAPI 多协议配置",
"Supports PNG, JPG, SVG, or WebP. Recommended size: 128×128 or smaller.": "支持 PNG、JPG、SVG 或 WebP建议尺寸不超过 128×128。",
"Sustained tokens per second": "持续每秒 Token 数",
"Swap Face": "换脸",
@ -4300,6 +4307,7 @@
"Vary": "变换",
"Vary (Strong)": "强变换",
"Vary (Subtle)": "弱变换",
"Vast Range of AI Models": "海量 AI 模型",
"Vendor": "供应商",
"Vendor deleted successfully": "供应商删除成功",
"Vendor Name *": "供应商名称 *",

View File

@ -29,6 +29,7 @@ const apiKeySearchSchema = z.object({
.optional()
.catch([]),
filter: z.string().optional().catch(''),
token: z.string().optional().catch(''),
})
export const Route = createFileRoute('/_authenticated/keys/')({