inbounds: allow custom monthly traffic reset days (#6071)

This commit is contained in:
Shichao Song
2026-07-29 05:27:09 +08:00
committed by GitHub
parent 34d2591e50
commit 17e6b5a460
37 changed files with 179 additions and 16 deletions
+1
View File
@@ -450,6 +450,7 @@ export const EXAMPLES: Record<string, unknown> = {
"tag": "in-443-tcp",
"total": 0,
"trafficReset": "never",
"trafficResetDay": 1,
"up": 0
},
"InboundClientIps": {
+8
View File
@@ -1842,6 +1842,13 @@ export const SCHEMAS: Record<string, unknown> = {
],
"type": "string"
},
"trafficResetDay": {
"description": "Day of month for monthly traffic resets",
"example": 1,
"maximum": 31,
"minimum": 1,
"type": "integer"
},
"up": {
"description": "Upload traffic in bytes",
"format": "int64",
@@ -1868,6 +1875,7 @@ export const SCHEMAS: Record<string, unknown> = {
"tag",
"total",
"trafficReset",
"trafficResetDay",
"up"
],
"type": "object"
+1
View File
@@ -428,6 +428,7 @@ export interface Inbound {
tag: string;
total: number;
trafficReset: string;
trafficResetDay: number;
up: number;
}
+1
View File
@@ -453,6 +453,7 @@ export const InboundSchema = z.object({
tag: z.string(),
total: z.number().int(),
trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']),
trafficResetDay: z.number().int().min(1).max(31),
up: z.number().int(),
});
export type Inbound = z.infer<typeof InboundSchema>;
@@ -41,6 +41,7 @@ export interface RawInboundRow {
enable?: boolean;
expiryTime?: number;
trafficReset?: string;
trafficResetDay?: number;
lastTrafficResetTime?: number;
nodeId?: number | null;
shareAddrStrategy?: string;
@@ -60,6 +61,7 @@ export interface WireInboundPayload {
enable: boolean;
expiryTime: number;
trafficReset: TrafficReset;
trafficResetDay: number;
lastTrafficResetTime: number;
listen: string;
port: number;
@@ -202,6 +204,7 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
down: row.down ?? 0,
total: row.total ?? 0,
trafficReset: coerceTrafficReset(row.trafficReset),
trafficResetDay: Math.min(31, Math.max(1, row.trafficResetDay ?? 1)),
lastTrafficResetTime: row.lastTrafficResetTime ?? 0,
nodeId: row.nodeId ?? null,
shareAddrStrategy: coerceShareAddrStrategy(row.shareAddrStrategy),
@@ -344,6 +347,7 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP
enable: values.enable,
expiryTime: values.expiryTime,
trafficReset: values.trafficReset,
trafficResetDay: values.trafficResetDay,
lastTrafficResetTime: values.lastTrafficResetTime,
listen: values.listen,
port: values.port,
+3
View File
@@ -30,6 +30,7 @@ export type DBInboundInit = Partial<{
enable: boolean;
expiryTime: number;
trafficReset: string;
trafficResetDay: number;
lastTrafficResetTime: number;
listen: string;
port: number;
@@ -76,6 +77,7 @@ export class DBInbound {
enable: boolean;
expiryTime: number;
trafficReset: string;
trafficResetDay: number;
lastTrafficResetTime: number;
listen: string;
@@ -105,6 +107,7 @@ export class DBInbound {
this.enable = true;
this.expiryTime = 0;
this.trafficReset = "never";
this.trafficResetDay = 1;
this.lastTrafficResetTime = 0;
this.listen = "";
@@ -33,6 +33,7 @@ import {
isSS2022,
} from '@/lib/xray/protocol-capabilities';
import {
InboundDbFieldsSchema,
InboundFormBaseSchema,
InboundFormSchema,
type InboundFormValues,
@@ -255,6 +256,7 @@ export default function InboundFormModal({
const wTunnelNetwork = useWatch({ control, name: 'settings.allowedNetwork' });
const wTotal = (useWatch({ control, name: 'total' }) as number | undefined) ?? 0;
const wExpiry = (useWatch({ control, name: 'expiryTime' }) as number | undefined) ?? 0;
const trafficReset = useWatch({ control, name: 'trafficReset' }) ?? 'never';
const autoTagRef = useRef(true);
const lastWrittenTagRef = useRef('');
const currentTagInput = (): InboundTagInput => ({
@@ -619,6 +621,16 @@ export default function InboundFormModal({
/>
</FormField>
{trafficReset === 'monthly' && (
<FormField
name="trafficResetDay"
label={t('pages.inbounds.periodicTrafficResetDay')}
rules={{ validate: rhfZodValidate(InboundDbFieldsSchema.shape.trafficResetDay) }}
>
<InputNumber min={1} max={31} />
</FormField>
)}
<Form.Item
label={
<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
@@ -22,6 +22,7 @@ export const InboundDbFieldsSchema = z.object({
down: z.number().int().min(0).default(0),
total: z.number().int().min(0).default(0),
trafficReset: TrafficResetSchema.default('never'),
trafficResetDay: z.number().int().min(1).max(31).default(1),
lastTrafficResetTime: z.number().int().default(0),
nodeId: z.number().int().nullable().optional(),
shareAddrStrategy: ShareAddrStrategySchema.default('node'),
@@ -32,6 +32,7 @@ const vlessRow: RawInboundRow = {
total: 1_000_000_000,
expiryTime: 0,
trafficReset: 'monthly',
trafficResetDay: 15,
lastTrafficResetTime: 0,
tag: 'inbound-1',
nodeId: null,
@@ -267,6 +268,7 @@ describe('formValuesToWirePayload', () => {
enable: payload.enable,
expiryTime: payload.expiryTime,
trafficReset: payload.trafficReset,
trafficResetDay: payload.trafficResetDay,
lastTrafficResetTime: payload.lastTrafficResetTime,
nodeId: payload.nodeId ?? null,
});
@@ -276,8 +278,13 @@ describe('formValuesToWirePayload', () => {
expect(replay.listen).toBe(original.listen);
expect(replay.up).toBe(original.up);
expect(replay.down).toBe(original.down);
expect(replay.trafficResetDay).toBe(original.trafficResetDay);
expect(replay.streamSettings).toEqual(original.streamSettings);
});
it('defaults a missing monthly reset day to the first', () => {
expect(rawInboundToFormValues({ ...vlessRow, trafficResetDay: undefined }).trafficResetDay).toBe(1);
});
});
describe('subSortIndex', () => {