mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-14 08:58:27 +08:00
fix(setting-model): fix instance display and api_key loss after auto-save (#16853)
This commit is contained in:
@@ -281,6 +281,19 @@ export const useAddProviderInstance = () => {
|
||||
true,
|
||||
);
|
||||
if (data.code === 0 && !params.verify) {
|
||||
// Invalidate `addedProviders` so `has_instance` flips to `true`
|
||||
// for providers that just gained their first instance. Without
|
||||
// this, the parent page keeps `providerQueryName === ''` (the
|
||||
// `has_instance` gate in index.tsx) and the `providerInstances`
|
||||
// query stays disabled, so the newly-saved instance never
|
||||
// appears. `exact: true` avoids cascading into every
|
||||
// providerInstances / instanceModels query (they share the
|
||||
// `['AddedProviders', ...]` prefix) - the dedicated invalidation
|
||||
// below handles those.
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: LlmKeys.addedProviders(),
|
||||
exact: true,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: LlmKeys.providerInstances(params.llm_factory),
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useTranslate } from '@/hooks/common-hooks';
|
||||
import {
|
||||
LlmKeys,
|
||||
useAddProviderInstance,
|
||||
useFetchAddedProviders,
|
||||
useFetchProviderInstances,
|
||||
} from '@/hooks/use-llm-request';
|
||||
import { IProviderInstance } from '@/interfaces/database/llm';
|
||||
@@ -61,9 +62,13 @@ const SettingModelV2: FC = () => {
|
||||
// for the current selection. Reset on every selection change.
|
||||
const cancelledRef = useRef(false);
|
||||
|
||||
// Always re-fetch when the selection changes. Passing an empty string
|
||||
// disables the query.
|
||||
const providerQueryName = selection === 'default' ? '' : selection;
|
||||
const { data: addedProviders } = useFetchAddedProviders();
|
||||
const providerQueryName = useMemo(() => {
|
||||
if (selection === 'default') return '';
|
||||
return addedProviders.some((p) => p.name === selection && p.has_instance)
|
||||
? selection
|
||||
: '';
|
||||
}, [selection, addedProviders]);
|
||||
const { data: instances, loading: instancesLoading } =
|
||||
useFetchProviderInstances(providerQueryName);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -63,7 +63,7 @@ export function SavedModeCard({
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="flex items-center gap-1 w-full mb-5">
|
||||
<div
|
||||
className="group w-[calc(100%-40px)] flex items-center flex-1 gap-2 px-2 py-1 cursor-pointer bg-bg-input rounded-md"
|
||||
className="group w-[calc(100%-40px)] flex items-center flex-1 gap-2 px-2 py-1 cursor-pointer bg-bg-card rounded-md"
|
||||
data-testid="instance-name-row"
|
||||
>
|
||||
<Button
|
||||
@@ -130,6 +130,7 @@ export function SavedModeCard({
|
||||
instance={instance}
|
||||
hideActions={false}
|
||||
hideIfEmpty={false}
|
||||
instanceDetailsLoaded={instanceDetailsLoaded}
|
||||
getFormValues={() => formRef.current?.getValues?.() ?? {}}
|
||||
onBlurSuppressChange={(s) => {
|
||||
blurSuppressRef.current = s;
|
||||
|
||||
@@ -655,6 +655,7 @@ export function useSavedAutoSave({
|
||||
if (isDraft) return;
|
||||
const resolvedId = instanceDetails?.id || instanceId;
|
||||
if (!resolvedId) return;
|
||||
if (hasSyncedInstanceRef.current) return;
|
||||
// Match the api_key shape performAutoSave produces (extra credential
|
||||
// fields nested inside api_key) so the first blur after mount
|
||||
// doesn't see a signature diff and fire a redundant save. model_info
|
||||
@@ -802,12 +803,15 @@ export function useFormFields(
|
||||
() => fields.filter((f) => f.name !== 'instance_name'),
|
||||
[fields],
|
||||
);
|
||||
|
||||
const defaultValuesKey = JSON.stringify(defaultValues);
|
||||
const formDefaultValues = useMemo(() => {
|
||||
const { instance_name: _ignored, ...rest } = (defaultValues ??
|
||||
{}) as Record<string, any>;
|
||||
void _ignored;
|
||||
return rest;
|
||||
}, [defaultValues]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValuesKey]);
|
||||
|
||||
return { formFields, formDefaultValues };
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LLMFactory } from '@/constants/llm';
|
||||
import {
|
||||
useAddInstanceModel,
|
||||
useDeleteInstanceModels,
|
||||
@@ -122,6 +123,7 @@ interface UseModelsCatalogArgs {
|
||||
isDraftInstance: boolean;
|
||||
resolveCreds: () => ResolvedCreds;
|
||||
instanceModels: IInstanceModel[] | undefined;
|
||||
instanceDetailsLoaded: boolean;
|
||||
}
|
||||
|
||||
export function useModelsCatalog({
|
||||
@@ -131,6 +133,7 @@ export function useModelsCatalog({
|
||||
isDraftInstance,
|
||||
resolveCreds,
|
||||
instanceModels,
|
||||
instanceDetailsLoaded,
|
||||
}: UseModelsCatalogArgs) {
|
||||
const { listProviderModels } = useListProviderModels();
|
||||
const [catalog, setCatalog] = useState<IProviderModelItem[]>([]);
|
||||
@@ -141,9 +144,13 @@ export function useModelsCatalog({
|
||||
// The result is merged into `catalog`; the displayed list then becomes
|
||||
// the union of catalog + instance models.
|
||||
const handleListModels = async () => {
|
||||
const { apiKey, baseUrl } = resolveCreds();
|
||||
if (providerName === LLMFactory.VolcEngine && !apiKey) {
|
||||
setHasFetched(true);
|
||||
return;
|
||||
}
|
||||
setManualListLoading(true);
|
||||
try {
|
||||
const { apiKey, baseUrl } = resolveCreds();
|
||||
const ret = await listProviderModels({
|
||||
provider_name: providerName,
|
||||
api_key: apiKey,
|
||||
@@ -163,16 +170,21 @@ export function useModelsCatalog({
|
||||
// Auto-fetch the provider's available-models catalog when this section
|
||||
// mounts (effectively "when the card is expanded"). Skipped for draft
|
||||
// instances and catalog-preview-only hosts.
|
||||
|
||||
const requiresApiKey = providerName === LLMFactory.VolcEngine;
|
||||
const credsReady = !requiresApiKey || instanceDetailsLoaded;
|
||||
|
||||
const hasAutoFetchedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasAutoFetchedRef.current) return;
|
||||
if (hideActions) return;
|
||||
if (!providerName) return;
|
||||
if (isDraftInstance) return;
|
||||
if (!credsReady) return;
|
||||
hasAutoFetchedRef.current = true;
|
||||
handleListModels();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [providerName, instanceName, hideActions]);
|
||||
}, [providerName, instanceName, hideActions, credsReady]);
|
||||
|
||||
// Mark `hasFetched` true once the per-instance query resolves — even if
|
||||
// it returned an empty array — so `hideIfEmpty` can safely take effect.
|
||||
|
||||
@@ -48,6 +48,7 @@ export function ModelsSection(props: ModelsSectionProps) {
|
||||
instance,
|
||||
hideActions = false,
|
||||
hideIfEmpty = false,
|
||||
instanceDetailsLoaded = false,
|
||||
getFormValues,
|
||||
onBlurSuppressChange,
|
||||
onInstanceModelsChange,
|
||||
@@ -80,6 +81,7 @@ export function ModelsSection(props: ModelsSectionProps) {
|
||||
isDraftInstance,
|
||||
resolveCreds,
|
||||
instanceModels,
|
||||
instanceDetailsLoaded,
|
||||
});
|
||||
|
||||
// 4. Derived union list (instance ∪ catalog) + push to host.
|
||||
|
||||
@@ -30,6 +30,15 @@ export interface ModelsSectionProps {
|
||||
* draft state where there is no backend instance to query yet).
|
||||
*/
|
||||
hideActions?: boolean;
|
||||
/**
|
||||
* True once the lazy-loaded instance details (which carry `api_key` /
|
||||
* `base_url` - the list endpoint omits them) have resolved. Providers
|
||||
* whose upstream model-list endpoint requires an api_key (e.g.
|
||||
* VolcEngine) use this to defer the auto-fetch until the credential
|
||||
* is available in the host form, instead of firing a request that is
|
||||
* guaranteed to fail and then refusing to retry.
|
||||
*/
|
||||
instanceDetailsLoaded?: boolean;
|
||||
/**
|
||||
* If true, the section renders nothing once the first catalog fetch
|
||||
* completes and no models are available. Used by draft instances to
|
||||
|
||||
@@ -62,9 +62,9 @@ const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [verifyResult, setVerifyResult] = useState<VerifyResult | null>(null);
|
||||
const contextForm = useFormContext();
|
||||
const form = formRef?.current ?? contextForm;
|
||||
|
||||
const onHandleVerify = useCallback(async () => {
|
||||
const form = formRef?.current ?? contextForm;
|
||||
const formValid = await form?.trigger();
|
||||
if (!formValid) {
|
||||
return;
|
||||
@@ -99,7 +99,7 @@ const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
} finally {
|
||||
// setVerifyLoading(false);
|
||||
}
|
||||
}, [form, onVerify, params, verifyCallback]);
|
||||
}, [formRef, contextForm, onVerify, params, verifyCallback]);
|
||||
const handleVerify = useCallback(async () => {
|
||||
setVerifyResult({
|
||||
isValid: null,
|
||||
@@ -157,13 +157,11 @@ const VerifyButton: React.FC<IVerifyButton> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{verifyResult && verifyResult.isValid !== null && (
|
||||
{verifyResult && verifyResult.isValid === false && verifyResult.logs && (
|
||||
<div className="space-y-2">
|
||||
{verifyResult.logs && (
|
||||
<div className="w-full whitespace-pre-line text-wrap bg-bg-card rounded-lg h-fit max-h-[250px] overflow-y-auto scrollbar-auto p-2.5">
|
||||
{replaceText(verifyResult.logs)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full whitespace-pre-line text-wrap bg-bg-card rounded-lg h-fit max-h-[250px] overflow-y-auto scrollbar-auto p-2.5">
|
||||
{replaceText(verifyResult.logs)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const GenericApiKeyConfig: ProviderConfig = {
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
placeholder: 'apiKeyMessage',
|
||||
validation: { message: 'apiKeyMessage' },
|
||||
|
||||
@@ -216,7 +216,7 @@ function buildLocalConfig(
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
placeholder: 'apiKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
placeholder: 'apiKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
@@ -99,7 +99,7 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
{
|
||||
name: 'api_key',
|
||||
label: 'addArkApiKey',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
placeholder: 'ArkApiKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
@@ -153,7 +153,7 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
{
|
||||
name: 'google_service_account_key',
|
||||
label: 'addGoogleServiceAccountKey',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
placeholder: 'GoogleServiceAccountKeyMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
@@ -246,7 +246,7 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
{
|
||||
name: 'spark_api_password',
|
||||
label: 'addSparkAPIPassword',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
placeholder: 'SparkAPIPasswordMessage',
|
||||
shouldRender: 'hideWhenInstanceExists',
|
||||
@@ -429,7 +429,7 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
{
|
||||
name: 'opendataloader_api_key',
|
||||
label: 'apiKey',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
placeholder: 'apiKeyPlaceholder',
|
||||
},
|
||||
@@ -491,7 +491,7 @@ export const ProviderConfigMap: Record<string, ProviderConfig> = {
|
||||
{
|
||||
name: 'paddleocr_access_token',
|
||||
label: 'paddleocrAccessToken',
|
||||
type: FormFieldType.Text,
|
||||
type: FormFieldType.Password,
|
||||
required: false,
|
||||
placeholder: 'paddleocrAccessTokenPlaceholder',
|
||||
validation: { message: 'paddleocrAccessTokenMessage' },
|
||||
|
||||
Reference in New Issue
Block a user