@lobehub/lobehub 2.0.0-next.280 → 2.0.0-next.281

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 (36) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +47 -3
  4. package/package.json +1 -1
  5. package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +19 -3
  6. package/packages/builtin-tool-group-agent-builder/src/client/Streaming/BatchCreateAgents/index.tsx +19 -4
  7. package/packages/builtin-tool-group-agent-builder/src/client/Streaming/UpdateAgentPrompt/index.tsx +3 -13
  8. package/packages/builtin-tool-group-agent-builder/src/client/Streaming/UpdateGroupPrompt/index.tsx +1 -1
  9. package/packages/builtin-tool-group-agent-builder/src/systemRole.ts +83 -121
  10. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/index.tsx +9 -1
  11. package/src/app/[variants]/(main)/agent/features/Conversation/ConversationArea.tsx +1 -28
  12. package/src/app/[variants]/(main)/community/(detail)/features/MakedownRender.tsx +3 -3
  13. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +8 -1
  14. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/Header/Avatar.tsx +2 -13
  15. package/src/app/[variants]/(main)/group/_layout/Sidebar/Header/Agent/index.tsx +3 -4
  16. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +17 -8
  17. package/src/app/[variants]/(main)/group/features/Conversation/ConversationArea.tsx +1 -29
  18. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +0 -2
  19. package/src/app/[variants]/(main)/group/features/GroupAvatar.tsx +17 -9
  20. package/src/app/[variants]/(main)/group/profile/features/AgentBuilder/TopicSelector.tsx +8 -5
  21. package/src/app/[variants]/(main)/group/profile/features/Header/ChromeTabs/index.tsx +20 -2
  22. package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -2
  23. package/src/app/[variants]/(main)/group/profile/features/ProfileHydration.tsx +5 -25
  24. package/src/features/AgentGroupAvatar/index.tsx +38 -0
  25. package/src/features/Conversation/Messages/Supervisor/index.tsx +8 -2
  26. package/src/features/Conversation/Messages/User/useMarkdown.tsx +1 -2
  27. package/src/features/NavPanel/components/EmptyNavItem.tsx +2 -2
  28. package/src/features/NavPanel/components/NavItem.tsx +27 -3
  29. package/src/features/ToolTag/index.tsx +167 -0
  30. package/src/server/routers/lambda/topic.ts +8 -1
  31. package/src/services/chat/mecha/contextEngineering.test.ts +1 -1
  32. package/src/services/chat/mecha/contextEngineering.ts +3 -4
  33. package/src/services/chat/mecha/memoryManager.ts +9 -38
  34. package/src/store/agentGroup/initialState.ts +1 -1
  35. package/src/store/agentGroup/slices/lifecycle.ts +15 -2
  36. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts +0 -49
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
4
+ import { Avatar, Icon, Tag } from '@lobehub/ui';
5
+ import { createStaticStyles, cssVar } from 'antd-style';
6
+ import isEqual from 'fast-deep-equal';
7
+ import { memo, useMemo } from 'react';
8
+
9
+ import PluginAvatar from '@/components/Plugins/PluginAvatar';
10
+ import { useIsDark } from '@/hooks/useIsDark';
11
+ import { useDiscoverStore } from '@/store/discover';
12
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
13
+ import { useToolStore } from '@/store/tool';
14
+ import {
15
+ builtinToolSelectors,
16
+ klavisStoreSelectors,
17
+ pluginSelectors,
18
+ } from '@/store/tool/selectors';
19
+
20
+ /**
21
+ * Klavis server icon component
22
+ */
23
+ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
24
+ if (typeof icon === 'string') {
25
+ return <img alt={label} height={16} src={icon} style={{ flexShrink: 0 }} width={16} />;
26
+ }
27
+
28
+ return <Icon fill={cssVar.colorText} icon={icon} size={16} />;
29
+ });
30
+
31
+ const styles = createStaticStyles(({ css, cssVar }) => ({
32
+ compact: css`
33
+ height: auto !important;
34
+ padding: 0 !important;
35
+ border: none !important;
36
+ background: transparent !important;
37
+ `,
38
+ tag: css`
39
+ height: 24px !important;
40
+ border-radius: ${cssVar.borderRadiusSM} !important;
41
+ `,
42
+ }));
43
+
44
+ export interface ToolTagProps {
45
+ /**
46
+ * The tool identifier to display
47
+ */
48
+ identifier: string;
49
+ /**
50
+ * Variant style of the tag
51
+ * - 'default': normal tag with background and border
52
+ * - 'compact': no padding, no background, no border (text only with icon)
53
+ * @default 'default'
54
+ */
55
+ variant?: 'compact' | 'default';
56
+ }
57
+
58
+ /**
59
+ * A readonly tag component that displays tool information based on identifier.
60
+ * Unlike PluginTag, this component is not closable and is designed for display-only purposes.
61
+ */
62
+ const ToolTag = memo<ToolTagProps>(({ identifier, variant = 'default' }) => {
63
+ const isDarkMode = useIsDark();
64
+ const isCompact = variant === 'compact';
65
+
66
+ // Get local plugin lists
67
+ const builtinList = useToolStore(builtinToolSelectors.metaList, isEqual);
68
+ const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
69
+
70
+ // Klavis related state
71
+ const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
72
+ const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
73
+
74
+ // Check if plugin is installed
75
+ const isInstalled = useToolStore(pluginSelectors.isPluginInstalled(identifier));
76
+
77
+ // Try to find in local lists first (including Klavis)
78
+ const localMeta = useMemo(() => {
79
+ // Check if it's a Klavis server type
80
+ if (isKlavisEnabledInEnv) {
81
+ const klavisType = KLAVIS_SERVER_TYPES.find((type) => type.identifier === identifier);
82
+ if (klavisType) {
83
+ const connectedServer = allKlavisServers.find((s) => s.identifier === identifier);
84
+ return {
85
+ icon: klavisType.icon,
86
+ isInstalled: !!connectedServer,
87
+ label: klavisType.label,
88
+ title: klavisType.label,
89
+ type: 'klavis' as const,
90
+ };
91
+ }
92
+ }
93
+
94
+ const builtinMeta = builtinList.find((p) => p.identifier === identifier);
95
+ if (builtinMeta) {
96
+ return {
97
+ avatar: builtinMeta.meta.avatar,
98
+ isInstalled: true,
99
+ title: builtinMeta.meta.title,
100
+ type: 'builtin' as const,
101
+ };
102
+ }
103
+
104
+ const installedMeta = installedPluginList.find((p) => p.identifier === identifier);
105
+ if (installedMeta) {
106
+ return {
107
+ avatar: installedMeta.avatar,
108
+ isInstalled: true,
109
+ title: installedMeta.title,
110
+ type: 'plugin' as const,
111
+ };
112
+ }
113
+
114
+ return null;
115
+ }, [identifier, builtinList, installedPluginList, isKlavisEnabledInEnv, allKlavisServers]);
116
+
117
+ // Fetch from remote if not found locally
118
+ const usePluginDetail = useDiscoverStore((s) => s.usePluginDetail);
119
+ const { data: remoteData, isLoading } = usePluginDetail({
120
+ identifier: !localMeta && !isInstalled ? identifier : undefined,
121
+ withManifest: false,
122
+ });
123
+
124
+ // Determine final metadata
125
+ const meta = localMeta || {
126
+ avatar: remoteData?.avatar,
127
+ isInstalled: false,
128
+ title: remoteData?.title || identifier,
129
+ type: 'plugin' as const,
130
+ };
131
+
132
+ const displayTitle = isLoading ? 'Loading...' : meta.title;
133
+
134
+ // Render icon based on type
135
+ const renderIcon = () => {
136
+ // Klavis type has icon property
137
+ if (meta.type === 'klavis' && 'icon' in meta && 'label' in meta) {
138
+ return <KlavisIcon icon={meta.icon} label={meta.label} />;
139
+ }
140
+
141
+ // Builtin type has avatar
142
+ if (meta.type === 'builtin' && 'avatar' in meta && meta.avatar) {
143
+ return <Avatar avatar={meta.avatar} shape={'square'} size={16} style={{ flexShrink: 0 }} />;
144
+ }
145
+
146
+ // Plugin type
147
+ if ('avatar' in meta) {
148
+ return <PluginAvatar avatar={meta.avatar} size={16} />;
149
+ }
150
+
151
+ return null;
152
+ };
153
+
154
+ return (
155
+ <Tag
156
+ className={isCompact ? styles.compact : styles.tag}
157
+ icon={renderIcon()}
158
+ variant={isCompact ? 'borderless' : isDarkMode ? 'filled' : 'outlined'}
159
+ >
160
+ {displayTitle}
161
+ </Tag>
162
+ );
163
+ });
164
+
165
+ ToolTag.displayName = 'ToolTag';
166
+
167
+ export default ToolTag;
@@ -3,6 +3,7 @@ import {
3
3
  type RecentTopicGroup,
4
4
  type RecentTopicGroupMember,
5
5
  } from '@lobechat/types';
