mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🐛 fix(chat): unify message stream for /agent/:topicId and /page/:docId
Before this change a page-scoped conversation (FloatingChatPanel with
scope='page' in the /Page route) partitioned the client message store by
scope, so /agent/:topicId and /agent/:topicId/page/:docId each built their
own messagesMap slot and SWR cache — but the TRPC getMessages endpoint
ignores scope and returned the same messages for both, producing duplicate
fetches and a visible message-history split between the two surfaces.
Fixes by keeping scope='page' as a capability/surfacing marker only:
- messageMapKey: collapse 'page' to the default scope early in
toMessageMapContext, so threadId/groupId still win and only the
main/page pair actually unifies.
- useFetchMessages: build the SWR key from identity fields
(agentId, groupId, threadId, topicId) instead of the full
ConversationContext, so scope no longer partitions the cache.
agentConfigResolver/streamingExecutor/composeEnabledTools still read
scope='page' from operation.context for PageAgent injection and
initialContext.pageEditor wiring — the capability layer is unchanged.
Also fix two pre-existing test regressions surfaced by re-running the
impacted suites:
- streamingExecutor page-editor initialContext test now mocks
pageAgentRuntime.isReady() (required since the PageAgent editor-ready
guard landed).
- FloatingChatPanel default shell props test updated to match the
[180,320,520,800] snap points introduced in 62dc91e444.
This commit is contained in:
@@ -122,7 +122,7 @@ describe('FloatingChatPanel', () => {
|
||||
it('applies default shell props', () => {
|
||||
const { getByTestId } = render(<FloatingChatPanel agentId="a" topicId="t" />);
|
||||
const sheet = getByTestId('floating-panel-shell');
|
||||
expect(sheet.dataset.snapPoints).toBe(JSON.stringify([180, 320, 520]));
|
||||
expect(sheet.dataset.snapPoints).toBe(JSON.stringify([180, 320, 520, 800]));
|
||||
expect(sheet.dataset.variant).toBe('embedded');
|
||||
expect(sheet.dataset.dismissible).toBe('false');
|
||||
});
|
||||
|
||||
@@ -818,6 +818,7 @@ describe('StreamingExecutor actions', () => {
|
||||
tools: [],
|
||||
}),
|
||||
} as any);
|
||||
vi.spyOn(pageAgentRuntime, 'isReady').mockReturnValue(true);
|
||||
vi.spyOn(pageAgentRuntime, 'getPageContentContext').mockReturnValue({
|
||||
markdown: '# Test Document',
|
||||
xml: '<root><h1>Test</h1></root>',
|
||||
|
||||
@@ -111,8 +111,25 @@ export class MessageQueryActionImpl {
|
||||
// Skip fetch when skipFetch is true or required fields are missing
|
||||
const shouldFetch = !skipFetch && !!context.agentId && !!context.topicId;
|
||||
|
||||
// Only identity fields partition the server-side fetch. Keeping `scope`
|
||||
// out of the SWR key lets main (`/agent/:topicId`) and page
|
||||
// (`/agent/:topicId/page`) share one cache — the TRPC `getMessages`
|
||||
// endpoint already ignores scope (see `MessageQueryContext`), so there's
|
||||
// no reason to refetch per surface.
|
||||
const swrKey = shouldFetch
|
||||
? [
|
||||
'CHAT_STORE_FETCH_MESSAGES',
|
||||
{
|
||||
agentId: context.agentId,
|
||||
groupId: context.groupId,
|
||||
threadId: context.threadId,
|
||||
topicId: context.topicId,
|
||||
},
|
||||
]
|
||||
: null;
|
||||
|
||||
return useClientDataSWRWithSync<UIChatMessage[]>(
|
||||
shouldFetch ? ['CHAT_STORE_FETCH_MESSAGES', context] : null,
|
||||
swrKey,
|
||||
() => messageService.getMessages(context),
|
||||
{
|
||||
onData: (data) => {
|
||||
|
||||
@@ -270,6 +270,44 @@ describe('messageMapKey', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page scope (collapses to main)', () => {
|
||||
it('should reuse the main key for page scope on an existing topic', () => {
|
||||
const result = messageMapKey({
|
||||
scope: 'page',
|
||||
agentId: 'agt_xxx',
|
||||
topicId: 'tpc_yyy',
|
||||
});
|
||||
expect(result).toBe('main_agt_xxx_tpc_yyy');
|
||||
});
|
||||
|
||||
it('should collapse page scope to main for a new topic', () => {
|
||||
const result = messageMapKey({ scope: 'page', agentId: 'agt_xxx' });
|
||||
expect(result).toBe('main_agt_xxx_new');
|
||||
});
|
||||
|
||||
it('should produce the same key for scope=main and scope=page on the same topic', () => {
|
||||
const mainKey = messageMapKey({ agentId: 'agt_xxx', topicId: 'tpc_yyy' });
|
||||
const pageKey = messageMapKey({
|
||||
scope: 'page',
|
||||
agentId: 'agt_xxx',
|
||||
topicId: 'tpc_yyy',
|
||||
});
|
||||
expect(pageKey).toBe(mainKey);
|
||||
});
|
||||
|
||||
it('should still promote to thread scope when both page scope and threadId are present', () => {
|
||||
// threadId wins over the page→main collapse, so thread-scoped messages
|
||||
// (e.g. a thread opened from inside the page editor) stay isolated.
|
||||
const result = messageMapKey({
|
||||
scope: 'page',
|
||||
agentId: 'agt_xxx',
|
||||
topicId: 'tpc_yyy',
|
||||
threadId: 'thd_zzz',
|
||||
});
|
||||
expect(result).toBe('thread_agt_xxx_tpc_yyy_thd_zzz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle null topicId as no topic (new)', () => {
|
||||
const result = messageMapKey({ agentId: 'agt_xxx', topicId: null });
|
||||
|
||||
@@ -41,7 +41,12 @@ export interface MessageMapKeyInput {
|
||||
* Handles mapping from agentId/threadId to scopeId/subTopicId format
|
||||
*/
|
||||
const toMessageMapContext = (input: MessageMapKeyInput): MessageMapContext => {
|
||||
const { agentId, topicId, threadId, isNew, scope, groupId, subAgentId } = input;
|
||||
const { agentId, topicId, threadId, isNew, groupId, subAgentId } = input;
|
||||
|
||||
// 'page' is a surfacing/capability marker, not a conversation partition.
|
||||
// Collapse it up-front so /agent/:topicId and /agent/:topicId/page share
|
||||
// one key; threadId/groupId still win below, keeping threads isolated.
|
||||
const scope = input.scope === 'page' ? undefined : input.scope;
|
||||
|
||||
// If threadId is present and scope is explicitly 'thread', use thread scope
|
||||
// Thread scope takes priority when explicitly requested, even with groupId
|
||||
|
||||
Reference in New Issue
Block a user