mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
fix(agentDocument): replace getDocuments with listDocuments in useFetchAgentDocuments to avoid over-fetching (#15301)
* 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 <noreply@anthropic.com> * 🐛 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { agentDocumentService } from '@/services/agentDocument';
|
||||
|
||||
export type AgentDocumentItem = Awaited<
|
||||
ReturnType<typeof agentDocumentService.getDocuments>
|
||||
ReturnType<typeof agentDocumentService.listDocuments>
|
||||
>[number];
|
||||
|
||||
export const PENDING_ID_PREFIX = 'pending:';
|
||||
|
||||
@@ -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: '',
|
||||
};
|
||||
};
|
||||
|
||||
+2
-2
@@ -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<ReturnType<typeof agentDocumentService.getDocuments>>[number];
|
||||
type AgentDocumentListItem = Awaited<ReturnType<typeof agentDocumentService.listDocuments>>[number];
|
||||
|
||||
interface DocumentItemProps {
|
||||
agentId: string;
|
||||
@@ -286,7 +286,7 @@ const AgentDocumentsGroup = memo<AgentDocumentsGroupProps>(
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useClientDataSWR(agentId ? agentDocumentSWRKeys.documentsList(agentId) : null, () =>
|
||||
agentDocumentService.getDocuments({ agentId: agentId! }),
|
||||
agentDocumentService.listDocuments({ agentId: agentId! }),
|
||||
);
|
||||
|
||||
const webData = useMemo(
|
||||
|
||||
@@ -292,3 +292,7 @@ export const resolveAgentDocumentsContext = async (params: {
|
||||
};
|
||||
|
||||
export const agentDocumentService = new AgentDocumentService();
|
||||
|
||||
export type AgentDocumentListItem = Awaited<
|
||||
ReturnType<typeof agentDocumentService.listDocuments>
|
||||
>[number];
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<AgentContextDocument[]> => {
|
||||
return useClientDataSWRWithSync<AgentContextDocument[]>(
|
||||
agentId ? agentDocumentSWRKeys.documents(agentId) : null,
|
||||
async () => (await resolveAgentDocumentsContext({ agentId: agentId! })) ?? [],
|
||||
useFetchAgentDocuments = (agentId?: string | null): SWRResponse<AgentDocumentListItem[]> => {
|
||||
return useClientDataSWRWithSync<AgentDocumentListItem[]>(
|
||||
agentId ? agentDocumentSWRKeys.documentsList(agentId) : null,
|
||||
async () => agentDocumentService.listDocuments({ agentId: agentId! }),
|
||||
{
|
||||
onData: (data) => {
|
||||
if (!agentId) return;
|
||||
|
||||
this.#syncAgentDocuments(agentId, data);
|
||||
},
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<ToolStore>;
|
||||
|
||||
const mapDocsToSkills = (
|
||||
docs: Awaited<ReturnType<typeof agentDocumentService.getDocuments>>,
|
||||
): 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<Awaited<ReturnType<typeof agentDocumentService.getDocuments>>> =>
|
||||
useSWR<Awaited<ReturnType<typeof agentDocumentService.getDocuments>>>(
|
||||
): SWRResponse<AgentDocumentListItem[]> =>
|
||||
useSWR<AgentDocumentListItem[]>(
|
||||
agentId ? agentDocumentSWRKeys.documentsList(agentId) : null,
|
||||
async () => agentDocumentService.getDocuments({ agentId: agentId! }),
|
||||
async () => agentDocumentService.listDocuments({ agentId: agentId! }),
|
||||
{
|
||||
onSuccess: (docs) => {
|
||||
if (!agentId) return;
|
||||
|
||||
Reference in New Issue
Block a user