6
+ import { cleanObject } from '@lobechat/utils';
6
7
  import { eq, inArray } from 'drizzle-orm';
7
8
  import { after } from 'next/server';
8
9
  import { z } from 'zod';
@@ -410,8 +411,14 @@ export const topicRouter = router({
410
411
  const agentId = topicAgentIdMap.get(topic.id);
411
412
  const agentInfo = agentId ? agentInfoMap.get(agentId) : null;
412
413
 
414
+ // Clean agent info - if avatar/title are all null, return null
415
+ const cleanedAgent = agentInfo ? cleanObject(agentInfo) : null;
416
+ // Only return agent if it has meaningful display info (avatar or title)
417
+ const validAgent =
418
+ cleanedAgent && (cleanedAgent.avatar || cleanedAgent.title) ? cleanedAgent : null;
419
+
413
420
  return {
414
- agent: agentInfo ?? null,
421
+ agent: validAgent,
415
422
  group: null,
416
423
  id: topic.id,
417
424
  title: topic.title,
@@ -448,7 +448,7 @@ describe('contextEngineering', () => {
448
448
  ];
449
449
 
450
450
  // Mock topic memories and global identities separately
451
- vi.spyOn(memoryManager, 'resolveTopicMemories').mockResolvedValue({
451
+ vi.spyOn(memoryManager, 'resolveTopicMemories').mockReturnValue({
452
452
  contexts: [
453
453
  {
454
454
  accessedAt: new Date('2024-01-01T00:00:00.000Z'),
@@ -252,12 +252,11 @@ export const contextEngineering = async ({
252
252
  .map((kb) => ({ description: kb.description, id: kb.id, name: kb.name }));
253
253
 
254
254
  // Resolve user memories: topic memories and global identities are independent layers
255
+ // Both functions now read from cache only (no network requests) to avoid blocking sendMessage
255
256
  let userMemoryData;
256
257
  if (enableUserMemories) {
257
- const [topicMemories, globalIdentities] = await Promise.all([
258
- resolveTopicMemories(),
259
- Promise.resolve(resolveGlobalIdentities()),
260
- ]);
258
+ const topicMemories = resolveTopicMemories();
259
+ const globalIdentities = resolveGlobalIdentities();
261
260
  userMemoryData = combineUserMemoryData(topicMemories, globalIdentities);
262
261
  }
263
262
 
@@ -1,10 +1,8 @@
1
1
  import type { UserMemoryData, UserMemoryIdentityItem } from '@lobechat/context-engine';
2
2
  import type { RetrieveMemoryResult } from '@lobechat/types';
3
3
 
4
- import { mutate } from '@/libs/swr';
5
- import { userMemoryService } from '@/services/userMemory';
6
4
  import { getChatStoreState } from '@/store/chat';
7
- import { getUserMemoryStoreState, useUserMemoryStore } from '@/store/userMemory';
5
+ import { getUserMemoryStoreState } from '@/store/userMemory';
8
6
  import { agentMemorySelectors, identitySelectors } from '@/store/userMemory/selectors';
9
7
 
10
8
  const EMPTY_MEMORIES: RetrieveMemoryResult = {
@@ -39,17 +37,13 @@ export interface TopicMemoryResolverContext {
39
37
  }
40
38
 
41
39
  /**
42
- * Resolves topic-based memories (contexts, experiences, preferences)
40
+ * Resolves topic-based memories (contexts, experiences, preferences) from cache only.
43
41
  *
44
- * This function handles:
45
- * 1. Getting the topic ID from context or active topic
46
- * 2. Checking if memories are already cached for the topic
47
- * 3. Fetching memories from the service if not cached
48
- * 4. Caching the fetched memories by topic ID
42
+ * This function only reads from cache and does NOT trigger network requests.
43
+ * Memory data is pre-loaded by SWR in ChatList via useFetchTopicMemories hook.
44
+ * This ensures sendMessage is not blocked by memory retrieval network calls.
49
45
  */
50
- export const resolveTopicMemories = async (
51
- ctx?: TopicMemoryResolverContext,
52
- ): Promise<RetrieveMemoryResult> => {
46
+ export const resolveTopicMemories = (ctx?: TopicMemoryResolverContext): RetrieveMemoryResult => {
53
47
  // Get topic ID from context or active topic
54
48
  const topicId = ctx?.topicId ?? getChatStoreState().activeTopicId;
55
49
 
@@ -60,34 +54,11 @@ export const resolveTopicMemories = async (
60
54
 
61
55
  const userMemoryStoreState = getUserMemoryStoreState();
62
56
 
63
- // Check if already have cached memories for this topic
57
+ // Only read from cache, do not trigger network request
58
+ // Memory data is pre-loaded by SWR in ChatList
64
59
  const cachedMemories = agentMemorySelectors.topicMemories(topicId)(userMemoryStoreState);
65
60
 
66
- if (cachedMemories) {
67
- return cachedMemories;
68
- }
69
-
70
- // Fetch memories for this topic
71
- try {
72
- const result = await userMemoryService.retrieveMemoryForTopic(topicId);
73
- const memories = result ?? EMPTY_MEMORIES;
74
-
75
- // Cache the fetched memories by topic ID
76
- useUserMemoryStore.setState((state) => ({
77
- topicMemoriesMap: {
78
- ...state.topicMemoriesMap,
79
- [topicId]: memories,
80
- },
81
- }));
82
-
83
- // Also trigger SWR mutate to keep in sync
84
- await mutate(['useFetchMemoriesForTopic', topicId]);
85
-
86
- return memories;
87
- } catch (error) {
88
- console.error('Failed to retrieve memories for topic:', error);
89
- return EMPTY_MEMORIES;
90
- }
61
+ return cachedMemories ?? EMPTY_MEMORIES;
91
62
  };
92
63
 
93
64
  /**
@@ -4,7 +4,7 @@ import type { ParsedQuery } from 'query-string';
4
4
  import type { ChatGroupItem } from '@/database/schemas/chatGroup';
5
5
 
6
6
  export interface QueryRouter {
7
- push: (url: string, options?: { query?: ParsedQuery }) => void;
7
+ push: (url: string, options?: { query?: ParsedQuery; replace?: boolean }) => void;
8
8
  }
9
9
 
10
10
  export interface ChatGroupState {
@@ -14,10 +14,16 @@ export interface ChatGroupLifecycleAction {
14
14
  silent?: boolean,
15
15
  ) => Promise<string>;
16
16
  /**
17
+ * @deprecated Use switchTopic(undefined) instead
17
18
  * Switch to a new topic in the group
18
19
  * Clears activeTopicId and navigates to group root
19
20
  */
20
21
  switchToNewTopic: () => void;
22
+ /**
23
+ * Switch to a topic in the group with proper route handling
24
+ * @param topicId - Topic ID to switch to, or undefined/null for new topic
25
+ */
26
+ switchTopic: (topicId?: string | null) => void;
21
27
  }
22
28
 
23
29
  export const chatGroupLifecycleSlice: StateCreator<
@@ -57,13 +63,20 @@ export const chatGroupLifecycleSlice: StateCreator<
57
63
  },
58
64
 
59
65
  switchToNewTopic: () => {
66
+ get().switchTopic(undefined);
67
+ },
68
+
69
+ switchTopic: (topicId) => {
60
70
  const { activeGroupId, router } = get();
61
71
  if (!activeGroupId || !router) return;
62
72
 
63
- useChatStore.setState({ activeTopicId: undefined });
73
+ // Update chat store's activeTopicId
74
+ useChatStore.getState().switchTopic(topicId ?? undefined);
64
75
 
76
+ // Navigate with replace to avoid stale query params
65
77
  router.push(urlJoin('/group', activeGroupId), {
66
- query: { bt: null, tab: null, thread: null, topic: null },
78
+ query: { topic: topicId ?? null },
79
+ replace: true,
67
80
  });
68
81
  },
69
82
  });
@@ -1,49 +0,0 @@
1
- import { usePathname } from 'next/navigation';
2
- import { useCallback } from 'react';
3
- import urlJoin from 'url-join';
4
-
5
- import { useQueryRoute } from '@/hooks/useQueryRoute';
6
- import { useAgentGroupStore } from '@/store/agentGroup';
7
- import { useChatStore } from '@/store/chat';
8
- import { useGlobalStore } from '@/store/global';
9
-
10
- /**
11
- * Hook to handle topic navigation with automatic route detection
12
- * If in agent sub-route (e.g., /agent/:aid/profile), navigate back to chat first
13
- */
14
- export const useTopicNavigation = () => {
15
- const pathname = usePathname();
16
- const activeGroupId = useAgentGroupStore((s) => s.activeGroupId);
17
- const router = useQueryRoute();
18
- const toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
19
- const switchTopic = useChatStore((s) => s.switchTopic);
20
-
21
- const isInAgentSubRoute = useCallback(() => {
22
- if (!activeGroupId) return false;
23
- const agentBasePath = `/group/${activeGroupId}`;
24
- // If pathname has more segments after /agent/:aid, it's a sub-route
25
- return (
26
- pathname.startsWith(agentBasePath) &&
27
- pathname !== agentBasePath &&
28
- pathname !== `${agentBasePath}/`
29
- );
30
- }, [pathname, activeGroupId]);
31
-
32
- const navigateToTopic = useCallback(
33
- (topicId?: string) => {
34
- // If in agent sub-route, navigate back to agent chat first
35
- if (isInAgentSubRoute() && activeGroupId) {
36
- router.push(urlJoin('/group', activeGroupId as string));
37
- }
38
-
39
- switchTopic(topicId);
40
- toggleConfig(false);
41
- },
42
- [activeGroupId, router, switchTopic, toggleConfig, isInAgentSubRoute],
43
- );
44
-
45
- return {
46
- isInAgentSubRoute: isInAgentSubRoute(),
47
- navigateToTopic,
48
- };
49
- };