feat: add site headers editor and scheduled log cleanup
This commit is contained in:
@@ -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" 重复了',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user