@lobehub/chat 1.134.4 → 1.134.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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/docs/development/database-schema.dbml +27 -0
- package/package.json +1 -1
- package/packages/database/migrations/0036_add_group_messages.sql +21 -0
- package/packages/database/migrations/meta/0036_snapshot.json +6772 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/package.json +1 -1
- package/packages/database/src/core/migrations.json +15 -0
- package/packages/database/src/models/__tests__/aiModel.test.ts +43 -0
- package/packages/database/src/models/aiModel.ts +25 -1
- package/packages/database/src/repositories/tableViewer/index.test.ts +1 -1
- package/packages/database/src/schemas/_helpers.ts +3 -1
- package/packages/database/src/schemas/message.ts +55 -4
- package/packages/database/src/schemas/relations.ts +24 -1
- package/packages/database/src/utils/idGenerator.ts +1 -0
|
@@ -252,6 +252,13 @@
|
|
|
252
252
|
"when": 1759116400580,
|
|
253
253
|
"tag": "0035_add_virtual",
|
|
254
254
|
"breakpoints": true
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"idx": 36,
|
|
258
|
+
"version": "7",
|
|
259
|
+
"when": 1759666151079,
|
|
260
|
+
"tag": "0036_add_group_messages",
|
|
261
|
+
"breakpoints": true
|
|
255
262
|
}
|
|
256
263
|
],
|
|
257
264
|
"version": "6"
|
|
@@ -632,5 +632,20 @@
|
|
|
632
632
|
"bps": true,
|
|
633
633
|
"folderMillis": 1759116400580,
|
|
634
634
|
"hash": "433ddae88e785f2db734e49a4c115eee93e60afe389f7919d66e5ba9aa159a37"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
"sql": [
|
|
638
|
+
"CREATE TABLE IF NOT EXISTS \"message_groups\" (\n\t\"id\" varchar(255) PRIMARY KEY NOT NULL,\n\t\"topic_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"parent_group_id\" varchar(255),\n\t\"parent_message_id\" text,\n\t\"title\" varchar(255),\n\t\"description\" text,\n\t\"client_id\" varchar(255),\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n",
|
|
639
|
+
"\nALTER TABLE \"messages\" ADD COLUMN IF NOT EXISTS \"message_group_id\" varchar(255);",
|
|
640
|
+
"\nALTER TABLE \"message_groups\" ADD CONSTRAINT \"message_groups_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;",
|
|
641
|
+
"\nALTER TABLE \"message_groups\" ADD CONSTRAINT \"message_groups_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;",
|
|
642
|
+
"\nALTER TABLE \"message_groups\" ADD CONSTRAINT \"message_groups_parent_group_id_message_groups_id_fk\" FOREIGN KEY (\"parent_group_id\") REFERENCES \"public\".\"message_groups\"(\"id\") ON DELETE cascade ON UPDATE no action;",
|
|
643
|
+
"\nALTER TABLE \"message_groups\" ADD CONSTRAINT \"message_groups_parent_message_id_messages_id_fk\" FOREIGN KEY (\"parent_message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;",
|
|
644
|
+
"\nCREATE UNIQUE INDEX IF NOT EXISTS \"message_groups_client_id_user_id_unique\" ON \"message_groups\" USING btree (\"client_id\",\"user_id\");",
|
|
645
|
+
"\nALTER TABLE \"messages\" ADD CONSTRAINT \"messages_message_group_id_message_groups_id_fk\" FOREIGN KEY (\"message_group_id\") REFERENCES \"public\".\"message_groups\"(\"id\") ON DELETE cascade ON UPDATE no action;\n"
|
|
646
|
+
],
|
|
647
|
+
"bps": true,
|
|
648
|
+
"folderMillis": 1759666151079,
|
|
649
|
+
"hash": "3a787841eede425d972f2a00cf2f7f8d0b27b391c46ed525c6dceaa79d58d887"
|
|
635
650
|
}
|
|
636
651
|
]
|
|
@@ -254,6 +254,15 @@ describe('AiModelModel', () => {
|
|
|
254
254
|
expect(allModels.find((m) => m.id === 'existing-model')?.displayName).toBe('Old Name');
|
|
255
255
|
expect(allModels.find((m) => m.id === 'new-model')?.displayName).toBe('New Model');
|
|
256
256
|
});
|
|
257
|
+
|
|
258
|
+
it('should return empty array when models array is empty', async () => {
|
|
259
|
+
const result = await aiProviderModel.batchUpdateAiModels('openai', []);
|
|
260
|
+
expect(result).toEqual([]);
|
|
261
|
+
|
|
262
|
+
// Verify no models were created
|
|
263
|
+
const allModels = await aiProviderModel.query();
|
|
264
|
+
expect(allModels).toHaveLength(0);
|
|
265
|
+
});
|
|
257
266
|
});
|
|
258
267
|
|
|
259
268
|
describe('batchToggleAiModels', () => {
|
|
@@ -274,6 +283,23 @@ describe('AiModelModel', () => {
|
|
|
274
283
|
const models = await aiProviderModel.query();
|
|
275
284
|
expect(models.every((m) => m.enabled)).toBe(true);
|
|
276
285
|
});
|
|
286
|
+
|
|
287
|
+
it('should return early when models array is empty', async () => {
|
|
288
|
+
// Create an initial model to verify it's not affected
|
|
289
|
+
await aiProviderModel.create({
|
|
290
|
+
id: 'model1',
|
|
291
|
+
providerId: 'openai',
|
|
292
|
+
enabled: false,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const result = await aiProviderModel.batchToggleAiModels('openai', [], true);
|
|
296
|
+
expect(result).toBeUndefined();
|
|
297
|
+
|
|
298
|
+
// Verify existing models were not affected
|
|
299
|
+
const models = await aiProviderModel.query();
|
|
300
|
+
expect(models).toHaveLength(1);
|
|
301
|
+
expect(models[0].enabled).toBe(false);
|
|
302
|
+
});
|
|
277
303
|
});
|
|
278
304
|
|
|
279
305
|
describe('clearRemoteModels', () => {
|
|
@@ -324,5 +350,22 @@ describe('AiModelModel', () => {
|
|
|
324
350
|
const model = await aiProviderModel.findById('image-model');
|
|
325
351
|
expect(model?.type).toBe('image');
|
|
326
352
|
});
|
|
353
|
+
|
|
354
|
+
it('should return early when sortMap array is empty', async () => {
|
|
355
|
+
// Create an initial model to verify it's not affected
|
|
356
|
+
await aiProviderModel.create({
|
|
357
|
+
id: 'model1',
|
|
358
|
+
providerId: 'openai',
|
|
359
|
+
sort: 1,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const result = await aiProviderModel.updateModelsOrder('openai', []);
|
|
363
|
+
expect(result).toBeUndefined();
|
|
364
|
+
|
|
365
|
+
// Verify existing models were not affected (check by querying the created model directly)
|
|
366
|
+
const models = await aiProviderModel.getModelListByProviderId('openai');
|
|
367
|
+
expect(models).toHaveLength(1);
|
|
368
|
+
expect(models[0].id).toBe('model1');
|
|
369
|
+
});
|
|
327
370
|
});
|
|
328
371
|
});
|
|
@@ -19,12 +19,21 @@ export class AiModelModel {
|
|
|
19
19
|
this.db = db;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Helper method to validate if array is empty and return early if needed
|
|
24
|
+
* @param array - Array to validate
|
|
25
|
+
* @returns true if array is empty, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
private isEmptyArray(array: unknown[]): boolean {
|
|
28
|
+
return array.length === 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
create = async (params: NewAiModelItem) => {
|
|
23
32
|
const [result] = await this.db
|
|
24
33
|
.insert(aiModels)
|
|
25
34
|
.values({
|
|
26
35
|
...params,
|
|
27
|
-
enabled: true, // enabled by default
|
|
36
|
+
enabled: params.enabled ?? true, // enabled by default, but respect explicit value
|
|
28
37
|
source: AiModelSourceEnum.Custom,
|
|
29
38
|
userId: this.userId,
|
|
30
39
|
})
|
|
@@ -148,6 +157,11 @@ export class AiModelModel {
|
|
|
148
157
|
};
|
|
149
158
|
|
|
150
159
|
batchUpdateAiModels = async (providerId: string, models: AiProviderModelListItem[]) => {
|
|
160
|
+
// Early return if models array is empty to prevent database insertion error
|
|
161
|
+
if (this.isEmptyArray(models)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
const records = models.map(({ id, ...model }) => ({
|
|
152
166
|
...model,
|
|
153
167
|
id,
|
|
@@ -166,6 +180,11 @@ export class AiModelModel {
|
|
|
166
180
|
};
|
|
167
181
|
|
|
168
182
|
batchToggleAiModels = async (providerId: string, models: string[], enabled: boolean) => {
|
|
183
|
+
// Early return if models array is empty to prevent database insertion error
|
|
184
|
+
if (this.isEmptyArray(models)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
return this.db.transaction(async (trx) => {
|
|
170
189
|
// 1. insert models that are not in the db
|
|
171
190
|
const insertedRecords = await trx
|
|
@@ -222,6 +241,11 @@ export class AiModelModel {
|
|
|
222
241
|
}
|
|
223
242
|
|
|
224
243
|
updateModelsOrder = async (providerId: string, sortMap: AiModelSortMap[]) => {
|
|
244
|
+
// Early return if sortMap array is empty
|
|
245
|
+
if (this.isEmptyArray(sortMap)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
225
249
|
await this.db.transaction(async (tx) => {
|
|
226
250
|
const updates = sortMap.map(({ id, sort, type }) => {
|
|
227
251
|
const now = new Date();
|
|
@@ -23,7 +23,7 @@ describe('TableViewerRepo', () => {
|
|
|
23
23
|
it('should return all tables with counts', async () => {
|
|
24
24
|
const result = await repo.getAllTables();
|
|
25
25
|
|
|
26
|
-
expect(result.length).toEqual(
|
|
26
|
+
expect(result.length).toEqual(63);
|
|
27
27
|
expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' });
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { timestamp } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { timestamp, varchar } from 'drizzle-orm/pg-core';
|
|
2
2
|
|
|
3
3
|
export const timestamptz = (name: string) => timestamp(name, { withTimezone: true });
|
|
4
4
|
|
|
5
|
+
export const varchar255 = (name: string) => varchar(name, { length: 255 });
|
|
6
|
+
|
|
5
7
|
export const createdAt = () => timestamptz('created_at').notNull().defaultNow();
|
|
6
8
|
export const updatedAt = () =>
|
|
7
9
|
timestamptz('updated_at')
|
|
@@ -9,15 +9,14 @@ import {
|
|
|
9
9
|
text,
|
|
10
10
|
uniqueIndex,
|
|
11
11
|
uuid,
|
|
12
|
-
varchar,
|
|
13
12
|
} from 'drizzle-orm/pg-core';
|
|
14
|
-
import { createSelectSchema } from 'drizzle-zod';
|
|
13
|
+
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
|
|
15
14
|
|
|
16
15
|
import { ModelReasoning } from '@/types/message';
|
|
17
16
|
import { GroundingSearch } from '@/types/search';
|
|
18
17
|
|
|
19
18
|
import { idGenerator } from '../utils/idGenerator';
|
|
20
|
-
import { timestamps } from './_helpers';
|
|
19
|
+
import { timestamps, varchar255 } from './_helpers';
|
|
21
20
|
import { agents } from './agent';
|
|
22
21
|
import { chatGroups } from './chatGroup';
|
|
23
22
|
import { files } from './file';
|
|
@@ -26,6 +25,53 @@ import { sessions } from './session';
|
|
|
26
25
|
import { threads, topics } from './topic';
|
|
27
26
|
import { users } from './user';
|
|
28
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Message groups table for multi-models parallel conversations
|
|
30
|
+
* Allows multiple AI models to respond to the same user message in parallel
|
|
31
|
+
*/
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
export const messageGroups = pgTable(
|
|
34
|
+
'message_groups',
|
|
35
|
+
{
|
|
36
|
+
id: varchar255('id')
|
|
37
|
+
.primaryKey()
|
|
38
|
+
.$defaultFn(() => idGenerator('messageGroups'))
|
|
39
|
+
.notNull(),
|
|
40
|
+
|
|
41
|
+
// 关联关系 - 只需要 topic 层级
|
|
42
|
+
topicId: text('topic_id').references(() => topics.id, { onDelete: 'cascade' }),
|
|
43
|
+
userId: text('user_id')
|
|
44
|
+
.references(() => users.id, { onDelete: 'cascade' })
|
|
45
|
+
.notNull(),
|
|
46
|
+
|
|
47
|
+
// 支持嵌套结构
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
parentGroupId: varchar255('parent_group_id').references(() => messageGroups.id, {
|
|
50
|
+
onDelete: 'cascade',
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
// 关联的用户消息
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
55
|
+
parentMessageId: text('parent_message_id').references(() => messages.id, {
|
|
56
|
+
onDelete: 'cascade',
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
// 元数据
|
|
60
|
+
title: varchar255('title'),
|
|
61
|
+
description: text('description'),
|
|
62
|
+
|
|
63
|
+
clientId: varchar255('client_id'),
|
|
64
|
+
|
|
65
|
+
...timestamps,
|
|
66
|
+
},
|
|
67
|
+
(t) => [uniqueIndex('message_groups_client_id_user_id_unique').on(t.clientId, t.userId)],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const insertMessageGroupSchema = createInsertSchema(messageGroups);
|
|
71
|
+
|
|
72
|
+
export type NewMessageGroup = typeof messageGroups.$inferInsert;
|
|
73
|
+
export type MessageGroupItem = typeof messageGroups.$inferSelect;
|
|
74
|
+
|
|
29
75
|
// @ts-ignore
|
|
30
76
|
export const messages = pgTable(
|
|
31
77
|
'messages',
|
|
@@ -34,7 +80,7 @@ export const messages = pgTable(
|
|
|
34
80
|
.$defaultFn(() => idGenerator('messages'))
|
|
35
81
|
.primaryKey(),
|
|
36
82
|
|
|
37
|
-
role:
|
|
83
|
+
role: varchar255('role').notNull(),
|
|
38
84
|
content: text('content'),
|
|
39
85
|
reasoning: jsonb('reasoning').$type<ModelReasoning>(),
|
|
40
86
|
search: jsonb('search').$type<GroundingSearch>(),
|
|
@@ -69,6 +115,11 @@ export const messages = pgTable(
|
|
|
69
115
|
groupId: text('group_id').references(() => chatGroups.id, { onDelete: 'set null' }),
|
|
70
116
|
// targetId can be an agent ID, "user", or null - no FK constraint
|
|
71
117
|
targetId: text('target_id'),
|
|
118
|
+
|
|
119
|
+
// used for multi-models parallel
|
|
120
|
+
messageGroupId: varchar255('message_group_id').references(() => messageGroups.id, {
|
|
121
|
+
onDelete: 'cascade',
|
|
122
|
+
}),
|
|
72
123
|
...timestamps,
|
|
73
124
|
},
|
|
74
125
|
(table) => [
|
|
@@ -9,7 +9,7 @@ import { chatGroups, chatGroupsAgents } from './chatGroup';
|
|
|
9
9
|
import { documentChunks, documents } from './document';
|
|
10
10
|
import { files, knowledgeBases } from './file';
|
|
11
11
|
import { generationBatches, generationTopics, generations } from './generation';
|
|
12
|
-
import { messages, messagesFiles } from './message';
|
|
12
|
+
import { messageGroups, messages, messagesFiles } from './message';
|
|
13
13
|
import { chunks, unstructuredChunks } from './rag';
|
|
14
14
|
import { sessionGroups, sessions } from './session';
|
|
15
15
|
import { threads, topicDocuments, topics } from './topic';
|
|
@@ -104,6 +104,11 @@ export const messagesRelations = relations(messages, ({ many, one }) => ({
|
|
|
104
104
|
fields: [messages.threadId],
|
|
105
105
|
references: [threads.id],
|
|
106
106
|
}),
|
|
107
|
+
|
|
108
|
+
messageGroup: one(messageGroups, {
|
|
109
|
+
fields: [messages.messageGroupId],
|
|
110
|
+
references: [messageGroups.id],
|
|
111
|
+
}),
|
|
107
112
|
}));
|
|
108
113
|
|
|
109
114
|
export const agentsRelations = relations(agents, ({ many }) => ({
|
|
@@ -306,3 +311,21 @@ export const chatGroupsAgentsRelations = relations(chatGroupsAgents, ({ one }) =
|
|
|
306
311
|
references: [users.id],
|
|
307
312
|
}),
|
|
308
313
|
}));
|
|
314
|
+
|
|
315
|
+
// Message Groups 相关关系定义
|
|
316
|
+
export const messageGroupsRelations = relations(messageGroups, ({ many, one }) => ({
|
|
317
|
+
user: one(users, {
|
|
318
|
+
fields: [messageGroups.userId],
|
|
319
|
+
references: [users.id],
|
|
320
|
+
}),
|
|
321
|
+
topic: one(topics, {
|
|
322
|
+
fields: [messageGroups.topicId],
|
|
323
|
+
references: [topics.id],
|
|
324
|
+
}),
|
|
325
|
+
parentGroup: one(messageGroups, {
|
|
326
|
+
fields: [messageGroups.parentGroupId],
|
|
327
|
+
references: [messageGroups.id],
|
|
328
|
+
}),
|
|
329
|
+
childGroups: many(messageGroups),
|
|
330
|
+
messages: many(messages),
|
|
331
|
+
}));
|