feat: add site headers editor and scheduled log cleanup

This commit is contained in:
bnvnvnv
2026-03-12 08:12:29 +00:00
parent 1da3608d42
commit b6da71b1d6
34 changed files with 1572 additions and 210 deletions
+56 -11
View File
@@ -1,5 +1,11 @@
import { describe, expect, it } from 'vitest';
import { buildSiteSaveAction, emptySiteForm, siteFormFromSite } from './sitesEditor.js';
import {
buildSiteSaveAction,
emptySiteCustomHeader,
emptySiteForm,
serializeSiteCustomHeaders,
siteFormFromSite,
} from './sitesEditor.js';
describe('buildSiteSaveAction', () => {
it('returns add action in add mode', () => {
@@ -10,8 +16,9 @@ describe('buildSiteSaveAction', () => {
url: 'https://a.example.com/',
externalCheckinUrl: 'https://checkin.a.example.com',
platform: 'new-api',
proxyUrl: 'http://127.0.0.1:7890',
globalWeight: '1.2',
customHeaders: '{"x-site-token":"alpha"}',
useSystemProxy: false,
globalWeight: 1.2,
},
);
@@ -22,8 +29,9 @@ describe('buildSiteSaveAction', () => {
url: 'https://a.example.com/',
externalCheckinUrl: 'https://checkin.a.example.com',
platform: 'new-api',
proxyUrl: 'http://127.0.0.1:7890',
globalWeight: '1.2',
customHeaders: '{"x-site-token":"alpha"}',
useSystemProxy: false,
globalWeight: 1.2,
},
});
});
@@ -36,8 +44,9 @@ describe('buildSiteSaveAction', () => {
url: 'https://b.example.com',
externalCheckinUrl: '',
platform: 'one-api',
proxyUrl: '',
globalWeight: '0.8',
useSystemProxy: true,
customHeaders: '',
globalWeight: 0.8,
},
);
@@ -49,8 +58,9 @@ describe('buildSiteSaveAction', () => {
url: 'https://b.example.com',
externalCheckinUrl: '',
platform: 'one-api',
proxyUrl: '',
globalWeight: '0.8',
useSystemProxy: true,
customHeaders: '',
globalWeight: 0.8,
},
});
});
@@ -64,8 +74,9 @@ describe('buildSiteSaveAction', () => {
url: 'https://c.example.com',
externalCheckinUrl: '',
platform: '',
proxyUrl: '',
globalWeight: '1',
useSystemProxy: false,
customHeaders: '',
globalWeight: 1,
},
),
).toThrow('editingSiteId is required in edit mode');
@@ -73,14 +84,48 @@ describe('buildSiteSaveAction', () => {
it('does not expose deprecated apiKey in site editor state', () => {
expect(emptySiteForm()).not.toHaveProperty('apiKey');
expect(emptySiteForm().customHeaders).toEqual([emptySiteCustomHeader()]);
expect(siteFormFromSite({
name: 'site-d',
url: 'https://d.example.com',
externalCheckinUrl: null,
platform: 'new-api',
proxyUrl: null,
customHeaders: '{"x-site-token":"alpha"}',
globalWeight: 1,
apiKey: 'sk-legacy-site-key',
})).not.toHaveProperty('apiKey');
});
it('parses custom headers json into key value rows', () => {
expect(siteFormFromSite({
name: 'site-e',
customHeaders: '{"x-site-token":"alpha","cf-access-client-id":"beta"}',
}).customHeaders).toEqual([
{ key: 'x-site-token', value: 'alpha' },
{ key: 'cf-access-client-id', value: 'beta' },
]);
});
it('serializes key value rows into json', () => {
expect(serializeSiteCustomHeaders([
{ key: 'x-site-token', value: 'alpha' },
{ key: 'cf-access-client-id', value: 'beta' },
emptySiteCustomHeader(),
])).toEqual({
valid: true,
customHeaders: '{"x-site-token":"alpha","cf-access-client-id":"beta"}',
});
});
it('rejects duplicate custom header names case-insensitively', () => {
expect(serializeSiteCustomHeaders([
{ key: 'Authorization', value: 'Bearer a' },
{ key: 'authorization', value: 'Bearer b' },
])).toEqual({
valid: false,
customHeaders: '',
error: '请求头 "authorization" 重复了',
});
});
});
+96 -7
View File
@@ -1,9 +1,15 @@
export type SiteCustomHeaderField = {
key: string;
value: string;
};
export type SiteForm = {
name: string;
url: string;
externalCheckinUrl: string;
platform: string;
useSystemProxy: boolean;
customHeaders: SiteCustomHeaderField[];
globalWeight: string;
};
@@ -11,17 +17,69 @@ export type SiteEditorState =
| { mode: 'add' }
| { mode: 'edit'; editingSiteId: number };
type SiteSaveAction =
| { kind: 'add'; payload: SiteForm }
| { kind: 'update'; id: number; payload: SiteForm };
export type SiteSavePayload = {
name: string;
url: string;
externalCheckinUrl: string;
platform: string;
useSystemProxy: boolean;
customHeaders: string;
globalWeight: number;
};
export function emptySiteForm(): SiteForm {
return { name: '', url: '', externalCheckinUrl: '', platform: '', useSystemProxy: false, globalWeight: '1' };
type SiteSaveAction =
| { kind: 'add'; payload: SiteSavePayload }
| { kind: 'update'; id: number; payload: SiteSavePayload };
export function emptySiteCustomHeader(): SiteCustomHeaderField {
return { key: '', value: '' };
}
export function siteFormFromSite(site: Partial<SiteForm> & {
function ensureSiteCustomHeaderRows(rows: SiteCustomHeaderField[]): SiteCustomHeaderField[] {
return rows.length > 0 ? rows : [emptySiteCustomHeader()];
}
export function emptySiteForm(): SiteForm {
return {
name: '',
url: '',
externalCheckinUrl: '',
platform: '',
useSystemProxy: false,
customHeaders: [emptySiteCustomHeader()],
globalWeight: '1',
};
}
function parseCustomHeadersForEditor(raw: unknown): SiteCustomHeaderField[] {
if (typeof raw !== 'string') {
return ensureSiteCustomHeaderRows([]);
}
const trimmed = raw.trim();
if (!trimmed) {
return ensureSiteCustomHeaderRows([]);
}
try {
const parsed = JSON.parse(trimmed);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return ensureSiteCustomHeaderRows([]);
}
return ensureSiteCustomHeaderRows(
Object.entries(parsed as Record<string, unknown>).map(([key, value]) => ({
key,
value: typeof value === 'string' ? value : String(value ?? ''),
})),
);
} catch {
return ensureSiteCustomHeaderRows([]);
}
}
export function siteFormFromSite(site: Partial<Omit<SiteForm, 'customHeaders' | 'globalWeight' | 'externalCheckinUrl' | 'useSystemProxy'>> & {
externalCheckinUrl?: string | null;
useSystemProxy?: boolean | null;
customHeaders?: string | null;
globalWeight?: number | string | null;
}): SiteForm {
const globalWeightRaw = Number(site.globalWeight);
@@ -32,11 +90,42 @@ export function siteFormFromSite(site: Partial<SiteForm> & {
externalCheckinUrl: site.externalCheckinUrl ?? '',
platform: site.platform ?? '',
useSystemProxy: !!site.useSystemProxy,
customHeaders: parseCustomHeadersForEditor(site.customHeaders),
globalWeight,
};
}
export function buildSiteSaveAction(editor: SiteEditorState, form: SiteForm): SiteSaveAction {
export function serializeSiteCustomHeaders(fields: SiteCustomHeaderField[]): {
valid: boolean;
customHeaders: string;
error?: string;
} {
const headers: Record<string, string> = {};
const seen = new Set<string>();
for (const field of fields) {
const key = field.key.trim();
const value = field.value;
const hasAnyInput = key.length > 0 || value.trim().length > 0;
if (!hasAnyInput) continue;
if (!key) {
return { valid: false, customHeaders: '', error: '请求头名称不能为空' };
}
const normalizedKey = key.toLowerCase();
if (seen.has(normalizedKey)) {
return { valid: false, customHeaders: '', error: `请求头 "${key}" 重复了` };
}
seen.add(normalizedKey);
headers[key] = value;
}
return {
valid: true,
customHeaders: Object.keys(headers).length > 0 ? JSON.stringify(headers) : '',
};
}
export function buildSiteSaveAction(editor: SiteEditorState, form: SiteSavePayload): SiteSaveAction {
if (editor.mode === 'edit') {
if (!Number.isFinite(editor.editingSiteId)) {
throw new Error('editingSiteId is required in edit mode');