@lobehub/chat 0.162.25 → 0.164.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/.github/workflows/release.yml +21 -2
- package/.github/workflows/sync.yml +1 -1
- package/.github/workflows/test.yml +35 -4
- package/CHANGELOG.md +63 -0
- package/LICENSE +38 -21
- package/codecov.yml +11 -0
- package/drizzle.config.ts +29 -0
- package/next.config.mjs +3 -0
- package/package.json +24 -4
- package/scripts/migrateServerDB/index.ts +30 -0
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
- package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
- package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
- package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
- package/src/app/api/webhooks/clerk/route.ts +159 -0
- package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
- package/src/app/trpc/edge/[trpc]/route.ts +1 -1
- package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
- package/src/config/auth.ts +2 -0
- package/src/config/db.ts +13 -1
- package/src/database/server/core/db.ts +44 -0
- package/src/database/server/core/dbForTest.ts +45 -0
- package/src/database/server/index.ts +1 -0
- package/src/database/server/migrations/0000_init.sql +439 -0
- package/src/database/server/migrations/0001_add_client_id.sql +9 -0
- package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
- package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
- package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
- package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
- package/src/database/server/migrations/meta/_journal.json +27 -0
- package/src/database/server/models/__tests__/file.test.ts +140 -0
- package/src/database/server/models/__tests__/message.test.ts +847 -0
- package/src/database/server/models/__tests__/plugin.test.ts +172 -0
- package/src/database/server/models/__tests__/session.test.ts +595 -0
- package/src/database/server/models/__tests__/topic.test.ts +623 -0
- package/src/database/server/models/__tests__/user.test.ts +173 -0
- package/src/database/server/models/_template.ts +44 -0
- package/src/database/server/models/file.ts +51 -0
- package/src/database/server/models/message.ts +378 -0
- package/src/database/server/models/plugin.ts +63 -0
- package/src/database/server/models/session.ts +290 -0
- package/src/database/server/models/sessionGroup.ts +69 -0
- package/src/database/server/models/topic.ts +265 -0
- package/src/database/server/models/user.ts +138 -0
- package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
- package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
- package/src/database/server/modules/DataImporter/index.ts +333 -0
- package/src/database/server/schemas/_id.ts +15 -0
- package/src/database/server/schemas/lobechat.ts +601 -0
- package/src/database/server/utils/idGenerator.test.ts +39 -0
- package/src/database/server/utils/idGenerator.ts +26 -0
- package/src/features/User/UserPanel/useMenu.tsx +43 -37
- package/src/libs/trpc/client.ts +52 -3
- package/src/server/files/s3.ts +21 -1
- package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
- package/src/server/keyVaultsEncrypt/index.ts +93 -0
- package/src/server/mock.ts +1 -1
- package/src/server/routers/{index.ts → edge/index.ts} +3 -3
- package/src/server/routers/lambda/file.ts +49 -0
- package/src/server/routers/lambda/importer.ts +54 -0
- package/src/server/routers/lambda/index.ts +28 -0
- package/src/server/routers/lambda/message.ts +165 -0
- package/src/server/routers/lambda/plugin.ts +100 -0
- package/src/server/routers/lambda/session.ts +194 -0
- package/src/server/routers/lambda/sessionGroup.ts +77 -0
- package/src/server/routers/lambda/topic.ts +134 -0
- package/src/server/routers/lambda/user.ts +57 -0
- package/src/services/file/index.ts +4 -7
- package/src/services/file/server.ts +45 -0
- package/src/services/import/index.ts +4 -1
- package/src/services/import/server.ts +115 -0
- package/src/services/message/index.ts +4 -8
- package/src/services/message/server.ts +93 -0
- package/src/services/plugin/index.ts +4 -9
- package/src/services/plugin/server.ts +46 -0
- package/src/services/session/index.ts +4 -8
- package/src/services/session/server.ts +148 -0
- package/src/services/topic/index.ts +4 -9
- package/src/services/topic/server.ts +68 -0
- package/src/services/user/index.ts +4 -9
- package/src/services/user/server.ts +28 -0
- package/tests/setup-db.ts +7 -0
- package/vitest.config.ts +2 -1
- package/vitest.server.config.ts +23 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
|
5
|
+
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
6
|
+
import { KeyVaultsGateKeeper } from '@/server/keyVaultsEncrypt';
|
|
7
|
+
import { UserPreference } from '@/types/user';
|
|
8
|
+
import { UserSettings } from '@/types/user/settings';
|
|
9
|
+
|
|
10
|
+
import { userSettings, users } from '../../schemas/lobechat';
|
|
11
|
+
import { SessionModel } from '../session';
|
|
12
|
+
import { UserModel } from '../user';
|
|
13
|
+
|
|
14
|
+
let serverDB = await getTestDBInstance();
|
|
15
|
+
|
|
16
|
+
vi.mock('@/database/server/core/db', async () => ({
|
|
17
|
+
get serverDB() {
|
|
18
|
+
return serverDB;
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const userId = 'user-db';
|
|
23
|
+
const userModel = new UserModel();
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await serverDB.delete(users);
|
|
27
|
+
await serverDB.delete(userSettings);
|
|
28
|
+
process.env.KEY_VAULTS_SECRET = 'ofQiJCXLF8mYemwfMWLOHoHimlPu91YmLfU7YZ4lreQ=';
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await serverDB.delete(users);
|
|
33
|
+
await serverDB.delete(userSettings);
|
|
34
|
+
process.env.KEY_VAULTS_SECRET = undefined;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('UserModel', () => {
|
|
38
|
+
describe('createUser', () => {
|
|
39
|
+
it('should create a new user and inbox session', async () => {
|
|
40
|
+
const params = {
|
|
41
|
+
id: userId,
|
|
42
|
+
username: 'testuser',
|
|
43
|
+
email: 'test@example.com',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
await userModel.createUser(params);
|
|
47
|
+
|
|
48
|
+
const user = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
|
|
49
|
+
expect(user).not.toBeNull();
|
|
50
|
+
expect(user?.username).toBe('testuser');
|
|
51
|
+
expect(user?.email).toBe('test@example.com');
|
|
52
|
+
|
|
53
|
+
const sessionModel = new SessionModel(userId);
|
|
54
|
+
const inbox = await sessionModel.findByIdOrSlug(INBOX_SESSION_ID);
|
|
55
|
+
expect(inbox).not.toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('deleteUser', () => {
|
|
60
|
+
it('should delete a user', async () => {
|
|
61
|
+
await serverDB.insert(users).values({ id: userId });
|
|
62
|
+
|
|
63
|
+
await userModel.deleteUser(userId);
|
|
64
|
+
|
|
65
|
+
const user = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
|
|
66
|
+
expect(user).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('findById', () => {
|
|
71
|
+
it('should find a user by ID', async () => {
|
|
72
|
+
await serverDB.insert(users).values({ id: userId, username: 'testuser' });
|
|
73
|
+
|
|
74
|
+
const user = await userModel.findById(userId);
|
|
75
|
+
|
|
76
|
+
expect(user).not.toBeNull();
|
|
77
|
+
expect(user?.id).toBe(userId);
|
|
78
|
+
expect(user?.username).toBe('testuser');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('getUserState', () => {
|
|
83
|
+
it('should get user state with decrypted keyVaults', async () => {
|
|
84
|
+
const preference = { useCmdEnterToSend: true } as UserPreference;
|
|
85
|
+
const keyVaults = { apiKey: 'secret' };
|
|
86
|
+
|
|
87
|
+
await serverDB.insert(users).values({ id: userId, preference });
|
|
88
|
+
|
|
89
|
+
const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
|
|
90
|
+
const encryptedKeyVaults = await gateKeeper.encrypt(JSON.stringify(keyVaults));
|
|
91
|
+
|
|
92
|
+
await serverDB.insert(userSettings).values({
|
|
93
|
+
id: userId,
|
|
94
|
+
keyVaults: encryptedKeyVaults,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const state = await userModel.getUserState(userId);
|
|
98
|
+
|
|
99
|
+
expect(state.userId).toBe(userId);
|
|
100
|
+
expect(state.preference).toEqual(preference);
|
|
101
|
+
expect(state.settings.keyVaults).toEqual(keyVaults);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should throw an error if user not found', async () => {
|
|
105
|
+
await expect(userModel.getUserState('invalid-user-id')).rejects.toThrow('user not found');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('updateUser', () => {
|
|
110
|
+
it('should update user fields', async () => {
|
|
111
|
+
await serverDB.insert(users).values({ id: userId, username: 'oldname' });
|
|
112
|
+
|
|
113
|
+
await userModel.updateUser(userId, { username: 'newname' });
|
|
114
|
+
|
|
115
|
+
const updatedUser = await serverDB.query.users.findFirst({
|
|
116
|
+
where: eq(users.id, userId),
|
|
117
|
+
});
|
|
118
|
+
expect(updatedUser?.username).toBe('newname');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('deleteSetting', () => {
|
|
123
|
+
it('should delete user settings', async () => {
|
|
124
|
+
await serverDB.insert(users).values({ id: userId });
|
|
125
|
+
await serverDB.insert(userSettings).values({ id: userId });
|
|
126
|
+
|
|
127
|
+
await userModel.deleteSetting(userId);
|
|
128
|
+
|
|
129
|
+
const settings = await serverDB.query.userSettings.findFirst({
|
|
130
|
+
where: eq(users.id, userId),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(settings).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('updateSetting', () => {
|
|
138
|
+
it('should update user settings with encrypted keyVaults', async () => {
|
|
139
|
+
const settings = {
|
|
140
|
+
general: { language: 'en-US' },
|
|
141
|
+
keyVaults: { openai: { apiKey: 'secret' } },
|
|
142
|
+
} as UserSettings;
|
|
143
|
+
await serverDB.insert(users).values({ id: userId });
|
|
144
|
+
|
|
145
|
+
await userModel.updateSetting(userId, settings);
|
|
146
|
+
|
|
147
|
+
const updatedSettings = await serverDB.query.userSettings.findFirst({
|
|
148
|
+
where: eq(users.id, userId),
|
|
149
|
+
});
|
|
150
|
+
expect(updatedSettings?.general).toEqual(settings.general);
|
|
151
|
+
expect(updatedSettings?.keyVaults).not.toBe(JSON.stringify(settings.keyVaults));
|
|
152
|
+
|
|
153
|
+
const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
|
|
154
|
+
const { plaintext } = await gateKeeper.decrypt(updatedSettings!.keyVaults!);
|
|
155
|
+
expect(JSON.parse(plaintext)).toEqual(settings.keyVaults);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('updatePreference', () => {
|
|
160
|
+
it('should update user preference', async () => {
|
|
161
|
+
const preference = { guide: { topic: false } } as UserPreference;
|
|
162
|
+
await serverDB.insert(users).values({ id: userId, preference });
|
|
163
|
+
|
|
164
|
+
const newPreference: Partial<UserPreference> = {
|
|
165
|
+
guide: { topic: true, moveSettingsToAvatar: true },
|
|
166
|
+
};
|
|
167
|
+
await userModel.updatePreference(userId, newPreference);
|
|
168
|
+
|
|
169
|
+
const updatedUser = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
|
|
170
|
+
expect(updatedUser?.preference).toEqual({ ...preference, ...newPreference });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { and, desc } from 'drizzle-orm/expressions';
|
|
3
|
+
|
|
4
|
+
import { serverDB } from '@/database/server';
|
|
5
|
+
|
|
6
|
+
import { NewSessionGroup, UserItem, sessionGroups } from '../schemas/lobechat';
|
|
7
|
+
|
|
8
|
+
export class TemplateModel {
|
|
9
|
+
private userId: string;
|
|
10
|
+
|
|
11
|
+
constructor(userId: string) {
|
|
12
|
+
this.userId = userId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
create = async (params: NewSessionGroup) => {
|
|
16
|
+
return serverDB.insert(sessionGroups).values({ ...params, userId: this.userId });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
delete = async (id: string) => {
|
|
20
|
+
return serverDB
|
|
21
|
+
.delete(sessionGroups)
|
|
22
|
+
.where(and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
query = async () => {
|
|
26
|
+
return serverDB.query.sessionGroups.findMany({
|
|
27
|
+
orderBy: [desc(sessionGroups.updatedAt)],
|
|
28
|
+
where: eq(sessionGroups.userId, this.userId),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
findById = async (id: string) => {
|
|
33
|
+
return serverDB.query.sessionGroups.findFirst({
|
|
34
|
+
where: and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)),
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
async update(id: string, value: Partial<UserItem>) {
|
|
39
|
+
return serverDB
|
|
40
|
+
.update(sessionGroups)
|
|
41
|
+
.set({ ...value, updatedAt: new Date() })
|
|
42
|
+
.where(and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { and, desc } from 'drizzle-orm/expressions';
|
|
3
|
+
|
|
4
|
+
import { serverDB } from '@/database/server/core/db';
|
|
5
|
+
|
|
6
|
+
import { FileItem, NewFile, files } from '../schemas/lobechat';
|
|
7
|
+
|
|
8
|
+
export class FileModel {
|
|
9
|
+
private readonly userId: string;
|
|
10
|
+
|
|
11
|
+
constructor(userId: string) {
|
|
12
|
+
this.userId = userId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
create = async (params: Omit<NewFile, 'id' | 'userId'>) => {
|
|
16
|
+
const result = await serverDB
|
|
17
|
+
.insert(files)
|
|
18
|
+
.values({ ...params, userId: this.userId })
|
|
19
|
+
.returning();
|
|
20
|
+
|
|
21
|
+
return { id: result[0].id };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
delete = async (id: string) => {
|
|
25
|
+
return serverDB.delete(files).where(and(eq(files.id, id), eq(files.userId, this.userId)));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
clear = async () => {
|
|
29
|
+
return serverDB.delete(files).where(eq(files.userId, this.userId));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
query = async () => {
|
|
33
|
+
return serverDB.query.files.findMany({
|
|
34
|
+
orderBy: [desc(files.updatedAt)],
|
|
35
|
+
where: eq(files.userId, this.userId),
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
findById = async (id: string) => {
|
|
40
|
+
return serverDB.query.files.findFirst({
|
|
41
|
+
where: and(eq(files.id, id), eq(files.userId, this.userId)),
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
async update(id: string, value: Partial<FileItem>) {
|
|
46
|
+
return serverDB
|
|
47
|
+
.update(files)
|
|
48
|
+
.set({ ...value, updatedAt: new Date() })
|
|
49
|
+
.where(and(eq(files.id, id), eq(files.userId, this.userId)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { count, sql } from 'drizzle-orm';
|
|
2
|
+
import { and, asc, desc, eq, isNull, like } from 'drizzle-orm/expressions';
|
|
3
|
+
import { inArray } from 'drizzle-orm/sql/expressions/conditions';
|
|
4
|
+
|
|
5
|
+
import { CreateMessageParams } from '@/database/client/models/message';
|
|
6
|
+
import { serverDB } from '@/database/server/core/db';
|
|
7
|
+
import { idGenerator } from '@/database/server/utils/idGenerator';
|
|
8
|
+
import { ChatTTS, ChatToolPayload } from '@/types/message';
|
|
9
|
+
import { merge } from '@/utils/merge';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
MessageItem,
|
|
13
|
+
filesToMessages,
|
|
14
|
+
messagePlugins,
|
|
15
|
+
messageTTS,
|
|
16
|
+
messageTranslates,
|
|
17
|
+
messages,
|
|
18
|
+
} from '../schemas/lobechat';
|
|
19
|
+
|
|
20
|
+
export interface QueryMessageParams {
|
|
21
|
+
current?: number;
|
|
22
|
+
pageSize?: number;
|
|
23
|
+
sessionId?: string | null;
|
|
24
|
+
topicId?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MessageModel {
|
|
28
|
+
private userId: string;
|
|
29
|
+
|
|
30
|
+
constructor(userId: string) {
|
|
31
|
+
this.userId = userId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// **************** Query *************** //
|
|
35
|
+
async query({
|
|
36
|
+
current = 0,
|
|
37
|
+
pageSize = 1000,
|
|
38
|
+
sessionId,
|
|
39
|
+
topicId,
|
|
40
|
+
}: QueryMessageParams = {}): Promise<MessageItem[]> {
|
|
41
|
+
const offset = current * pageSize;
|
|
42
|
+
|
|
43
|
+
const result = await serverDB
|
|
44
|
+
.select({
|
|
45
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix*/
|
|
46
|
+
id: messages.id,
|
|
47
|
+
role: messages.role,
|
|
48
|
+
content: messages.content,
|
|
49
|
+
error: messages.error,
|
|
50
|
+
|
|
51
|
+
model: messages.model,
|
|
52
|
+
provider: messages.provider,
|
|
53
|
+
|
|
54
|
+
createdAt: messages.createdAt,
|
|
55
|
+
updatedAt: messages.updatedAt,
|
|
56
|
+
|
|
57
|
+
parentId: messages.parentId,
|
|
58
|
+
|
|
59
|
+
tools: messages.tools,
|
|
60
|
+
tool_call_id: messagePlugins.toolCallId,
|
|
61
|
+
|
|
62
|
+
plugin: {
|
|
63
|
+
apiName: messagePlugins.apiName,
|
|
64
|
+
arguments: messagePlugins.arguments,
|
|
65
|
+
identifier: messagePlugins.identifier,
|
|
66
|
+
type: messagePlugins.type,
|
|
67
|
+
},
|
|
68
|
+
pluginError: messagePlugins.error,
|
|
69
|
+
pluginState: messagePlugins.state,
|
|
70
|
+
|
|
71
|
+
translate: {
|
|
72
|
+
content: messageTranslates.content,
|
|
73
|
+
from: messageTranslates.from,
|
|
74
|
+
to: messageTranslates.to,
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
ttsId: messageTTS.id,
|
|
78
|
+
|
|
79
|
+
// TODO: 确认下如何处理 TTS 的读取
|
|
80
|
+
// ttsContentMd5: messageTTS.contentMd5,
|
|
81
|
+
// ttsFile: messageTTS.fileId,
|
|
82
|
+
// ttsVoice: messageTTS.voice,
|
|
83
|
+
/* eslint-enable */
|
|
84
|
+
})
|
|
85
|
+
.from(messages)
|
|
86
|
+
.where(
|
|
87
|
+
and(
|
|
88
|
+
eq(messages.userId, this.userId),
|
|
89
|
+
this.matchSession(sessionId),
|
|
90
|
+
this.matchTopic(topicId),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
.leftJoin(messagePlugins, eq(messagePlugins.id, messages.id))
|
|
94
|
+
.leftJoin(messageTranslates, eq(messageTranslates.id, messages.id))
|
|
95
|
+
.leftJoin(messageTTS, eq(messageTTS.id, messages.id))
|
|
96
|
+
.orderBy(asc(messages.createdAt))
|
|
97
|
+
.limit(pageSize)
|
|
98
|
+
.offset(offset);
|
|
99
|
+
|
|
100
|
+
const messageIds = result.map((message) => message.id as string);
|
|
101
|
+
|
|
102
|
+
if (messageIds.length === 0) return result;
|
|
103
|
+
|
|
104
|
+
const fileIds = await serverDB
|
|
105
|
+
.select({
|
|
106
|
+
fileId: filesToMessages.fileId,
|
|
107
|
+
messageId: filesToMessages.messageId,
|
|
108
|
+
})
|
|
109
|
+
.from(filesToMessages)
|
|
110
|
+
.where(inArray(filesToMessages.messageId, messageIds));
|
|
111
|
+
|
|
112
|
+
return result.map(
|
|
113
|
+
({
|
|
114
|
+
model,
|
|
115
|
+
provider,
|
|
116
|
+
translate,
|
|
117
|
+
ttsId,
|
|
118
|
+
// ttsFile, ttsId, ttsContentMd5, ttsVoice,
|
|
119
|
+
...item
|
|
120
|
+
}) => ({
|
|
121
|
+
...item,
|
|
122
|
+
extra: {
|
|
123
|
+
fromModel: model,
|
|
124
|
+
fromProvider: provider,
|
|
125
|
+
translate,
|
|
126
|
+
tts: ttsId
|
|
127
|
+
? {
|
|
128
|
+
// contentMd5: ttsContentMd5,
|
|
129
|
+
// file: ttsFile,
|
|
130
|
+
// voice: ttsVoice,
|
|
131
|
+
}
|
|
132
|
+
: undefined,
|
|
133
|
+
},
|
|
134
|
+
files: fileIds.filter((relation) => relation.messageId === item.id).map((r) => r.fileId),
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async findById(id: string) {
|
|
140
|
+
return serverDB.query.messages.findFirst({
|
|
141
|
+
where: and(eq(messages.id, id), eq(messages.userId, this.userId)),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async queryAll(): Promise<MessageItem[]> {
|
|
146
|
+
return serverDB
|
|
147
|
+
.select()
|
|
148
|
+
.from(messages)
|
|
149
|
+
.orderBy(messages.createdAt)
|
|
150
|
+
.where(eq(messages.userId, this.userId))
|
|
151
|
+
|
|
152
|
+
.execute();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async queryBySessionId(sessionId?: string | null): Promise<MessageItem[]> {
|
|
156
|
+
return serverDB.query.messages.findMany({
|
|
157
|
+
orderBy: [asc(messages.createdAt)],
|
|
158
|
+
where: and(eq(messages.userId, this.userId), this.matchSession(sessionId)),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async queryByKeyword(keyword: string): Promise<MessageItem[]> {
|
|
163
|
+
if (!keyword) return [];
|
|
164
|
+
|
|
165
|
+
return serverDB.query.messages.findMany({
|
|
166
|
+
orderBy: [desc(messages.createdAt)],
|
|
167
|
+
where: and(eq(messages.userId, this.userId), like(messages.content, `%${keyword}%`)),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async count() {
|
|
172
|
+
const result = await serverDB
|
|
173
|
+
.select({
|
|
174
|
+
count: count(),
|
|
175
|
+
})
|
|
176
|
+
.from(messages)
|
|
177
|
+
.where(eq(messages.userId, this.userId))
|
|
178
|
+
.execute();
|
|
179
|
+
|
|
180
|
+
return result[0].count;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async countToday() {
|
|
184
|
+
const today = new Date();
|
|
185
|
+
today.setHours(0, 0, 0, 0);
|
|
186
|
+
const tomorrow = new Date(today);
|
|
187
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
188
|
+
|
|
189
|
+
const result = await serverDB
|
|
190
|
+
.select({
|
|
191
|
+
count: count(),
|
|
192
|
+
})
|
|
193
|
+
.from(messages)
|
|
194
|
+
.where(
|
|
195
|
+
and(
|
|
196
|
+
eq(messages.userId, this.userId),
|
|
197
|
+
sql`${messages.createdAt} >= ${today} AND ${messages.createdAt} < ${tomorrow}`,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
.execute();
|
|
201
|
+
|
|
202
|
+
return result[0].count;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// **************** Create *************** //
|
|
206
|
+
|
|
207
|
+
async create(
|
|
208
|
+
{ fromModel, fromProvider, files, ...message }: CreateMessageParams,
|
|
209
|
+
id: string = this.genId(),
|
|
210
|
+
): Promise<MessageItem> {
|
|
211
|
+
return serverDB.transaction(async (trx) => {
|
|
212
|
+
const [item] = (await trx
|
|
213
|
+
.insert(messages)
|
|
214
|
+
.values({
|
|
215
|
+
...message,
|
|
216
|
+
id,
|
|
217
|
+
model: fromModel,
|
|
218
|
+
provider: fromProvider,
|
|
219
|
+
userId: this.userId,
|
|
220
|
+
})
|
|
221
|
+
.returning()) as MessageItem[];
|
|
222
|
+
|
|
223
|
+
// Insert the plugin data if the message is a tool
|
|
224
|
+
if (message.role === 'tool') {
|
|
225
|
+
await trx.insert(messagePlugins).values({
|
|
226
|
+
apiName: message.plugin?.apiName,
|
|
227
|
+
arguments: message.plugin?.arguments,
|
|
228
|
+
id,
|
|
229
|
+
identifier: message.plugin?.identifier,
|
|
230
|
+
toolCallId: message.tool_call_id,
|
|
231
|
+
type: message.plugin?.type,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (files && files.length > 0) {
|
|
236
|
+
await trx
|
|
237
|
+
.insert(filesToMessages)
|
|
238
|
+
.values(files.map((file) => ({ fileId: file, messageId: id })));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return item;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async batchCreate(newMessages: MessageItem[]) {
|
|
246
|
+
const messagesToInsert = newMessages.map((m) => {
|
|
247
|
+
return { ...m, userId: this.userId };
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return serverDB.insert(messages).values(messagesToInsert);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// **************** Update *************** //
|
|
254
|
+
|
|
255
|
+
async update(id: string, message: Partial<MessageItem>) {
|
|
256
|
+
return serverDB
|
|
257
|
+
.update(messages)
|
|
258
|
+
.set(message)
|
|
259
|
+
.where(and(eq(messages.id, id), eq(messages.userId, this.userId)));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async updatePluginState(id: string, state: Record<string, any>) {
|
|
263
|
+
const item = await serverDB.query.messagePlugins.findFirst({
|
|
264
|
+
where: eq(messagePlugins.id, id),
|
|
265
|
+
});
|
|
266
|
+
if (!item) throw new Error('Plugin not found');
|
|
267
|
+
|
|
268
|
+
return serverDB
|
|
269
|
+
.update(messagePlugins)
|
|
270
|
+
.set({ state: merge(item.state || {}, state) })
|
|
271
|
+
.where(eq(messagePlugins.id, id));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async updateTranslate(id: string, translate: Partial<MessageItem>) {
|
|
275
|
+
const result = await serverDB.query.messageTranslates.findFirst({
|
|
276
|
+
where: and(eq(messageTranslates.id, id)),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// If the message does not exist in the translate table, insert it
|
|
280
|
+
if (!result) {
|
|
281
|
+
return serverDB.insert(messageTranslates).values({ ...translate, id });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// or just update the existing one
|
|
285
|
+
return serverDB.update(messageTranslates).set(translate).where(eq(messageTranslates.id, id));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async updateTTS(id: string, tts: Partial<ChatTTS>) {
|
|
289
|
+
const result = await serverDB.query.messageTTS.findFirst({
|
|
290
|
+
where: and(eq(messageTTS.id, id)),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// If the message does not exist in the translate table, insert it
|
|
294
|
+
if (!result) {
|
|
295
|
+
return serverDB
|
|
296
|
+
.insert(messageTTS)
|
|
297
|
+
.values({ contentMd5: tts.contentMd5, fileId: tts.file, id, voice: tts.voice });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// or just update the existing one
|
|
301
|
+
return serverDB
|
|
302
|
+
.update(messageTTS)
|
|
303
|
+
.set({ contentMd5: tts.contentMd5, fileId: tts.file, voice: tts.voice })
|
|
304
|
+
.where(eq(messageTTS.id, id));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// **************** Delete *************** //
|
|
308
|
+
|
|
309
|
+
async deleteMessage(id: string) {
|
|
310
|
+
return serverDB.transaction(async (tx) => {
|
|
311
|
+
// 1. 查询要删除的 message 的完整信息
|
|
312
|
+
const message = await tx
|
|
313
|
+
.select()
|
|
314
|
+
.from(messages)
|
|
315
|
+
.where(and(eq(messages.id, id), eq(messages.userId, this.userId)))
|
|
316
|
+
.limit(1);
|
|
317
|
+
|
|
318
|
+
// 如果找不到要删除的 message,直接返回
|
|
319
|
+
if (message.length === 0) return;
|
|
320
|
+
|
|
321
|
+
// 2. 检查 message 是否包含 tools
|
|
322
|
+
const toolCallIds = message[0].tools?.map((tool: ChatToolPayload) => tool.id).filter(Boolean);
|
|
323
|
+
|
|
324
|
+
let relatedMessageIds: string[] = [];
|
|
325
|
+
|
|
326
|
+
if (toolCallIds?.length > 0) {
|
|
327
|
+
// 3. 如果 message 包含 tools,查询出所有相关联的 message id
|
|
328
|
+
const res = await tx
|
|
329
|
+
.select({ id: messagePlugins.id })
|
|
330
|
+
.from(messagePlugins)
|
|
331
|
+
.where(inArray(messagePlugins.toolCallId, toolCallIds))
|
|
332
|
+
.execute();
|
|
333
|
+
|
|
334
|
+
relatedMessageIds = res.map((row) => row.id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 4. 合并要删除的 message id 列表
|
|
338
|
+
const messageIdsToDelete = [id, ...relatedMessageIds];
|
|
339
|
+
|
|
340
|
+
// 5. 删除所有相关的 message
|
|
341
|
+
await tx.delete(messages).where(inArray(messages.id, messageIdsToDelete));
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async deleteMessageTranslate(id: string) {
|
|
346
|
+
return serverDB.delete(messageTranslates).where(and(eq(messageTranslates.id, id)));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async deleteMessageTTS(id: string) {
|
|
350
|
+
return serverDB.delete(messageTTS).where(and(eq(messageTTS.id, id)));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async deleteMessages(sessionId?: string | null, topicId?: string | null) {
|
|
354
|
+
return serverDB
|
|
355
|
+
.delete(messages)
|
|
356
|
+
.where(
|
|
357
|
+
and(
|
|
358
|
+
eq(messages.userId, this.userId),
|
|
359
|
+
this.matchSession(sessionId),
|
|
360
|
+
this.matchTopic(topicId),
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async deleteAllMessages() {
|
|
366
|
+
return serverDB.delete(messages).where(eq(messages.userId, this.userId));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// **************** Helper *************** //
|
|
370
|
+
|
|
371
|
+
private genId = () => idGenerator('messages', 14);
|
|
372
|
+
|
|
373
|
+
private matchSession = (sessionId?: string | null) =>
|
|
374
|
+
sessionId ? eq(messages.sessionId, sessionId) : isNull(messages.sessionId);
|
|
375
|
+
|
|
376
|
+
private matchTopic = (topicId?: string | null) =>
|
|
377
|
+
topicId ? eq(messages.topicId, topicId) : isNull(messages.topicId);
|
|
378
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { and, desc, eq } from 'drizzle-orm/expressions';
|
|
2
|
+
|
|
3
|
+
import { serverDB } from '@/database/server';
|
|
4
|
+
|
|
5
|
+
import { InstalledPluginItem, NewInstalledPlugin, installedPlugins } from '../schemas/lobechat';
|
|
6
|
+
|
|
7
|
+
export class PluginModel {
|
|
8
|
+
private userId: string;
|
|
9
|
+
|
|
10
|
+
constructor(userId: string) {
|
|
11
|
+
this.userId = userId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
create = async (
|
|
15
|
+
params: Pick<NewInstalledPlugin, 'type' | 'identifier' | 'manifest' | 'customParams'>,
|
|
16
|
+
) => {
|
|
17
|
+
const [result] = await serverDB
|
|
18
|
+
.insert(installedPlugins)
|
|
19
|
+
.values({ ...params, createdAt: new Date(), updatedAt: new Date(), userId: this.userId })
|
|
20
|
+
.returning();
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
delete = async (id: string) => {
|
|
26
|
+
return serverDB
|
|
27
|
+
.delete(installedPlugins)
|
|
28
|
+
.where(and(eq(installedPlugins.identifier, id), eq(installedPlugins.userId, this.userId)));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
deleteAll = async () => {
|
|
32
|
+
return serverDB.delete(installedPlugins).where(eq(installedPlugins.userId, this.userId));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
query = async () => {
|
|
36
|
+
return serverDB
|
|
37
|
+
.select({
|
|
38
|
+
createdAt: installedPlugins.createdAt,
|
|
39
|
+
customParams: installedPlugins.customParams,
|
|
40
|
+
identifier: installedPlugins.identifier,
|
|
41
|
+
manifest: installedPlugins.manifest,
|
|
42
|
+
settings: installedPlugins.settings,
|
|
43
|
+
type: installedPlugins.type,
|
|
44
|
+
updatedAt: installedPlugins.updatedAt,
|
|
45
|
+
})
|
|
46
|
+
.from(installedPlugins)
|
|
47
|
+
.where(eq(installedPlugins.userId, this.userId))
|
|
48
|
+
.orderBy(desc(installedPlugins.createdAt));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
findById = async (id: string) => {
|
|
52
|
+
return serverDB.query.installedPlugins.findFirst({
|
|
53
|
+
where: and(eq(installedPlugins.identifier, id), eq(installedPlugins.userId, this.userId)),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
async update(id: string, value: Partial<InstalledPluginItem>) {
|
|
58
|
+
return serverDB
|
|
59
|
+
.update(installedPlugins)
|
|
60
|
+
.set({ ...value, updatedAt: new Date() })
|
|
61
|
+
.where(and(eq(installedPlugins.identifier, id), eq(installedPlugins.userId, this.userId)));
|
|
62
|
+
}
|
|
63
|
+
}
|