@lobehub/chat 0.147.16 → 0.147.17

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 (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/contributing/Basic/Feature-Development.md +7 -7
  3. package/contributing/Basic/Feature-Development.zh-CN.md +7 -7
  4. package/next.config.mjs +61 -13
  5. package/package.json +2 -1
  6. package/sentry.client.config.ts +30 -0
  7. package/sentry.edge.config.ts +17 -0
  8. package/sentry.server.config.ts +19 -0
  9. package/src/app/home/Redirect.tsx +2 -2
  10. package/src/database/client/models/__tests__/session.test.ts +1 -3
  11. package/src/database/client/models/session.ts +4 -4
  12. package/src/services/config.ts +4 -4
  13. package/src/services/file/client.test.ts +2 -2
  14. package/src/services/file/client.ts +35 -33
  15. package/src/services/file/index.ts +8 -2
  16. package/src/services/file/type.ts +11 -0
  17. package/src/services/message/client.test.ts +6 -32
  18. package/src/services/message/client.ts +24 -37
  19. package/src/services/message/index.test.ts +48 -0
  20. package/src/services/message/index.ts +22 -2
  21. package/src/services/message/type.ts +33 -0
  22. package/src/services/plugin/client.test.ts +2 -2
  23. package/src/services/plugin/client.ts +1 -1
  24. package/src/services/plugin/index.ts +9 -3
  25. package/src/services/session/client.test.ts +37 -44
  26. package/src/services/session/client.ts +30 -22
  27. package/src/services/session/index.ts +9 -2
  28. package/src/services/session/type.ts +44 -0
  29. package/src/services/topic/client.test.ts +18 -22
  30. package/src/services/topic/client.ts +31 -23
  31. package/src/services/topic/index.ts +10 -2
  32. package/src/services/topic/type.ts +32 -0
  33. package/src/services/user/client.ts +1 -1
  34. package/src/services/user/index.ts +10 -2
  35. package/src/store/chat/slices/message/action.test.ts +12 -12
  36. package/src/store/chat/slices/message/action.ts +4 -4
  37. package/src/store/chat/slices/plugin/action.test.ts +5 -6
  38. package/src/store/chat/slices/plugin/action.ts +1 -1
  39. package/src/store/chat/slices/topic/action.test.ts +11 -6
  40. package/src/store/chat/slices/topic/action.ts +7 -5
  41. package/src/store/session/slices/agent/action.test.ts +175 -0
  42. package/src/store/session/slices/agent/action.ts +1 -1
  43. package/src/store/session/slices/session/action.test.ts +14 -15
  44. package/src/store/session/slices/session/action.ts +4 -4
  45. package/src/store/session/slices/sessionGroup/action.test.ts +6 -4
  46. package/src/store/session/slices/sessionGroup/action.ts +3 -3
@@ -0,0 +1,175 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import * as immer from 'immer';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { sessionService } from '@/services/session';
6
+ import { useGlobalStore } from '@/store/global';
7
+ import { useSessionStore } from '@/store/session';
8
+ import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
9
+
10
+ describe('AgentSlice', () => {
11
+ describe('removePlugin', () => {
12
+ it('should call togglePlugin with the provided id and false', async () => {
13
+ const { result } = renderHook(() => useSessionStore());
14
+ const pluginId = 'plugin-id';
15
+ const togglePluginMock = vi.spyOn(result.current, 'togglePlugin');
16
+
17
+ await act(async () => {
18
+ await result.current.removePlugin(pluginId);
19
+ });
20
+
21
+ expect(togglePluginMock).toHaveBeenCalledWith(pluginId, false);
22
+ togglePluginMock.mockRestore();
23
+ });
24
+ });
25
+
26
+ describe('togglePlugin', () => {
27
+ it('should add plugin id to plugins array if not present and open is true or undefined', async () => {
28
+ const { result } = renderHook(() => useSessionStore());
29
+ const pluginId = 'plugin-id';
30
+ const updateAgentConfigMock = vi.spyOn(result.current, 'updateAgentConfig');
31
+
32
+ // 模拟当前配置不包含插件 ID
33
+ vi.spyOn(agentSelectors, 'currentAgentConfig').mockReturnValue({ plugins: [] } as any);
34
+
35
+ await act(async () => {
36
+ await result.current.togglePlugin(pluginId);
37
+ });
38
+
39
+ expect(updateAgentConfigMock).toHaveBeenCalledWith(
40
+ expect.objectContaining({ plugins: [pluginId] }),
41
+ );
42
+ updateAgentConfigMock.mockRestore();
43
+ });
44
+
45
+ it('should remove plugin id from plugins array if present and open is false', async () => {
46
+ const { result } = renderHook(() => useSessionStore());
47
+ const pluginId = 'plugin-id';
48
+ const updateAgentConfigMock = vi.spyOn(result.current, 'updateAgentConfig');
49
+
50
+ // 模拟当前配置包含插件 ID
51
+ vi.spyOn(agentSelectors, 'currentAgentConfig').mockReturnValue({
52
+ plugins: [pluginId],
53
+ } as any);
54
+
55
+ await act(async () => {
56
+ await result.current.togglePlugin(pluginId, false);
57
+ });
58
+
59
+ expect(updateAgentConfigMock).toHaveBeenCalledWith(expect.objectContaining({ plugins: [] }));
60
+ updateAgentConfigMock.mockRestore();
61
+ });
62
+
63
+ it('should not modify plugins array if plugin id is not present and open is false', async () => {
64
+ const { result } = renderHook(() => useSessionStore());
65
+ const pluginId = 'plugin-id';
66
+ const updateAgentConfigMock = vi.spyOn(result.current, 'updateAgentConfig');
67
+
68
+ // 模拟当前配置不包含插件 ID
69
+ vi.spyOn(agentSelectors, 'currentAgentConfig').mockReturnValue({ plugins: [] } as any);
70
+
71
+ await act(async () => {
72
+ await result.current.togglePlugin(pluginId, false);
73
+ });
74
+
75
+ expect(updateAgentConfigMock).toHaveBeenCalledWith(expect.objectContaining({ plugins: [] }));
76
+ updateAgentConfigMock.mockRestore();
77
+ });
78
+ });
79
+
80
+ describe('updateAgentConfig', () => {
81
+ it('should update global config if current session is inbox session', async () => {
82
+ const { result } = renderHook(() => useSessionStore());
83
+ const config = { model: 'gpt-3.5-turbo' };
84
+ const updateDefaultAgentMock = vi.spyOn(useGlobalStore.getState(), 'updateDefaultAgent');
85
+
86
+ // 模拟当前会话是收件箱会话
87
+ vi.spyOn(sessionSelectors, 'isInboxSession').mockReturnValue(true);
88
+
89
+ await act(async () => {
90
+ await result.current.updateAgentConfig(config);
91
+ });
92
+
93
+ expect(updateDefaultAgentMock).toHaveBeenCalledWith({ config });
94
+ updateDefaultAgentMock.mockRestore();
95
+ });
96
+
97
+ it('should update session config if current session is not inbox session', async () => {
98
+ const { result } = renderHook(() => useSessionStore());
99
+ const config = { model: 'gpt-3.5-turbo' };
100
+ const updateSessionConfigMock = vi.spyOn(sessionService, 'updateSessionConfig');
101
+ const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
102
+
103
+ // 模拟当前会话不是收件箱会话
104
+ vi.spyOn(sessionSelectors, 'isInboxSession').mockReturnValue(false);
105
+ vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue({ id: 'session-id' } as any);
106
+ vi.spyOn(result.current, 'activeId', 'get').mockReturnValue('session-id');
107
+
108
+ await act(async () => {
109
+ await result.current.updateAgentConfig(config);
110
+ });
111
+
112
+ expect(updateSessionConfigMock).toHaveBeenCalledWith('session-id', config);
113
+ expect(refreshSessionsMock).toHaveBeenCalled();
114
+ updateSessionConfigMock.mockRestore();
115
+ refreshSessionsMock.mockRestore();
116
+ });
117
+
118
+ it('should not update config if there is no current session', async () => {
119
+ const { result } = renderHook(() => useSessionStore());
120
+ const config = { model: 'gpt-3.5-turbo' };
121
+ const updateSessionConfigMock = vi.spyOn(sessionService, 'updateSessionConfig');
122
+
123
+ // 模拟没有当前会话
124
+ vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue(null as any);
125
+
126
+ await act(async () => {
127
+ await result.current.updateAgentConfig(config);
128
+ });
129
+
130
+ expect(updateSessionConfigMock).not.toHaveBeenCalled();
131
+ updateSessionConfigMock.mockRestore();
132
+ });
133
+ });
134
+
135
+ describe('updateAgentMeta', () => {
136
+ it('should not update meta if there is no current session', async () => {
137
+ const { result } = renderHook(() => useSessionStore());
138
+ const meta = { title: 'Test Agent' };
139
+ const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
140
+ const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
141
+
142
+ // 模拟没有当前会话
143
+ vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue(null as any);
144
+
145
+ await act(async () => {
146
+ await result.current.updateAgentMeta(meta as any);
147
+ });
148
+
149
+ expect(updateSessionMock).not.toHaveBeenCalled();
150
+ expect(refreshSessionsMock).not.toHaveBeenCalled();
151
+ updateSessionMock.mockRestore();
152
+ refreshSessionsMock.mockRestore();
153
+ });
154
+
155
+ it('should update session meta and refresh sessions', async () => {
156
+ const { result } = renderHook(() => useSessionStore());
157
+ const meta = { title: 'Test Agent' };
158
+ const updateSessionMock = vi.spyOn(sessionService, 'updateSession');
159
+ const refreshSessionsMock = vi.spyOn(result.current, 'refreshSessions');
160
+
161
+ // 模拟有当前会话
162
+ vi.spyOn(sessionSelectors, 'currentSession').mockReturnValue({ id: 'session-id' } as any);
163
+ vi.spyOn(result.current, 'activeId', 'get').mockReturnValue('session-id');
164
+
165
+ await act(async () => {
166
+ await result.current.updateAgentMeta(meta);
167
+ });
168
+
169
+ expect(updateSessionMock).toHaveBeenCalledWith('session-id', { meta });
170
+ expect(refreshSessionsMock).toHaveBeenCalled();
171
+ updateSessionMock.mockRestore();
172
+ refreshSessionsMock.mockRestore();
173
+ });
174
+ });
175
+ });
@@ -78,7 +78,7 @@ export const createAgentSlice: StateCreator<
78
78
 
79
79
  const { activeId, refreshSessions } = get();
80
80
 
81
- await sessionService.updateSessionMeta(activeId, meta);
81
+ await sessionService.updateSession(activeId, { meta });
82
82
  await refreshSessions();
83
83
  },
