mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-09-01 15:34:05 +08:00
feat(auth-files): add WRR weight management and update priority handling
This commit is contained in:
@@ -383,7 +383,7 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ---------- 元数据行:size · modified · P{n} ---------- */
|
||||
/* ---------- 元数据行:size · modified · priority · WRR weight ---------- */
|
||||
|
||||
.metaRow {
|
||||
display: flex;
|
||||
@@ -401,9 +401,36 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.metaPriority {
|
||||
.metaPriority,
|
||||
.metaWeight {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $radius-full;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metaPriority {
|
||||
color: var(--text-secondary);
|
||||
background: color-mix(in srgb, var(--text-quaternary) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--text-quaternary) 24%, transparent);
|
||||
}
|
||||
|
||||
.metaWeight {
|
||||
color: var(--primary-active);
|
||||
background: var(--primary-10);
|
||||
border-color: var(--primary-30);
|
||||
}
|
||||
|
||||
.metaMetricLabel {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: currentColor;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
/* ---------- 页脚动作区 ---------- */
|
||||
|
||||
@@ -114,7 +114,8 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
const rawStatusMessage = getAuthFileStatusMessage(file);
|
||||
const hasStatusWarning = hasAuthFileStatusWarning(file);
|
||||
|
||||
const priorityValue = typeof file.priority === 'number' ? file.priority : undefined;
|
||||
const priorityValue = Number.isSafeInteger(file.priority) ? file.priority : undefined;
|
||||
const weightValue = Number.isSafeInteger(file.weight) ? file.weight : undefined;
|
||||
const noteValue = typeof file.note === 'string' ? file.note.trim() : '';
|
||||
|
||||
const stateLabel = isRuntimeOnly
|
||||
@@ -243,9 +244,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
</div>
|
||||
|
||||
<div className={styles.metaRow}>
|
||||
<span title={t('auth_files.file_size')}>
|
||||
{file.size ? formatFileSize(file.size) : '-'}
|
||||
</span>
|
||||
<span title={t('auth_files.file_size')}>{file.size ? formatFileSize(file.size) : '-'}</span>
|
||||
<span className={styles.metaDivider} aria-hidden="true">
|
||||
·
|
||||
</span>
|
||||
@@ -255,8 +254,20 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
<span className={styles.metaDivider} aria-hidden="true">
|
||||
·
|
||||
</span>
|
||||
<span className={styles.metaPriority} title={t('auth_files.priority_display')}>
|
||||
P{priorityValue}
|
||||
<span className={styles.metaPriority} title={t('auth_files.priority_hint')}>
|
||||
<span className={styles.metaMetricLabel}>{t('auth_files.priority_display')}</span>
|
||||
<span>{priorityValue}</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{weightValue !== undefined && (
|
||||
<>
|
||||
<span className={styles.metaDivider} aria-hidden="true">
|
||||
·
|
||||
</span>
|
||||
<span className={styles.metaWeight} title={t('auth_files.weight_hint')}>
|
||||
<span className={styles.metaMetricLabel}>{t('auth_files.weight_display')}</span>
|
||||
<span>{weightValue}</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -284,6 +284,7 @@
|
||||
"sort_az": "A-Z Name",
|
||||
"sort_priority": "Priority",
|
||||
"priority_display": "Priority",
|
||||
"weight_display": "WRR weight",
|
||||
"page_size_label": "Per page",
|
||||
"view_mode_paged": "Paged",
|
||||
"view_mode_all": "Show all",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"sort_az": "A-Z Имя",
|
||||
"sort_priority": "Приоритет",
|
||||
"priority_display": "Приоритет",
|
||||
"weight_display": "Вес WRR",
|
||||
"page_size_label": "На странице",
|
||||
"view_mode_paged": "Постранично",
|
||||
"view_mode_all": "Показать все",
|
||||
|
||||
@@ -284,6 +284,7 @@
|
||||
"sort_az": "A-Z 名称",
|
||||
"sort_priority": "优先级",
|
||||
"priority_display": "优先级",
|
||||
"weight_display": "WRR 权重",
|
||||
"page_size_label": "单页数量",
|
||||
"view_mode_paged": "按页显示",
|
||||
"view_mode_all": "显示全部",
|
||||
|
||||
@@ -284,6 +284,7 @@
|
||||
"sort_az": "A-Z 名稱",
|
||||
"sort_priority": "優先順序",
|
||||
"priority_display": "優先順序",
|
||||
"weight_display": "WRR 權重",
|
||||
"page_size_label": "單頁數量",
|
||||
"view_mode_paged": "分頁顯示",
|
||||
"view_mode_all": "顯示全部",
|
||||
|
||||
@@ -215,8 +215,8 @@ const mergeAuthFileEntries = (entries: AuthFileEntry[]): AuthFileEntry => {
|
||||
|
||||
const INTEGER_STRING_PATTERN = /^[+-]?\d+$/;
|
||||
|
||||
const readPriorityField = (value: unknown): number | undefined => {
|
||||
if (typeof value === 'number') return Number.isInteger(value) ? value : undefined;
|
||||
const readIntegerField = (value: unknown): number | undefined => {
|
||||
if (typeof value === 'number') return Number.isSafeInteger(value) ? value : undefined;
|
||||
if (typeof value !== 'string') return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || !INTEGER_STRING_PATTERN.test(trimmed)) return undefined;
|
||||
@@ -242,7 +242,8 @@ const normalizeAuthFileEntry = (entry: AuthFileEntry): AuthFileEntry => {
|
||||
const statusMessage = readTextField(entry, 'status_message') || declaredStatusMessage;
|
||||
const note = readTextField(entry, 'note');
|
||||
const modified = readDateField(entry);
|
||||
const priority = readPriorityField(entry['priority']);
|
||||
const priority = readIntegerField(entry['priority']);
|
||||
const weight = readIntegerField(entry['weight']);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
@@ -253,12 +254,13 @@ const normalizeAuthFileEntry = (entry: AuthFileEntry): AuthFileEntry => {
|
||||
failureCount: normalizeUsageTotal(entry.failed),
|
||||
...(statusMessage ? { statusMessage } : {}),
|
||||
...(modified > 0 ? { modified } : {}),
|
||||
...(priority !== undefined ? { priority } : {}),
|
||||
priority,
|
||||
weight,
|
||||
...(note ? { note } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const dedupeAuthFilesResponse = (payload: AuthFilesResponse): AuthFilesResponse => {
|
||||
export const normalizeAuthFilesResponse = (payload: AuthFilesResponse): AuthFilesResponse => {
|
||||
const files = Array.isArray(payload?.files) ? payload.files : [];
|
||||
const grouped = new Map<string, AuthFileEntry[]>();
|
||||
|
||||
@@ -398,7 +400,8 @@ export const buildManualRefreshExpiredAt = (nowMs = Date.now()): string =>
|
||||
new Date(nowMs - MANUAL_REFRESH_EXPIRY_OFFSET_MS).toISOString();
|
||||
|
||||
export const authFilesApi = {
|
||||
list: async () => dedupeAuthFilesResponse(await apiClient.get<AuthFilesResponse>('/auth-files')),
|
||||
list: async () =>
|
||||
normalizeAuthFilesResponse(await apiClient.get<AuthFilesResponse>('/auth-files')),
|
||||
|
||||
setStatus: (name: string, disabled: boolean) =>
|
||||
apiClient.patch<AuthFileStatusResponse>('/auth-files/status', { name, disabled }),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { normalizeAuthFilesResponse } from '../src/services/api/authFiles';
|
||||
import type { AuthFilesResponse } from '../src/types/authFile';
|
||||
|
||||
const responseWithRawFiles = (files: Array<Record<string, unknown>>): AuthFilesResponse =>
|
||||
({ files }) as unknown as AuthFilesResponse;
|
||||
|
||||
describe('auth-files response normalization', () => {
|
||||
test('normalizes WRR weights while preserving zero and negative values', () => {
|
||||
const result = normalizeAuthFilesResponse(
|
||||
responseWithRawFiles([
|
||||
{ name: 'positive.json', weight: 5 },
|
||||
{ name: 'string.json', weight: '7' },
|
||||
{ name: 'zero.json', weight: 0 },
|
||||
{ name: 'negative.json', weight: -2 },
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.files.map((file) => file.weight)).toEqual([-2, 5, 7, 0]);
|
||||
});
|
||||
|
||||
test('omits missing, fractional, and unsafe WRR weights from the normalized field', () => {
|
||||
const result = normalizeAuthFilesResponse(
|
||||
responseWithRawFiles([
|
||||
{ name: 'missing.json' },
|
||||
{ name: 'fractional.json', weight: '1.5' },
|
||||
{ name: 'unsafe.json', weight: Number.MAX_SAFE_INTEGER + 1 },
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.files.map((file) => file.weight)).toEqual([undefined, undefined, undefined]);
|
||||
});
|
||||
|
||||
test('applies the same safe-integer normalization to priority', () => {
|
||||
const result = normalizeAuthFilesResponse(
|
||||
responseWithRawFiles([
|
||||
{ name: 'valid.json', priority: '-3' },
|
||||
{ name: 'invalid.json', priority: '3.5' },
|
||||
])
|
||||
);
|
||||
|
||||
expect(result.files.map((file) => file.priority)).toEqual([undefined, -3]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user