@lobehub/lobehub 2.0.0-next.264 → 2.0.0-next.266
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.
- package/.github/workflows/manual-build-desktop.yml +16 -37
- package/CHANGELOG.md +52 -0
- package/apps/desktop/native-deps.config.mjs +19 -3
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +13 -0
- package/apps/desktop/src/main/utils/permissions.ts +86 -22
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +1 -0
- package/locales/ar/modelProvider.json +20 -0
- package/locales/ar/models.json +33 -10
- package/locales/ar/plugin.json +1 -0
- package/locales/ar/providers.json +1 -0
- package/locales/ar/setting.json +2 -0
- package/locales/bg-BG/chat.json +1 -0
- package/locales/bg-BG/modelProvider.json +20 -0
- package/locales/bg-BG/models.json +27 -7
- package/locales/bg-BG/plugin.json +1 -0
- package/locales/bg-BG/providers.json +1 -0
- package/locales/bg-BG/setting.json +2 -0
- package/locales/de-DE/chat.json +1 -0
- package/locales/de-DE/modelProvider.json +20 -0
- package/locales/de-DE/models.json +44 -10
- package/locales/de-DE/plugin.json +1 -0
- package/locales/de-DE/providers.json +1 -0
- package/locales/de-DE/setting.json +2 -0
- package/locales/en-US/chat.json +1 -0
- package/locales/en-US/modelProvider.json +20 -0
- package/locales/en-US/models.json +10 -10
- package/locales/en-US/providers.json +1 -0
- package/locales/en-US/setting.json +2 -1
- package/locales/es-ES/chat.json +1 -0
- package/locales/es-ES/modelProvider.json +20 -0
- package/locales/es-ES/models.json +53 -10
- package/locales/es-ES/plugin.json +1 -0
- package/locales/es-ES/providers.json +1 -0
- package/locales/es-ES/setting.json +2 -0
- package/locales/fa-IR/chat.json +1 -0
- package/locales/fa-IR/modelProvider.json +20 -0
- package/locales/fa-IR/models.json +33 -10
- package/locales/fa-IR/plugin.json +1 -0
- package/locales/fa-IR/providers.json +1 -0
- package/locales/fa-IR/setting.json +2 -0
- package/locales/fr-FR/chat.json +1 -0
- package/locales/fr-FR/modelProvider.json +20 -0
- package/locales/fr-FR/models.json +27 -7
- package/locales/fr-FR/plugin.json +1 -0
- package/locales/fr-FR/providers.json +1 -0
- package/locales/fr-FR/setting.json +2 -0
- package/locales/it-IT/chat.json +1 -0
- package/locales/it-IT/modelProvider.json +20 -0
- package/locales/it-IT/models.json +10 -10
- package/locales/it-IT/plugin.json +1 -0
- package/locales/it-IT/providers.json +1 -0
- package/locales/it-IT/setting.json +2 -0
- package/locales/ja-JP/chat.json +1 -0
- package/locales/ja-JP/modelProvider.json +20 -0
- package/locales/ja-JP/models.json +5 -10
- package/locales/ja-JP/plugin.json +1 -0
- package/locales/ja-JP/providers.json +1 -0
- package/locales/ja-JP/setting.json +2 -0
- package/locales/ko-KR/chat.json +1 -0
- package/locales/ko-KR/modelProvider.json +20 -0
- package/locales/ko-KR/models.json +36 -10
- package/locales/ko-KR/plugin.json +1 -0
- package/locales/ko-KR/providers.json +1 -0
- package/locales/ko-KR/setting.json +2 -0
- package/locales/nl-NL/chat.json +1 -0
- package/locales/nl-NL/modelProvider.json +20 -0
- package/locales/nl-NL/models.json +35 -4
- package/locales/nl-NL/plugin.json +1 -0
- package/locales/nl-NL/providers.json +1 -0
- package/locales/nl-NL/setting.json +2 -0
- package/locales/pl-PL/chat.json +1 -0
- package/locales/pl-PL/modelProvider.json +20 -0
- package/locales/pl-PL/models.json +37 -7
- package/locales/pl-PL/plugin.json +1 -0
- package/locales/pl-PL/providers.json +1 -0
- package/locales/pl-PL/setting.json +2 -0
- package/locales/pt-BR/chat.json +1 -0
- package/locales/pt-BR/modelProvider.json +20 -0
- package/locales/pt-BR/models.json +51 -9
- package/locales/pt-BR/plugin.json +1 -0
- package/locales/pt-BR/providers.json +1 -0
- package/locales/pt-BR/setting.json +2 -0
- package/locales/ru-RU/chat.json +1 -0
- package/locales/ru-RU/modelProvider.json +20 -0
- package/locales/ru-RU/models.json +48 -7
- package/locales/ru-RU/plugin.json +1 -0
- package/locales/ru-RU/providers.json +1 -0
- package/locales/ru-RU/setting.json +2 -0
- package/locales/tr-TR/chat.json +1 -0
- package/locales/tr-TR/modelProvider.json +20 -0
- package/locales/tr-TR/models.json +48 -7
- package/locales/tr-TR/plugin.json +1 -0
- package/locales/tr-TR/providers.json +1 -0
- package/locales/tr-TR/setting.json +2 -0
- package/locales/vi-VN/chat.json +1 -0
- package/locales/vi-VN/modelProvider.json +20 -0
- package/locales/vi-VN/models.json +5 -5
- package/locales/vi-VN/plugin.json +1 -0
- package/locales/vi-VN/providers.json +1 -0
- package/locales/vi-VN/setting.json +2 -0
- package/locales/zh-CN/modelProvider.json +20 -20
- package/locales/zh-CN/models.json +49 -8
- package/locales/zh-CN/providers.json +1 -0
- package/locales/zh-CN/setting.json +2 -1
- package/locales/zh-TW/chat.json +1 -0
- package/locales/zh-TW/modelProvider.json +20 -0
- package/locales/zh-TW/models.json +29 -10
- package/locales/zh-TW/plugin.json +1 -0
- package/locales/zh-TW/providers.json +1 -0
- package/locales/zh-TW/setting.json +2 -0
- package/package.json +2 -2
- package/packages/database/src/models/__tests__/agent.test.ts +165 -4
- package/packages/database/src/models/agent.ts +46 -0
- package/packages/database/src/repositories/agentGroup/index.test.ts +498 -0
- package/packages/database/src/repositories/agentGroup/index.ts +150 -0
- package/packages/database/src/repositories/home/__tests__/index.test.ts +113 -1
- package/packages/database/src/repositories/home/index.ts +48 -67
- package/pnpm-workspace.yaml +1 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Body.tsx +1 -1
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +84 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/CronTopicItem.tsx +1 -1
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/index.tsx +23 -33
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/Editing.tsx +12 -49
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/index.tsx +3 -1
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Editing.tsx +12 -40
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts +5 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/index.tsx +2 -2
- package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/CronJobCards.tsx +1 -1
- package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/CronJobForm.tsx +1 -1
- package/src/app/[variants]/(main)/group/_layout/Sidebar/AddGroupMemberModal/AvailableAgentList.tsx +0 -1
- package/src/app/[variants]/(main)/group/_layout/Sidebar/AddGroupMemberModal/index.tsx +5 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +2 -6
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +100 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -4
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +149 -0
- package/src/app/[variants]/(main)/home/_layout/hooks/index.ts +0 -1
- package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +1 -1
- package/src/components/InlineRename/index.tsx +121 -0
- package/src/features/ChatInput/InputEditor/index.tsx +1 -0
- package/src/features/EditorCanvas/DiffAllToolbar.tsx +1 -1
- package/src/features/NavPanel/components/NavItem.tsx +1 -1
- package/src/locales/default/setting.ts +2 -0
- package/src/server/routers/lambda/agent.ts +15 -0
- package/src/server/routers/lambda/agentGroup.ts +16 -0
- package/src/services/agent.ts +11 -0
- package/src/services/chatGroup/index.ts +11 -0
- package/src/store/agent/slices/cron/action.ts +108 -0
- package/src/store/agent/slices/cron/index.ts +1 -0
- package/src/store/agent/store.ts +3 -0
- package/src/store/home/slices/sidebarUI/action.test.ts +23 -22
- package/src/store/home/slices/sidebarUI/action.ts +37 -9
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +0 -74
- package/src/app/[variants]/(main)/group/features/ChangelogModal.tsx +0 -11
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +0 -62
- package/src/app/[variants]/(main)/home/_layout/hooks/useSessionItemMenuItems.tsx +0 -238
- package/src/hooks/useFetchCronTopicsWithJobInfo.ts +0 -56
|
@@ -36,6 +36,86 @@ describe('HomeRepository', () => {
|
|
|
36
36
|
expect(result.groups).toEqual([]);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it('should return non-virtual agents without agentsToSessions relationship', async () => {
|
|
40
|
+
// Create an agent without session relationship (e.g., duplicated agent)
|
|
41
|
+
const agentId = 'standalone-agent';
|
|
42
|
+
|
|
43
|
+
await clientDB.insert(Schema.agents).values({
|
|
44
|
+
id: agentId,
|
|
45
|
+
userId,
|
|
46
|
+
title: 'Standalone Agent',
|
|
47
|
+
description: 'Agent without session',
|
|
48
|
+
pinned: false,
|
|
49
|
+
virtual: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = await homeRepo.getSidebarAgentList();
|
|
53
|
+
|
|
54
|
+
// Agent should appear in ungrouped list even without agentsToSessions
|
|
55
|
+
expect(result.ungrouped).toHaveLength(1);
|
|
56
|
+
expect(result.ungrouped[0].id).toBe(agentId);
|
|
57
|
+
expect(result.ungrouped[0].title).toBe('Standalone Agent');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return pinned non-virtual agents without agentsToSessions relationship', async () => {
|
|
61
|
+
// Create a pinned agent without session relationship
|
|
62
|
+
const agentId = 'pinned-standalone';
|
|
63
|
+
|
|
64
|
+
await clientDB.insert(Schema.agents).values({
|
|
65
|
+
id: agentId,
|
|
66
|
+
userId,
|
|
67
|
+
title: 'Pinned Standalone Agent',
|
|
68
|
+
pinned: true,
|
|
69
|
+
virtual: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await homeRepo.getSidebarAgentList();
|
|
73
|
+
|
|
74
|
+
// Agent should appear in pinned list
|
|
75
|
+
expect(result.pinned).toHaveLength(1);
|
|
76
|
+
expect(result.pinned[0].id).toBe(agentId);
|
|
77
|
+
expect(result.pinned[0].pinned).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return mixed agents with and without session relationships', async () => {
|
|
81
|
+
// Agent with session
|
|
82
|
+
await clientDB.transaction(async (tx) => {
|
|
83
|
+
await tx.insert(Schema.agents).values({
|
|
84
|
+
id: 'with-session',
|
|
85
|
+
userId,
|
|
86
|
+
title: 'Agent With Session',
|
|
87
|
+
pinned: false,
|
|
88
|
+
virtual: false,
|
|
89
|
+
});
|
|
90
|
+
await tx.insert(Schema.sessions).values({
|
|
91
|
+
id: 'session-1',
|
|
92
|
+
slug: 'session-1',
|
|
93
|
+
userId,
|
|
94
|
+
});
|
|
95
|
+
await tx.insert(Schema.agentsToSessions).values({
|
|
96
|
+
agentId: 'with-session',
|
|
97
|
+
sessionId: 'session-1',
|
|
98
|
+
userId,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Agent without session (e.g., duplicated)
|
|
103
|
+
await clientDB.insert(Schema.agents).values({
|
|
104
|
+
id: 'without-session',
|
|
105
|
+
userId,
|
|
106
|
+
title: 'Agent Without Session',
|
|
107
|
+
pinned: false,
|
|
108
|
+
virtual: false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const result = await homeRepo.getSidebarAgentList();
|
|
112
|
+
|
|
113
|
+
// Both agents should appear
|
|
114
|
+
expect(result.ungrouped).toHaveLength(2);
|
|
115
|
+
expect(result.ungrouped.map((a) => a.id)).toContain('with-session');
|
|
116
|
+
expect(result.ungrouped.map((a) => a.id)).toContain('without-session');
|
|
117
|
+
});
|
|
118
|
+
|
|
39
119
|
it('should return agents with pinned status from agents table', async () => {
|
|
40
120
|
// Create an agent with pinned=true
|
|
41
121
|
const agentId = 'agent-1';
|
|
@@ -380,6 +460,16 @@ describe('HomeRepository', () => {
|
|
|
380
460
|
sessionId: 'session-search-unpinned',
|
|
381
461
|
userId,
|
|
382
462
|
});
|
|
463
|
+
|
|
464
|
+
// Agent without session (e.g., duplicated agent)
|
|
465
|
+
await tx.insert(Schema.agents).values({
|
|
466
|
+
id: 'search-standalone',
|
|
467
|
+
userId,
|
|
468
|
+
title: 'Standalone Searchable Agent',
|
|
469
|
+
description: 'A standalone agent without session',
|
|
470
|
+
pinned: false,
|
|
471
|
+
virtual: false,
|
|
472
|
+
});
|
|
383
473
|
});
|
|
384
474
|
});
|
|
385
475
|
|
|
@@ -388,6 +478,24 @@ describe('HomeRepository', () => {
|
|
|
388
478
|
expect(result).toEqual([]);
|
|
389
479
|
});
|
|
390
480
|
|
|
481
|
+
it('should search agents without agentsToSessions relationship', async () => {
|
|
482
|
+
const result = await homeRepo.searchAgents('Standalone');
|
|
483
|
+
|
|
484
|
+
expect(result).toHaveLength(1);
|
|
485
|
+
expect(result[0].id).toBe('search-standalone');
|
|
486
|
+
expect(result[0].title).toBe('Standalone Searchable Agent');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should search and return mixed agents with and without session relationships', async () => {
|
|
490
|
+
// Search for "Searchable" should return all 3 agents
|
|
491
|
+
const result = await homeRepo.searchAgents('Searchable');
|
|
492
|
+
|
|
493
|
+
expect(result).toHaveLength(3);
|
|
494
|
+
expect(result.map((a) => a.id)).toContain('search-pinned');
|
|
495
|
+
expect(result.map((a) => a.id)).toContain('search-unpinned');
|
|
496
|
+
expect(result.map((a) => a.id)).toContain('search-standalone');
|
|
497
|
+
});
|
|
498
|
+
|
|
391
499
|
it('should search agents by title and return correct pinned status', async () => {
|
|
392
500
|
const result = await homeRepo.searchAgents('Searchable Pinned');
|
|
393
501
|
|
|
@@ -407,15 +515,19 @@ describe('HomeRepository', () => {
|
|
|
407
515
|
it('should return multiple matching agents with correct pinned status', async () => {
|
|
408
516
|
const result = await homeRepo.searchAgents('Searchable');
|
|
409
517
|
|
|
410
|
-
|
|
518
|
+
// 3 agents: search-pinned, search-unpinned, search-standalone
|
|
519
|
+
expect(result).toHaveLength(3);
|
|
411
520
|
|
|
412
521
|
const pinnedAgent = result.find((a) => a.id === 'search-pinned');
|
|
413
522
|
const unpinnedAgent = result.find((a) => a.id === 'search-unpinned');
|
|
523
|
+
const standaloneAgent = result.find((a) => a.id === 'search-standalone');
|
|
414
524
|
|
|
415
525
|
expect(pinnedAgent).toBeDefined();
|
|
416
526
|
expect(pinnedAgent!.pinned).toBe(true);
|
|
417
527
|
expect(unpinnedAgent).toBeDefined();
|
|
418
528
|
expect(unpinnedAgent!.pinned).toBe(false);
|
|
529
|
+
expect(standaloneAgent).toBeDefined();
|
|
530
|
+
expect(standaloneAgent!.pinned).toBe(false);
|
|
419
531
|
});
|
|
420
532
|
|
|
421
533
|
it('should not return virtual agents in search', async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SidebarAgentItem, SidebarAgentListResponse, SidebarGroup } from '@lobechat/types';
|
|
2
2
|
import { cleanObject } from '@lobechat/utils';
|
|
3
|
-
import { and, desc, eq, ilike, inArray, or } from 'drizzle-orm';
|
|
3
|
+
import { and, desc, eq, ilike, inArray, not, or } from 'drizzle-orm';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
agents,
|
|
@@ -36,11 +36,7 @@ export class HomeRepository {
|
|
|
36
36
|
* Get sidebar agent list with pinned, grouped, and ungrouped items
|
|
37
37
|
*/
|
|
38
38
|
async getSidebarAgentList(): Promise<SidebarAgentListResponse> {
|
|
39
|
-
// 1. Query all agents (non-virtual) with their session info
|
|
40
|
-
// Note: We query both agents.pinned and sessions.pinned for backward compatibility
|
|
41
|
-
// agents.pinned takes priority, falling back to sessions.pinned for legacy data
|
|
42
|
-
// Note: We query both agents.sessionGroupId and sessions.groupId for backward compatibility
|
|
43
|
-
// agents.sessionGroupId takes priority, falling back to sessions.groupId for legacy data
|
|
39
|
+
// 1. Query all agents (non-virtual) with their session info (if exists)
|
|
44
40
|
const agentList = await this.db
|
|
45
41
|
.select({
|
|
46
42
|
agentSessionGroupId: agents.sessionGroupId,
|
|
@@ -55,9 +51,9 @@ export class HomeRepository {
|
|
|
55
51
|
updatedAt: agents.updatedAt,
|
|
56
52
|
})
|
|
57
53
|
.from(agents)
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.where(and(eq(agents.userId, this.userId), eq(agents.virtual,
|
|
54
|
+
.leftJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
|
|
55
|
+
.leftJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
|
|
56
|
+
.where(and(eq(agents.userId, this.userId), not(eq(agents.virtual, true))))
|
|
61
57
|
.orderBy(desc(agents.updatedAt));
|
|
62
58
|
|
|
63
59
|
// 2. Query all chatGroups (group chats)
|
|
@@ -75,32 +71,7 @@ export class HomeRepository {
|
|
|
75
71
|
.orderBy(desc(chatGroups.updatedAt));
|
|
76
72
|
|
|
77
73
|
// 2.1 Query member avatars for each chat group
|
|
78
|
-
const
|
|
79
|
-
const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
|
|
80
|
-
|
|
81
|
-
if (chatGroupIds.length > 0) {
|
|
82
|
-
const memberAvatars = await this.db
|
|
83
|
-
.select({
|
|
84
|
-
avatar: agents.avatar,
|
|
85
|
-
backgroundColor: agents.backgroundColor,
|
|
86
|
-
chatGroupId: chatGroupsAgents.chatGroupId,
|
|
87
|
-
})
|
|
88
|
-
.from(chatGroupsAgents)
|
|
89
|
-
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
90
|
-
.where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
|
|
91
|
-
.orderBy(chatGroupsAgents.order);
|
|
92
|
-
|
|
93
|
-
for (const member of memberAvatars) {
|
|
94
|
-
const existing = memberAvatarsMap.get(member.chatGroupId) || [];
|
|
95
|
-
if (member.avatar) {
|
|
96
|
-
existing.push({
|
|
97
|
-
avatar: member.avatar,
|
|
98
|
-
background: member.backgroundColor ?? undefined,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
memberAvatarsMap.set(member.chatGroupId, existing);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
74
|
+
const memberAvatarsMap = await this.getChatGroupMemberAvatars(chatGroupList.map((g) => g.id));
|
|
104
75
|
|
|
105
76
|
// 3. Query all sessionGroups (user-defined folders)
|
|
106
77
|
const groupList = await this.db
|
|
@@ -125,7 +96,7 @@ export class HomeRepository {
|
|
|
125
96
|
id: string;
|
|
126
97
|
pinned: boolean | null;
|
|
127
98
|
sessionGroupId: string | null;
|
|
128
|
-
sessionId: string;
|
|
99
|
+
sessionId: string | null;
|
|
129
100
|
sessionPinned: boolean | null;
|
|
130
101
|
title: string | null;
|
|
131
102
|
updatedAt: Date;
|
|
@@ -217,7 +188,6 @@ export class HomeRepository {
|
|
|
217
188
|
const searchPattern = `%${keyword.toLowerCase()}%`;
|
|
218
189
|
|
|
219
190
|
// 1. Search agents by title or description
|
|
220
|
-
// Note: We query both agents.pinned and sessions.pinned for backward compatibility
|
|
221
191
|
const agentResults = await this.db
|
|
222
192
|
.select({
|
|
223
193
|
avatar: agents.avatar,
|
|
@@ -230,12 +200,12 @@ export class HomeRepository {
|
|
|
230
200
|
updatedAt: agents.updatedAt,
|
|
231
201
|
})
|
|
232
202
|
.from(agents)
|
|
233
|
-
.
|
|
234
|
-
.
|
|
203
|
+
.leftJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
|
|
204
|
+
.leftJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
|
|
235
205
|
.where(
|
|
236
206
|
and(
|
|
237
207
|
eq(agents.userId, this.userId),
|
|
238
|
-
eq(agents.virtual,
|
|
208
|
+
not(eq(agents.virtual, true)),
|
|
239
209
|
or(ilike(agents.title, searchPattern), ilike(agents.description, searchPattern)),
|
|
240
210
|
),
|
|
241
211
|
)
|
|
@@ -260,35 +230,11 @@ export class HomeRepository {
|
|
|
260
230
|
.orderBy(desc(chatGroups.updatedAt));
|
|
261
231
|
|
|
262
232
|
// 2.1 Query member avatars for matching chat groups
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (chatGroupIds.length > 0) {
|
|
267
|
-
const memberAvatars = await this.db
|
|
268
|
-
.select({
|
|
269
|
-
avatar: agents.avatar,
|
|
270
|
-
backgroundColor: agents.backgroundColor,
|
|
271
|
-
chatGroupId: chatGroupsAgents.chatGroupId,
|
|
272
|
-
})
|
|
273
|
-
.from(chatGroupsAgents)
|
|
274
|
-
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
275
|
-
.where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
|
|
276
|
-
.orderBy(chatGroupsAgents.order);
|
|
277
|
-
|
|
278
|
-
for (const member of memberAvatars) {
|
|
279
|
-
const existing = memberAvatarsMap.get(member.chatGroupId) || [];
|
|
280
|
-
if (member.avatar) {
|
|
281
|
-
existing.push({
|
|
282
|
-
avatar: member.avatar,
|
|
283
|
-
background: member.backgroundColor ?? undefined,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
memberAvatarsMap.set(member.chatGroupId, existing);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
233
|
+
const memberAvatarsMap = await this.getChatGroupMemberAvatars(
|
|
234
|
+
chatGroupResults.map((g) => g.id),
|
|
235
|
+
);
|
|
289
236
|
|
|
290
237
|
// 3. Combine and format results
|
|
291
|
-
// For pinned status: agents.pinned takes priority, fallback to sessions.pinned for backward compatibility
|
|
292
238
|
const results: SidebarAgentItem[] = [
|
|
293
239
|
...agentResults.map((a) =>
|
|
294
240
|
cleanObject({
|
|
@@ -320,4 +266,39 @@ export class HomeRepository {
|
|
|
320
266
|
|
|
321
267
|
return results;
|
|
322
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Query member avatars for chat groups
|
|
272
|
+
*/
|
|
273
|
+
private async getChatGroupMemberAvatars(
|
|
274
|
+
chatGroupIds: string[],
|
|
275
|
+
): Promise<Map<string, Array<{ avatar: string; background?: string }>>> {
|
|
276
|
+
const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
|
|
277
|
+
|
|
278
|
+
if (chatGroupIds.length === 0) return memberAvatarsMap;
|
|
279
|
+
|
|
280
|
+
const memberAvatars = await this.db
|
|
281
|
+
.select({
|
|
282
|
+
avatar: agents.avatar,
|
|
283
|
+
backgroundColor: agents.backgroundColor,
|
|
284
|
+
chatGroupId: chatGroupsAgents.chatGroupId,
|
|
285
|
+
})
|
|
286
|
+
.from(chatGroupsAgents)
|
|
287
|
+
.innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
|
|
288
|
+
.where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
|
|
289
|
+
.orderBy(chatGroupsAgents.order);
|
|
290
|
+
|
|
291
|
+
for (const member of memberAvatars) {
|
|
292
|
+
const existing = memberAvatarsMap.get(member.chatGroupId) || [];
|
|
293
|
+
if (member.avatar) {
|
|
294
|
+
existing.push({
|
|
295
|
+
avatar: member.avatar,
|
|
296
|
+
background: member.backgroundColor ?? undefined,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
memberAvatarsMap.set(member.chatGroupId, existing);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return memberAvatarsMap;
|
|
303
|
+
}
|
|
323
304
|
}
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Accordion, Flexbox } from '@lobehub/ui';
|
|
2
2
|
import React, { memo } from 'react';
|
|
3
3
|
|
|
4
|
+
import CronTopicList from './Cron';
|
|
4
5
|
import Topic from './Topic';
|
|
5
|
-
import CronTopicList from './Topic/CronTopicList';
|
|
6
6
|
|
|
7
7
|
export enum ChatSidebarKey {
|
|
8
8
|
CronTopics = 'cronTopics',
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AccordionItem, ActionIcon, Flexbox, Icon, Text } from '@lobehub/ui';
|
|
4
|
+
import { Settings2Icon, TimerIcon, TimerOffIcon } from 'lucide-react';
|
|
5
|
+
import { memo, useCallback } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { useParams } from 'react-router-dom';
|
|
8
|
+
|
|
9
|
+
import { useRouter } from '@/app/[variants]/(main)/hooks/useRouter';
|
|
10
|
+
import type { AgentCronJob } from '@/database/schemas/agentCronJob';
|
|
11
|
+
|
|
12
|
+
import CronTopicItem from './CronTopicItem';
|
|
13
|
+
|
|
14
|
+
interface CronTopicGroupProps {
|
|
15
|
+
cronJob: AgentCronJob | null;
|
|
16
|
+
cronJobId: string;
|
|
17
|
+
topics: Array<{
|
|
18
|
+
createdAt: Date | string;
|
|
19
|
+
favorite?: boolean | null;
|
|
20
|
+
historySummary?: string | null;
|
|
21
|
+
id: string;
|
|
22
|
+
metadata?: any;
|
|
23
|
+
title?: string | null;
|
|
24
|
+
trigger?: string | null;
|
|
25
|
+
updatedAt: Date | string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CronTopicGroup = memo<CronTopicGroupProps>(({ cronJob, cronJobId, topics }) => {
|
|
30
|
+
const { t } = useTranslation('setting');
|
|
31
|
+
const { aid, cronId } = useParams<{ aid?: string; cronId?: string }>();
|
|
32
|
+
const router = useRouter();
|
|
33
|
+
|
|
34
|
+
const handleOpenCronJob = useCallback(() => {
|
|
35
|
+
if (!aid) return;
|
|
36
|
+
router.push(`/agent/${aid}/cron/${cronJobId}`);
|
|
37
|
+
}, [aid, cronJobId, router]);
|
|
38
|
+
|
|
39
|
+
const cronJobName = cronJob?.name || t('agentCronJobs.unnamedTask');
|
|
40
|
+
const isEnabled = cronJob?.enabled ?? false;
|
|
41
|
+
const isActive = cronId === cronJobId;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<AccordionItem
|
|
45
|
+
action={
|
|
46
|
+
<ActionIcon
|
|
47
|
+
icon={Settings2Icon}
|
|
48
|
+
onClick={handleOpenCronJob}
|
|
49
|
+
size="small"
|
|
50
|
+
title={t('agentCronJobs.editJob')}
|
|
51
|
+
/>
|
|
52
|
+
}
|
|
53
|
+
itemKey={cronJobId}
|
|
54
|
+
paddingBlock={4}
|
|
55
|
+
paddingInline={'8px 4px'}
|
|
56
|
+
title={
|
|
57
|
+
<Flexbox align="center" gap={6} height={24} horizontal style={{ overflow: 'hidden' }}>
|
|
58
|
+
<Icon icon={isEnabled ? TimerIcon : TimerOffIcon} style={{ opacity: 0.5 }} />
|
|
59
|
+
<Text ellipsis style={{ flex: 1 }} type={isActive ? undefined : 'secondary'}>
|
|
60
|
+
{cronJobName}
|
|
61
|
+
</Text>
|
|
62
|
+
{topics.length > 0 && (
|
|
63
|
+
<Text fontSize={11} type="secondary">
|
|
64
|
+
{topics.length}
|
|
65
|
+
</Text>
|
|
66
|
+
)}
|
|
67
|
+
</Flexbox>
|
|
68
|
+
}
|
|
69
|
+
variant={isActive ? 'filled' : 'borderless'}
|
|
70
|
+
>
|
|
71
|
+
<Flexbox gap={1} paddingBlock={1}>
|
|
72
|
+
{topics.length > 0 ? (
|
|
73
|
+
topics.map((topic) => <CronTopicItem key={topic.id} topic={topic} />)
|
|
74
|
+
) : (
|
|
75
|
+
<Text fontSize={12} style={{ padding: '8px 12px' }} type="secondary">
|
|
76
|
+
{t('agentCronJobs.noExecutionResults')}
|
|
77
|
+
</Text>
|
|
78
|
+
)}
|
|
79
|
+
</Flexbox>
|
|
80
|
+
</AccordionItem>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export default CronTopicGroup;
|
package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/index.tsx
RENAMED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
|
4
|
-
import { AccordionItem, ActionIcon, Flexbox,
|
|
5
|
-
import {
|
|
6
|
-
import { Calendar, Plus } from 'lucide-react';
|
|
4
|
+
import { Accordion, AccordionItem, ActionIcon, Flexbox, Text } from '@lobehub/ui';
|
|
5
|
+
import { Plus } from 'lucide-react';
|
|
7
6
|
import { memo, useCallback } from 'react';
|
|
8
7
|
import { useTranslation } from 'react-i18next';
|
|
9
8
|
import urlJoin from 'url-join';
|
|
@@ -11,9 +10,7 @@ import urlJoin from 'url-join';
|
|
|
11
10
|
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
|
12
11
|
import EmptyNavItem from '@/features/NavPanel/components/EmptyNavItem';
|
|
13
12
|
import SkeletonList from '@/features/NavPanel/components/SkeletonList';
|
|
14
|
-
import { useFetchCronTopicsWithJobInfo } from '@/hooks/useFetchCronTopicsWithJobInfo';
|
|
15
13
|
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
|
16
|
-
import { agentCronJobService } from '@/services/agentCronJob';
|
|
17
14
|
import { useAgentStore } from '@/store/agent';
|
|
18
15
|
|
|
19
16
|
import CronTopicGroup from './CronTopicGroup';
|
|
@@ -25,33 +22,22 @@ interface CronTopicListProps {
|
|
|
25
22
|
const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
|
26
23
|
const { t } = useTranslation('setting');
|
|
27
24
|
const router = useQueryRoute();
|
|
28
|
-
const agentId = useAgentStore((s) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const [agentId, createAgentCronJob, useFetchCronTopicsWithJobInfo] = useAgentStore((s) => [
|
|
26
|
+
s.activeAgentId,
|
|
27
|
+
s.createAgentCronJob,
|
|
28
|
+
s.useFetchCronTopicsWithJobInfo,
|
|
29
|
+
]);
|
|
30
|
+
const { data: cronTopicsGroupsWithJobInfo = [], isLoading } =
|
|
31
|
+
useFetchCronTopicsWithJobInfo(agentId);
|
|
34
32
|
|
|
35
33
|
const handleCreateCronJob = useCallback(async () => {
|
|
36
34
|
if (!agentId) return;
|
|
37
|
-
try {
|
|
38
|
-
const result = await agentCronJobService.create({
|
|
39
|
-
agentId,
|
|
40
|
-
content: t('agentCronJobs.form.content.placeholder') || 'This is a cron job',
|
|
41
|
-
cronPattern: '*/30 * * * *',
|
|
42
|
-
enabled: true,
|
|
43
|
-
name: t('agentCronJobs.addJob') || 'Cron Job Task',
|
|
44
|
-
});
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Failed to create cron job:', error);
|
|
52
|
-
message.error('Failed to create scheduled task');
|
|
36
|
+
const cronJobId = await createAgentCronJob();
|
|
37
|
+
if (cronJobId) {
|
|
38
|
+
router.push(urlJoin('/agent', agentId, 'cron', cronJobId));
|
|
53
39
|
}
|
|
54
|
-
}, [agentId,
|
|
40
|
+
}, [agentId, createAgentCronJob, router]);
|
|
55
41
|
|
|
56
42
|
if (!ENABLE_BUSINESS_FEATURES) return null;
|
|
57
43
|
|
|
@@ -74,7 +60,6 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
|
|
74
60
|
paddingInline={'8px 4px'}
|
|
75
61
|
title={
|
|
76
62
|
<Flexbox align="center" gap={4} horizontal>
|
|
77
|
-
<Icon icon={Calendar} size={12} />
|
|
78
63
|
<Text ellipsis fontSize={12} type={'secondary'} weight={500}>
|
|
79
64
|
{t('agentCronJobs.title')}
|
|
80
65
|
</Text>
|
|
@@ -96,7 +81,6 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
|
|
96
81
|
paddingInline={'8px 4px'}
|
|
97
82
|
title={
|
|
98
83
|
<Flexbox align="center" gap={4} horizontal>
|
|
99
|
-
<Icon icon={Calendar} size={12} />
|
|
100
84
|
<Text ellipsis fontSize={12} type={'secondary'} weight={500}>
|
|
101
85
|
{t('agentCronJobs.title')}
|
|
102
86
|
</Text>
|
|
@@ -108,6 +92,8 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
|
|
108
92
|
);
|
|
109
93
|
}
|
|
110
94
|
|
|
95
|
+
const totalCronJobs = cronTopicsGroupsWithJobInfo.length;
|
|
96
|
+
|
|
111
97
|
return (
|
|
112
98
|
<AccordionItem
|
|
113
99
|
action={addAction}
|
|
@@ -116,14 +102,18 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
|
|
116
102
|
paddingInline={'8px 4px'}
|
|
117
103
|
title={
|
|
118
104
|
<Flexbox align="center" gap={4} horizontal>
|
|
119
|
-
<Icon icon={Calendar} size={12} />
|
|
120
105
|
<Text ellipsis fontSize={12} type={'secondary'} weight={500}>
|
|
121
|
-
{
|
|
106
|
+
{t('agentCronJobs.title')}
|
|
122
107
|
</Text>
|
|
108
|
+
{totalCronJobs > 0 && (
|
|
109
|
+
<Text fontSize={11} type="secondary">
|
|
110
|
+
{totalCronJobs}
|
|
111
|
+
</Text>
|
|
112
|
+
)}
|
|
123
113
|
</Flexbox>
|
|
124
114
|
}
|
|
125
115
|
>
|
|
126
|
-
<
|
|
116
|
+
<Accordion defaultExpandedKeys={cronTopicsGroupsWithJobInfo.map((g) => g.cronJobId)} gap={2}>
|
|
127
117
|
{cronTopicsGroupsWithJobInfo.map((group) => (
|
|
128
118
|
<CronTopicGroup
|
|
129
119
|
cronJob={group.cronJob}
|
|
@@ -132,7 +122,7 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
|
|
|
132
122
|
topics={group.topics}
|
|
133
123
|
/>
|
|
134
124
|
))}
|
|
135
|
-
</
|
|
125
|
+
</Accordion>
|
|
136
126
|
</AccordionItem>
|
|
137
127
|
);
|
|
138
128
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { InputRef } from 'antd';
|
|
3
|
-
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
4
2
|
|
|
3
|
+
import InlineRename from '@/components/InlineRename';
|
|
5
4
|
import { useChatStore } from '@/store/chat';
|
|
6
5
|
|
|
7
6
|
interface EditingProps {
|
|
@@ -10,27 +9,14 @@ interface EditingProps {
|
|
|
10
9
|
toggleEditing: (visible?: boolean) => void;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
function FocusableInput({ ...props }: InputProps) {
|
|
14
|
-
const ref = useRef<InputRef>(null);
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
queueMicrotask(() => {
|
|
17
|
-
if (ref.current) {
|
|
18
|
-
ref.current.input?.focus();
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
}, []);
|
|
22
|
-
return <Input {...props} ref={ref} />;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
12
|
const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
|
|
26
|
-
const [newTitle, setNewTitle] = useState(title);
|
|
27
13
|
const [editing, updateTopicTitle] = useChatStore((s) => [
|
|
28
14
|
s.topicRenamingId === id,
|
|
29
15
|
s.updateTopicTitle,
|
|
30
16
|
]);
|
|
31
17
|
|
|
32
|
-
const
|
|
33
|
-
|
|
18
|
+
const handleSave = useCallback(
|
|
19
|
+
async (newTitle: string) => {
|
|
34
20
|
try {
|
|
35
21
|
// Set loading state
|
|
36
22
|
useChatStore.setState(
|
|
@@ -53,40 +39,17 @@ const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
|
|
|
53
39
|
'clearTopicUpdating',
|
|
54
40
|
);
|
|
55
41
|
}
|
|
56
|
-
}
|
|
57
|
-
|
|
42
|
+
},
|
|
43
|
+
[id, updateTopicTitle],
|
|
44
|
+
);
|
|
58
45
|
|
|
59
46
|
return (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
defaultValue={title}
|
|
64
|
-
onBlur={handleUpdate}
|
|
65
|
-
onChange={(e) => setNewTitle(e.target.value)}
|
|
66
|
-
onClick={(e) => e.stopPropagation()}
|
|
67
|
-
onPressEnter={() => {
|
|
68
|
-
handleUpdate();
|
|
69
|
-
toggleEditing(false);
|
|
70
|
-
}}
|
|
71
|
-
/>
|
|
72
|
-
}
|
|
73
|
-
onOpenChange={(open) => {
|
|
74
|
-
if (!open) handleUpdate();
|
|
75
|
-
|
|
76
|
-
toggleEditing(open);
|
|
77
|
-
}}
|
|
47
|
+
<InlineRename
|
|
48
|
+
onOpenChange={(open) => toggleEditing(open)}
|
|
49
|
+
onSave={handleSave}
|
|
78
50
|
open={editing}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
content: {
|
|
82
|
-
padding: 4,
|
|
83
|
-
width: 320,
|
|
84
|
-
},
|
|
85
|
-
}}
|
|
86
|
-
trigger="click"
|
|
87
|
-
>
|
|
88
|
-
<div />
|
|
89
|
-
</Popover>
|
|
51
|
+
title={title}
|
|
52
|
+
/>
|
|
90
53
|
);
|
|
91
54
|
});
|
|
92
55
|
|
|
@@ -18,6 +18,8 @@ import AllTopicsDrawer from '../AllTopicsDrawer';
|
|
|
18
18
|
import ByTimeMode from '../TopicListContent/ByTimeMode';
|
|
19
19
|
import FlatMode from '../TopicListContent/FlatMode';
|
|
20
20
|
|
|
21
|
+
const fetchParams = { excludeTriggers: ['cron'] };
|
|
22
|
+
|
|
21
23
|
const TopicList = memo(() => {
|
|
22
24
|
const { t } = useTranslation('topic');
|
|
23
25
|
const router = useQueryRoute();
|
|
@@ -32,7 +34,7 @@ const TopicList = memo(() => {
|
|
|
32
34
|
|
|
33
35
|
const [topicDisplayMode] = useUserStore((s) => [preferenceSelectors.topicDisplayMode(s)]);
|
|
34
36
|
|
|
35
|
-
useFetchTopics(
|
|
37
|
+
useFetchTopics(fetchParams);
|
|
36
38
|
|
|
37
39
|
// Show skeleton when current session's topic data is not yet loaded
|
|
38
40
|
if (isUndefinedTopics) return <SkeletonList />;
|