84
84
  });
@@ -1,8 +1,6 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
- import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
3
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
3
 
5
- import { INBOX_SESSION_ID } from '@/const/session';
6
4
  import { SESSION_CHAT_URL } from '@/const/url';
7
5
  import { sessionService } from '@/services/session';
8
6
  import { useSessionStore } from '@/store/session';
@@ -12,11 +10,12 @@ import { LobeSessionType } from '@/types/session';
12
10
  vi.mock('@/services/session', () => ({
13
11
  sessionService: {
14
12
  removeAllSessions: vi.fn(),
15
- createNewSession: vi.fn(),
16
- duplicateSession: vi.fn(),
13
+ createSession: vi.fn(),
14
+ cloneSession: vi.fn(),
17
15
  updateSessionGroup: vi.fn(),
18
16
  removeSession: vi.fn(),
19
- getSessions: vi.fn(),
17
+ getAllSessions: vi.fn(),
18
+ updateSession: vi.fn(),
20
19
  updateSessionGroupId: vi.fn(),
21
20
  searchSessions: vi.fn(),
22
21
  updateSessionPinned: vi.fn(),
@@ -56,7 +55,7 @@ describe('SessionAction', () => {
56
55
  it('should create a new session and switch to it', async () => {
57
56
  const { result } = renderHook(() => useSessionStore());
58
57
  const newSessionId = 'new-session-id';
59
- vi.mocked(sessionService.createNewSession).mockResolvedValue(newSessionId);
58
+ vi.mocked(sessionService.createSession).mockResolvedValue(newSessionId);
60
59
 
61
60
  let createdSessionId;
62
61
 
@@ -64,7 +63,7 @@ describe('SessionAction', () => {
64
63
  createdSessionId = await result.current.createSession({ config: { displayMode: 'docs' } });
65
64
  });
66
65
 
67
- const call = vi.mocked(sessionService.createNewSession).mock.calls[0];
66
+ const call = vi.mocked(sessionService.createSession).mock.calls[0];
68
67
  expect(call[0]).toEqual(LobeSessionType.Agent);
69
68
  expect(call[1]).toMatchObject({ config: { displayMode: 'docs' } });
70
69
 
@@ -74,7 +73,7 @@ describe('SessionAction', () => {
74
73
  it('should create a new session but not switch to it if isSwitchSession is false', async () => {
75
74
  const { result } = renderHook(() => useSessionStore());
76
75
  const newSessionId = 'new-session-id';
77
- vi.mocked(sessionService.createNewSession).mockResolvedValue(newSessionId);
76
+ vi.mocked(sessionService.createSession).mockResolvedValue(newSessionId);
78
77
 
79
78
  let createdSessionId;
80
79
 
@@ -85,7 +84,7 @@ describe('SessionAction', () => {
85
84
  );
86
85
  });
87
86
 
88
- const call = vi.mocked(sessionService.createNewSession).mock.calls[0];
87
+ const call = vi.mocked(sessionService.createSession).mock.calls[0];
89
88
  expect(call[0]).toEqual(LobeSessionType.Agent);
90
89
  expect(call[1]).toMatchObject({ config: { displayMode: 'docs' } });
91
90
 
@@ -96,18 +95,18 @@ describe('SessionAction', () => {
96
95
  });
97
96
  });
98
97
 
99
- describe('duplicateSession', () => {
98
+ describe('cloneSession', () => {
100
99
  it('should duplicate a session and switch to the new one', async () => {
101
100
  const { result } = renderHook(() => useSessionStore());
102
101
  const sessionId = 'session-id';
103
102
  const duplicatedSessionId = 'duplicated-session-id';
104
- vi.mocked(sessionService.duplicateSession).mockResolvedValue(duplicatedSessionId);
103
+ vi.mocked(sessionService.cloneSession).mockResolvedValue(duplicatedSessionId);
105
104
 
106
105
  await act(async () => {
107
106
  await result.current.duplicateSession(sessionId);
108
107
  });
109
108
 
110
- expect(sessionService.duplicateSession).toHaveBeenCalledWith(sessionId, undefined);
109
+ expect(sessionService.cloneSession).toHaveBeenCalledWith(sessionId, undefined);
111
110
  });
112
111
  });
113
112
 
@@ -147,7 +146,7 @@ describe('SessionAction', () => {
147
146
  await result.current.pinSession(sessionId, true);
148
147
  });
149
148
 
150
- expect(sessionService.updateSessionPinned).toHaveBeenCalledWith(sessionId, true);
149
+ expect(sessionService.updateSession).toHaveBeenCalledWith(sessionId, { pinned: true });
151
150
  expect(mockRefresh).toHaveBeenCalled();
152
151
  });
153
152
 
@@ -159,7 +158,7 @@ describe('SessionAction', () => {
159
158
  await result.current.pinSession(sessionId, false);
160
159
  });
161
160
 
162
- expect(sessionService.updateSessionPinned).toHaveBeenCalledWith(sessionId, false);
161
+ expect(sessionService.updateSession).toHaveBeenCalledWith(sessionId, { pinned: false });
163
162
  expect(mockRefresh).toHaveBeenCalled();
164
163
  });
165
164
  });
@@ -174,7 +173,7 @@ describe('SessionAction', () => {
174
173
  await result.current.updateSessionGroupId(sessionId, groupId);
175
174
  });
176
175
 
177
- expect(sessionService.updateSessionGroupId).toHaveBeenCalledWith(sessionId, groupId);
176
+ expect(sessionService.updateSession).toHaveBeenCalledWith(sessionId, { group: groupId });
178
177
  expect(mockRefresh).toHaveBeenCalled();
179
178
  });
180
179
  });
@@ -91,7 +91,7 @@ export const createSessionSlice: StateCreator<
91
91
 
92
92
  const newSession: LobeAgentSession = merge(defaultAgent, agent);
93
93
 
94
- const id = await sessionService.createNewSession(LobeSessionType.Agent, newSession);
94
+ const id = await sessionService.createSession(LobeSessionType.Agent, newSession);
95
95
  await refreshSessions();
96
96
 
97
97
  // Whether to goto to the new session after creation, the default is to switch to
@@ -109,7 +109,7 @@ export const createSessionSlice: StateCreator<
109
109
 
110
110
  const newTitle = t('duplicateTitle', { ns: 'chat', title: title });
111
111
 
112
- const newId = await sessionService.duplicateSession(id, newTitle);
112
+ const newId = await sessionService.cloneSession(id, newTitle);
113
113
 
114
114
  // duplicate Session Error
115
115
  if (!newId) {
@@ -122,7 +122,7 @@ export const createSessionSlice: StateCreator<
122
122
  },
123
123
 
124
124
  pinSession: async (sessionId, pinned) => {
125
- await sessionService.updateSessionPinned(sessionId, pinned);
125
+ await sessionService.updateSession(sessionId, { pinned });
126
126
 
127
127
  await get().refreshSessions();
128
128
  },
@@ -142,7 +142,7 @@ export const createSessionSlice: StateCreator<
142
142
  },
