diff --git a/src/components/DataPreview/session/entryWhitelist/index.tsx b/src/components/DataPreview/session/entryWhitelist/index.tsx index 2185d4a04865e9c277421c97af446a87c8f660e3..794d33dd3e8a01a755c852b1b575ae6e4c882f1e 100644 --- a/src/components/DataPreview/session/entryWhitelist/index.tsx +++ b/src/components/DataPreview/session/entryWhitelist/index.tsx @@ -42,7 +42,7 @@ const INNER_RESOURCE_NAME_PATTERNS = [ /^专利库[--]/, /^A+$/i, ]; -const initializingEntryWhitelistBelongIds = new Set(); +const initializingEntryWhitelistTasks = new Map>(); interface EntryWhitelistEntry { id?: string; @@ -628,26 +628,35 @@ async function createEntryWhitelistFromCapture( export async function ensureEntryWhitelistInitialized(): Promise { const { belongId, belongName } = await waitForCurrentEntryWhitelistBelong(); - if (!belongId || initializingEntryWhitelistBelongIds.has(belongId)) { - return false; - } - initializingEntryWhitelistBelongIds.add(belongId); - try { - const existedRecord = await fetchEntryWhitelistRecord(belongId); - if (Array.isArray(existedRecord?.entryNames) && existedRecord.entryNames.length > 0) { + if (!belongId) return false; + const existingTask = initializingEntryWhitelistTasks.get(belongId); + if (existingTask) return existingTask; + + const task = (async () => { + try { + const existedRecord = await fetchEntryWhitelistRecord(belongId); + if ( + Array.isArray(existedRecord?.entryNames) && + existedRecord.entryNames.length > 0 + ) { + return false; + } + return Boolean( + await createEntryWhitelistFromCapture( + belongId, + belongName, + 'agent-entry-config-auto', + ), + ); + } catch { return false; } - return Boolean( - await createEntryWhitelistFromCapture( - belongId, - belongName, - 'agent-entry-config-auto', - ), - ); - } catch { - return false; + })(); + initializingEntryWhitelistTasks.set(belongId, task); + try { + return await task; } finally { - initializingEntryWhitelistBelongIds.delete(belongId); + initializingEntryWhitelistTasks.delete(belongId); } } diff --git a/src/pages/Home/components/Search/index.tsx b/src/pages/Home/components/Search/index.tsx index 9a1ae7ad4bfee121d6b52c301e7a642a6e78e936..809803936e327446b9f95d6a92939b609b4295aa 100644 --- a/src/pages/Home/components/Search/index.tsx +++ b/src/pages/Home/components/Search/index.tsx @@ -27,6 +27,7 @@ import LoadingView from '@/components/Common/Loading'; import useAsyncLoad from '@/hooks/useAsyncLoad'; import { useDebounce } from '@/hooks/useDebounce'; import FullScreenModal from '@/components/Common/fullScreen'; +import { ensureEntryWhitelistInitialized } from '@/components/DataPreview/session/entryWhitelist'; interface IProps { isGlobal: boolean; @@ -47,9 +48,10 @@ const ASSISTANT_REPLY_SETTLE_MS = 1000; const ASSISTANT_REPLY_POLL_MS = 800; const RAG_ADMIN_BASE_URL = 'http://127.0.0.1:5179'; const RAG_LIST_TIMEOUT_MS = 8000; -const RAG_ASK_TIMEOUT_MS = 15000; +const RAG_ASK_TIMEOUT_MS = 60000; const FIXED_RAG_KNOWLEDGE_BASE_CODE = 'RAGZSK'; const FIXED_RAG_KNOWLEDGE_BASE_NAME = 'RAG知识库'; +const RAG_DOCUMENT_FILE_PATTERN = /\.(?:pdf|txt|md)$/i; interface DecodedAssistantMessage { body: string; @@ -100,9 +102,16 @@ function normalizeSources(sources: string[], fallback: string[] = []): string[] .filter( (item) => Boolean(item) && + RAG_DOCUMENT_FILE_PATTERN.test(item.replace(/^.*[\\/]/, '')) && ![ 'cache', 'cached', + 'rag', + 'llm', + 'web', + 'rag+web', + 'none', + 'null', '缓存', '无', FIXED_RAG_KNOWLEDGE_BASE_CODE.toLowerCase(), @@ -112,6 +121,33 @@ function normalizeSources(sources: string[], fallback: string[] = []): string[] return normalized.length > 0 ? normalized : fallback; } +function normalizeRagDocumentNames(files: string[]): string[] { + return [ + ...new Set( + files + .map((item) => item.trim().replace(/^.*[\\/]/, '')) + .filter((item) => Boolean(item) && RAG_DOCUMENT_FILE_PATTERN.test(item)), + ), + ]; +} + +async function loadMcpRagDocumentSources(): Promise { + const response = await fetchWithTimeout(`${RAG_ADMIN_BASE_URL}/rag/files`); + const payload = await response.json(); + if (!response.ok || ![0, 200, '0', '200'].includes(payload?.code)) { + throw new Error(payload?.msg || '无法读取 RAG MCP 文件列表'); + } + const groups = payload?.data; + if (!groups || typeof groups !== 'object') { + throw new Error('RAG MCP 文件列表格式不正确'); + } + return normalizeRagDocumentNames( + Object.values(groups).flatMap((items) => + Array.isArray(items) ? items.map((item) => String(item)) : [], + ), + ); +} + function pickPrimarySource(sources: string[], fallback: string[] = []): string[] { const normalized = normalizeSources(sources, []); const candidates = normalized.length > 0 ? normalized : normalizeSources(fallback, []); @@ -309,7 +345,7 @@ async function loadFixedRagDocumentSources( return { directoryFound, - files: [...new Set(normalizeSources(scopedFiles, []))], + files: normalizeRagDocumentNames(scopedFiles), }; } catch { return { directoryFound: false, files: [] }; @@ -474,8 +510,6 @@ async function queryAssistantRagContext( query: string, selectedKnowledgeBases: string[], ): Promise<{ context: string; sources: string[] }> { - if (selectedKnowledgeBases.length === 0) return { context: '', sources: [] }; - try { const response = await fetchWithTimeout( `${RAG_ADMIN_BASE_URL}/rag/ask`, @@ -788,17 +822,41 @@ const Search: React.FC = (props) => { }); return; } + setAssistantStage('正在核对 RAG MCP 已入库文件...'); + const mcpRagDocuments = await loadMcpRagDocumentSources(); + if (!isSubmitUnitActive()) return; + const mcpFileByName = new Map( + mcpRagDocuments.map((fileName) => [fileName.toLowerCase(), fileName]), + ); + const intendedRagDocuments = ragScopeLimited + ? currentSelectedScopedDocuments + : currentRagDocuments; + const unsyncedRagDocuments = intendedRagDocuments.filter( + (fileName) => !mcpFileByName.has(fileName.toLowerCase()), + ); + if (unsyncedRagDocuments.length > 0) { + throw new Error( + `以下当前单位知识文件尚未同步到 RAG MCP:${unsyncedRagDocuments.join( + '、', + )}。请先完成知识文件上传或入库同步。`, + ); + } + const searchableRagDocuments = intendedRagDocuments + .map((fileName) => mcpFileByName.get(fileName.toLowerCase())) + .filter((fileName): fileName is string => Boolean(fileName)); + if (searchableRagDocuments.length === 0) { + throw new Error('当前单位没有已同步到 RAG MCP 的可检索文件'); + } + setAssistantStage('正在同步当前单位入口白名单...'); + await ensureEntryWhitelistInitialized(); + if (!isSubmitUnitActive()) return; const applications = (await orgCtrl.loadApplications()).filter((app) => app.isAuth); if (!isSubmitUnitActive()) return; if (applications.length === 0) { throw new Error('当前用户没有可访问的应用'); } - const fixedRagFiles = ragScopeLimited - ? currentSelectedScopedDocuments - : [FIXED_RAG_KNOWLEDGE_BASE_CODE]; - const sourceFallbackDocuments = ragScopeLimited - ? currentSelectedScopedDocuments - : currentRagDocuments; + const fixedRagFiles = searchableRagDocuments; + const sourceFallbackDocuments = searchableRagDocuments; const useKnowledgeBaseScope = true; const knowledgeBasePlatformName = FIXED_RAG_KNOWLEDGE_BASE_NAME; const currentBelongId = String((orgCtrl.home.current as any)?.id ?? ''); @@ -844,10 +902,7 @@ const Search: React.FC = (props) => { applications.find((app) => app.name.includes(data.applicationName.trim())) : applications.find((app) => app.id === data.applicationId) ?? applications.find((app) => app.name.trim() === data.applicationName.trim()); - const normalizedDataSources = pickPrimarySource( - data.sources ?? [], - sourceFallbackDocuments, - ); + const normalizedDataSources = pickPrimarySource(data.sources ?? [], []); const formattedAnswer = data.text || (data.resultKind === 'business-entry' @@ -870,7 +925,7 @@ const Search: React.FC = (props) => { /消息(?:溯源|来源)[::]\s*(cache|cached|缓存)\s*$/im, '消息溯源:无', ), - sourceFallbackDocuments, + normalizedDataSources, ); setAssistantAnswer(displayAnswer); @@ -959,8 +1014,8 @@ const Search: React.FC = (props) => { applications.find((app) => answer.includes(app.name)); const answerSources = recommendation?.sources && recommendation.sources.length > 0 - ? pickPrimarySource(recommendation.sources, sourceFallbackDocuments) - : pickPrimarySource(ragContext.sources, sourceFallbackDocuments); + ? pickPrimarySource(recommendation.sources, []) + : pickPrimarySource(ragContext.sources, []); if (useKnowledgeBaseScope) { const entryName =