mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: fixed the group topic copy not right (#11730)
fix: update the group topic copy way
This commit is contained in:
@@ -2,7 +2,7 @@ import { eq, inArray } from 'drizzle-orm';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { getTestDB } from '../../../core/getTestDB';
|
||||
import { agents, messages, sessions, topics, users } from '../../../schemas';
|
||||
import { agents, messagePlugins, messages, sessions, topics, users } from '../../../schemas';
|
||||
import { LobeChatDatabase } from '../../../type';
|
||||
import { CreateTopicParams, TopicModel } from '../../topic';
|
||||
|
||||
@@ -279,6 +279,129 @@ describe('TopicModel - Create', () => {
|
||||
expect(duplicatedMessages[1].content).toBe('Assistant message');
|
||||
});
|
||||
|
||||
it('should correctly map parentId references when duplicating messages', async () => {
|
||||
const topicId = 'topic-with-parent-refs';
|
||||
|
||||
await serverDB.transaction(async (tx) => {
|
||||
await tx.insert(topics).values({ id: topicId, sessionId, userId, title: 'Original Topic' });
|
||||
await tx.insert(messages).values([
|
||||
{ id: 'msg1', role: 'user', topicId, userId, content: 'First message', parentId: null },
|
||||
{
|
||||
id: 'msg2',
|
||||
role: 'assistant',
|
||||
topicId,
|
||||
userId,
|
||||
content: 'Reply to first',
|
||||
parentId: 'msg1',
|
||||
},
|
||||
{
|
||||
id: 'msg3',
|
||||
role: 'tool',
|
||||
topicId,
|
||||
userId,
|
||||
content: 'Tool response',
|
||||
parentId: 'msg2',
|
||||
},
|
||||
{
|
||||
id: 'msg4',
|
||||
role: 'assistant',
|
||||
topicId,
|
||||
userId,
|
||||
content: 'Final message',
|
||||
parentId: 'msg3',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const { topic: duplicatedTopic, messages: duplicatedMessages } = await topicModel.duplicate(
|
||||
topicId,
|
||||
'Duplicated Topic',
|
||||
);
|
||||
|
||||
expect(duplicatedMessages).toHaveLength(4);
|
||||
|
||||
const msgMap = new Map(duplicatedMessages.map((m) => [m.content, m]));
|
||||
const newMsg1 = msgMap.get('First message')!;
|
||||
const newMsg2 = msgMap.get('Reply to first')!;
|
||||
const newMsg3 = msgMap.get('Tool response')!;
|
||||
const newMsg4 = msgMap.get('Final message')!;
|
||||
|
||||
expect(newMsg1.parentId).toBeNull();
|
||||
expect(newMsg2.parentId).toBe(newMsg1.id);
|
||||
expect(newMsg3.parentId).toBe(newMsg2.id);
|
||||
expect(newMsg4.parentId).toBe(newMsg3.id);
|
||||
|
||||
expect(newMsg1.id).not.toBe('msg1');
|
||||
expect(newMsg2.id).not.toBe('msg2');
|
||||
expect(newMsg3.id).not.toBe('msg3');
|
||||
expect(newMsg4.id).not.toBe('msg4');
|
||||
});
|
||||
|
||||
it('should correctly map tool_call_id when duplicating messages with tools', async () => {
|
||||
const topicId = 'topic-with-tools';
|
||||
const originalToolId = 'toolu_original_123';
|
||||
|
||||
await serverDB.transaction(async (tx) => {
|
||||
await tx.insert(topics).values({ id: topicId, sessionId, userId, title: 'Original Topic' });
|
||||
|
||||
// Insert assistant message with tools
|
||||
await tx.insert(messages).values({
|
||||
id: 'msg1',
|
||||
role: 'assistant',
|
||||
topicId,
|
||||
userId,
|
||||
content: 'Using tool',
|
||||
parentId: null,
|
||||
tools: [{ id: originalToolId, type: 'builtin', apiName: 'broadcast' }],
|
||||
});
|
||||
|
||||
// Insert tool message
|
||||
await tx.insert(messages).values({
|
||||
id: 'msg2',
|
||||
role: 'tool',
|
||||
topicId,
|
||||
userId,
|
||||
content: 'Tool response',
|
||||
parentId: 'msg1',
|
||||
});
|
||||
|
||||
// Insert messagePlugins entry
|
||||
await tx.insert(messagePlugins).values({
|
||||
id: 'msg2',
|
||||
userId,
|
||||
toolCallId: originalToolId,
|
||||
apiName: 'broadcast',
|
||||
});
|
||||
});
|
||||
|
||||
const { topic: duplicatedTopic, messages: duplicatedMessages } = await topicModel.duplicate(
|
||||
topicId,
|
||||
'Duplicated Topic',
|
||||
);
|
||||
|
||||
expect(duplicatedMessages).toHaveLength(2);
|
||||
|
||||
const msgMap = new Map(duplicatedMessages.map((m) => [m.role, m]));
|
||||
const newAssistant = msgMap.get('assistant')!;
|
||||
const newTool = msgMap.get('tool')!;
|
||||
|
||||
// Check that tools array has new IDs
|
||||
expect(newAssistant.tools).toBeDefined();
|
||||
const newTools = newAssistant.tools as any[];
|
||||
expect(newTools).toHaveLength(1);
|
||||
expect(newTools[0].id).not.toBe(originalToolId);
|
||||
expect(newTools[0].id).toMatch(/^toolu_/);
|
||||
|
||||
// Check that messagePlugins was copied with new toolCallId
|
||||
const newPlugin = await serverDB.query.messagePlugins.findFirst({
|
||||
where: eq(messagePlugins.id, newTool.id),
|
||||
});
|
||||
|
||||
expect(newPlugin).toBeDefined();
|
||||
expect(newPlugin!.toolCallId).toBe(newTools[0].id);
|
||||
expect(newPlugin!.toolCallId).not.toBe(originalToolId);
|
||||
});
|
||||
|
||||
it('should throw an error if the topic to duplicate does not exist', async () => {
|
||||
const topicId = 'nonexistent-topic';
|
||||
|
||||
|
||||
@@ -17,7 +17,14 @@ import {
|
||||
sql,
|
||||
} from 'drizzle-orm';
|
||||
|
||||
import { TopicItem, agents, agentsToSessions, messages, topics } from '../schemas';
|
||||
import {
|
||||
TopicItem,
|
||||
agents,
|
||||
agentsToSessions,
|
||||
messagePlugins,
|
||||
messages,
|
||||
topics,
|
||||
} from '../schemas';
|
||||
import { LobeChatDatabase } from '../type';
|
||||
import { genEndDateWhere, genRangeWhere, genStartDateWhere, genWhere } from '../utils/genWhere';
|
||||
import { idGenerator } from '../utils/idGenerator';
|
||||
@@ -498,28 +505,83 @@ export class TopicModel {
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Find messages associated with the original topic
|
||||
// Find messages associated with the original topic, ordered by createdAt
|
||||
const originalMessages = await tx
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)));
|
||||
.where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)))
|
||||
.orderBy(messages.createdAt);
|
||||
|
||||
// copy messages
|
||||
const duplicatedMessages = await Promise.all(
|
||||
originalMessages.map(async (message) => {
|
||||
const result = (await tx
|
||||
.insert(messages)
|
||||
.values({
|
||||
...message,
|
||||
clientId: null,
|
||||
id: idGenerator('messages'),
|
||||
topicId: duplicatedTopic.id,
|
||||
})
|
||||
.returning()) as DBMessageItem[];
|
||||
// Find all messagePlugins for this topic
|
||||
const messageIds = originalMessages.map((m) => m.id);
|
||||
const originalPlugins =
|
||||
messageIds.length > 0
|
||||
? await tx
|
||||
.select()
|
||||
.from(messagePlugins)
|
||||
.where(inArray(messagePlugins.id, messageIds))
|
||||
: [];
|
||||
|
||||
return result[0];
|
||||
}),
|
||||
);
|
||||
// Build oldId -> newId mapping for messages
|
||||
const idMap = new Map<string, string>();
|
||||
originalMessages.forEach((message) => {
|
||||
idMap.set(message.id, idGenerator('messages'));
|
||||
});
|
||||
|
||||
// Build oldToolId -> newToolId mapping for tools
|
||||
const toolIdMap = new Map<string, string>();
|
||||
originalMessages.forEach((message) => {
|
||||
if (message.tools && Array.isArray(message.tools)) {
|
||||
(message.tools as any[]).forEach((tool: any) => {
|
||||
if (tool.id) {
|
||||
toolIdMap.set(tool.id, `toolu_${idGenerator('messages')}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// copy messages sequentially to respect foreign key constraints
|
||||
const duplicatedMessages: DBMessageItem[] = [];
|
||||
for (const message of originalMessages) {
|
||||
const newId = idMap.get(message.id)!;
|
||||
const newParentId = message.parentId ? idMap.get(message.parentId) || null : null;
|
||||
|
||||
// Update tool IDs in tools array
|
||||
let newTools = message.tools;
|
||||
if (newTools && Array.isArray(newTools)) {
|
||||
newTools = (newTools as any[]).map((tool: any) => ({
|
||||
...tool,
|
||||
id: tool.id ? toolIdMap.get(tool.id) || tool.id : tool.id,
|
||||
}));
|
||||
}
|
||||
|
||||
const result = (await tx
|
||||
.insert(messages)
|
||||
.values({
|
||||
...message,
|
||||
clientId: null,
|
||||
id: newId,
|
||||
parentId: newParentId,
|
||||
topicId: duplicatedTopic.id,
|
||||
tools: newTools,
|
||||
})
|
||||
.returning()) as DBMessageItem[];
|
||||
|
||||
duplicatedMessages.push(result[0]);
|
||||
|
||||
// Copy messagePlugins if exists for this message
|
||||
const plugin = originalPlugins.find((p) => p.id === message.id);
|
||||
if (plugin) {
|
||||
const newToolCallId = plugin.toolCallId ? toolIdMap.get(plugin.toolCallId) || null : null;
|
||||
|
||||
await tx.insert(messagePlugins).values({
|
||||
...plugin,
|
||||
id: newId,
|
||||
clientId: null,
|
||||
toolCallId: newToolCallId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: duplicatedMessages,
|
||||
|
||||
Reference in New Issue
Block a user