143
143
 
144
144
  useFetchSessions: () =>
145
- useClientDataSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getSessionsWithGroup, {
145
+ useClientDataSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getGroupedSessions, {
146
146
  onSuccess: (data) => {
147
147
  // 由于 https://github.com/lobehub/lobe-chat/pull/541 的关系
148
148
  // 只有触发了 refreshSessions 才会更新 sessions,进而触发页面 rerender
@@ -31,7 +31,7 @@ describe('createSessionGroupSlice', () => {
31
31
 
32
32
  describe('clearSessionGroups', () => {
33
33
  it('should clear session groups and refresh sessions', async () => {
34
- vi.spyOn(sessionService, 'clearSessionGroups');
34
+ vi.spyOn(sessionService, 'removeSessionGroups');
35
35
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
36
36
 
37
37
  const { result } = renderHook(() => useSessionStore());
@@ -40,7 +40,7 @@ describe('createSessionGroupSlice', () => {
40
40
  await result.current.clearSessionGroups();
41
41
  });
42
42
 
43
- expect(sessionService.clearSessionGroups).toHaveBeenCalled();
43
+ expect(sessionService.removeSessionGroups).toHaveBeenCalled();
44
44
  expect(spyOnRefreshSessions).toHaveBeenCalled();
45
45
  });
46
46
  });
@@ -66,7 +66,7 @@ describe('createSessionGroupSlice', () => {
66
66
  it('should update a session group id and refresh sessions', async () => {
67
67
  const mockSessionId = 'session-id';
68
68
  const mockGroupId = 'group-id';
69
- vi.spyOn(sessionService, 'updateSessionGroupId');
69
+ vi.spyOn(sessionService, 'updateSession');
70
70
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
71
71
 
72
72
  const { result } = renderHook(() => useSessionStore());
@@ -75,7 +75,9 @@ describe('createSessionGroupSlice', () => {
75
75
  await result.current.updateSessionGroupId(mockSessionId, mockGroupId);
76
76
  });
77
77
 
78
- expect(sessionService.updateSessionGroupId).toHaveBeenCalledWith(mockSessionId, mockGroupId);
78
+ expect(sessionService.updateSession).toHaveBeenCalledWith(mockSessionId, {
79
+ group: mockGroupId,
80
+ });
79
81
  expect(spyOnRefreshSessions).toHaveBeenCalled();
80
82
  });
81
83
  });
@@ -28,7 +28,7 @@ export const createSessionGroupSlice: StateCreator<
28
28
  },
29
29
 
30
30
  clearSessionGroups: async () => {
31
- await sessionService.clearSessionGroups();
31
+ await sessionService.removeSessionGroups();
32
32
  await get().refreshSessions();
33
33
  },
34
34
 
@@ -36,8 +36,8 @@ export const createSessionGroupSlice: StateCreator<
36
36
  await sessionService.removeSessionGroup(id);
37
37
  await get().refreshSessions();
38
38
  },
39
- updateSessionGroupId: async (sessionId, groupId) => {
40
- await sessionService.updateSessionGroupId(sessionId, groupId);
39
+ updateSessionGroupId: async (sessionId, group) => {
40
+ await sessionService.updateSession(sessionId, { group });
41
41
 
42
42
  await get().refreshSessions();
43
43
  },