fix(web): identify provider instances by id and skip clean cards on save (#17424)

This commit is contained in:
chanx
2026-07-28 09:49:57 +08:00
committed by GitHub
parent 619b58faef
commit 326893811a
9 changed files with 44 additions and 44 deletions

View File

@@ -57,13 +57,8 @@ export const LlmKeys = {
[LLMApiAction.AllModels, modelType] as const,
providerInstances: (providerName: string) =>
[LLMApiAction.AddedProviders, providerName, 'instances'] as const,
providerInstance: (providerName: string, instanceName: string) =>
[
LLMApiAction.AddedProviders,
providerName,
instanceName,
'instance',
] as const,
providerInstance: (providerName: string, id: string) =>
[LLMApiAction.AddedProviders, providerName, id, 'instance'] as const,
instanceModels: (providerName: string, instanceName: string) =>
[
LLMApiAction.AddedProviders,
@@ -165,18 +160,15 @@ export const useFetchProviderInstances = (providerName: string) => {
return { data, loading };
};
export const useFetchProviderInstance = (
providerName: string,
instanceName: string,
) => {
export const useFetchProviderInstance = (providerName: string, id: string) => {
return useQuery<IProviderInstance>({
queryKey: LlmKeys.providerInstance(providerName, instanceName),
queryKey: LlmKeys.providerInstance(providerName, id),
initialData: undefined as unknown as IProviderInstance,
gcTime: 0,
enabled: false,
queryFn: async () => {
const { data } = await llmService.showProviderInstance(
{ provider_name: providerName, instance_name: instanceName },
{ provider_name: providerName, id },
true,
);
return (data?.data ?? {}) as IProviderInstance;
@@ -584,10 +576,7 @@ export const useUpdateProviderInstance = () => {
queryKey: LlmKeys.providerInstances(params.provider_name),
});
queryClient.invalidateQueries({
queryKey: LlmKeys.providerInstance(
params.provider_name,
params.instance_name,
),
queryKey: LlmKeys.providerInstance(params.provider_name, params.id),
});
queryClient.invalidateQueries({
queryKey: LlmKeys.instanceModels(

View File

@@ -54,7 +54,7 @@ export interface IDeleteProviderInstanceRequestBody {
export interface IShowProviderInstanceRequestParams {
provider_name: string;
instance_name: string;
id: string;
}
export interface IAddInstanceModelRequestBody {
@@ -104,7 +104,7 @@ export interface IDeleteInstanceModelsRequestBody {
export interface IUpdateProviderInstanceRequestBody {
provider_name: string;
instance_name: string;
id?: string;
id: string;
/**
* Either a plain API-key string, or — for providers that need an
* extra credential such as MiniMax's `group_id` — an object bundling

View File

@@ -176,20 +176,18 @@ const SettingModelV2: FC = () => {
);
if (refs.length === 0) return;
// 1. Validate every card up front. Block all saves if any is invalid.
const dirty = refs
.map((r) => ({ ref: r, payload: r.getSavePayload() }))
.filter((e) => e.payload !== null);
if (dirty.length === 0) return;
const validations = await Promise.all(
refs.map(async (r) => ({ ref: r, valid: await r.validate() })),
dirty.map(async (e) => ({ ...e, valid: await e.ref.validate() })),
);
if (validations.some((v) => !v.valid)) {
return;
}
// 2. Collect dirty payloads (null = nothing to save for this card).
const entries = validations
.map((v) => ({ ref: v.ref, payload: v.ref.getSavePayload() }))
.filter((e) => e.payload !== null);
if (entries.length === 0) return;
setSaving(true);
// Pin the auto-show guard so a draft isn't re-spawned while the
// providerInstances refetch is in flight (during that window both
@@ -200,7 +198,7 @@ const SettingModelV2: FC = () => {
// 3. Dispatch one API call per dirty card. Sequential so any
// backend error stops the batch and the user can retry the
// remaining cards after fixing the issue.
for (const { ref, payload } of entries) {
for (const { ref, payload } of validations) {
if (!payload) continue;
if (payload.apiKind === 'add') {
const ret = await addProviderInstance(payload.payload as any);

View File

@@ -167,7 +167,7 @@ export const BedrockInstanceCard = forwardRef<
const { data: instanceDetails, refetch: refetchInstanceDetails } =
useFetchProviderInstance(
isDraft ? '' : providerName,
isDraft ? '' : instance.instance_name,
isDraft ? '' : instance.id,
);
// Lazily fetch full instance details only when the card is open.
@@ -365,6 +365,12 @@ export const BedrockInstanceCard = forwardRef<
apiKind: 'add' as const,
};
}
// Skip cards the user hasn't actually edited. Collapsed cards
// never fetch `instanceDetails`, so their form is still on empty
// defaults - the signature check below would false-positive on
// defaulting differences. `form.formState.isDirty` tracks real
// user interaction.
if (!form.formState.isDirty) return null;
const values = form.getValues();
const payload = buildPayload(values, instance.instance_name);
const finalPayload = {

View File

@@ -232,21 +232,21 @@ export function useProviderInitialValues(
*/
export function useLazyInstanceDetails(
providerName: string,
instanceName: string,
instanceId: string,
isDraft: boolean,
open: boolean,
) {
const { data: instanceDetails, refetch: refetchInstanceDetails } =
useFetchProviderInstance(
isDraft ? '' : providerName,
isDraft ? '' : instanceName,
isDraft ? '' : instanceId,
);
useEffect(() => {
if (!isDraft && open && providerName && instanceName) {
if (!isDraft && open && providerName && instanceId) {
refetchInstanceDetails();
}
}, [isDraft, open, providerName, instanceName, refetchInstanceDetails]);
}, [isDraft, open, providerName, instanceId, refetchInstanceDetails]);
return { instanceDetails, refetchInstanceDetails };
}
@@ -590,9 +590,14 @@ export function useInstanceSaveState({
// `getSavePayload()` is the imperative entry point the parent calls
// when the user clicks the top Save button. For drafts it always
// returns a payload (provided the name is non-empty); for saved
// cards it returns `null` when the current signature matches the
// baseline, so the parent skips the no-op PUT.
// cards it returns `null` when the card hasn't been touched by the
// user, so the parent skips both validation and the no-op PUT.
const getSavePayload = useCallback((): InstanceSavePayload | null => {
if (!isDraft) {
const formDirty = formRef.current?.isDirty() ?? false;
const renamed = editedNameRef.current !== instanceName;
if (!formDirty && !renamed) return null;
}
const payload = buildPayload();
if (!payload) return null;
if (!isDraft) {
@@ -610,7 +615,7 @@ export function useInstanceSaveState({
// `IUpdateProviderInstanceRequestBody`).
apiKind: isDraft ? 'add' : 'update',
};
}, [buildPayload, isDraft, instanceName]);
}, [buildPayload, isDraft, instanceName, formRef]);
// After a successful save the parent calls `markSaved()` so the
// baseline catches up to the just-persisted values. Without this,

View File

@@ -592,6 +592,7 @@ export function useModelMutations({
const { apiKey, baseUrl } = resolveCreds();
await updateProviderInstance({
provider_name: providerName,
id: instance!.id,
instance_name: instanceName,
api_key: apiKey,
base_url: baseUrl,

View File

@@ -127,7 +127,7 @@ const GenericProviderInstanceCard = forwardRef<
const { baseUrlOptions } = useProviderBaseUrlOptions(providerName);
const { instanceDetails } = useLazyInstanceDetails(
providerName,
instance.instance_name,
instance.id,
isDraft,
open,
);

View File

@@ -169,7 +169,7 @@ export const SoMarkInstanceCard = forwardRef<
const { data: instanceDetails, refetch: refetchInstanceDetails } =
useFetchProviderInstance(
isDraft ? '' : providerName,
isDraft ? '' : instance.instance_name,
isDraft ? '' : instance.id,
);
// Lazily fetch full instance details only when the card is open.
@@ -398,6 +398,7 @@ export const SoMarkInstanceCard = forwardRef<
};
}
const values = form.getValues();
if (!form.formState.isDirty) return null;
const payload = buildPayload(values, instance.instance_name);
const finalPayload = {
...payload,

View File

@@ -49,11 +49,11 @@ export default {
`${restAPIv1}/providers/${provider_name}/instances/${instance_name}/models`,
showProviderInstance: ({
provider_name,
instance_name,
id,
}: {
provider_name: string;
instance_name: string;
}) => `${restAPIv1}/providers/${provider_name}/instances/${instance_name}`,
id: string;
}) => `${restAPIv1}/providers/${provider_name}/instances/${id}`,
addInstanceModel: ({
provider_name,
instance_name,
@@ -74,11 +74,11 @@ export default {
`${restAPIv1}/providers/${provider_name}/instances`,
updateProviderInstance: ({
provider_name,
instance_name,
id,
}: {
provider_name: string;
instance_name: string;
}) => `${restAPIv1}/providers/${provider_name}/instances/${instance_name}`,
id: string;
}) => `${restAPIv1}/providers/${provider_name}/instances/${id}`,
updateModelStatus: ({
provider_name,
instance_name,