feat: add /api/v1/language endpoint for Go/Python runtime detection (#16952)

Both backends serve GET /api/v1/language. Frontend calls it once and
caches. By this way, front end can know the backend is go or python and
thus can determine which part of logic to load.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jack
2026-07-16 15:13:43 +08:00
committed by GitHub
parent 92ae4f6769
commit fd5487e064
8 changed files with 111 additions and 1 deletions

View File

@@ -62,6 +62,13 @@ def version():
return get_json_result(data=get_ragflow_version())
@manager.route("/language", methods=["GET"]) # noqa: F821
def language():
"""Backend runtime language detection for front-end compatibility."""
logging.info("Language endpoint called, returning python")
return get_json_result(data={"language": "python"})
@manager.route("/system/status", methods=["GET"]) # noqa: F821
@login_required
def status():

View File

@@ -260,3 +260,9 @@ func (h *SystemHandler) ListEnvironments(c *gin.Context) {
common.SuccessWithData(c, environments, "SUCCESS")
}
// Language returns the backend runtime language so the front end can
// choose the appropriate code path (Go vs Python).
func (h *SystemHandler) Language(c *gin.Context) {
common.SuccessWithData(c, map[string]string{"language": "go"}, "success")
}

View File

@@ -159,6 +159,9 @@ func (r *Router) Setup(engine *gin.Engine) {
apiNoAuth.GET("/system/config", r.systemHandler.GetConfig)
apiNoAuth.GET("/system/version", r.systemHandler.GetVersion)
apiNoAuth.GET("/system/healthz", r.systemHandler.Healthz)
// Backend runtime language detection. The front end calls this once
// to choose between Go and Python code paths.
apiNoAuth.GET("/language", r.systemHandler.Language)
// Pipeline catalog. Public static data (shipped with the binary),
// no auth required. The front end uses it to populate the parser

View File

@@ -10,6 +10,11 @@ import {
} from '@/interfaces/database/user-setting';
import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system';
import { DEFAULT_LANGUAGE_CODE, supportedLanguages } from '@/locales/config';
import kbService from '@/services/knowledge-service';
import {
getBackendLanguage,
subscribeBackendLanguage,
} from '@/utils/backend-runtime';
import userService, {
addTenantUser,
agreeTenant,
@@ -19,6 +24,7 @@ import userService, {
} from '@/services/user-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useSyncExternalStore } from 'react';
import { useTranslation } from 'react-i18next';
import { useWarnEmptyModel } from './use-warn-empty-model';
@@ -38,6 +44,7 @@ export const enum UserSettingApiAction {
AgreeTenant = 'agreeTenant',
SetLangfuseConfig = 'setLangfuseConfig',
DeleteLangfuseConfig = 'deleteLangfuseConfig',
ListPipelines = 'listPipelines',
FetchLangfuseConfig = 'fetchLangfuseConfig',
}
@@ -102,6 +109,27 @@ export const useSelectParserList = (): Array<{
const { data: tenantInfo } = useFetchTenantInfo(true);
const { t } = useTranslation();
// Detect backend runtime language (Go vs Python) so we can choose
// the matching parser-list code path at runtime.
// fetchBackendLanguage / getBackendLanguage handle their own caching
// internally; no need for an extra useQuery layer.
const backendLang = useSyncExternalStore(
subscribeBackendLanguage,
getBackendLanguage,
getBackendLanguage,
);
// Go backend: fetch pipeline catalog dynamically.
const { data: pipelineListData } = useQuery({
queryKey: [UserSettingApiAction.ListPipelines],
queryFn: async () => {
const { data } = await kbService.listPipelines();
return data;
},
staleTime: Infinity,
enabled: backendLang === 'go',
});
const defaultParsers = useMemo(
() => [
{ value: 'naive', label: t('knowledgeConfiguration.parserLabel.naive') },
@@ -135,6 +163,25 @@ export const useSelectParserList = (): Array<{
);
const parserList = useMemo(() => {
// Go backend: prefer the dynamic pipeline catalog from the API.
if (backendLang === 'go') {
const pipelineList: Array<{ parser_id: string; title: string; dsl: Record<string, any> }> =
pipelineListData?.data ?? [];
if (pipelineList.length > 0) {
const labelFromAPI = (parserId: string, title: string) => {
const key = `knowledgeConfiguration.parserLabel.${parserId}`;
const translated = t(key);
return translated !== key ? translated : title;
};
return pipelineList.map((item) => ({
value: item.parser_id,
label: labelFromAPI(item.parser_id, item.title),
}));
}
}
// Python backend (or fallback): use tenant-level parser_ids or
// the hardcoded default parsers.
const parserArray: Array<string> = tenantInfo?.parser_ids?.split(',') ?? [];
const filteredArray = parserArray.filter((x) => x.trim() !== '');
@@ -146,7 +193,7 @@ export const useSelectParserList = (): Array<{
const arr = x.split(':');
return { value: arr[0], label: arr[1] };
});
}, [tenantInfo, defaultParsers]);
}, [tenantInfo, defaultParsers, backendLang, pipelineListData, t]);
return parserList;
};

View File

@@ -778,6 +778,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
'Re-parse existing documents for the new column roles to take effect.',
parserLabel: {
naive: 'General',
general: 'General',
qa: 'Q&A',
resume: 'Resume',
manual: 'Manual',

View File

@@ -19,6 +19,7 @@ const {
documentThumbnails,
documentIngest,
listTagByKnowledgeIds,
listPipelines,
setMeta,
getMeta,
getMetaKeys,
@@ -66,6 +67,10 @@ const methods = {
url: retrievalTestShare,
method: 'post',
},
listPipelines: {
url: listPipelines,
method: 'get',
},
pipelineRerun: {
url: api.pipelineRerun,
method: 'post',

View File

@@ -181,6 +181,7 @@ export default {
`${restAPIv1}/datasets/${datasetId}/ingestions/${logId}`,
fetchPipelineDatasetLogs: (datasetId: string) =>
`${restAPIv1}/datasets/${datasetId}/ingestions`,
listPipelines: `${restAPIv1}/pipelines?type=builtin`,
runIndex: (datasetId: string, indexType: string) =>
`${restAPIv1}/datasets/${datasetId}/index?type=${indexType.toLowerCase()}`,
traceIndex: (datasetId: string, indexType: string) =>

View File

@@ -0,0 +1,40 @@
/**
* Backend runtime language detection.
*
* Fetches /api/v1/language once at module load (app start) and caches the
* result. Components subscribe via React's useSyncExternalStore; there is no
* per-component fetch or useEffect.
*
* Pattern: module-level fetch + listener set, same subscription approach as
* enterprise billingStatus.ts.
*/
type Listener = () => void;
const listeners = new Set<Listener>();
let backendLanguage: string | null = null;
// Kick off the fetch at module load — app start, not component mount.
const promise: Promise<string> = fetch('/api/v1/language')
.then((r) => r.json())
.then((body: { data?: { language?: string } }) => {
backendLanguage = body.data?.language === 'go' ? 'go' : 'python';
listeners.forEach((fn) => fn());
return backendLanguage;
})
.catch(() => {
backendLanguage = 'python';
listeners.forEach((fn) => fn());
return 'python';
});
export const fetchBackendLanguage = (): Promise<string> => promise;
export const getBackendLanguage = (): string | null => backendLanguage;
export const subscribeBackendLanguage = (listener: Listener): (() => void) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};