@lobehub/chat 0.149.3 → 0.149.5

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 (88) hide show
  1. package/.github/FUNDING.yml +1 -1
  2. package/CHANGELOG.md +58 -0
  3. package/package.json +1 -1
  4. package/src/app/chat/(desktop)/features/ChatHeader/Main.tsx +5 -5
  5. package/src/app/chat/(desktop)/features/ChatHeader/Tags.tsx +3 -3
  6. package/src/app/chat/(desktop)/features/ChatInput/Footer/DragUpload.tsx +9 -9
  7. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +3 -3
  8. package/src/app/chat/(desktop)/features/SideBar/SystemRole/index.tsx +8 -3
  9. package/src/app/chat/(mobile)/mobile/ChatHeader/ChatHeaderTitle.tsx +2 -2
  10. package/src/app/chat/(mobile)/mobile/page.tsx +0 -6
  11. package/src/app/chat/_layout/Desktop/SessionList.tsx +2 -0
  12. package/src/app/chat/features/PageTitle/index.tsx +3 -3
  13. package/src/app/chat/features/PluginTag/PluginStatus.tsx +2 -2
  14. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +4 -2
  15. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +10 -17
  16. package/src/app/chat/features/SessionListContent/index.tsx +2 -0
  17. package/src/app/chat/features/ShareButton/Preview.tsx +15 -11
  18. package/src/app/chat/features/ShareButton/useScreenshot.ts +2 -2
  19. package/src/app/chat/settings/features/EditPage.tsx +10 -7
  20. package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +5 -3
  21. package/src/app/metadata.ts +3 -3
  22. package/src/app/settings/(mobile)/features/AvatarBanner.tsx +1 -0
  23. package/src/config/modelProviders/ollama.ts +11 -12
  24. package/src/const/session.ts +1 -0
  25. package/src/database/client/models/session.ts +1 -0
  26. package/src/database/client/models/user.ts +6 -0
  27. package/src/features/ChatInput/ActionBar/FileUpload.tsx +11 -5
  28. package/src/features/ChatInput/ActionBar/History.tsx +3 -3
  29. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +2 -0
  30. package/src/features/ChatInput/ActionBar/Temperature.tsx +3 -3
  31. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +4 -4
  32. package/src/features/ChatInput/ActionBar/Token/index.tsx +3 -3
  33. package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +3 -3
  34. package/src/features/ChatInput/ActionBar/Tools/index.tsx +4 -4
  35. package/src/features/ChatInput/STT/browser.tsx +3 -3
  36. package/src/features/ChatInput/STT/openai.tsx +3 -3
  37. package/src/features/ChatInput/useChatInput.ts +3 -3
  38. package/src/features/Conversation/Extras/Assistant.test.tsx +7 -7
  39. package/src/features/Conversation/Extras/Assistant.tsx +3 -3
  40. package/src/features/Conversation/Extras/TTS/index.tsx +3 -3
  41. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +2 -2
  42. package/src/features/Conversation/components/ChatItem/index.tsx +6 -4
  43. package/src/features/Conversation/hooks/useInitConversation.ts +10 -7
  44. package/src/features/Conversation/index.tsx +6 -3
  45. package/src/features/ModelSwitchPanel/index.tsx +6 -4
  46. package/src/hooks/useTTS.ts +4 -4
  47. package/src/libs/agent-runtime/anthropic/index.test.ts +44 -32
  48. package/src/libs/agent-runtime/anthropic/index.ts +12 -9
  49. package/src/libs/agent-runtime/azureOpenai/index.ts +3 -4
  50. package/src/libs/agent-runtime/bedrock/index.ts +1 -1
  51. package/src/libs/agent-runtime/ollama/index.ts +7 -0
  52. package/src/libs/agent-runtime/perplexity/index.ts +1 -0
  53. package/src/libs/agent-runtime/types/chat.ts +2 -1
  54. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -0
  55. package/src/services/chat.ts +18 -15
  56. package/src/services/session/client.ts +19 -0
  57. package/src/services/session/type.ts +2 -0
  58. package/src/store/agent/index.ts +2 -0
  59. package/src/store/agent/initialState.ts +7 -0
  60. package/src/store/agent/selectors.ts +1 -0
  61. package/src/store/{session/slices/agent → agent/slices/chat}/action.test.ts +26 -63
  62. package/src/store/agent/slices/chat/action.ts +107 -0
  63. package/src/store/agent/slices/chat/initialState.ts +14 -0
  64. package/src/store/agent/slices/chat/selectors.test.ts +82 -0
  65. package/src/store/agent/slices/chat/selectors.ts +81 -0
  66. package/src/store/agent/store.ts +27 -0
  67. package/src/store/chat/slices/message/action.test.ts +3 -2
  68. package/src/store/chat/slices/message/action.ts +3 -3
  69. package/src/store/chat/slices/message/selectors.test.ts +9 -2
  70. package/src/store/chat/slices/message/selectors.ts +6 -4
  71. package/src/store/chat/slices/share/action.ts +5 -3
  72. package/src/store/global/slices/preference/selectors.ts +3 -1
  73. package/src/store/session/selectors.ts +1 -2
  74. package/src/store/session/slices/session/action.test.ts +43 -0
  75. package/src/store/session/slices/session/action.ts +28 -18
  76. package/src/store/session/slices/session/helpers.ts +2 -3
  77. package/src/store/session/slices/session/initialState.ts +1 -17
  78. package/src/store/session/slices/session/selectors/index.ts +1 -0
  79. package/src/store/session/slices/session/selectors/list.test.ts +5 -3
  80. package/src/store/session/slices/session/selectors/list.ts +2 -3
  81. package/src/store/session/slices/session/selectors/meta.test.ts +108 -0
  82. package/src/store/session/slices/session/selectors/meta.ts +45 -0
  83. package/src/store/session/store.ts +1 -7
  84. package/src/types/session.ts +1 -0
  85. package/src/store/session/slices/agent/action.ts +0 -84
  86. package/src/store/session/slices/agent/selectors.test.ts +0 -180
  87. package/src/store/session/slices/agent/selectors.ts +0 -129
  88. /package/src/store/{session/slices/agent → agent/slices/chat}/index.ts +0 -0
