mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
feat(frontend): add security rate-limits page and inherit UI
This commit is contained in:
@@ -16,7 +16,7 @@ export const proxyRouteConfigSections = [
|
|||||||
{
|
{
|
||||||
key: 'limits' as const,
|
key: 'limits' as const,
|
||||||
label: '流量限制',
|
label: '流量限制',
|
||||||
description: '设置连接数和限速。',
|
description: '设置连接数和限速(可继承全局默认)。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'proxy' as const,
|
key: 'proxy' as const,
|
||||||
@@ -263,18 +263,19 @@ export function customHeadersToText(headers: ProxyRouteCustomHeader[]) {
|
|||||||
|
|
||||||
export function validateLimitRate(value: string) {
|
export function validateLimitRate(value: string) {
|
||||||
const normalized = value.trim();
|
const normalized = value.trim();
|
||||||
if (!normalized || normalized === '0') {
|
if (!normalized || normalized === '0' || normalized === '-1') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!limitRatePattern.test(normalized)) {
|
if (!limitRatePattern.test(normalized)) {
|
||||||
return '限速格式不合法,请使用 512k、1m 或纯数字';
|
return '限速格式不合法,请使用 512k、1m、纯数字,或 -1 关闭';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeLimitRate(value: string) {
|
export function normalizeLimitRate(value: string) {
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
return normalized === '0' ? '' : normalized;
|
if (normalized === '0') return '';
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateCacheRules(
|
export function validateCacheRules(
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ const rateLimitSchema = z
|
|||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!/^\d+$/.test(rawValue)) {
|
if (!/^-1$|^\d+$/.test(rawValue)) {
|
||||||
context.addIssue({
|
context.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
path: [field],
|
path: [field],
|
||||||
message: '请输入大于等于 0 的整数',
|
message: '请输入 -1、0 或正整数',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,21 @@ const rateLimitSchema = z
|
|||||||
|
|
||||||
type RateLimitValues = z.infer<typeof rateLimitSchema>;
|
type RateLimitValues = z.infer<typeof rateLimitSchema>;
|
||||||
|
|
||||||
|
function formatConnValue(value: number | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConnValue(value: string) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
interface LimitsSectionProps {
|
interface LimitsSectionProps {
|
||||||
route: ProxyRouteItem;
|
route: ProxyRouteItem;
|
||||||
onRouteUpdate: (route: ProxyRouteItem) => void;
|
onRouteUpdate: (route: ProxyRouteItem) => void;
|
||||||
@@ -81,24 +96,16 @@ export function LimitsSection({
|
|||||||
const form = useForm<RateLimitValues>({
|
const form = useForm<RateLimitValues>({
|
||||||
resolver: zodResolver(rateLimitSchema),
|
resolver: zodResolver(rateLimitSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
limit_conn_per_server: route.limit_conn_per_server
|
limit_conn_per_server: formatConnValue(route.limit_conn_per_server),
|
||||||
? String(route.limit_conn_per_server)
|
limit_conn_per_ip: formatConnValue(route.limit_conn_per_ip),
|
||||||
: '',
|
|
||||||
limit_conn_per_ip: route.limit_conn_per_ip
|
|
||||||
? String(route.limit_conn_per_ip)
|
|
||||||
: '',
|
|
||||||
limit_rate: route.limit_rate || '',
|
limit_rate: route.limit_rate || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
limit_conn_per_server: route.limit_conn_per_server
|
limit_conn_per_server: formatConnValue(route.limit_conn_per_server),
|
||||||
? String(route.limit_conn_per_server)
|
limit_conn_per_ip: formatConnValue(route.limit_conn_per_ip),
|
||||||
: '',
|
|
||||||
limit_conn_per_ip: route.limit_conn_per_ip
|
|
||||||
? String(route.limit_conn_per_ip)
|
|
||||||
: '',
|
|
||||||
limit_rate: route.limit_rate || '',
|
limit_rate: route.limit_rate || '',
|
||||||
});
|
});
|
||||||
}, [form, route]);
|
}, [form, route]);
|
||||||
@@ -106,7 +113,7 @@ export function LimitsSection({
|
|||||||
return (
|
return (
|
||||||
<SectionShell
|
<SectionShell
|
||||||
title='流量限制'
|
title='流量限制'
|
||||||
description='站点限流,空值或 0 表示关闭。'
|
description='站点限流。空或 0 继承全局默认;-1 显式关闭;大于 0 为自定义。'
|
||||||
formId={proxyRouteFormIds.limits}
|
formId={proxyRouteFormIds.limits}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
>
|
>
|
||||||
@@ -117,12 +124,10 @@ export function LimitsSection({
|
|||||||
onSubmit={form.handleSubmit(async (values) => {
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
await save(
|
await save(
|
||||||
{
|
{
|
||||||
limit_conn_per_server: Number(
|
limit_conn_per_server: parseConnValue(
|
||||||
values.limit_conn_per_server.trim() || '0',
|
values.limit_conn_per_server,
|
||||||
),
|
|
||||||
limit_conn_per_ip: Number(
|
|
||||||
values.limit_conn_per_ip.trim() || '0',
|
|
||||||
),
|
),
|
||||||
|
limit_conn_per_ip: parseConnValue(values.limit_conn_per_ip),
|
||||||
limit_rate: normalizeLimitRate(values.limit_rate),
|
limit_rate: normalizeLimitRate(values.limit_rate),
|
||||||
},
|
},
|
||||||
'流量限制已保存',
|
'流量限制已保存',
|
||||||
@@ -138,7 +143,9 @@ export function LimitsSection({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='120' {...field} />
|
<Input placeholder='120' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>限制当前站点最大并发连接数。</FormDescription>
|
<FormDescription>
|
||||||
|
空或 0 继承全局默认;-1 关闭;大于 0 为自定义并发上限。
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -153,7 +160,9 @@ export function LimitsSection({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='12' {...field} />
|
<Input placeholder='12' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>限制单个 IP 的最大并发数。</FormDescription>
|
<FormDescription>
|
||||||
|
空或 0 继承全局默认;-1 关闭;大于 0 为单 IP 自定义上限。
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -166,10 +175,10 @@ export function LimitsSection({
|
|||||||
<FormItem className='md:col-span-2'>
|
<FormItem className='md:col-span-2'>
|
||||||
<FormLabel>限速</FormLabel>
|
<FormLabel>限速</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='512k/1m' {...field} />
|
<Input placeholder='512k/1m 或 -1' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
限制单请求带宽,例如 512k 或 1m。
|
空或 0 继承全局默认;-1 关闭;例如 512k、1m 为自定义带宽。
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
'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 { useAuth } from '@/components/providers/auth-provider';
|
||||||
|
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||||
|
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 limitRatePattern = /^\d+(?:[kKmM])?$/;
|
||||||
|
|
||||||
|
type RateLimitFields = {
|
||||||
|
openresty_default_limit_conn_per_server: string;
|
||||||
|
openresty_default_limit_conn_per_ip: string;
|
||||||
|
openresty_default_limit_rate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFields: RateLimitFields = {
|
||||||
|
openresty_default_limit_conn_per_server: '0',
|
||||||
|
openresty_default_limit_conn_per_ip: '0',
|
||||||
|
openresty_default_limit_rate: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function optionsToMap(options: Array<{ key: string; value: string }>) {
|
||||||
|
return options.reduce<Record<string, string>>((acc, option) => {
|
||||||
|
acc[option.key] = option.value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOptionsToFields(
|
||||||
|
optionMap: Record<string, string>,
|
||||||
|
): 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] ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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、纯数字,或留空关闭');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 default function RateLimitsPage() {
|
||||||
|
const { user, loading: authLoading } = useAuth();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [fields, setFields] = useState<RateLimitFields>(defaultFields);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const optionsQuery = useQuery({
|
||||||
|
queryKey: optionsQueryKey,
|
||||||
|
queryFn: () => OptionService.list(),
|
||||||
|
enabled: !!user?.is_admin,
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
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 = <K extends keyof RateLimitFields>(
|
||||||
|
key: K,
|
||||||
|
value: RateLimitFields[K],
|
||||||
|
) => {
|
||||||
|
setFields((prev) => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authLoading) {
|
||||||
|
return (
|
||||||
|
<div className='py-6 px-1'>
|
||||||
|
<LoadingStateWithBorder icon={Gauge} description='加载权限信息...' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user?.is_admin) {
|
||||||
|
return (
|
||||||
|
<div className='py-6 px-1'>
|
||||||
|
<EmptyStateWithBorder
|
||||||
|
icon={Gauge}
|
||||||
|
title='权限不足'
|
||||||
|
description='只有管理员可以访问限流设置。'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionsQuery.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className='py-6 px-1'>
|
||||||
|
<LoadingStateWithBorder icon={Gauge} description='加载限流参数...' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionsQuery.isError) {
|
||||||
|
return (
|
||||||
|
<div className='py-6 px-1'>
|
||||||
|
<ErrorInline
|
||||||
|
message={
|
||||||
|
optionsQuery.error instanceof Error
|
||||||
|
? optionsQuery.error.message
|
||||||
|
: '加载失败'
|
||||||
|
}
|
||||||
|
onRetry={() => void optionsQuery.refetch()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='py-6 px-1 space-y-6'>
|
||||||
|
<div className='flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Gauge className='size-5 text-primary' />
|
||||||
|
<div>
|
||||||
|
<h1 className='text-2xl font-semibold tracking-tight'>限流</h1>
|
||||||
|
<p className='text-sm text-muted-foreground'>
|
||||||
|
配置边缘站点默认并发与带宽限流。0
|
||||||
|
或空表示默认关闭;站点未单独配置时继承此处设置。修改后需在版本发布中生效。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant='outline' size='sm' asChild>
|
||||||
|
<Link href='/config-versions'>
|
||||||
|
<ExternalLink className='size-3.5 mr-1' />
|
||||||
|
查看配置预览
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className='border-dashed shadow-none'>
|
||||||
|
<CardHeader className='flex flex-row items-center justify-between'>
|
||||||
|
<div>
|
||||||
|
<CardTitle className='text-base'>全局默认限流</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
0 或空表示默认关闭;站点未单独配置时继承此处设置。
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
disabled={saving}
|
||||||
|
onClick={() => saveMutation.mutate()}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<Loader2 className='size-4 animate-spin mr-1' />
|
||||||
|
) : (
|
||||||
|
<Save className='size-3.5 mr-1' />
|
||||||
|
)}
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||||
|
<div className='space-y-1.5'>
|
||||||
|
<Label className='text-xs text-muted-foreground'>
|
||||||
|
默认并发限制(每站点)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={fields.openresty_default_limit_conn_per_server}
|
||||||
|
placeholder='0'
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(
|
||||||
|
'openresty_default_limit_conn_per_server',
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='h-9 text-xs'
|
||||||
|
/>
|
||||||
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
站点最大并发连接数默认值
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1.5'>
|
||||||
|
<Label className='text-xs text-muted-foreground'>
|
||||||
|
默认单 IP 并发
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={fields.openresty_default_limit_conn_per_ip}
|
||||||
|
placeholder='0'
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(
|
||||||
|
'openresty_default_limit_conn_per_ip',
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='h-9 text-xs'
|
||||||
|
/>
|
||||||
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
单个 IP 最大并发连接数默认值
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-1.5'>
|
||||||
|
<Label className='text-xs text-muted-foreground'>默认限速</Label>
|
||||||
|
<Input
|
||||||
|
value={fields.openresty_default_limit_rate}
|
||||||
|
placeholder='512k / 1m'
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField('openresty_default_limit_rate', e.target.value)
|
||||||
|
}
|
||||||
|
className='h-9 text-xs'
|
||||||
|
/>
|
||||||
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
单请求带宽默认值,例如 512k 或 1m
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ export const openflareSecurityNavGroup: OpenFlareNavGroup = {
|
|||||||
items: [
|
items: [
|
||||||
{ title: 'WAF', url: '/waf' },
|
{ title: 'WAF', url: '/waf' },
|
||||||
{ title: 'IP 组', url: '/ip-groups' },
|
{ title: 'IP 组', url: '/ip-groups' },
|
||||||
|
{ title: '限流', url: '/rate-limits' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,21 @@ export const searchData: SearchItem[] = [
|
|||||||
category: 'page',
|
category: 'page',
|
||||||
keywords: ['ip', 'ip group', 'ip组', '黑名单', '白名单', '访问控制'],
|
keywords: ['ip', 'ip group', 'ip组', '黑名单', '白名单', '访问控制'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'console-rate-limits',
|
||||||
|
title: '限流',
|
||||||
|
description: '配置边缘站点默认并发与带宽限流策略',
|
||||||
|
url: '/rate-limits',
|
||||||
|
category: 'page',
|
||||||
|
keywords: [
|
||||||
|
'限流',
|
||||||
|
'rate limit',
|
||||||
|
'limit_conn',
|
||||||
|
'limit_rate',
|
||||||
|
'并发',
|
||||||
|
'带宽',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'console-pages',
|
id: 'console-pages',
|
||||||
title: 'Pages 静态托管',
|
title: 'Pages 静态托管',
|
||||||
|
|||||||
Reference in New Issue
Block a user