feat(auth-files): add WRR weight management and update priority handling

This commit is contained in:
Supra4E8C
2026-07-30 07:45:55 +08:00
parent 8faaa39534
commit 90971e8f6a
8 changed files with 103 additions and 14 deletions
@@ -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>
</>
)}
+1
View File
@@ -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",
+1
View File
@@ -283,6 +283,7 @@
"sort_az": "A-Z Имя",
"sort_priority": "Приоритет",
"priority_display": "Приоритет",
"weight_display": "Вес WRR",
"page_size_label": "На странице",
"view_mode_paged": "Постранично",
"view_mode_all": "Показать все",
+1
View File
@@ -284,6 +284,7 @@
"sort_az": "A-Z 名称",
"sort_priority": "优先级",
"priority_display": "优先级",
"weight_display": "WRR 权重",
"page_size_label": "单页数量",
"view_mode_paged": "按页显示",
"view_mode_all": "显示全部",
+1
View File
@@ -284,6 +284,7 @@
"sort_az": "A-Z 名稱",
"sort_priority": "優先順序",
"priority_display": "優先順序",
"weight_display": "WRR 權重",
"page_size_label": "單頁數量",
"view_mode_paged": "分頁顯示",
"view_mode_all": "顯示全部",
+9 -6
View File
@@ -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]);
});
});