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:
Arvin Xu
2026-06-11 22:41:24 +08:00
committed by GitHub
parent 575ef1e8ee
commit 2dd4cf7a1d
8 changed files with 54 additions and 64 deletions
@@ -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 -1
View File
@@ -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: '',
};
};
@@ -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(
+4
View File
@@ -292,3 +292,7 @@ export const resolveAgentDocumentsContext = async (params: {
};
export const agentDocumentService = new AgentDocumentService();
export type AgentDocumentListItem = Awaited<
ReturnType<typeof agentDocumentService.listDocuments>
>[number];
+16 -22
View File
@@ -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' });
});
});
+10 -10
View File
@@ -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;