@lobehub/lobehub 2.0.0-next.264 → 2.0.0-next.265

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.github/workflows/manual-build-desktop.yml +16 -37
  2. package/CHANGELOG.md +27 -0
  3. package/apps/desktop/native-deps.config.mjs +19 -3
  4. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +13 -0
  5. package/apps/desktop/src/main/utils/permissions.ts +86 -22
  6. package/changelog/v1.json +9 -0
  7. package/package.json +2 -2
  8. package/packages/database/src/models/__tests__/agent.test.ts +165 -4
  9. package/packages/database/src/models/agent.ts +46 -0
  10. package/packages/database/src/repositories/agentGroup/index.test.ts +498 -0
  11. package/packages/database/src/repositories/agentGroup/index.ts +150 -0
  12. package/packages/database/src/repositories/home/__tests__/index.test.ts +113 -1
  13. package/packages/database/src/repositories/home/index.ts +48 -67
  14. package/pnpm-workspace.yaml +1 -0
  15. package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/index.tsx +2 -2
  16. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +2 -6
  17. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +100 -0
  18. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -4
  19. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +149 -0
  20. package/src/app/[variants]/(main)/home/_layout/hooks/index.ts +0 -1
  21. package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +1 -1
  22. package/src/features/ChatInput/InputEditor/index.tsx +1 -0
  23. package/src/features/EditorCanvas/DiffAllToolbar.tsx +1 -1
  24. package/src/server/routers/lambda/agent.ts +15 -0
  25. package/src/server/routers/lambda/agentGroup.ts +16 -0
  26. package/src/services/agent.ts +11 -0
  27. package/src/services/chatGroup/index.ts +11 -0
  28. package/src/store/home/slices/sidebarUI/action.test.ts +23 -22
  29. package/src/store/home/slices/sidebarUI/action.ts +37 -9
  30. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +0 -62
  31. package/src/app/[variants]/(main)/home/_layout/hooks/useSessionItemMenuItems.tsx +0 -238
@@ -452,6 +452,52 @@ export class AgentModel {
452
452
  return result[0];
453
453
  };
454
454
 
