'use client'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ExternalLink, Gauge, Loader2, Save } from 'lucide-react'; import { toast } from 'sonner'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { OptionService } from '@/lib/services/openflare'; const optionsQueryKey = ['openflare', 'options'] as const; const KEY_CONN_PER_SERVER = 'openresty_default_limit_conn_per_server'; const KEY_CONN_PER_IP = 'openresty_default_limit_conn_per_ip'; const KEY_LIMIT_RATE = 'openresty_default_limit_rate'; const KEY_LIMIT_REQ_PER_IP = 'openresty_default_limit_req_per_ip'; const limitRatePattern = /^\d+(?:[kKmM])?$/; type RateLimitFields = { openresty_default_limit_conn_per_server: string; openresty_default_limit_conn_per_ip: string; openresty_default_limit_rate: string; openresty_default_limit_req_per_ip: string; }; const defaultFields: RateLimitFields = { openresty_default_limit_conn_per_server: '0', openresty_default_limit_conn_per_ip: '0', openresty_default_limit_rate: '', openresty_default_limit_req_per_ip: '', }; function optionsToMap(options: Array<{ key: string; value: string }>) { return options.reduce>((acc, option) => { acc[option.key] = option.value; return acc; }, {}); } function mapOptionsToFields( optionMap: Record, ): RateLimitFields { return { openresty_default_limit_conn_per_server: optionMap[KEY_CONN_PER_SERVER] ?? '0', openresty_default_limit_conn_per_ip: optionMap[KEY_CONN_PER_IP] ?? '0', openresty_default_limit_rate: optionMap[KEY_LIMIT_RATE] ?? '', openresty_default_limit_req_per_ip: optionMap[KEY_LIMIT_REQ_PER_IP] ?? '', }; } function validateFields(fields: RateLimitFields) { for (const key of [KEY_CONN_PER_SERVER, KEY_CONN_PER_IP] as const) { const raw = fields[key].trim(); if (!raw) continue; if (!/^\d+$/.test(raw)) { throw new Error('并发限制请输入非负整数,或留空表示关闭'); } } const rate = fields.openresty_default_limit_rate.trim(); if (rate && rate !== '0' && !limitRatePattern.test(rate)) { throw new Error('限速格式不合法,请使用 512k、1m、纯数字,或留空关闭'); } const reqRate = fields.openresty_default_limit_req_per_ip.trim(); if (reqRate && reqRate !== '0' && !/^\d+r\/[sm]$/i.test(reqRate)) { throw new Error( '请求频率限制格式不合法,请使用类似 10r/s、100r/m,或留空关闭', ); } } function normalizeConnValue(value: string) { const trimmed = value.trim(); if (!trimmed) return '0'; return trimmed; } function normalizeRateValue(value: string) { const normalized = value.trim().toLowerCase(); if (!normalized || normalized === '0') return ''; return normalized; } export function ConfigTab() { const queryClient = useQueryClient(); const [fields, setFields] = useState(defaultFields); const [saving, setSaving] = useState(false); const optionsQuery = useQuery({ queryKey: optionsQueryKey, queryFn: () => OptionService.list(), }); useEffect(() => { if (!optionsQuery.data) return; setFields(mapOptionsToFields(optionsToMap(optionsQuery.data))); }, [optionsQuery.data]); const saveMutation = useMutation({ mutationFn: async () => { validateFields(fields); setSaving(true); await OptionService.updateBatch([ { key: KEY_CONN_PER_SERVER, value: normalizeConnValue( fields.openresty_default_limit_conn_per_server, ), }, { key: KEY_CONN_PER_IP, value: normalizeConnValue(fields.openresty_default_limit_conn_per_ip), }, { key: KEY_LIMIT_RATE, value: normalizeRateValue(fields.openresty_default_limit_rate), }, { key: KEY_LIMIT_REQ_PER_IP, value: normalizeRateValue(fields.openresty_default_limit_req_per_ip), }, ]); }, onSuccess: async () => { toast.success('限流参数已保存'); await Promise.all([ queryClient.invalidateQueries({ queryKey: optionsQueryKey }), queryClient.invalidateQueries({ queryKey: ['openflare', 'config-preview'], }), queryClient.invalidateQueries({ queryKey: ['openflare', 'config-versions'], }), ]); setSaving(false); }, onError: (error) => { setSaving(false); toast.error(error instanceof Error ? error.message : '保存失败'); }, }); const updateField = ( key: K, value: RateLimitFields[K], ) => { setFields((prev) => ({ ...prev, [key]: value })); }; if (optionsQuery.isLoading) { return ( ); } if (optionsQuery.isError) { return ( void optionsQuery.refetch()} /> ); } return (
全局默认限流 0 或空表示默认关闭;站点未单独配置时继承此处设置。修改后需在版本发布中生效。
updateField( 'openresty_default_limit_conn_per_server', e.target.value, ) } className='h-9 text-xs' />

站点最大并发连接数默认值

updateField( 'openresty_default_limit_conn_per_ip', e.target.value, ) } className='h-9 text-xs' />

单个 IP 最大并发连接数默认值

updateField('openresty_default_limit_rate', e.target.value) } className='h-9 text-xs' />

单请求带宽默认值,例如 512k 或 1m

updateField( 'openresty_default_limit_req_per_ip', e.target.value, ) } className='h-9 text-xs' />

单个 IP 请求频率默认值,例如 10r/s 或 100r/m,留空关闭

); }