@@ -0,0 +1,81 @@
1
+ import { VoiceList } from '@lobehub/tts';
2
+
3
+ import { INBOX_SESSION_ID } from '@/const/session';
4
+ import { DEFAULT_AGENT_CONFIG, DEFAUTT_AGENT_TTS_CONFIG } from '@/const/settings';
5
+ import { AgentStore } from '@/store/agent';
6
+ import { LobeAgentTTSConfig } from '@/types/agent';
7
+ import { merge } from '@/utils/merge';
8
+
9
+ const isInboxSession = (s: AgentStore) => s.activeId === INBOX_SESSION_ID;
10
+
11
+ // ========== Config ============== //
12
+ const currentAgentConfig = (s: AgentStore) => merge(DEFAULT_AGENT_CONFIG, s.agentConfig);
13
+
14
+ const currentAgentSystemRole = (s: AgentStore) => {
15
+ return currentAgentConfig(s).systemRole;
16
+ };
17
+
18
+ const currentAgentModel = (s: AgentStore): string => {
19
+ const config = currentAgentConfig(s);
20
+
21
+ return config?.model || 'gpt-3.5-turbo';
22
+ };
23
+
24
+ const currentAgentModelProvider = (s: AgentStore) => {
25
+ const config = currentAgentConfig(s);
26
+
27
+ return config?.provider;
28
+ };
29
+
30
+ const currentAgentPlugins = (s: AgentStore) => {
31
+ const config = currentAgentConfig(s);
32
+
33
+ return config?.plugins || [];
34
+ };
35
+
36
+ const currentAgentTTS = (s: AgentStore): LobeAgentTTSConfig => {
37
+ const config = currentAgentConfig(s);
38
+
39
+ return config?.tts || DEFAUTT_AGENT_TTS_CONFIG;
40
+ };
41
+
42
+ const currentAgentTTSVoice =
43
+ (lang: string) =>
44
+ (s: AgentStore): string => {
45
+ const { voice, ttsService } = currentAgentTTS(s);
46
+ const voiceList = new VoiceList(lang);
47
+ let currentVoice;
48
+ switch (ttsService) {
49
+ case 'openai': {
50
+ currentVoice = voice.openai || (VoiceList.openaiVoiceOptions?.[0].value as string);
51
+ break;
52
+ }
53
+ case 'edge': {
54
+ currentVoice = voice.edge || (voiceList.edgeVoiceOptions?.[0].value as string);
55
+ break;
56
+ }
57
+ case 'microsoft': {
58
+ currentVoice = voice.microsoft || (voiceList.microsoftVoiceOptions?.[0].value as string);
59
+ break;
60
+ }
61
+ }
62
+ return currentVoice || 'alloy';
63
+ };
64
+
65
+ const hasSystemRole = (s: AgentStore) => {
66
+ const config = currentAgentConfig(s);
67
+
68
+ return !!config.systemRole;
69
+ };
70
+
71
+ export const agentSelectors = {
72
+ currentAgentConfig,
73
+ currentAgentModel,
74
+ currentAgentModelProvider,
75
+ currentAgentPlugins,
76
+ currentAgentSystemRole,
77
+ currentAgentTTS,
78
+ currentAgentTTSVoice,
79
+ hasSystemRole,
80
+ isInboxSession,
81
+ };
@@ -0,0 +1,27 @@
1
+ import { devtools } from 'zustand/middleware';
2
+ import { shallow } from 'zustand/shallow';
3
+ import { createWithEqualityFn } from 'zustand/traditional';
4
+ import { StateCreator } from 'zustand/vanilla';
5
+
6
+ import { isDev } from '@/utils/env';
7
+
8
+ import { SessionStoreState, initialState } from './initialState';
9
+ import { AgentChatAction, createChatSlice } from './slices/chat/action';
10
+
11
+ // =============== aggregate createStoreFn ============ //
12
+
13
+ export interface AgentStore extends AgentChatAction, SessionStoreState {}
14
+
15
+ const createStore: StateCreator<AgentStore, [['zustand/devtools', never]]> = (...parameters) => ({
16
+ ...initialState,
17
+ ...createChatSlice(...parameters),
18
+ });
19
+
20
+ // =============== implement useStore ============ //
21
+
22
+ export const useAgentStore = createWithEqualityFn<AgentStore>()(
23
+ devtools(createStore, {
24
+ name: 'LobeChat_Agent' + (isDev ? '_DEV' : ''),
25
+ }),
26
+ shallow,
27
+ );
@@ -7,8 +7,9 @@ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
7
7
  import { chatService } from '@/services/chat';
