From 2dd4cf7a1d4b58ae6315a7ed1ca97cb1c809a2ba Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 11 Jun 2026 22:41:24 +0800 Subject: [PATCH] fix(agentDocument): replace getDocuments with listDocuments in useFetchAgentDocuments to avoid over-fetching (#15301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(agentDocument): listDocuments returns templateId and derived fields * fix(agentDocument): useFetchAgentDocuments use listDocuments instead of getDocuments * fix(agentDocument): derive AgentDocumentItem from listDocuments return type * fix(agentDocument): export AgentDocumentListItem type * 🐛 fix(agentDocument): align list projections and consumers after rebase onto canary - listDocumentsForTopic now returns the same projection as listDocuments (derived fields + templateId), so the tRPC union no longer collapses the inferred client type to the old 8-field shape - add description/updatedAt to both projections for sidebar consumers - AgentDocumentsGroup switches getDocuments -> listDocuments (it already shared the documentsList SWR key) - makePendingDocument trimmed to the lean list item shape - update useFetchAgentDocuments test to the listDocuments behavior Co-Authored-By: Claude Fable 5 * 🐛 fix(agentDocument): migrate agentDocumentSkills sync to slim listDocuments The tool store's skill registry sync shared agentDocumentSWRKeys.documentsList with the working sidebar and the new useFetchAgentDocuments hook, but still fetched the full getDocuments payload. Sharing one SWR key across different payload shapes made the cached result order-dependent: whichever consumer mounted first decided whether the cache held the heavy full documents or the slim list items. Migrate the skills sync to listDocuments, whose projection covers every field mapDocsToSkills reads. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- .../src/services/agentDocuments/index.ts | 8 ++++ src/features/AgentDocumentsExplorer/types.ts | 2 +- .../utils/pendingDocument.ts | 21 +--------- .../ResourcesSection/AgentDocumentsGroup.tsx | 4 +- src/services/agentDocument.ts | 4 ++ src/store/agent/slices/agent/action.test.ts | 38 ++++++++----------- src/store/agent/slices/agent/action.ts | 20 +++++----- .../tool/slices/agentDocumentSkills/action.ts | 21 +++++----- 8 files changed, 54 insertions(+), 64 deletions(-) diff --git a/apps/server/src/services/agentDocuments/index.ts b/apps/server/src/services/agentDocuments/index.ts index c7cc6d0078..4360bfa82b 100644 --- a/apps/server/src/services/agentDocuments/index.ts +++ b/apps/server/src/services/agentDocuments/index.ts @@ -616,6 +616,8 @@ export class AgentDocumentsService { const filtered = sourceType && sourceType !== 'all' ? docs.filter((d) => d.sourceType === sourceType) : docs; return filtered.map((d) => ({ + ...deriveAgentDocumentFields(d), + description: d.description, documentId: d.documentId, fileType: d.fileType, filename: d.filename, @@ -623,7 +625,9 @@ export class AgentDocumentsService { loadPosition: d.policy?.context?.position, parentId: d.parentId, sourceType: d.sourceType, + templateId: d.templateId, title: d.title, + updatedAt: d.updatedAt, })); } @@ -642,6 +646,8 @@ export class AgentDocumentsService { .filter((doc): doc is AgentDocumentWithRules => Boolean(doc)) .filter((doc) => !sourceType || sourceType === 'all' || doc.sourceType === sourceType) .map((doc) => ({ + ...deriveAgentDocumentFields(doc), + description: doc.description, documentId: doc.documentId, fileType: doc.fileType, filename: doc.filename, @@ -649,7 +655,9 @@ export class AgentDocumentsService { loadPosition: doc.policy?.context?.position, parentId: doc.parentId, sourceType: doc.sourceType, + templateId: doc.templateId, title: doc.title, + updatedAt: doc.updatedAt, })); } diff --git a/src/features/AgentDocumentsExplorer/types.ts b/src/features/AgentDocumentsExplorer/types.ts index 675676efd7..39cf91d5f2 100644 --- a/src/features/AgentDocumentsExplorer/types.ts +++ b/src/features/AgentDocumentsExplorer/types.ts @@ -1,7 +1,7 @@ import type { agentDocumentService } from '@/services/agentDocument'; export type AgentDocumentItem = Awaited< - ReturnType + ReturnType >[number]; export const PENDING_ID_PREFIX = 'pending:'; diff --git a/src/features/AgentDocumentsExplorer/utils/pendingDocument.ts b/src/features/AgentDocumentsExplorer/utils/pendingDocument.ts index 841c0ec2ea..9a14d8d22f 100644 --- a/src/features/AgentDocumentsExplorer/utils/pendingDocument.ts +++ b/src/features/AgentDocumentsExplorer/utils/pendingDocument.ts @@ -24,39 +24,20 @@ export const makePendingDocument = ({ const id = `${PENDING_ID_PREFIX}${nanoid(10)}`; const now = new Date(); return { - accessPublic: 0, - accessSelf: 1, - accessShared: 0, - agentId, category: AGENT_DOCUMENT_CATEGORY, - content: '', - createdAt: now, - deletedAt: null, - deletedByAgentId: null, - deletedByUserId: null, - deleteReason: null, description: null, documentId: id, - editorData: null, filename: title, fileType: isFolder ? CUSTOM_FOLDER_FILE_TYPE : CUSTOM_DOCUMENT_FILE_TYPE, id, isFolder, isSkillBundle: false, isSkillIndex: false, - loadRules: {} as AgentDocumentItem['loadRules'], - metadata: null, + loadPosition: undefined, parentId, - policy: null, - policyLoad: 'always' as AgentDocumentItem['policyLoad'], - policyLoadFormat: 'raw' as AgentDocumentItem['policyLoadFormat'], - policyLoadPosition: '', - policyLoadRule: '', - source: null, sourceType: AGENT_DOCUMENT_SOURCE_TYPE, templateId: null, title, updatedAt: now, - userId: '', }; }; diff --git a/src/routes/(main)/agent/features/Conversation/WorkingSidebar/ResourcesSection/AgentDocumentsGroup.tsx b/src/routes/(main)/agent/features/Conversation/WorkingSidebar/ResourcesSection/AgentDocumentsGroup.tsx index 94ce70f239..2109a665db 100644 --- a/src/routes/(main)/agent/features/Conversation/WorkingSidebar/ResourcesSection/AgentDocumentsGroup.tsx +++ b/src/routes/(main)/agent/features/Conversation/WorkingSidebar/ResourcesSection/AgentDocumentsGroup.tsx @@ -105,7 +105,7 @@ const FILTER_OPTIONS = [ { labelKey: 'workingPanel.resources.filter.web', value: 'web' }, ] as const satisfies readonly { labelKey: string; value: ResourceFilter }[]; -type AgentDocumentListItem = Awaited>[number]; +type AgentDocumentListItem = Awaited>[number]; interface DocumentItemProps { agentId: string; @@ -286,7 +286,7 @@ const AgentDocumentsGroup = memo( isLoading, mutate, } = useClientDataSWR(agentId ? agentDocumentSWRKeys.documentsList(agentId) : null, () => - agentDocumentService.getDocuments({ agentId: agentId! }), + agentDocumentService.listDocuments({ agentId: agentId! }), ); const webData = useMemo( diff --git a/src/services/agentDocument.ts b/src/services/agentDocument.ts index 8fe03f4314..d8a292d851 100644 --- a/src/services/agentDocument.ts +++ b/src/services/agentDocument.ts @@ -292,3 +292,7 @@ export const resolveAgentDocumentsContext = async (params: { }; export const agentDocumentService = new AgentDocumentService(); + +export type AgentDocumentListItem = Awaited< + ReturnType +>[number]; diff --git a/src/store/agent/slices/agent/action.test.ts b/src/store/agent/slices/agent/action.test.ts index ce4dad9cf9..712af2599b 100644 --- a/src/store/agent/slices/agent/action.test.ts +++ b/src/store/agent/slices/agent/action.test.ts @@ -3,7 +3,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { agentService } from '@/services/agent'; -import { resolveAgentDocumentsContext } from '@/services/agentDocument'; +import { agentDocumentService } from '@/services/agentDocument'; import { type LobeAgentConfig } from '@/types/agent'; import { withSWR } from '~test-utils'; @@ -26,8 +26,12 @@ vi.mock('@/services/agent', () => ({ })); vi.mock('@/services/agentDocument', () => ({ + agentDocumentService: { + listDocuments: vi.fn(), + }, agentDocumentSWRKeys: { documents: (agentId: string) => ['agent-documents', agentId] as const, + documentsList: (agentId: string) => ['agent-documents-list', agentId] as const, }, resolveAgentDocumentsContext: vi.fn(), })); @@ -96,37 +100,27 @@ describe('AgentSlice Actions', () => { }); describe('useFetchAgentDocuments', () => { - it('should sync fetched agent documents into store cache', async () => { - vi.mocked(resolveAgentDocumentsContext).mockResolvedValue([ + it('should fetch agent documents via listDocuments', async () => { + const docs = [ { - content: 'setup steps', + documentId: 'doc-1', filename: 'setup.md', id: 'doc-1', - loadRules: {}, - policyId: null, - policyLoadFormat: undefined, title: 'Setup', }, - ]); + ]; + vi.mocked(agentDocumentService.listDocuments).mockResolvedValue(docs as any); - const { result } = renderHook(() => useAgentStore(), { wrapper: withSWR }); + const store = renderHook(() => useAgentStore(), { wrapper: withSWR }); - renderHook(() => result.current.useFetchAgentDocuments('agent-1'), { wrapper: withSWR }); + const { result } = renderHook(() => store.result.current.useFetchAgentDocuments('agent-1'), { + wrapper: withSWR, + }); await waitFor(() => { - expect(result.current.agentDocumentsMap['agent-1']).toEqual([ - { - content: 'setup steps', - filename: 'setup.md', - id: 'doc-1', - loadRules: {}, - policyId: null, - policyLoadFormat: undefined, - title: 'Setup', - }, - ]); + expect(result.current.data).toEqual(docs); }); - expect(resolveAgentDocumentsContext).toHaveBeenCalledWith({ agentId: 'agent-1' }); + expect(agentDocumentService.listDocuments).toHaveBeenCalledWith({ agentId: 'agent-1' }); }); }); diff --git a/src/store/agent/slices/agent/action.ts b/src/store/agent/slices/agent/action.ts index 1dd5c7a044..c00a36088f 100644 --- a/src/store/agent/slices/agent/action.ts +++ b/src/store/agent/slices/agent/action.ts @@ -11,7 +11,12 @@ import { MESSAGE_CANCEL_FLAT } from '@/const/message'; import { mutate, useClientDataSWRWithSync } from '@/libs/swr'; import type { AvailableAgentItem, CreateAgentParams, CreateAgentResult } from '@/services/agent'; import { agentService, AVAILABLE_AGENTS_CONTEXT_QUERY_LIMIT } from '@/services/agent'; -import { agentDocumentSWRKeys, resolveAgentDocumentsContext } from '@/services/agentDocument'; +import { + type AgentDocumentListItem, + agentDocumentService, + agentDocumentSWRKeys, + resolveAgentDocumentsContext, +} from '@/services/agentDocument'; import type { StoreSetter } from '@/store/types'; import { getUserStoreState } from '@/store/user'; import { userProfileSelectors } from '@/store/user/selectors'; @@ -371,16 +376,11 @@ export class AgentSliceActionImpl { ); }; - useFetchAgentDocuments = (agentId?: string | null): SWRResponse => { - return useClientDataSWRWithSync( - agentId ? agentDocumentSWRKeys.documents(agentId) : null, - async () => (await resolveAgentDocumentsContext({ agentId: agentId! })) ?? [], + useFetchAgentDocuments = (agentId?: string | null): SWRResponse => { + return useClientDataSWRWithSync( + agentId ? agentDocumentSWRKeys.documentsList(agentId) : null, + async () => agentDocumentService.listDocuments({ agentId: agentId! }), { - onData: (data) => { - if (!agentId) return; - - this.#syncAgentDocuments(agentId, data); - }, revalidateOnFocus: false, }, ); diff --git a/src/store/tool/slices/agentDocumentSkills/action.ts b/src/store/tool/slices/agentDocumentSkills/action.ts index bfdc920028..674ab07b41 100644 --- a/src/store/tool/slices/agentDocumentSkills/action.ts +++ b/src/store/tool/slices/agentDocumentSkills/action.ts @@ -1,7 +1,11 @@ import { buildAgentSkillIdentifier } from '@lobechat/const'; import useSWR, { type SWRResponse } from 'swr'; -import { agentDocumentService, agentDocumentSWRKeys } from '@/services/agentDocument'; +import { + type AgentDocumentListItem, + agentDocumentService, + agentDocumentSWRKeys, +} from '@/services/agentDocument'; import { type StoreSetter } from '@/store/types'; import { setNamespace } from '@/utils/storeDebug'; @@ -12,9 +16,7 @@ const n = setNamespace('agentDocumentSkills'); type Setter = StoreSetter; -const mapDocsToSkills = ( - docs: Awaited>, -): AgentDocumentSkillItem[] => +const mapDocsToSkills = (docs: AgentDocumentListItem[]): AgentDocumentSkillItem[] => docs .filter((doc) => doc.isSkillBundle) .map((doc) => ({ @@ -56,7 +58,7 @@ export class AgentDocumentSkillsActionImpl { } try { - const docs = await agentDocumentService.getDocuments({ agentId }); + const docs = await agentDocumentService.listDocuments({ agentId }); const items = mapDocsToSkills(docs); this.#set( { agentDocumentSkills: items, agentDocumentSkillsAgentId: agentId }, @@ -88,14 +90,15 @@ export class AgentDocumentSkillsActionImpl { /** * SWR-backed hook that fetches the agent's skill bundles and keeps the store * in sync. Shares the same SWR key as the working-sidebar panel so the panel - * fetch and the registry sync collapse into one network request. + * fetch and the registry sync collapse into one network request — both must + * fetch the same slim `listDocuments` payload to keep the cache shape stable. */ useFetchAgentDocumentSkills = ( agentId: string | undefined, - ): SWRResponse>> => - useSWR>>( + ): SWRResponse => + useSWR( agentId ? agentDocumentSWRKeys.documentsList(agentId) : null, - async () => agentDocumentService.getDocuments({ agentId: agentId! }), + async () => agentDocumentService.listDocuments({ agentId: agentId! }), { onSuccess: (docs) => { if (!agentId) return;