🐛 fix: fixed the group topic copy not right (#11730)

fix: update the group topic copy way
This commit is contained in:
Shinji-Li
2026-01-23 17:12:23 +08:00
committed by GitHub
parent e3046c7166
commit 282c1fb128
2 changed files with 204 additions and 19 deletions

View File

@@ -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';

View File

@@ -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,