From b88eb4791843df6d6395f1080b5e9142d674a711 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Fri, 3 Apr 2026 01:33:11 +0800 Subject: [PATCH] perf(claude): replace signature dirty tracking --- src/pages/AiProvidersClaudeEditLayout.tsx | 96 +++++++++++++++++------ src/stores/useClaudeEditDraftStore.ts | 28 +++++-- 2 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/pages/AiProvidersClaudeEditLayout.tsx b/src/pages/AiProvidersClaudeEditLayout.tsx index c5cfef9..7025e0e 100644 --- a/src/pages/AiProvidersClaudeEditLayout.tsx +++ b/src/pages/AiProvidersClaudeEditLayout.tsx @@ -9,8 +9,10 @@ import type { ProviderKeyConfig } from '@/types'; import type { ModelInfo } from '@/utils/models'; import type { ModelEntry, ProviderFormState } from '@/components/providers/types'; import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers'; +import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare'; import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils'; import { modelsToEntries } from '@/components/ui/modelInputListUtils'; +import type { ClaudeEditBaseline } from '@/stores/useClaudeEditDraftStore'; type LocationState = { fromAiProviders?: boolean } | null; @@ -89,19 +91,28 @@ const normalizeCloakConfig = (cloak: ProviderFormState['cloak']) => { }; }; -const buildClaudeSignature = (form: ProviderFormState) => - JSON.stringify({ - apiKey: String(form.apiKey ?? '').trim(), - priority: - form.priority !== undefined && Number.isFinite(form.priority) ? Math.trunc(form.priority) : null, - prefix: String(form.prefix ?? '').trim(), - baseUrl: String(form.baseUrl ?? '').trim(), - proxyUrl: String(form.proxyUrl ?? '').trim(), - headers: normalizeHeaderEntries(form.headers), - models: normalizeClaudeModelEntries(form.modelEntries), - excludedModels: parseExcludedModels(form.excludedText ?? ''), - cloak: normalizeCloakConfig(form.cloak), - }); +const buildClaudeBaseline = (form: ProviderFormState): ClaudeEditBaseline => ({ + apiKey: String(form.apiKey ?? '').trim(), + priority: + form.priority !== undefined && Number.isFinite(form.priority) ? Math.trunc(form.priority) : null, + prefix: String(form.prefix ?? '').trim(), + baseUrl: String(form.baseUrl ?? '').trim(), + proxyUrl: String(form.proxyUrl ?? '').trim(), + headers: normalizeHeaderEntries(form.headers), + models: normalizeClaudeModelEntries(form.modelEntries), + excludedModels: parseExcludedModels(form.excludedText ?? ''), + cloak: normalizeCloakConfig(form.cloak), +}); + +const areCloakConfigsEqual = (left: ClaudeEditBaseline['cloak'], right: ClaudeEditBaseline['cloak']) => { + if (left === right) return true; + if (!left || !right) return false; + if (left.mode !== right.mode || left.strictMode !== right.strictMode) return false; + if (left.sensitiveWords === null || right.sensitiveWords === null) { + return left.sensitiveWords === right.sensitiveWords; + } + return areStringArraysEqual(left.sensitiveWords, right.sensitiveWords); +}; export function AiProvidersClaudeEditLayout() { const { t } = useTranslation(); @@ -137,7 +148,7 @@ export function AiProvidersClaudeEditLayout() { const acquireDraft = useClaudeEditDraftStore((state) => state.acquireDraft); const releaseDraft = useClaudeEditDraftStore((state) => state.releaseDraft); const initDraft = useClaudeEditDraftStore((state) => state.initDraft); - const setDraftBaselineSignature = useClaudeEditDraftStore((state) => state.setDraftBaselineSignature); + const setDraftBaseline = useClaudeEditDraftStore((state) => state.setDraftBaseline); const setDraftForm = useClaudeEditDraftStore((state) => state.setDraftForm); const setDraftTestModel = useClaudeEditDraftStore((state) => state.setDraftTestModel); const setDraftTestStatus = useClaudeEditDraftStore((state) => state.setDraftTestStatus); @@ -241,9 +252,9 @@ export function AiProvidersClaudeEditLayout() { excludedText: excludedModelsToText(initialData.excludedModels), }; const available = seededForm.modelEntries.map((entry) => entry.name.trim()).filter(Boolean); - const baselineSignature = buildClaudeSignature(seededForm); + const baseline = buildClaudeBaseline(seededForm); initDraft(draftKey, { - baselineSignature, + baseline, form: seededForm, testModel: available[0] || '', testStatus: 'idle', @@ -254,7 +265,7 @@ export function AiProvidersClaudeEditLayout() { const emptyForm = buildEmptyForm(); initDraft(draftKey, { - baselineSignature: buildClaudeSignature(emptyForm), + baseline: buildClaudeBaseline(emptyForm), form: emptyForm, testModel: '', testStatus: 'idle', @@ -263,9 +274,50 @@ export function AiProvidersClaudeEditLayout() { }, [draft?.initialized, draftKey, initDraft, initialData, loading]); const resolvedLoading = !draft?.initialized; - const currentSignature = useMemo(() => buildClaudeSignature(form), [form]); - const baselineSignature = draft?.baselineSignature ?? ''; - const isDirty = Boolean(draft?.initialized) && baselineSignature !== currentSignature; + const baseline = draft?.baseline ?? null; + const normalizedHeaders = useMemo(() => normalizeHeaderEntries(form.headers), [form.headers]); + const normalizedModels = useMemo( + () => normalizeClaudeModelEntries(form.modelEntries), + [form.modelEntries] + ); + const normalizedExcludedModels = useMemo( + () => parseExcludedModels(form.excludedText ?? ''), + [form.excludedText] + ); + const normalizedCloak = useMemo(() => normalizeCloakConfig(form.cloak), [form.cloak]); + const normalizedPriority = useMemo(() => { + return form.priority !== undefined && Number.isFinite(form.priority) + ? Math.trunc(form.priority) + : null; + }, [form.priority]); + const isHeadersDirty = useMemo(() => { + if (!baseline) return false; + return !areKeyValueEntriesEqual(baseline.headers, normalizedHeaders); + }, [baseline, normalizedHeaders]); + const isModelsDirty = useMemo(() => { + if (!baseline) return false; + return !areModelEntriesEqual(baseline.models, normalizedModels); + }, [baseline, normalizedModels]); + const isExcludedModelsDirty = useMemo(() => { + if (!baseline) return false; + return !areStringArraysEqual(baseline.excludedModels, normalizedExcludedModels); + }, [baseline, normalizedExcludedModels]); + const isCloakDirty = useMemo(() => { + if (!baseline) return false; + return !areCloakConfigsEqual(baseline.cloak, normalizedCloak); + }, [baseline, normalizedCloak]); + const isDirty = + Boolean(draft?.initialized) && + baseline !== null && + (baseline.apiKey !== form.apiKey.trim() || + baseline.priority !== normalizedPriority || + baseline.prefix !== String(form.prefix ?? '').trim() || + baseline.baseUrl !== String(form.baseUrl ?? '').trim() || + baseline.proxyUrl !== String(form.proxyUrl ?? '').trim() || + isHeadersDirty || + isModelsDirty || + isExcludedModelsDirty || + isCloakDirty); const editorRootPath = useMemo(() => { if (hasIndexParam) { return `/ai-providers/claude/${params.index ?? ''}`; @@ -384,7 +436,7 @@ export function AiProvidersClaudeEditLayout() { 'success' ); allowNextNavigation(); - setDraftBaselineSignature(draftKey, buildClaudeSignature(form)); + setDraftBaseline(draftKey, buildClaudeBaseline(form)); handleBack(); } catch (err: unknown) { showNotification(`${t('notification.update_failed')}: ${getErrorMessage(err)}`, 'error'); @@ -403,7 +455,7 @@ export function AiProvidersClaudeEditLayout() { invalidIndex, invalidIndexParam, resolvedLoading, - setDraftBaselineSignature, + setDraftBaseline, saving, showNotification, t, diff --git a/src/stores/useClaudeEditDraftStore.ts b/src/stores/useClaudeEditDraftStore.ts index f0bd7f2..d53a488 100644 --- a/src/stores/useClaudeEditDraftStore.ts +++ b/src/stores/useClaudeEditDraftStore.ts @@ -14,9 +14,27 @@ import type { ProviderFormState } from '@/components/providers/types'; export type ClaudeTestStatus = 'idle' | 'loading' | 'success' | 'error'; +export type ClaudeCloakBaseline = { + mode: string; + strictMode: boolean; + sensitiveWords: string[] | null; +} | null; + +export type ClaudeEditBaseline = { + apiKey: string; + priority: number | null; + prefix: string; + baseUrl: string; + proxyUrl: string; + headers: Array<{ key: string; value: string }>; + models: Array<{ name: string; alias: string }>; + excludedModels: string[]; + cloak: ClaudeCloakBaseline; +}; + type ClaudeEditDraft = { initialized: boolean; - baselineSignature: string; + baseline: ClaudeEditBaseline | null; form: ProviderFormState; testModel: string; testStatus: ClaudeTestStatus; @@ -33,7 +51,7 @@ interface ClaudeEditDraftState { key: string, draft: Omit ) => void; - setDraftBaselineSignature: (key: string, signature: string) => void; + setDraftBaseline: (key: string, baseline: ClaudeEditBaseline) => void; setDraftForm: ( key: string, action: SetStateAction @@ -64,7 +82,7 @@ const buildEmptyForm = (): ProviderFormState => ({ const buildEmptyDraft = (): ClaudeEditDraft => ({ initialized: false, - baselineSignature: '', + baseline: null, form: buildEmptyForm(), testModel: '', testStatus: 'idle', @@ -124,14 +142,14 @@ export const useClaudeEditDraftStore = create((set, get) = })); }, - setDraftBaselineSignature: (key, signature) => { + setDraftBaseline: (key, baseline) => { if (!key) return; set((state) => { const existing = state.drafts[key] ?? buildEmptyDraft(); return { drafts: { ...state.drafts, - [key]: { ...existing, initialized: true, baselineSignature: signature }, + [key]: { ...existing, initialized: true, baseline }, }, }; });