From cb908d1979c038018a16764177b4170c4166fa88 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Thu, 30 Jul 2026 11:22:06 +0800 Subject: [PATCH] fix: restore provider-specific fields on form echo via echoTransform (#17550) --- .../setting-model/instance-card/hooks.tsx | 31 +++++++--- .../instance-card/provider-instance-card.tsx | 1 + .../field-config/provider-config-map.ts | 59 +++++++++++++++++++ .../provider-schema/field-config/utils.ts | 27 +++++++++ .../setting-model/provider-schema/types.ts | 13 ++++ 5 files changed, 122 insertions(+), 9 deletions(-) diff --git a/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx b/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx index 71ff157192..00f7c2f4a4 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx @@ -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) => Record, ) { return useMemo(() => { const defaultBaseUrl = pickDefaultUrl(baseUrlOptions); @@ -187,12 +193,19 @@ export function useProviderInitialValues( const values: Record = { 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]); } // --------------------------------------------------------------------------- diff --git a/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx b/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx index ab8a145cc3..5f839ba7e7 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx @@ -138,6 +138,7 @@ const GenericProviderInstanceCard = forwardRef< instanceDetails, isDraft, baseUrlOptions, + providerConfig.echoTransform, ); const { formFields, formDefaultValues } = useFormFields( providerName, diff --git a/web/src/pages/user-setting/setting-model/provider-schema/field-config/provider-config-map.ts b/web/src/pages/user-setting/setting-model/provider-schema/field-config/provider-config-map.ts index b3b956bf59..31731da9d9 100644 --- a/web/src/pages/user-setting/setting-model/provider-schema/field-config/provider-config-map.ts +++ b/web/src/pages/user-setting/setting-model/provider-schema/field-config/provider-config-map.ts @@ -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 = { 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 = { 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 = { }, 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 = { }, 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 = { 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 = { 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 = { 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 = { 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, + }; + }, }, }; diff --git a/web/src/pages/user-setting/setting-model/provider-schema/field-config/utils.ts b/web/src/pages/user-setting/setting-model/provider-schema/field-config/utils.ts index 0d4a654b9e..2f50b5912a 100644 --- a/web/src/pages/user-setting/setting-model/provider-schema/field-config/utils.ts +++ b/web/src/pages/user-setting/setting-model/provider-schema/field-config/utils.ts @@ -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 | 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. * diff --git a/web/src/pages/user-setting/setting-model/provider-schema/types.ts b/web/src/pages/user-setting/setting-model/provider-schema/types.ts index 5aeb49db03..66bcbd485e 100644 --- a/web/src/pages/user-setting/setting-model/provider-schema/types.ts +++ b/web/src/pages/user-setting/setting-model/provider-schema/types.ts @@ -132,6 +132,19 @@ export interface ProviderConfig { * `modelInfo` is assembled from `values` by the transform itself (same rules as verifyTransform). */ submitTransform?: (values: Record) => Record; + /** + * 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) => Record; /** * Optional link at the bottom of the modal * e.g. the official documentation link for Ollama-family providers