@lobehub/chat 1.36.46 → 1.37.0
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/CHANGELOG.md +25 -0
- package/README.ja-JP.md +8 -8
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/changelog/v1.json +9 -0
- package/next.config.mjs +4 -1
- package/package.json +5 -3
- package/scripts/migrateClientDB/compile-migrations.ts +14 -0
- package/src/app/(main)/(mobile)/me/(home)/layout.tsx +2 -0
- package/src/app/(main)/chat/_layout/Desktop/index.tsx +3 -2
- package/src/app/(main)/chat/_layout/Mobile.tsx +5 -3
- package/src/app/(main)/chat/features/Migration/DBReader.ts +290 -0
- package/src/app/(main)/chat/features/Migration/UpgradeButton.tsx +4 -8
- package/src/app/(main)/chat/features/Migration/index.tsx +26 -15
- package/src/app/(main)/settings/_layout/Desktop/index.tsx +2 -0
- package/src/app/loading/Client/Content.tsx +11 -1
- package/src/app/loading/Client/Error.tsx +27 -0
- package/src/app/loading/stage.ts +8 -0
- package/src/components/FullscreenLoading/index.tsx +4 -3
- package/src/const/version.ts +1 -0
- package/src/database/client/db.test.ts +172 -0
- package/src/database/client/db.ts +246 -0
- package/src/database/client/migrations.json +289 -0
- package/src/features/InitClientDB/EnableModal.tsx +111 -0
- package/src/features/InitClientDB/ErrorResult.tsx +125 -0
- package/src/features/InitClientDB/InitIndicator.tsx +124 -0
- package/src/features/InitClientDB/PGliteSVG.tsx +22 -0
- package/src/features/InitClientDB/index.tsx +37 -0
- package/src/hooks/useCheckPluginsIsInstalled.ts +2 -2
- package/src/hooks/useFetchInstalledPlugins.ts +2 -2
- package/src/hooks/useFetchMessages.ts +2 -2
- package/src/hooks/useFetchSessions.ts +2 -2
- package/src/hooks/useFetchThreads.ts +2 -2
- package/src/hooks/useFetchTopics.ts +2 -2
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -2
- package/src/services/baseClientService/index.ts +9 -0
- package/src/services/debug.ts +32 -34
- package/src/services/file/index.ts +6 -2
- package/src/services/file/pglite.test.ts +198 -0
- package/src/services/file/pglite.ts +84 -0
- package/src/services/file/type.ts +4 -3
- package/src/services/github.ts +17 -0
- package/src/services/import/index.ts +6 -2
- package/src/services/import/pglite.test.ts +997 -0
- package/src/services/import/pglite.ts +34 -0
- package/src/services/message/client.ts +2 -0
- package/src/services/message/index.ts +6 -2
- package/src/services/message/pglite.test.ts +430 -0
- package/src/services/message/pglite.ts +118 -0
- package/src/services/message/server.ts +9 -9
- package/src/services/message/type.ts +3 -4
- package/src/services/plugin/index.ts +6 -2
- package/src/services/plugin/pglite.test.ts +175 -0
- package/src/services/plugin/pglite.ts +51 -0
- package/src/services/session/client.ts +1 -1
- package/src/services/session/index.ts +6 -2
- package/src/services/session/pglite.test.ts +411 -0
- package/src/services/session/pglite.ts +184 -0
- package/src/services/session/type.ts +14 -1
- package/src/services/topic/index.ts +6 -3
- package/src/services/topic/pglite.test.ts +212 -0
- package/src/services/topic/pglite.ts +85 -0
- package/src/services/user/client.test.ts +0 -1
- package/src/services/user/index.ts +8 -2
- package/src/services/user/pglite.test.ts +98 -0
- package/src/services/user/pglite.ts +92 -0
- package/src/store/chat/slices/builtinTool/action.test.ts +3 -4
- package/src/store/global/actions/clientDb.ts +51 -0
- package/src/store/global/initialState.ts +13 -0
- package/src/store/global/selectors.ts +24 -3
- package/src/store/global/store.ts +3 -1
- package/src/store/session/slices/sessionGroup/reducer.test.ts +6 -6
- package/src/store/user/slices/common/action.ts +2 -4
- package/src/types/clientDB.ts +29 -0
- package/src/types/importer.ts +17 -5
- package/src/types/meta.ts +0 -9
- package/src/types/session/sessionGroup.ts +3 -3
- package/src/services/message/index.test.ts +0 -48
@@ -0,0 +1,184 @@
|
|
1
|
+
import { DeepPartial } from 'utility-types';
|
2
|
+
|
3
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
4
|
+
import { clientDB } from '@/database/client/db';
|
5
|
+
import { AgentItem } from '@/database/schemas';
|
6
|
+
import { SessionModel } from '@/database/server/models/session';
|
7
|
+
import { SessionGroupModel } from '@/database/server/models/sessionGroup';
|
8
|
+
import { BaseClientService } from '@/services/baseClientService';
|
9
|
+
import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
|
10
|
+
import { MetaData } from '@/types/meta';
|
11
|
+
import {
|
12
|
+
ChatSessionList,
|
13
|
+
LobeAgentSession,
|
14
|
+
LobeSessionType,
|
15
|
+
LobeSessions,
|
16
|
+
SessionGroupItem,
|
17
|
+
SessionGroups,
|
18
|
+
UpdateSessionParams,
|
19
|
+
} from '@/types/session';
|
20
|
+
|
21
|
+
import { ISessionService } from './type';
|
22
|
+
|
23
|
+
export class ClientService extends BaseClientService implements ISessionService {
|
24
|
+
private get sessionModel(): SessionModel {
|
25
|
+
return new SessionModel(clientDB as any, this.userId);
|
26
|
+
}
|
27
|
+
|
28
|
+
private get sessionGroupModel(): SessionGroupModel {
|
29
|
+
return new SessionGroupModel(clientDB as any, this.userId);
|
30
|
+
}
|
31
|
+
|
32
|
+
async createSession(type: LobeSessionType, data: Partial<LobeAgentSession>): Promise<string> {
|
33
|
+
const { config, group, meta, ...session } = data;
|
34
|
+
|
35
|
+
const item = await this.sessionModel.create({
|
36
|
+
config: { ...config, ...meta } as any,
|
37
|
+
session: { ...session, groupId: group },
|
38
|
+
type,
|
39
|
+
});
|
40
|
+
if (!item) {
|
41
|
+
throw new Error('session create Error');
|
42
|
+
}
|
43
|
+
return item.id;
|
44
|
+
}
|
45
|
+
|
46
|
+
async batchCreateSessions(importSessions: LobeSessions) {
|
47
|
+
// @ts-ignore
|
48
|
+
return this.sessionModel.batchCreate(importSessions);
|
49
|
+
}
|
50
|
+
|
51
|
+
async cloneSession(id: string, newTitle: string): Promise<string | undefined> {
|
52
|
+
const res = await this.sessionModel.duplicate(id, newTitle);
|
53
|
+
|
54
|
+
if (res) return res?.id;
|
55
|
+
}
|
56
|
+
|
57
|
+
async getGroupedSessions(): Promise<ChatSessionList> {
|
58
|
+
return this.sessionModel.queryWithGroups();
|
59
|
+
}
|
60
|
+
|
61
|
+
async getSessionConfig(id: string): Promise<LobeAgentConfig> {
|
62
|
+
const res = await this.sessionModel.findByIdOrSlug(id);
|
63
|
+
|
64
|
+
if (!res) throw new Error('Session not found');
|
65
|
+
|
66
|
+
return res.agent as LobeAgentConfig;
|
67
|
+
}
|
68
|
+
|
69
|
+
/**
|
70
|
+
* 这个方法要对应移除的
|
71
|
+
*/
|
72
|
+
async getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise<LobeSessions> {
|
73
|
+
switch (type) {
|
74
|
+
// TODO: add a filter to get only agents or agents
|
75
|
+
case 'group': {
|
76
|
+
// @ts-ignore
|
77
|
+
return this.sessionModel.query();
|
78
|
+
}
|
79
|
+
case 'agent': {
|
80
|
+
// @ts-ignore
|
81
|
+
return this.sessionModel.query();
|
82
|
+
}
|
83
|
+
|
84
|
+
case 'all': {
|
85
|
+
// @ts-ignore
|
86
|
+
return this.sessionModel.query();
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
async countSessions() {
|
92
|
+
return this.sessionModel.count();
|
93
|
+
}
|
94
|
+
|
95
|
+
async searchSessions(keyword: string) {
|
96
|
+
return this.sessionModel.queryByKeyword(keyword);
|
97
|
+
}
|
98
|
+
|
99
|
+
async updateSession(id: string, value: Partial<UpdateSessionParams>) {
|
100
|
+
return this.sessionModel.update(id, {
|
101
|
+
...value,
|
102
|
+
groupId: value.group === 'default' ? null : value.group,
|
103
|
+
});
|
104
|
+
}
|
105
|
+
|
106
|
+
async updateSessionConfig(
|
107
|
+
activeId: string,
|
108
|
+
config: DeepPartial<LobeAgentConfig>,
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
110
|
+
_?: AbortSignal,
|
111
|
+
) {
|
112
|
+
const session = await this.sessionModel.findByIdOrSlug(activeId);
|
113
|
+
if (!session || !config) return;
|
114
|
+
|
115
|
+
return this.sessionModel.updateConfig(session.agent.id, config as AgentItem);
|
116
|
+
}
|
117
|
+
|
118
|
+
async updateSessionMeta(
|
119
|
+
activeId: string,
|
120
|
+
meta: Partial<MetaData>,
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
122
|
+
_?: AbortSignal,
|
123
|
+
) {
|
124
|
+
// inbox 不允许修改 meta
|
125
|
+
if (activeId === INBOX_SESSION_ID) return;
|
126
|
+
|
127
|
+
return this.sessionModel.update(activeId, meta);
|
128
|
+
}
|
129
|
+
|
130
|
+
async updateSessionChatConfig(
|
131
|
+
activeId: string,
|
132
|
+
config: DeepPartial<LobeAgentChatConfig>,
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
134
|
+
_?: AbortSignal,
|
135
|
+
) {
|
136
|
+
return this.updateSessionConfig(activeId, { chatConfig: config });
|
137
|
+
}
|
138
|
+
|
139
|
+
async removeSession(id: string) {
|
140
|
+
return this.sessionModel.delete(id);
|
141
|
+
}
|
142
|
+
|
143
|
+
async removeAllSessions() {
|
144
|
+
return this.sessionModel.deleteAll();
|
145
|
+
}
|
146
|
+
|
147
|
+
// ************************************** //
|
148
|
+
// *********** SessionGroup *********** //
|
149
|
+
// ************************************** //
|
150
|
+
|
151
|
+
async createSessionGroup(name: string, sort?: number) {
|
152
|
+
const item = await this.sessionGroupModel.create({ name, sort });
|
153
|
+
if (!item) {
|
154
|
+
throw new Error('session group create Error');
|
155
|
+
}
|
156
|
+
|
157
|
+
return item.id;
|
158
|
+
}
|
159
|
+
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
161
|
+
async batchCreateSessionGroups(_groups: SessionGroups) {
|
162
|
+
return { added: 0, ids: [], skips: [], success: true };
|
163
|
+
}
|
164
|
+
|
165
|
+
async removeSessionGroup(id: string) {
|
166
|
+
return await this.sessionGroupModel.delete(id);
|
167
|
+
}
|
168
|
+
|
169
|
+
async updateSessionGroup(id: string, data: Partial<SessionGroupItem>) {
|
170
|
+
return this.sessionGroupModel.update(id, data);
|
171
|
+
}
|
172
|
+
|
173
|
+
async updateSessionGroupOrder(sortMap: { id: string; sort: number }[]) {
|
174
|
+
return this.sessionGroupModel.updateOrder(sortMap);
|
175
|
+
}
|
176
|
+
|
177
|
+
async getSessionGroups(): Promise<SessionGroupItem[]> {
|
178
|
+
return this.sessionGroupModel.query();
|
179
|
+
}
|
180
|
+
|
181
|
+
async removeSessionGroups() {
|
182
|
+
return this.sessionGroupModel.deleteAll();
|
183
|
+
}
|
184
|
+
}
|
@@ -16,13 +16,21 @@ import {
|
|
16
16
|
|
17
17
|
export interface ISessionService {
|
18
18
|
createSession(type: LobeSessionType, defaultValue: Partial<LobeAgentSession>): Promise<string>;
|
19
|
+
|
20
|
+
/**
|
21
|
+
* 需要废弃
|
22
|
+
* @deprecated
|
23
|
+
*/
|
19
24
|
batchCreateSessions(importSessions: LobeSessions): Promise<any>;
|
20
25
|
cloneSession(id: string, newTitle: string): Promise<string | undefined>;
|
21
26
|
|
22
27
|
getGroupedSessions(): Promise<ChatSessionList>;
|
28
|
+
|
29
|
+
/**
|
30
|
+
* @deprecated
|
31
|
+
*/
|
23
32
|
getSessionsByType(type: 'agent' | 'group' | 'all'): Promise<LobeSessions>;
|
24
33
|
countSessions(): Promise<number>;
|
25
|
-
hasSessions(): Promise<boolean>;
|
26
34
|
searchSessions(keyword: string): Promise<LobeSessions>;
|
27
35
|
|
28
36
|
updateSession(
|
@@ -53,6 +61,11 @@ export interface ISessionService {
|
|
53
61
|
// ************************************** //
|
54
62
|
|
55
63
|
createSessionGroup(name: string, sort?: number): Promise<string>;
|
64
|
+
|
65
|
+
/**
|
66
|
+
* 需要废弃
|
67
|
+
* @deprecated
|
68
|
+
*/
|
56
69
|
batchCreateSessionGroups(groups: SessionGroups): Promise<BatchTaskResult>;
|
57
70
|
|
58
71
|
getSessionGroups(): Promise<SessionGroupItem[]>;
|
@@ -1,6 +1,9 @@
|
|
1
|
-
|
2
|
-
import { ClientService } from './
|
1
|
+
import { ClientService as DeprecatedService } from './client';
|
2
|
+
import { ClientService } from './pglite';
|
3
3
|
import { ServerService } from './server';
|
4
4
|
|
5
|
+
const clientService =
|
6
|
+
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
|
+
|
5
8
|
export const topicService =
|
6
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() :
|
9
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
|
@@ -0,0 +1,212 @@
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { clientDB, initializeDB } from '@/database/client/db';
|
5
|
+
import { sessions, topics, users } from '@/database/schemas';
|
6
|
+
import { ChatTopic } from '@/types/topic';
|
7
|
+
|
8
|
+
import { ClientService } from './pglite';
|
9
|
+
|
10
|
+
// Mock data
|
11
|
+
const userId = 'topic-user-test';
|
12
|
+
const sessionId = 'topic-session';
|
13
|
+
const mockTopicId = 'mock-topic-id';
|
14
|
+
|
15
|
+
const mockTopic = {
|
16
|
+
id: mockTopicId,
|
17
|
+
title: 'Mock Topic',
|
18
|
+
};
|
19
|
+
|
20
|
+
const topicService = new ClientService(userId);
|
21
|
+
|
22
|
+
beforeEach(async () => {
|
23
|
+
await initializeDB();
|
24
|
+
|
25
|
+
await clientDB.delete(users);
|
26
|
+
|
27
|
+
// 创建测试数据
|
28
|
+
await clientDB.transaction(async (tx) => {
|
29
|
+
await tx.insert(users).values({ id: userId });
|
30
|
+
await tx.insert(sessions).values({ id: sessionId, userId });
|
31
|
+
await tx.insert(topics).values({ ...mockTopic, sessionId, userId });
|
32
|
+
});
|
33
|
+
});
|
34
|
+
|
35
|
+
describe('TopicService', () => {
|
36
|
+
describe('createTopic', () => {
|
37
|
+
it('should create a topic and return its id', async () => {
|
38
|
+
// Setup
|
39
|
+
const createParams = {
|
40
|
+
title: 'New Topic',
|
41
|
+
sessionId: sessionId,
|
42
|
+
};
|
43
|
+
|
44
|
+
// Execute
|
45
|
+
const topicId = await topicService.createTopic(createParams);
|
46
|
+
|
47
|
+
// Assert
|
48
|
+
expect(topicId).toBeDefined();
|
49
|
+
});
|
50
|
+
|
51
|
+
it('should throw an error if topic creation fails', async () => {
|
52
|
+
// Setup
|
53
|
+
const createParams = {
|
54
|
+
title: 'New Topic',
|
55
|
+
sessionId: 123 as any, // sessionId should be string
|
56
|
+
};
|
57
|
+
|
58
|
+
// Execute & Assert
|
59
|
+
await expect(topicService.createTopic(createParams)).rejects.toThrowError();
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
describe('getTopics', () => {
|
64
|
+
// Example for getTopics
|
65
|
+
it('should query topics with given parameters', async () => {
|
66
|
+
// Setup
|
67
|
+
const queryParams = { sessionId };
|
68
|
+
|
69
|
+
// Execute
|
70
|
+
const data = await topicService.getTopics(queryParams);
|
71
|
+
|
72
|
+
// Assert
|
73
|
+
expect(data[0]).toMatchObject(mockTopic);
|
74
|
+
});
|
75
|
+
});
|
76
|
+
|
77
|
+
describe('updateTopic', () => {
|
78
|
+
// Example for updateFavorite
|
79
|
+
it('should toggle favorite status of a topic', async () => {
|
80
|
+
// Execute
|
81
|
+
const result = await topicService.updateTopic(mockTopicId, { favorite: true });
|
82
|
+
|
83
|
+
// Assert
|
84
|
+
expect(result[0].favorite).toBeTruthy();
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should update the title of a topic', async () => {
|
88
|
+
// Setup
|
89
|
+
const newTitle = 'Updated Topic Title';
|
90
|
+
|
91
|
+
// Execute
|
92
|
+
const result = await topicService.updateTopic(mockTopicId, { title: newTitle });
|
93
|
+
|
94
|
+
// Assert
|
95
|
+
expect(result[0].title).toEqual(newTitle);
|
96
|
+
});
|
97
|
+
});
|
98
|
+
|
99
|
+
describe('removeTopic', () => {
|
100
|
+
it('should remove a topic by id', async () => {
|
101
|
+
// Execute
|
102
|
+
await topicService.removeTopic(mockTopicId);
|
103
|
+
const result = await clientDB.query.topics.findFirst({ where: eq(topics.id, mockTopicId) });
|
104
|
+
|
105
|
+
// Assert
|
106
|
+
expect(result).toBeUndefined();
|
107
|
+
});
|
108
|
+
});
|
109
|
+
|
110
|
+
describe('removeTopics', () => {
|
111
|
+
it('should remove all topics with a given session id', async () => {
|
112
|
+
// Setup
|
113
|
+
const sessionId = 'session-id';
|
114
|
+
|
115
|
+
// Execute
|
116
|
+
await topicService.removeTopics(sessionId);
|
117
|
+
const result = await clientDB.query.topics.findMany({
|
118
|
+
where: eq(topics.sessionId, sessionId),
|
119
|
+
});
|
120
|
+
|
121
|
+
expect(result.length).toEqual(0);
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
describe('batchRemoveTopics', () => {
|
126
|
+
it('should batch remove topics', async () => {
|
127
|
+
await clientDB.insert(topics).values([{ id: 'topic-id-1', title: 'topic-title', userId }]);
|
128
|
+
// Setup
|
129
|
+
const topicIds = [mockTopicId, 'another-topic-id'];
|
130
|
+
|
131
|
+
// Execute
|
132
|
+
await topicService.batchRemoveTopics(topicIds);
|
133
|
+
|
134
|
+
const count = await clientDB.$count(topics);
|
135
|
+
|
136
|
+
// Assert
|
137
|
+
expect(count).toBe(1);
|
138
|
+
});
|
139
|
+
});
|
140
|
+
|
141
|
+
describe('removeAllTopic', () => {
|
142
|
+
it('should clear all topics from the table', async () => {
|
143
|
+
// Execute
|
144
|
+
await topicService.removeAllTopic();
|
145
|
+
|
146
|
+
const count = await clientDB.$count(topics);
|
147
|
+
// Assert
|
148
|
+
expect(count).toBe(0);
|
149
|
+
});
|
150
|
+
});
|
151
|
+
|
152
|
+
describe('batchCreateTopics', () => {
|
153
|
+
it('should batch create topics', async () => {
|
154
|
+
// Execute
|
155
|
+
const result = await topicService.batchCreateTopics([
|
156
|
+
{ id: 'topic-id-1', title: 'topic-title' },
|
157
|
+
{ id: 'topic-id-2', title: 'topic-title' },
|
158
|
+
] as ChatTopic[]);
|
159
|
+
|
160
|
+
// Assert
|
161
|
+
expect(result.success).toBeTruthy();
|
162
|
+
expect(result.added).toBe(2);
|
163
|
+
});
|
164
|
+
});
|
165
|
+
|
166
|
+
describe('getAllTopics', () => {
|
167
|
+
it('should retrieve all topics', async () => {
|
168
|
+
await clientDB.insert(topics).values([
|
169
|
+
{ id: 'topic-id-1', title: 'topic-title', userId },
|
170
|
+
{ id: 'topic-id-2', title: 'topic-title', userId },
|
171
|
+
]);
|
172
|
+
// Execute
|
173
|
+
const result = await topicService.getAllTopics();
|
174
|
+
|
175
|
+
// Assert
|
176
|
+
expect(result.length).toEqual(3);
|
177
|
+
});
|
178
|
+
});
|
179
|
+
|
180
|
+
describe('searchTopics', () => {
|
181
|
+
it('should return all topics that match the keyword', async () => {
|
182
|
+
// Setup
|
183
|
+
const keyword = 'Topic';
|
184
|
+
|
185
|
+
// Execute
|
186
|
+
const result = await topicService.searchTopics(keyword, sessionId);
|
187
|
+
|
188
|
+
// Assert
|
189
|
+
expect(result.length).toEqual(1);
|
190
|
+
});
|
191
|
+
it('should return empty topic if not match the keyword', async () => {
|
192
|
+
// Setup
|
193
|
+
const keyword = 'search';
|
194
|
+
|
195
|
+
// Execute
|
196
|
+
const result = await topicService.searchTopics(keyword, sessionId);
|
197
|
+
|
198
|
+
// Assert
|
199
|
+
expect(result.length).toEqual(0);
|
200
|
+
});
|
201
|
+
});
|
202
|
+
|
203
|
+
describe('countTopics', () => {
|
204
|
+
it('should return topic counts', async () => {
|
205
|
+
// Execute
|
206
|
+
const result = await topicService.countTopics();
|
207
|
+
|
208
|
+
// Assert
|
209
|
+
expect(result).toBe(1);
|
210
|
+
});
|
211
|
+
});
|
212
|
+
});
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
2
|
+
import { clientDB } from '@/database/client/db';
|
3
|
+
import { TopicModel } from '@/database/server/models/topic';
|
4
|
+
import { BaseClientService } from '@/services/baseClientService';
|
5
|
+
import { ChatTopic } from '@/types/topic';
|
6
|
+
|
7
|
+
import { CreateTopicParams, ITopicService, QueryTopicParams } from './type';
|
8
|
+
|
9
|
+
export class ClientService extends BaseClientService implements ITopicService {
|
10
|
+
private get topicModel(): TopicModel {
|
11
|
+
return new TopicModel(clientDB as any, this.userId);
|
12
|
+
}
|
13
|
+
|
14
|
+
async createTopic(params: CreateTopicParams): Promise<string> {
|
15
|
+
const item = await this.topicModel.create({
|
16
|
+
...params,
|
17
|
+
sessionId: this.toDbSessionId(params.sessionId),
|
18
|
+
} as any);
|
19
|
+
|
20
|
+
if (!item) {
|
21
|
+
throw new Error('topic create Error');
|
22
|
+
}
|
23
|
+
|
24
|
+
return item.id;
|
25
|
+
}
|
26
|
+
|
27
|
+
async batchCreateTopics(importTopics: ChatTopic[]) {
|
28
|
+
const data = await this.topicModel.batchCreate(importTopics as any);
|
29
|
+
|
30
|
+
return { added: data.length, ids: [], skips: [], success: true };
|
31
|
+
}
|
32
|
+
|
33
|
+
async cloneTopic(id: string, newTitle?: string) {
|
34
|
+
const data = await this.topicModel.duplicate(id, newTitle);
|
35
|
+
return data.topic.id;
|
36
|
+
}
|
37
|
+
|
38
|
+
async getTopics(params: QueryTopicParams) {
|
39
|
+
const data = await this.topicModel.query({
|
40
|
+
...params,
|
41
|
+
sessionId: this.toDbSessionId(params.sessionId),
|
42
|
+
});
|
43
|
+
return data as unknown as Promise<ChatTopic[]>;
|
44
|
+
}
|
45
|
+
|
46
|
+
async searchTopics(keyword: string, sessionId?: string) {
|
47
|
+
const data = await this.topicModel.queryByKeyword(keyword, this.toDbSessionId(sessionId));
|
48
|
+
|
49
|
+
return data as unknown as Promise<ChatTopic[]>;
|
50
|
+
}
|
51
|
+
|
52
|
+
async getAllTopics() {
|
53
|
+
const data = await this.topicModel.queryAll();
|
54
|
+
|
55
|
+
return data as unknown as Promise<ChatTopic[]>;
|
56
|
+
}
|
57
|
+
|
58
|
+
async countTopics() {
|
59
|
+
return this.topicModel.count();
|
60
|
+
}
|
61
|
+
|
62
|
+
async updateTopic(id: string, data: Partial<ChatTopic>) {
|
63
|
+
return this.topicModel.update(id, data as any);
|
64
|
+
}
|
65
|
+
|
66
|
+
async removeTopic(id: string) {
|
67
|
+
return this.topicModel.delete(id);
|
68
|
+
}
|
69
|
+
|
70
|
+
async removeTopics(sessionId: string) {
|
71
|
+
return this.topicModel.batchDeleteBySessionId(this.toDbSessionId(sessionId));
|
72
|
+
}
|
73
|
+
|
74
|
+
async batchRemoveTopics(topics: string[]) {
|
75
|
+
return this.topicModel.batchDelete(topics);
|
76
|
+
}
|
77
|
+
|
78
|
+
async removeAllTopic() {
|
79
|
+
return this.topicModel.deleteAll();
|
80
|
+
}
|
81
|
+
|
82
|
+
private toDbSessionId(sessionId?: string | null) {
|
83
|
+
return sessionId === INBOX_SESSION_ID ? null : sessionId;
|
84
|
+
}
|
85
|
+
}
|
@@ -4,7 +4,6 @@ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { UserModel } from '@/database/_deprecated/models/user';
|
5
5
|
import { UserPreference } from '@/types/user';
|
6
6
|
import { UserSettings } from '@/types/user/settings';
|
7
|
-
import { AsyncLocalStorage } from '@/utils/localStorage';
|
8
7
|
|
9
8
|
import { ClientService } from './client';
|
10
9
|
|
@@ -1,5 +1,11 @@
|
|
1
|
-
import { ClientService } from './client';
|
1
|
+
import { ClientService as DeprecatedService } from './client';
|
2
|
+
import { ClientService } from './pglite';
|
2
3
|
import { ServerService } from './server';
|
3
4
|
|
5
|
+
const clientService =
|
6
|
+
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
|
+
|
4
8
|
export const userService =
|
5
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() :
|
9
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
|
10
|
+
|
11
|
+
export const userClientService = clientService;
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
2
|
+
import { DeepPartial } from 'utility-types';
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
4
|
+
|
5
|
+
import { clientDB, initializeDB } from '@/database/client/db';
|
6
|
+
import { userSettings, users } from '@/database/schemas';
|
7
|
+
import { UserPreference } from '@/types/user';
|
8
|
+
import { UserSettings } from '@/types/user/settings';
|
9
|
+
|
10
|
+
import { ClientService } from './pglite';
|
11
|
+
|
12
|
+
const mockUser = {
|
13
|
+
avatar: 'avatar.png',
|
14
|
+
settings: { themeMode: 'light' } as unknown as UserSettings,
|
15
|
+
uuid: 'user-id',
|
16
|
+
};
|
17
|
+
|
18
|
+
const mockPreference = {
|
19
|
+
useCmdEnterToSend: true,
|
20
|
+
} as UserPreference;
|
21
|
+
const clientService = new ClientService(mockUser.uuid);
|
22
|
+
|
23
|
+
beforeEach(async () => {
|
24
|
+
vi.clearAllMocks();
|
25
|
+
|
26
|
+
await initializeDB();
|
27
|
+
await clientDB.delete(users);
|
28
|
+
|
29
|
+
await clientDB.insert(users).values({ id: mockUser.uuid, avatar: 'avatar.png' });
|
30
|
+
await clientDB
|
31
|
+
.insert(userSettings)
|
32
|
+
.values({ id: mockUser.uuid, general: { themeMode: 'light' } });
|
33
|
+
});
|
34
|
+
|
35
|
+
describe('ClientService', () => {
|
36
|
+
it('should get user state correctly', async () => {
|
37
|
+
const spyOn = vi
|
38
|
+
.spyOn(clientService['preferenceStorage'], 'getFromLocalStorage')
|
39
|
+
.mockResolvedValue(mockPreference);
|
40
|
+
|
41
|
+
const userState = await clientService.getUserState();
|
42
|
+
|
43
|
+
expect(userState).toMatchObject({
|
44
|
+
avatar: mockUser.avatar,
|
45
|
+
isOnboard: true,
|
46
|
+
canEnablePWAGuide: false,
|
47
|
+
hasConversation: false,
|
48
|
+
canEnableTrace: false,
|
49
|
+
preference: mockPreference,
|
50
|
+
settings: { general: { themeMode: 'light' } },
|
51
|
+
userId: mockUser.uuid,
|
52
|
+
});
|
53
|
+
expect(spyOn).toHaveBeenCalledTimes(1);
|
54
|
+
});
|
55
|
+
|
56
|
+
it('should update user settings correctly', async () => {
|
57
|
+
const settingsPatch: DeepPartial<UserSettings> = { general: { themeMode: 'dark' } };
|
58
|
+
|
59
|
+
await clientService.updateUserSettings(settingsPatch);
|
60
|
+
|
61
|
+
const result = await clientDB.query.userSettings.findFirst({
|
62
|
+
where: eq(userSettings.id, mockUser.uuid),
|
63
|
+
});
|
64
|
+
|
65
|
+
expect(result).toMatchObject(settingsPatch);
|
66
|
+
});
|
67
|
+
|
68
|
+
it('should reset user settings correctly', async () => {
|
69
|
+
await clientService.resetUserSettings();
|
70
|
+
|
71
|
+
const result = await clientDB.query.userSettings.findFirst({
|
72
|
+
where: eq(userSettings.id, mockUser.uuid),
|
73
|
+
});
|
74
|
+
|
75
|
+
expect(result).toBeUndefined();
|
76
|
+
});
|
77
|
+
|
78
|
+
it('should update user avatar correctly', async () => {
|
79
|
+
const newAvatar = 'new-avatar.png';
|
80
|
+
|
81
|
+
await clientService.updateAvatar(newAvatar);
|
82
|
+
});
|
83
|
+
|
84
|
+
it('should update user preference correctly', async () => {
|
85
|
+
const newPreference = {
|
86
|
+
useCmdEnterToSend: false,
|
87
|
+
} as UserPreference;
|
88
|
+
|
89
|
+
const spyOn = vi
|
90
|
+
.spyOn(clientService['preferenceStorage'], 'saveToLocalStorage')
|
91
|
+
.mockResolvedValue(undefined);
|
92
|
+
|
93
|
+
await clientService.updatePreference(newPreference);
|
94
|
+
|
95
|
+
expect(spyOn).toHaveBeenCalledWith(newPreference);
|
96
|
+
expect(spyOn).toHaveBeenCalledTimes(1);
|
97
|
+
});
|
98
|
+
});
|