8
8
  import { messageService } from '@/services/message';
9
9
  import { topicService } from '@/services/topic';
10
+ import { agentSelectors } from '@/store/agent/selectors';
10
11
  import { chatSelectors } from '@/store/chat/selectors';
11
- import { agentSelectors } from '@/store/session/selectors';
12
+ import { sessionMetaSelectors } from '@/store/session/selectors';
12
13
  import { ChatMessage } from '@/types/message';
13
14
 
14
15
  import { useChatStore } from '../../store';
@@ -69,7 +70,7 @@ beforeEach(() => {
69
70
  vi.clearAllMocks();
70
71
  useChatStore.setState(mockState, false);
71
72
  vi.spyOn(agentSelectors, 'currentAgentConfig').mockImplementation(() => DEFAULT_AGENT_CONFIG);
72
- vi.spyOn(agentSelectors, 'currentAgentMeta').mockImplementation(() => ({ tags: [] }));
73
+ vi.spyOn(sessionMetaSelectors, 'currentAgentMeta').mockImplementation(() => ({ tags: [] }));
73
74
  });
74
75
 
75
76
  afterEach(() => {
@@ -13,10 +13,10 @@ import { chatService } from '@/services/chat';
13
13
  import { CreateMessageParams, messageService } from '@/services/message';
14
14
  import { topicService } from '@/services/topic';
15
15
  import { traceService } from '@/services/trace';
16
+ import { useAgentStore } from '@/store/agent';
17
+ import { agentSelectors } from '@/store/agent/selectors';
16
18
  import { chatHelpers } from '@/store/chat/helpers';
17
19
  import { ChatStore } from '@/store/chat/store';
18
- import { useSessionStore } from '@/store/session';
19
- import { agentSelectors } from '@/store/session/selectors';
20
20
  import { ChatMessage } from '@/types/message';
21
21
  import { TraceEventPayloads } from '@/types/trace';
22
22
  import { setNamespace } from '@/utils/storeDebug';
@@ -120,7 +120,7 @@ export interface ChatMessageAction {
120
120
  internalTraceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
121
121
  }
122
122
 
123
- const getAgentConfig = () => agentSelectors.currentAgentConfig(useSessionStore.getState());
123
+ const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
124
124
 
125
125
  const preventLeavingFn = (e: BeforeUnloadEvent) => {
126
126
  // set returnValue to trigger alert modal
@@ -3,10 +3,12 @@ import { describe, expect, it } from 'vitest';
3
3
 
4
4
  import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
5
5
  import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { useAgentStore } from '@/store/agent';
6
7
  import { ChatStore } from '@/store/chat';
7
8
  import { initialState } from '@/store/chat/initialState';
8
9
  import { useGlobalStore } from '@/store/global';
9
10
  import { useSessionStore } from '@/store/session';
11
+ import { LobeAgentConfig } from '@/types/agent';
10
12
  import { ChatMessage } from '@/types/message';
11
13
  import { MetaData } from '@/types/meta';
12
14
  import { merge } from '@/utils/merge';
@@ -158,8 +160,13 @@ describe('chatSelectors', () => {
158
160
  it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
159
161
  const state = merge(initialStore, { messages: mockMessages });
160
162
  act(() => {
161
- useGlobalStore.setState({
162
- settings: { defaultAgent: { config: { historyCount: 2, enableHistoryCount: true } } },
163
+ useAgentStore.setState({
164
+ activeId: 'inbox',
165
+ agentConfig: {
166
+ historyCount: 2,
167
+ enableHistoryCount: true,
168
+ model: 'abc',
169
+ } as LobeAgentConfig,
163
170
  });
164
171
  });
165
172
 
@@ -3,10 +3,12 @@ import { t } from 'i18next';
3
3
 
4
4
  import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta';
5
5
  import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { useAgentStore } from '@/store/agent';
7
+ import { agentSelectors } from '@/store/agent/selectors';
6
8
  import { useGlobalStore } from '@/store/global';
7
9
  import { commonSelectors } from '@/store/global/selectors';
8
10
  import { useSessionStore } from '@/store/session';
9
- import { agentSelectors } from '@/store/session/selectors';
11
+ import { sessionMetaSelectors } from '@/store/session/selectors';
10
12
  import { ChatMessage } from '@/types/message';
11
13
  import { MetaData } from '@/types/meta';
12
14
  import { merge } from '@/utils/merge';
@@ -27,7 +29,7 @@ const getMeta = (message: ChatMessage) => {
27
29
  }
28
30
 
29
31
  case 'assistant': {
30
- return agentSelectors.currentAgentMeta(useSessionStore.getState());
32
+ return sessionMetaSelectors.currentAgentMeta(useSessionStore.getState());
31
33
  }
32
34
 
33
35
  case 'function': {
@@ -88,14 +90,14 @@ const currentChatsWithGuideMessage =
88
90
  };
89
91
 
90
92
  const currentChatIDsWithGuideMessage = (s: ChatStore) => {
91
- const meta = agentSelectors.currentAgentMeta(useSessionStore.getState());
93
+ const meta = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState());
92
94
 
93
95
  return currentChatsWithGuideMessage(meta)(s).map((s) => s.id);
94
96
  };
95
97
 
96
98
  const currentChatsWithHistoryConfig = (s: ChatStore): ChatMessage[] => {
97
99
  const chats = currentChats(s);
98
- const config = agentSelectors.currentAgentConfig(useSessionStore.getState());
100
+ const config = agentSelectors.currentAgentConfig(useAgentStore.getState());
99
101
 
100
102
  return chatHelpers.getSlicedMessagesWithConfig(chats, config);
101
103
  };
@@ -4,8 +4,10 @@ import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
6
6
  import { shareGPTService } from '@/services/share';
7
+ import { useAgentStore } from '@/store/agent';
8
+ import { agentSelectors } from '@/store/agent/selectors';
7
9
  import { useSessionStore } from '@/store/session';
8
- import { agentSelectors } from '@/store/session/selectors';
10
+ import { sessionMetaSelectors } from '@/store/session/selectors';
9
11
  import { ShareGPTConversation } from '@/types/share';
10
12
 
11
13
  import { chatSelectors } from '../../selectors';
@@ -56,8 +58,8 @@ export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [
56
58
  ) => ({
57
59
  shareToShareGPT: async ({ withSystemRole, withPluginInfo, avatar }) => {
58
60
  const messages = chatSelectors.currentChats(get());
59
- const config = agentSelectors.currentAgentConfig(useSessionStore.getState());
60
- const meta = agentSelectors.currentAgentMeta(useSessionStore.getState());
61
+ const config = agentSelectors.currentAgentConfig(useAgentStore.getState());
62
+ const meta = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState());
61
63
 
62
64
  const defaultMsg: ShareGPTConversation['items'] = [];
63
65
  const showSystemRole = withSystemRole && !!config.systemRole;
@@ -1,6 +1,8 @@
1
1
  import { GlobalStore } from '@/store/global';
2
+ import { SessionDefaultGroup } from '@/types/session';
2
3
 
3
- const sessionGroupKeys = (s: GlobalStore): string[] => s.preference.expandSessionGroupKeys || [];
4
+ const sessionGroupKeys = (s: GlobalStore): string[] =>
5
+ s.preference.expandSessionGroupKeys || [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default];
4
6
 
5
7
  const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterToSend || false;
6
8
 
@@ -1,3 +1,2 @@
1
- export { agentSelectors } from './slices/agent/selectors';
2
- export { sessionSelectors } from './slices/session/selectors';
1
+ export { sessionMetaSelectors, sessionSelectors } from './slices/session/selectors';
3
2
  export { sessionGroupSelectors } from './slices/sessionGroup/selectors';
@@ -7,6 +7,8 @@ import { sessionService } from '@/services/session';
7
7
  import { useSessionStore } from '@/store/session';
8
8
  import { LobeSessionType } from '@/types/session';
9
9
 
10
+ import { sessionSelectors } from './selectors';
11
+
10
12
  // Mock sessionService 和其他依赖项
11
13
  vi.mock('@/services/session', () => ({
12
14
  sessionService: {
@@ -189,4 +191,45 @@ describe('SessionAction', () => {
189
191
  expect(mockRefresh).toHaveBeenCalled();
190
192
  });
191
193
  });
194
+
195
+ describe('updateAgentMeta', () => {
196
+ it('should not update meta if there is no current session', async () => {
197
+ const { result } = renderHook(() => useSessionStore());
198
+ const meta = { title: 'Test Agent' };
199
+ const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
200
+ const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
201
+
202
+ // 模拟没有当前会话
203
+ vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue(null as any);
204
+
205
+ await act(async () => {
206
+ await result.current.updateSessionMeta(meta as any);
207
+ });
208
+
209
+ expect(updateSessionMock).not.toHaveBeenCalled();
210
+ expect(refreshSessionsMock).not.toHaveBeenCalled();
211
+ updateSessionMock.mockRestore();
212
+ refreshSessionsMock.mockRestore();
213
+ });
214
+
215
+ it('should update session meta and refresh sessions', async () => {
216
+ const { result } = renderHook(() => useSessionStore());
217
+ const meta = { title: 'Test Agent' };
218
+ const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
219
+ const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
220
+
221
+ // 模拟有当前会话
222
+ vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue({ id: 'session-id' } as any);
223
+ vi.spyOn(result.current, 'activeId', 'get').mockReturnValue('session-id');
224
+
225
+ await act(async () => {
226
+ await result.current.updateSessionMeta(meta);
227
+ });
228
+
229
+ expect(updateSessionMock).toHaveBeenCalledWith('session-id', { meta });
230
+ expect(refreshSessionsMock).toHaveBeenCalled();
231
+ updateSessionMock.mockRestore();
232
+ refreshSessionsMock.mockRestore();
233
+ });
234
+ });
192
235
  });
@@ -1,15 +1,17 @@
1
1
  import { t } from 'i18next';
2
+ import { isEqual } from 'lodash-es';
2
3
  import useSWR, { SWRResponse, mutate } from 'swr';
3
4
  import { DeepPartial } from 'utility-types';
4
5
  import { StateCreator } from 'zustand/vanilla';
5
6
 
6
7
  import { message } from '@/components/AntdStaticMethods';
7
- import { INBOX_SESSION_ID } from '@/const/session';
8
- import { SWRRefreshParams, useClientDataSWR } from '@/libs/swr';
8
+ import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
9
+ import { useClientDataSWR } from '@/libs/swr';
9
10
  import { sessionService } from '@/services/session';
10
11
  import { useGlobalStore } from '@/store/global';
11
12
  import { settingsSelectors } from '@/store/global/selectors';
12
13
  import { SessionStore } from '@/store/session';
14
+ import { MetaData } from '@/types/meta';
13
15
  import {
14
16
  ChatSessionList,
15
17
  LobeAgentSession,
@@ -21,10 +23,9 @@ import {
21
23
  import { merge } from '@/utils/merge';
22
24
  import { setNamespace } from '@/utils/storeDebug';
23
25
 
24
- import { agentSelectors } from '../agent/selectors';
25
- import { initLobeSession } from './initialState';
26
26
  import { SessionDispatch, sessionsReducer } from './reducers';
27
27
  import { sessionSelectors } from './selectors';
28
+ import { sessionMetaSelectors } from './selectors/meta';
28
29
 
29
30
  const n = setNamespace('session');
30
31
 
@@ -53,6 +54,7 @@ export interface SessionAction {
53
54
  ) => Promise<string>;
54
55
  duplicateSession: (id: string) => Promise<void>;
55
56
  updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
57
+ updateSessionMeta: (meta: Partial<MetaData>) => void;
56
58
 
57
59
  /**
58
60
  * Pins or unpins a session.
@@ -61,7 +63,7 @@ export interface SessionAction {
61
63
  /**
62
64
  * re-fetch the data
63
65
  */
64
- refreshSessions: (params?: SWRRefreshParams<ChatSessionList>) => Promise<void>;
66
+ refreshSessions: () => Promise<void>;
65
67
  /**
66
68
  * remove session
67
69
  * @param id - sessionId
@@ -106,7 +108,7 @@ export const createSessionSlice: StateCreator<
106
108
 
107
109
  // merge the defaultAgent in settings
108
110
  const defaultAgent = merge(
109
- initLobeSession,
111
+ DEFAULT_AGENT_LOBE_SESSION,
110
112
  settingsSelectors.defaultAgent(useGlobalStore.getState()),
111
113
  );
112
114
 
@@ -125,7 +127,7 @@ export const createSessionSlice: StateCreator<
125
127
  const session = sessionSelectors.getSessionById(id)(get());
126
128
 
127
129
  if (!session) return;
128
- const title = agentSelectors.getTitle(session.meta);
130
+ const title = sessionMetaSelectors.getTitle(session.meta);
129
131
 
130
132
  const newTitle = t('duplicateSession.title', { ns: 'chat', title: title });
131
133
 
@@ -157,10 +159,6 @@ export const createSessionSlice: StateCreator<
157
159
  await get().internal_updateSession(id, { pinned });
158
160
  },
159
161
 
160
- refreshSessions: async () => {
161
- await mutate(FETCH_SESSIONS_KEY);
162
- },
163
-
164
162
  removeSession: async (sessionId) => {
165
163
  await sessionService.removeSession(sessionId);
166
164
  await get().refreshSessions();
@@ -175,16 +173,25 @@ export const createSessionSlice: StateCreator<
175
173
  await get().internal_updateSession(sessionId, { group });
176
174
  },
177
175
 
176
+ updateSessionMeta: async (meta) => {
177
+ const session = sessionSelectors.currentSession(get());
178
+ if (!session) return;
179
+
180
+ const { activeId, refreshSessions } = get();
181
+
182
+ await sessionService.updateSession(activeId, { meta });
183
+ await refreshSessions();
184
+ },
185
+
178
186
  useFetchSessions: () =>
179
187
  useClientDataSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getGroupedSessions, {
180
188
  onSuccess: (data) => {
181
- // 由于 https://github.com/lobehub/lobe-chat/pull/541 的关系
182
- // 只有触发了 refreshSessions 才会更新 sessions,进而触发页面 rerender
183
- // 因此这里不能补充 equal 判断,否则会导致页面不更新
184
- // if (get().isSessionsFirstFetchFinished && isEqual(get().sessions, data)) return;
185
-
186
- // TODO:后续的根本解法应该是解除 inbox 和 session 的数据耦合
187
- // 避免互相依赖的情况出现
189
+ if (
190
+ get().isSessionsFirstFetchFinished &&
191
+ isEqual(get().sessions, data.sessions) &&
192
+ isEqual(get().sessionGroups, data.sessionGroups)
193
+ )
194
+ return;
188
195
 
189
196
  get().internal_processSessions(
190
197
  data.sessions,
@@ -239,4 +246,7 @@ export const createSessionSlice: StateCreator<
239
246
  n('processSessions'),
240
247
  );
241
248
  },
249
+ refreshSessions: async () => {
250
+ await mutate(FETCH_SESSIONS_KEY);
251
+ },
242
252
  });
@@ -1,13 +1,12 @@
1
+ import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
1
2
  import { LobeAgentSession, LobeSessions } from '@/types/session';
2
3
 
3
- import { initLobeSession } from './initialState';
4
-
5
4
  export const getSessionPinned = (session: LobeAgentSession) => session.pinned;
6
5
 
7
6
  const getSessionById = (id: string, sessions: LobeSessions): LobeAgentSession => {
8
7
  const session = sessions.find((s) => s.id === id);
9
8
 
10
- if (!session) return initLobeSession;
9
+ if (!session) return DEFAULT_AGENT_LOBE_SESSION;
11
10
 
12
11
  return session;
13
12
  };
@@ -1,20 +1,4 @@
1
- import { DEFAULT_AGENT_META } from '@/const/meta';
2
- import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
3
- import {
4
- CustomSessionGroup,
5
- LobeAgentSession,
6
- LobeSessionGroups,
7
- LobeSessionType,
8
- } from '@/types/session';
9
-
10
- export const initLobeSession: LobeAgentSession = {
11
- config: DEFAULT_AGENT_CONFIG,
12
- createdAt: Date.now(),
13
- id: '',
14
- meta: DEFAULT_AGENT_META,
15
- type: LobeSessionType.Agent,
16
- updatedAt: Date.now(),
17
- };
1
+ import { CustomSessionGroup, LobeAgentSession, LobeSessionGroups } from '@/types/session';
18
2
 
19
3
  export interface SessionState {
20
4
  /**
@@ -1 +1,2 @@
1
1
  export * from './list';
2
+ export * from './meta';
@@ -1,7 +1,7 @@
1
+ import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
1
2
  import type { SessionStore } from '@/store/session';
2
3
  import { LobeAgentSession, LobeSessionType } from '@/types/session';
3
4
 
4
- import { initLobeSession } from '../initialState';
5
5
  import { sessionSelectors } from './list';
6
6
 
7
7
  describe('currentSession', () => {
@@ -64,7 +64,9 @@ describe('currentSessionSafe', () => {
64
64
  } as unknown as SessionStore;
65
65
 
66
66
  it('should return initLobeSession when currentSession(s) returns undefined', () => {
67
- expect(sessionSelectors.currentSessionSafe({ sessions: {} } as any)).toEqual(initLobeSession);
67
+ expect(sessionSelectors.currentSessionSafe({ sessions: {} } as any)).toEqual(
68
+ DEFAULT_AGENT_LOBE_SESSION,
69
+ );
68
70
  });
69
71
 
70
72
  it('should return the result of currentSession(s) when it returns a non-undefined value', () => {
@@ -102,7 +104,7 @@ describe('getSessionById', () => {
102
104
  });
103
105
 
104
106
  it('should return initLobeSession when the session with the specified id does not exist', () => {
105
- expect(sessionSelectors.getSessionById('3')(s)).toEqual(initLobeSession);
107
+ expect(sessionSelectors.getSessionById('3')(s)).toEqual(DEFAULT_AGENT_LOBE_SESSION);
106
108
  });
107
109
  });
108
110
 
@@ -1,10 +1,9 @@
1
- import { INBOX_SESSION_ID } from '@/const/session';
1
+ import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
2
2
  import { sessionHelpers } from '@/store/session/slices/session/helpers';
3
3
  import { MetaData } from '@/types/meta';
4
4
  import { CustomSessionGroup, LobeAgentSession, LobeSessions } from '@/types/session';
5
5
 
6
6
  import { SessionStore } from '../../../store';
7
- import { initLobeSession } from '../initialState';
8
7
 
9
8
  const defaultSessions = (s: SessionStore): LobeSessions => s.defaultSessions;
10
9
  const pinnedSessions = (s: SessionStore): LobeSessions => s.pinnedSessions;
@@ -33,7 +32,7 @@ const currentSession = (s: SessionStore): LobeAgentSession | undefined => {
33
32
  };
34
33
 
35
34
  const currentSessionSafe = (s: SessionStore): LobeAgentSession => {
36
- return currentSession(s) || initLobeSession;
35
+ return currentSession(s) || DEFAULT_AGENT_LOBE_SESSION;
37
36
  };
38
37
 
39
38
  const hasCustomAgents = (s: SessionStore) => defaultSessions(s).length > 0;
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { DEFAULT_AVATAR } from '@/const/meta';
4
+ import { DEFAULT_AGENT_CONFIG, DEFAUTT_AGENT_TTS_CONFIG } from '@/const/settings';
5
+ import { SessionStore } from '@/store/session';
6
+ import { MetaData } from '@/types/meta';
7
+ import { LobeAgentSession, LobeSessionType } from '@/types/session';
8
+
9
+ import { sessionMetaSelectors } from './meta';
10
+
11
+ vi.mock('i18next', () => ({
12
+ t: vi.fn((key) => key), // Simplified mock return value
13
+ }));
14
+
15
+ const mockSessionStore = {
16
+ activeId: '1',
17
+ sessions: [
18
+ {
19
+ id: '1',
20
+ config: DEFAULT_AGENT_CONFIG,
21
+ meta: {
22
+ title: 'title1',
23
+ description: 'description1',
24
+ },
25
+ type: LobeSessionType.Agent,
26
+ } as LobeAgentSession,
27
+ {
28
+ id: '2',
29
+ meta: {
30
+ title: 'title2',
31
+ description: 'description2',
32
+ },
33
+ config: DEFAULT_AGENT_CONFIG,
34
+ type: LobeSessionType.Agent,
35
+ } as LobeAgentSession,
36
+ ],
37
+ } as unknown as SessionStore;
38
+
39
+ describe('sessionMetaSelectors', () => {
40
+ describe('currentAgentMeta', () => {
41
+ it('should return the merged default and session-specific meta data', () => {
42
+ const meta = sessionMetaSelectors.currentAgentMeta(mockSessionStore);
43
+ expect(meta).toEqual(expect.objectContaining(mockSessionStore.sessions[0].meta));
44
+ });
45
+
46
+ it('should return inbox defaults if it is an inbox session', () => {
47
+ // Assume sessionSelectors.isInboxSession() is mocked to return true for this test
48
+ const meta = sessionMetaSelectors.currentAgentMeta(mockSessionStore);
49
+ expect(meta.avatar).toBe(DEFAULT_AVATAR);
50
+ });
51
+ });
52
+
53
+ describe('currentAgentTitle', () => {
54
+ it('should return the title from the session meta data', () => {
55
+ const title = sessionMetaSelectors.currentAgentTitle(mockSessionStore);
56
+ expect(title).toBe(mockSessionStore.sessions[0].meta.title);
57
+ });
58
+ });
59
+
60
+ describe('currentAgentDescription', () => {
61
+ it('should return the description from the session meta data', () => {
62
+ const description = sessionMetaSelectors.currentAgentDescription(mockSessionStore);
63
+ expect(description).toBe(mockSessionStore.sessions[0].meta.description);
64
+ });
65
+ });
66
+
67
+ describe('getAvatar', () => {
68
+ it('should return the avatar from the meta data', () => {
69
+ const meta: MetaData = { avatar: 'custom-avatar.png' };
70
+ const avatar = sessionMetaSelectors.getAvatar(meta);
71
+ expect(avatar).toBe(meta.avatar);
72
+ });
73
+
74
+ it('should return the default avatar if none is defined in the meta data', () => {
75
+ const meta: MetaData = {};
76
+ const avatar = sessionMetaSelectors.getAvatar(meta);
77
+ expect(avatar).toBe(DEFAULT_AVATAR);
78
+ });
79
+ });
80
+
81
+ describe('getTitle', () => {
82
+ it('should return the title from the meta data', () => {
83
+ const meta: MetaData = { title: 'Custom Title' };
84
+ const title = sessionMetaSelectors.getTitle(meta);
85
+ expect(title).toBe(meta.title);
86
+ });
87
+
88
+ it('should return the default title if none is defined in the meta data', () => {
89
+ const meta: MetaData = {};
90
+ const title = sessionMetaSelectors.getTitle(meta);
91
+ expect(title).toBe('defaultSession'); // Assuming translation returns this key
92
+ });
93
+ });
94
+
95
+ describe('getDescription', () => {
96
+ it('should return the description from the meta data', () => {
97
+ const meta: MetaData = { description: 'Custom Description' };
98
+ const description = sessionMetaSelectors.getDescription(meta);
99
+ expect(description).toBe(meta.description);
100
+ });
101
+
102
+ it('should return the default description if none is defined in the meta data', () => {
103
+ const meta: MetaData = {};
104
+ const description = sessionMetaSelectors.getDescription(meta);
105
+ expect(description).toBe('noDescription'); // Assuming translation returns this key
106
+ });
107
+ });
108
+ });