fix: restore provider-specific fields on form echo via echoTransform (#17550)

This commit is contained in:
chanx
2026-07-30 11:22:06 +08:00
committed by GitHub
parent 9bb037271e
commit cb908d1979
5 changed files with 122 additions and 9 deletions

View File

@@ -160,14 +160,20 @@ export function useProviderBaseUrlOptions(providerName: string) {
* - Draft: empty form, with `base_url` pre-filled from the
* provider's `default` URL when available.
* - Saved: prefer `instanceDetails` (which carries api_key / base_url);
* normalise api_key into the bare key plus any nested credential
* fields (see {@link unwrapApiKey}).
* when `echoTransform` is supplied (provider-specific field mapping,
* e.g. Google Cloud's top-level `google_project_id` or XunFei Spark's
* nested `spark_api_password`), use it to reverse the corresponding
* `submitTransform` so the provider-specific credential fields
* pre-fill. Otherwise fall back to the generic `unwrapApiKey` path
* which lifts the bare `api_key` plus `API_KEY_NESTED_FIELDS`
* (`group_id` / `api_version` / `provider_order`).
*/
export function useProviderInitialValues(
instance: IProviderInstance,
instanceDetails: IProviderInstance | undefined,
isDraft: boolean,
baseUrlOptions: SelectOption[] | undefined,
echoTransform?: (instance: Record<string, any>) => Record<string, any>,
) {
return useMemo(() => {
const defaultBaseUrl = pickDefaultUrl(baseUrlOptions);
@@ -187,12 +193,19 @@ export function useProviderInitialValues(
const values: Record<string, any> = {
instance_name: merged.instance_name,
};
// api_key may come back as a JSON string, an already-parsed object,
// or a plain bare key (see `unwrapApiKey`). Normalise it so the
// api_key text field shows the bare key and the nested credential
// fields (MiniMax group_id, Azure api_version, OpenRouter
// provider_order) pre-fill their own form inputs.
if (merged.api_key) {
if (echoTransform) {
// Provider-specific echo: reverse the `submitTransform` mapping so
// credential fields that the backend persists either at the top
// level (Google Cloud, Tencent Cloud, Fish Audio) or nested inside
// `api_key` (XunFei Spark, Baidu YiYan, OpenDataLoader, PaddleOCR,
// MinerU) are restored into their own form inputs.
Object.assign(values, echoTransform(merged));
} else if (merged.api_key) {
// api_key may come back as a JSON string, an already-parsed object,
// or a plain bare key (see `unwrapApiKey`). Normalise it so the
// api_key text field shows the bare key and the nested credential
// fields (MiniMax group_id, Azure api_version, OpenRouter
// provider_order) pre-fill their own form inputs.
const { apiKey, nested } = unwrapApiKey(merged.api_key);
values.api_key = apiKey;
Object.assign(values, nested);
@@ -215,7 +228,7 @@ export function useProviderInitialValues(
}
}
return values;
}, [instance, instanceDetails, isDraft, baseUrlOptions]);
}, [instance, instanceDetails, isDraft, baseUrlOptions, echoTransform]);
}
// ---------------------------------------------------------------------------

View File

@@ -138,6 +138,7 @@ const GenericProviderInstanceCard = forwardRef<
instanceDetails,
isDraft,
baseUrlOptions,
providerConfig.echoTransform,
);
const { formFields, formDefaultValues } = useFormFields(
providerName,

View File

@@ -17,6 +17,7 @@
import { FormFieldType } from '@/components/dynamic-form';
import { LLMFactory } from '@/constants/llm';
import type { ProviderConfig } from '../types';
import { parseApiKeyAsObject } from './utils';
/**
* Factory configuration mapping table
@@ -180,6 +181,11 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
google_service_account_key: values.google_service_account_key,
model_info: [],
}),
echoTransform: (instance) => ({
google_project_id: instance.google_project_id ?? '',
google_region: instance.google_region ?? '',
google_service_account_key: instance.google_service_account_key ?? '',
}),
},
// ============ Tencent Cloud ============
@@ -231,6 +237,10 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
TencentCloud_sk: values.TencentCloud_sk,
model_info: [],
}),
echoTransform: (instance) => ({
TencentCloud_sid: instance.TencentCloud_sid ?? '',
TencentCloud_sk: instance.TencentCloud_sk ?? '',
}),
},
// ============ XunFei Spark ============
@@ -304,6 +314,15 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
},
model_info: [],
}),
echoTransform: (instance) => {
const obj = parseApiKeyAsObject(instance.api_key) ?? {};
return {
spark_api_password: obj.spark_api_password ?? '',
spark_app_id: obj.spark_app_id ?? '',
spark_api_secret: obj.spark_api_secret ?? '',
spark_api_key: obj.spark_api_key ?? '',
};
},
},
// ============ Baidu YiYan ============
@@ -355,6 +374,13 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
},
model_info: [],
}),
echoTransform: (instance) => {
const obj = parseApiKeyAsObject(instance.api_key) ?? {};
return {
yiyan_ak: obj.yiyan_ak ?? '',
yiyan_sk: obj.yiyan_sk ?? '',
};
},
},
// ============ Fish Audio ============
@@ -406,6 +432,10 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
fish_audio_refid: values.fish_audio_refid,
model_info: [],
}),
echoTransform: (instance) => ({
fish_audio_ak: instance.fish_audio_ak ?? '',
fish_audio_refid: instance.fish_audio_refid ?? '',
}),
},
// ============ OpenDataLoader ============
@@ -468,6 +498,13 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
model_info: [],
};
},
echoTransform: (instance) => {
const obj = parseApiKeyAsObject(instance.api_key) ?? {};
return {
opendataloader_apiserver: obj.opendataloader_apiserver ?? '',
opendataloader_api_key: obj.opendataloader_api_key ?? '',
};
},
},
// ============ PaddleOCR ============
@@ -547,6 +584,14 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
model_info: [],
};
},
echoTransform: (instance) => {
const obj = parseApiKeyAsObject(instance.api_key) ?? {};
return {
paddleocr_api_url: obj.paddleocr_api_url ?? '',
paddleocr_access_token: obj.paddleocr_access_token ?? '',
paddleocr_algorithm: obj.paddleocr_algorithm ?? 'PaddleOCR-VL',
};
},
},
// ============ MinerU ============
@@ -642,5 +687,19 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
model_info: [],
};
},
echoTransform: (instance) => {
const obj = parseApiKeyAsObject(instance.api_key) ?? {};
const rawDelete = obj.mineru_delete_output;
return {
mineru_apiserver: obj.mineru_apiserver ?? '',
mineru_output_dir: obj.mineru_output_dir ?? '',
mineru_backend: obj.mineru_backend ?? 'pipeline',
mineru_server_url: obj.mineru_server_url ?? '',
mineru_delete_output:
rawDelete === undefined
? true
: rawDelete === '1' || rawDelete === true,
};
},
},
};

View File

@@ -41,6 +41,33 @@ export function applyChatToImage2Text(
return arr;
}
/**
* Parse a backend `api_key` value (which may be a JSON string, an
* already-parsed object, or a bare string) into the underlying object.
* Used by `echoTransform` implementations to lift provider-specific
* credential fields that the backend persists nested inside `api_key`
* (e.g. XunFei Spark's `spark_api_password`, PaddleOCR's nested config,
* MinerU's full config bundle).
*
* Returns `undefined` for bare strings or unparseable input so callers
* can fall back to empty values via `??`.
*/
export function parseApiKeyAsObject(
raw: unknown,
): Record<string, any> | undefined {
let obj: any = raw;
if (typeof raw === 'string') {
const trimmed = raw.trim();
if (!trimmed.startsWith('{')) return undefined;
try {
obj = JSON.parse(trimmed);
} catch {
return undefined;
}
}
return obj && typeof obj === 'object' ? obj : undefined;
}
/**
* Build the IModelInfo[] payload for verify/submit from the form values.
*

View File

@@ -132,6 +132,19 @@ export interface ProviderConfig {
* `modelInfo` is assembled from `values` by the transform itself (same rules as verifyTransform).
*/
submitTransform?: (values: Record<string, any>) => Record<string, any>;
/**
* Inverse of `submitTransform`: maps the raw merged backend instance
* (as returned by `showProviderInstance`) back into form values so the
* form pre-fills when editing a saved instance. Returns ONLY the
* provider-specific credential fields (e.g. `google_project_id`,
* `spark_api_password`); the hook layers `instance_name` and
* `base_url` fallback on top.
*
* When absent, the generic echo path runs: `unwrapApiKey` lifts the
* bare `api_key` plus `API_KEY_NESTED_FIELDS` (`group_id` /
* `api_version` / `provider_order`).
*/
echoTransform?: (instance: Record<string, any>) => Record<string, any>;
/**
* Optional link at the bottom of the modal
* e.g. the official documentation link for Ollama-family providers