455
+ /**
456
+ * Duplicate an agent.
457
+ * Returns the new agent ID.
458
+ */
459
+ duplicate = async (agentId: string, newTitle?: string): Promise<{ agentId: string } | null> => {
460
+ // Get the source agent
461
+ const sourceAgent = await this.db.query.agents.findFirst({
462
+ where: and(eq(agents.id, agentId), eq(agents.userId, this.userId)),
463
+ });
464
+
465
+ if (!sourceAgent) return null;
466
+
467
+ // Create new agent with explicit include fields
468
+ const [newAgent] = await this.db
469
+ .insert(agents)
470
+ .values({
471
+ avatar: sourceAgent.avatar,
472
+ backgroundColor: sourceAgent.backgroundColor,
473
+ chatConfig: sourceAgent.chatConfig,
474
+ description: sourceAgent.description,
475
+ fewShots: sourceAgent.fewShots,
476
+ model: sourceAgent.model,
477
+ openingMessage: sourceAgent.openingMessage,
478
+ openingQuestions: sourceAgent.openingQuestions,
479
+ params: sourceAgent.params,
480
+ pinned: sourceAgent.pinned,
481
+ // Config
482
+ plugins: sourceAgent.plugins,
483
+ provider: sourceAgent.provider,
484
+
485
+ // Session group
486
+ sessionGroupId: sourceAgent.sessionGroupId,
487
+ systemRole: sourceAgent.systemRole,
488
+
489
+ tags: sourceAgent.tags,
490
+ // Metadata
491
+ title: newTitle || (sourceAgent.title ? `${sourceAgent.title} (Copy)` : 'Copy'),
492
+ tts: sourceAgent.tts,
493
+ // User
494
+ userId: this.userId,
495
+ })
496
+ .returning();
497
+
498
+ return { agentId: newAgent.id };
499
+ };
500
+
455
501
  /**
456
502
  * Get a builtin agent by slug, creating it if it doesn't exist.
457
503
  * Builtin agents are standalone agents not bound to sessions.
@@ -766,4 +766,502 @@ describe('AgentGroupRepository', () => {
766
766
  expect(virtualAgents).toHaveLength(0);
767
767
  });
768
768
  });
769
+
770
+ describe('duplicate', () => {
771
+ it('should duplicate a group with all config fields', async () => {
772
+ // Create source group with full config
773
+ await serverDB.insert(chatGroups).values({
774
+ config: {
775
+ maxResponseInRow: 5,
776
+ orchestratorModel: 'gpt-4o',
777
+ orchestratorProvider: 'openai',
778
+ responseOrder: 'sequential',
779
+ scene: 'productive',
780
+ },
781
+ id: 'source-group',
782
+ pinned: true,
783
+ title: 'Source Group',
784
+ userId,
785
+ });
786
+
787
+ // Create supervisor agent
788
+ await serverDB.insert(agents).values({
789
+ id: 'source-supervisor',
790
+ model: 'gpt-4o',
791
+ provider: 'openai',
792
+ title: 'Supervisor',
793
+ userId,
794
+ virtual: true,
795
+ });
796
+
797
+ // Link supervisor to group
798
+ await serverDB.insert(chatGroupsAgents).values({
799
+ agentId: 'source-supervisor',
800
+ chatGroupId: 'source-group',
801
+ order: -1,
802
+ role: 'supervisor',
803
+ userId,
804
+ });
805
+
806
+ const result = await agentGroupRepo.duplicate('source-group');
807
+
808
+ expect(result).not.toBeNull();
809
+ expect(result!.groupId).toBeDefined();
810
+ expect(result!.supervisorAgentId).toBeDefined();
811
+ expect(result!.groupId).not.toBe('source-group');
812
+ expect(result!.supervisorAgentId).not.toBe('source-supervisor');
813
+
814
+ // Verify duplicated group has correct config
815
+ const duplicatedGroup = await serverDB.query.chatGroups.findFirst({
816
+ where: (cg, { eq }) => eq(cg.id, result!.groupId),
817
+ });
818
+
819
+ expect(duplicatedGroup).toEqual(
820
+ expect.objectContaining({
821
+ config: {
822
+ maxResponseInRow: 5,
823
+ orchestratorModel: 'gpt-4o',
824
+ orchestratorProvider: 'openai',
825
+ responseOrder: 'sequential',
826
+ scene: 'productive',
827
+ },
828
+ pinned: true,
829
+ title: 'Source Group (Copy)',
830
+ userId,
831
+ }),
832
+ );
833
+ });
834
+
835
+ it('should duplicate group with custom title', async () => {
836
+ await serverDB.insert(chatGroups).values({
837
+ id: 'title-group',
838
+ title: 'Original Title',
839
+ userId,
840
+ });
841
+
842
+ await serverDB.insert(agents).values({
843
+ id: 'title-supervisor',
844
+ title: 'Supervisor',
845
+ userId,
846
+ virtual: true,
847
+ });
848
+
849
+ await serverDB.insert(chatGroupsAgents).values({
850
+ agentId: 'title-supervisor',
851
+ chatGroupId: 'title-group',
852
+ order: -1,
853
+ role: 'supervisor',
854
+ userId,
855
+ });
856
+
857
+ const result = await agentGroupRepo.duplicate('title-group', 'Custom New Title');
858
+
859
+ expect(result).not.toBeNull();
860
+
861
+ const duplicatedGroup = await serverDB.query.chatGroups.findFirst({
862
+ where: (cg, { eq }) => eq(cg.id, result!.groupId),
863
+ });
864
+
865
+ expect(duplicatedGroup!.title).toBe('Custom New Title');
866
+ });
867
+
868
+ it('should copy virtual member agents (create new agents)', async () => {
869
+ // Create source group
870
+ await serverDB.insert(chatGroups).values({
871
+ id: 'virtual-member-group',
872
+ title: 'Virtual Member Group',
873
+ userId,
874
+ });
875
+
876
+ // Create supervisor and virtual member agents
877
+ await serverDB.insert(agents).values([
878
+ {
879
+ id: 'vm-supervisor',
880
+ title: 'Supervisor',
881
+ userId,
882
+ virtual: true,
883
+ },
884
+ {
885
+ avatar: 'virtual-avatar.png',
886
+ backgroundColor: '#ff0000',
887
+ description: 'Virtual member description',
888
+ id: 'vm-virtual-member',
889
+ model: 'gpt-4',
890
+ provider: 'openai',
891
+ systemRole: 'You are a virtual assistant',
892
+ tags: ['tag1', 'tag2'],
893
+ title: 'Virtual Member',
894
+ userId,
895
+ virtual: true,
896
+ },
897
+ ]);
898
+
899
+ // Link agents to group
900
+ await serverDB.insert(chatGroupsAgents).values([
901
+ {
902
+ agentId: 'vm-supervisor',
903
+ chatGroupId: 'virtual-member-group',
904
+ order: -1,
905
+ role: 'supervisor',
906
+ userId,
907
+ },
908
+ {
909
+ agentId: 'vm-virtual-member',
910
+ chatGroupId: 'virtual-member-group',
911
+ enabled: true,
912
+ order: 0,
913
+ role: 'participant',
914
+ userId,
915
+ },
916
+ ]);
917
+
918
+ const result = await agentGroupRepo.duplicate('virtual-member-group');
919
+
920
+ expect(result).not.toBeNull();
921
+
922
+ // Verify new group has agents
923
+ const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
924
+ where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
925
+ });
926
+
927
+ // 1 supervisor + 1 virtual member
928
+ expect(groupAgents).toHaveLength(2);
929
+
930
+ // Verify virtual member agent was copied (new agent created)
931
+ const virtualMemberRelation = groupAgents.find(
932
+ (ga) => ga.role === 'participant' && ga.agentId !== 'vm-virtual-member',
933
+ );
934
+ expect(virtualMemberRelation).toBeDefined();
935
+
936
+ // Verify copied agent has all fields
937
+ const copiedAgent = await serverDB.query.agents.findFirst({
938
+ where: (a, { eq }) => eq(a.id, virtualMemberRelation!.agentId),
939
+ });
940
+
941
+ expect(copiedAgent).toEqual(
942
+ expect.objectContaining({
943
+ avatar: 'virtual-avatar.png',
944
+ backgroundColor: '#ff0000',
945
+ description: 'Virtual member description',
946
+ model: 'gpt-4',
947
+ provider: 'openai',
948
+ systemRole: 'You are a virtual assistant',
949
+ tags: ['tag1', 'tag2'],
950
+ title: 'Virtual Member',
951
+ userId,
952
+ virtual: true,
953
+ }),
954
+ );
955
+
956
+ // Verify original virtual member still exists
957
+ const originalAgent = await serverDB.query.agents.findFirst({
958
+ where: (a, { eq }) => eq(a.id, 'vm-virtual-member'),
959
+ });
960
+ expect(originalAgent).toBeDefined();
961
+ });
962
+
963
+ it('should reference non-virtual member agents (only add relationship)', async () => {
964
+ // Create source group
965
+ await serverDB.insert(chatGroups).values({
966
+ id: 'nonvirtual-member-group',
967
+ title: 'Non-Virtual Member Group',
968
+ userId,
969
+ });
970
+
971
+ // Create supervisor and non-virtual member agents
972
+ await serverDB.insert(agents).values([
973
+ {
974
+ id: 'nvm-supervisor',
975
+ title: 'Supervisor',
976
+ userId,
977
+ virtual: true,
978
+ },
979
+ {
980
+ description: 'Regular agent description',
981
+ id: 'nvm-regular-member',
982
+ model: 'claude-3-opus',
983
+ provider: 'anthropic',
984
+ title: 'Regular Member',
985
+ userId,
986
+ virtual: false,
987
+ },
988
+ ]);
989
+
990
+ // Link agents to group
991
+ await serverDB.insert(chatGroupsAgents).values([
992
+ {
993
+ agentId: 'nvm-supervisor',
994
+ chatGroupId: 'nonvirtual-member-group',
995
+ order: -1,
996
+ role: 'supervisor',
997
+ userId,
998
+ },
999
+ {
1000
+ agentId: 'nvm-regular-member',
1001
+ chatGroupId: 'nonvirtual-member-group',
1002
+ enabled: true,
1003
+ order: 0,
1004
+ role: 'participant',
1005
+ userId,
1006
+ },
1007
+ ]);
1008
+
1009
+ const result = await agentGroupRepo.duplicate('nonvirtual-member-group');
1010
+
1011
+ expect(result).not.toBeNull();
1012
+
1013
+ // Verify new group has agents
1014
+ const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
1015
+ where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
1016
+ });
1017
+
1018
+ // 1 supervisor + 1 non-virtual member
1019
+ expect(groupAgents).toHaveLength(2);
1020
+
1021
+ // Verify non-virtual member uses the SAME agent ID (just added relationship)
1022
+ const regularMemberRelation = groupAgents.find((ga) => ga.agentId === 'nvm-regular-member');
1023
+ expect(regularMemberRelation).toBeDefined();
1024
+ expect(regularMemberRelation!.role).toBe('participant');
1025
+ expect(regularMemberRelation!.enabled).toBe(true);
1026
+
1027
+ // Verify no new agent was created for the regular member
1028
+ const allAgentsWithTitle = await serverDB.query.agents.findMany({
1029
+ where: (a, { and, eq }) => and(eq(a.userId, userId), eq(a.title, 'Regular Member')),
1030
+ });
1031
+ // Should only have the original one
1032
+ expect(allAgentsWithTitle).toHaveLength(1);
1033
+ expect(allAgentsWithTitle[0].id).toBe('nvm-regular-member');
1034
+ });
1035
+
1036
+ it('should handle mixed virtual and non-virtual members', async () => {
1037
+ // Create source group
1038
+ await serverDB.insert(chatGroups).values({
1039
+ id: 'mixed-member-group',
1040
+ title: 'Mixed Member Group',
1041
+ userId,
1042
+ });
1043
+
1044
+ // Create supervisor, virtual member, and non-virtual member agents
1045
+ await serverDB.insert(agents).values([
1046
+ { id: 'mixed-supervisor', title: 'Supervisor', userId, virtual: true },
1047
+ { id: 'mixed-virtual', title: 'Virtual Agent', userId, virtual: true },
1048
+ { id: 'mixed-regular', title: 'Regular Agent', userId, virtual: false },
1049
+ ]);
1050
+
1051
+ // Link agents to group
1052
+ await serverDB.insert(chatGroupsAgents).values([
1053
+ {
1054
+ agentId: 'mixed-supervisor',
1055
+ chatGroupId: 'mixed-member-group',
1056
+ order: -1,
1057
+ role: 'supervisor',
1058
+ userId,
1059
+ },
1060
+ {
1061
+ agentId: 'mixed-virtual',
1062
+ chatGroupId: 'mixed-member-group',
1063
+ order: 0,
1064
+ role: 'participant',
1065
+ userId,
1066
+ },
1067
+ {
1068
+ agentId: 'mixed-regular',
1069
+ chatGroupId: 'mixed-member-group',
1070
+ order: 1,
1071
+ role: 'participant',
1072
+ userId,
1073
+ },
1074
+ ]);
1075
+
1076
+ const result = await agentGroupRepo.duplicate('mixed-member-group');
1077
+
1078
+ expect(result).not.toBeNull();
1079
+
1080
+ const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
1081
+ where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
1082
+ });
1083
+
1084
+ // 1 supervisor + 1 virtual (copied) + 1 non-virtual (referenced)
1085
+ expect(groupAgents).toHaveLength(3);
1086
+
1087
+ // Verify non-virtual member references original agent
1088
+ const regularRelation = groupAgents.find((ga) => ga.agentId === 'mixed-regular');
1089
+ expect(regularRelation).toBeDefined();
1090
+
1091
+ // Verify virtual member was copied (new agent ID)
1092
+ const virtualRelation = groupAgents.find(
1093
+ (ga) => ga.role === 'participant' && ga.agentId !== 'mixed-regular',
1094
+ );
1095
+ expect(virtualRelation).toBeDefined();
1096
+ expect(virtualRelation!.agentId).not.toBe('mixed-virtual');
1097
+ });
1098
+
1099
+ it('should return null for non-existent group', async () => {
1100
+ const result = await agentGroupRepo.duplicate('non-existent-group');
1101
+
1102
+ expect(result).toBeNull();
1103
+ });
1104
+
1105
+ it('should not duplicate group belonging to another user', async () => {
1106
+ // Create group for other user
1107
+ await serverDB.insert(chatGroups).values({
1108
+ id: 'other-user-dup-group',
1109
+ title: 'Other User Group',
1110
+ userId: otherUserId,
1111
+ });
1112
+
1113
+ const result = await agentGroupRepo.duplicate('other-user-dup-group');
1114
+
1115
+ expect(result).toBeNull();
1116
+ });
1117
+
1118
+ it('should preserve member order in duplicated group', async () => {
1119
+ // Create source group
1120
+ await serverDB.insert(chatGroups).values({
1121
+ id: 'order-group',
1122
+ title: 'Order Group',
1123
+ userId,
1124
+ });
1125
+
1126
+ // Create agents
1127
+ await serverDB.insert(agents).values([
1128
+ { id: 'order-supervisor', title: 'Supervisor', userId, virtual: true },
1129
+ { id: 'order-agent-1', title: 'Agent 1', userId, virtual: false },
1130
+ { id: 'order-agent-2', title: 'Agent 2', userId, virtual: false },
1131
+ { id: 'order-agent-3', title: 'Agent 3', userId, virtual: false },
1132
+ ]);
1133
+
1134
+ // Link agents with specific order
1135
+ await serverDB.insert(chatGroupsAgents).values([
1136
+ {
1137
+ agentId: 'order-supervisor',
1138
+ chatGroupId: 'order-group',
1139
+ order: -1,
1140
+ role: 'supervisor',
1141
+ userId,
1142
+ },
1143
+ {
1144
+ agentId: 'order-agent-1',
1145
+ chatGroupId: 'order-group',
1146
+ order: 2,
1147
+ role: 'participant',
1148
+ userId,
1149
+ },
1150
+ {
1151
+ agentId: 'order-agent-2',
1152
+ chatGroupId: 'order-group',
1153
+ order: 0,
1154
+ role: 'participant',
1155
+ userId,
1156
+ },
1157
+ {
1158
+ agentId: 'order-agent-3',
1159
+ chatGroupId: 'order-group',
1160
+ order: 1,
1161
+ role: 'participant',
1162
+ userId,
1163
+ },
1164
+ ]);
1165
+
1166
+ const result = await agentGroupRepo.duplicate('order-group');
1167
+
1168
+ expect(result).not.toBeNull();
1169
+
1170
+ const groupAgents = await serverDB.query.chatGroupsAgents.findMany({
1171
+ where: (cga, { eq }) => eq(cga.chatGroupId, result!.groupId),
1172
+ });
1173
+
1174
+ // Verify order is preserved
1175
+ const supervisorRelation = groupAgents.find((ga) => ga.role === 'supervisor');
1176
+ expect(supervisorRelation!.order).toBe(-1);
1177
+
1178
+ const agent1Relation = groupAgents.find((ga) => ga.agentId === 'order-agent-1');
1179
+ expect(agent1Relation!.order).toBe(2);
1180
+
1181
+ const agent2Relation = groupAgents.find((ga) => ga.agentId === 'order-agent-2');
1182
+ expect(agent2Relation!.order).toBe(0);
1183
+
1184
+ const agent3Relation = groupAgents.find((ga) => ga.agentId === 'order-agent-3');
1185
+ expect(agent3Relation!.order).toBe(1);
1186
+ });
1187
+
1188
+ it('should duplicate group with default title when source has no title', async () => {
1189
+ // Create source group without title
1190
+ await serverDB.insert(chatGroups).values({
1191
+ id: 'no-title-group',
1192
+ title: null,
1193
+ userId,
1194
+ });
1195
+
1196
+ await serverDB.insert(agents).values({
1197
+ id: 'no-title-supervisor',
1198
+ title: 'Supervisor',
1199
+ userId,
1200
+ virtual: true,
1201
+ });
1202
+
1203
+ await serverDB.insert(chatGroupsAgents).values({
1204
+ agentId: 'no-title-supervisor',
1205
+ chatGroupId: 'no-title-group',
1206
+ order: -1,
1207
+ role: 'supervisor',
1208
+ userId,
1209
+ });
1210
+
1211
+ const result = await agentGroupRepo.duplicate('no-title-group');
1212
+
1213
+ expect(result).not.toBeNull();
1214
+
1215
+ const duplicatedGroup = await serverDB.query.chatGroups.findFirst({
1216
+ where: (cg, { eq }) => eq(cg.id, result!.groupId),
1217
+ });
1218
+
1219
+ expect(duplicatedGroup!.title).toBe('Copy');
1220
+ });
1221
+
1222
+ it('should create new supervisor agent with source supervisor config', async () => {
1223
+ // Create source group
1224
+ await serverDB.insert(chatGroups).values({
1225
+ id: 'supervisor-config-group',
1226
+ title: 'Supervisor Config Group',
1227
+ userId,
1228
+ });
1229
+
1230
+ // Create supervisor with specific config
1231
+ await serverDB.insert(agents).values({
1232
+ id: 'source-supervisor-with-config',
1233
+ model: 'claude-3-opus',
1234
+ provider: 'anthropic',
1235
+ title: 'Custom Supervisor',
1236
+ userId,
1237
+ virtual: true,
1238
+ });
1239
+
1240
+ await serverDB.insert(chatGroupsAgents).values({
1241
+ agentId: 'source-supervisor-with-config',
1242
+ chatGroupId: 'supervisor-config-group',
1243
+ order: -1,
1244
+ role: 'supervisor',
1245
+ userId,
1246
+ });
1247
+
1248
+ const result = await agentGroupRepo.duplicate('supervisor-config-group');
1249
+
1250
+ expect(result).not.toBeNull();
1251
+
1252
+ // Verify new supervisor has same config
1253
+ const newSupervisor = await serverDB.query.agents.findFirst({
1254
+ where: (a, { eq }) => eq(a.id, result!.supervisorAgentId),
1255
+ });
1256
+
1257
+ expect(newSupervisor).toEqual(
1258
+ expect.objectContaining({
1259
+ model: 'claude-3-opus',
1260
+ provider: 'anthropic',
1261
+ title: 'Custom Supervisor',
1262
+ virtual: true,
1263
+ }),
1264
+ );
1265
+ });
1266
+ });
769
1267
  });
@@ -304,4 +304,154 @@ export class AgentGroupRepository {
304
304
  removedFromGroup: agentIds.length,
305
305
  };
306
306
  }
307
+
308
+ /**
309
+ * Duplicate a chat group with all its members.
310
+ * - Creates a new group with the same config
311
+ * - Creates a new supervisor agent
312
+ * - For virtual member agents: creates new copies
313
+ * - For non-virtual member agents: adds relationship only (references same agents)
314
+ *
315
+ * @param groupId - The chat group ID to duplicate
316
+ * @param newTitle - Optional new title for the duplicated group
317
+ * @returns The new group ID and supervisor agent ID, or null if source not found
318
+ */
319
+ async duplicate(
320
+ groupId: string,
321
+ newTitle?: string,
322
+ ): Promise<{ groupId: string; supervisorAgentId: string } | null> {
323
+ // 1. Get the source group
324
+ const sourceGroup = await this.db.query.chatGroups.findFirst({
325
+ where: and(eq(chatGroups.id, groupId), eq(chatGroups.userId, this.userId)),
326
+ });
327
+
328
+ if (!sourceGroup) return null;
329
+
330
+ // 2. Get all agents in the group with their details
331
+ const groupAgentsWithDetails = await this.db
332
+ .select({
333
+ agent: agents,
334
+ enabled: chatGroupsAgents.enabled,
335
+ order: chatGroupsAgents.order,
336
+ role: chatGroupsAgents.role,
337
+ })
338
+ .from(chatGroupsAgents)
339
+ .innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
340
+ .where(eq(chatGroupsAgents.chatGroupId, groupId))
341
+ .orderBy(chatGroupsAgents.order);
342
+
343
+ // 3. Separate supervisor, virtual members, and non-virtual members
344
+ let sourceSupervisor: (typeof groupAgentsWithDetails)[number] | undefined;
345
+ const virtualMembers: (typeof groupAgentsWithDetails)[number][] = [];
346
+ const nonVirtualMembers: (typeof groupAgentsWithDetails)[number][] = [];
347
+
348
+ for (const row of groupAgentsWithDetails) {
349
+ if (row.role === 'supervisor') {
350
+ sourceSupervisor = row;
351
+ } else if (row.agent.virtual) {
352
+ virtualMembers.push(row);
353
+ } else {
354
+ nonVirtualMembers.push(row);
355
+ }
356
+ }
357
+
358
+ // Use transaction to ensure atomicity
359
+ return this.db.transaction(async (trx) => {
360
+ // 4. Create the new group
361
+ const [newGroup] = await trx
362
+ .insert(chatGroups)
363
+ .values({
364
+ config: sourceGroup.config,
365
+ pinned: sourceGroup.pinned,
366
+ title: newTitle || (sourceGroup.title ? `${sourceGroup.title} (Copy)` : 'Copy'),
367
+ userId: this.userId,
368
+ })
369
+ .returning();
370
+
371
+ // 5. Create new supervisor agent
372
+ const supervisorAgent = sourceSupervisor?.agent;
373
+ const [newSupervisor] = await trx
374
+ .insert(agents)
375
+ .values({
376
+ model: supervisorAgent?.model,
377
+ provider: supervisorAgent?.provider,
378
+ title: supervisorAgent?.title || 'Supervisor',
379
+ userId: this.userId,
380
+ virtual: true,
381
+ })
382
+ .returning();
383
+
384
+ // 6. Create copies of virtual member agents using include mode
385
+ const newVirtualAgentMap = new Map<string, string>(); // oldId -> newId
386
+ if (virtualMembers.length > 0) {
387
+ const virtualAgentConfigs = virtualMembers.map((member) => ({
388
+ // Metadata
389
+ avatar: member.agent.avatar,
390
+ backgroundColor: member.agent.backgroundColor,
391
+ // Config
392
+ chatConfig: member.agent.chatConfig,
393
+ description: member.agent.description,
394
+ fewShots: member.agent.fewShots,
395
+
396
+ model: member.agent.model,
397
+ openingMessage: member.agent.openingMessage,
398
+ openingQuestions: member.agent.openingQuestions,
399
+ params: member.agent.params,
400
+ plugins: member.agent.plugins,
401
+ provider: member.agent.provider,
402
+ systemRole: member.agent.systemRole,
403
+ tags: member.agent.tags,
404
+ title: member.agent.title,
405
+ tts: member.agent.tts,
406
+ // User & virtual flag
407
+ userId: this.userId,
408
+ virtual: true,
409
+ }));
410
+
411
+ const newVirtualAgents = await trx.insert(agents).values(virtualAgentConfigs).returning();
412
+
413
+ // Map old agent IDs to new agent IDs
414
+ for (const [i, virtualMember] of virtualMembers.entries()) {
415
+ newVirtualAgentMap.set(virtualMember.agent.id, newVirtualAgents[i].id);
416
+ }
417
+ }
418
+
419
+ // 7. Create group-agent relationships
420
+ const groupAgentValues: NewChatGroupAgent[] = [
421
+ // Supervisor
422
+ {
423
+ agentId: newSupervisor.id,
424
+ chatGroupId: newGroup.id,
425
+ order: -1,
426
+ role: 'supervisor',
427
+ userId: this.userId,
428
+ },
429
+ // Virtual members (using new copied agents)
430
+ ...virtualMembers.map((member) => ({
431
+ agentId: newVirtualAgentMap.get(member.agent.id)!,
432
+ chatGroupId: newGroup.id,
433
+ enabled: member.enabled,
434
+ order: member.order,
435
+ role: member.role || 'participant',
436
+ userId: this.userId,
437
+ })),
438
+ // Non-virtual members (referencing same agents - only add relationship)
439
+ ...nonVirtualMembers.map((member) => ({
440
+ agentId: member.agent.id,
441
+ chatGroupId: newGroup.id,
442
+ enabled: member.enabled,
443
+ order: member.order,
444
+ role: member.role || 'participant',
445
+ userId: this.userId,
446
+ })),
447
+ ];
448
+
449
+ await trx.insert(chatGroupsAgents).values(groupAgentValues);
450
+
451
+ return {
452
+ groupId: newGroup.id,
453
+ supervisorAgentId: newSupervisor.id,
454
+ };
455
+ });
456
+ }
307
457
  }