@lobehub/chat 1.92.0 → 1.92.2

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.
@@ -168,6 +168,13 @@
168
168
  "when": 1748925630721,
169
169
  "tag": "0023_remove_param_and_doubao",
170
170
  "breakpoints": true
171
+ },
172
+ {
173
+ "idx": 24,
174
+ "version": "7",
175
+ "when": 1749301573666,
176
+ "tag": "0024_add_rbac_tables",
177
+ "breakpoints": true
171
178
  }
172
179
  ],
173
180
  "version": "6"
@@ -8,6 +8,7 @@ export * from './nextauth';
8
8
  export * from './oidc';
9
9
  export * from './rag';
10
10
  export * from './ragEvals';
11
+ export * from './rbac';
11
12
  export * from './relations';
12
13
  export * from './session';
13
14
  export * from './topic';
@@ -0,0 +1,82 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { boolean, index, integer, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
3
+
4
+ import { timestamps } from './_helpers';
5
+ import { users } from './user';
6
+
7
+ // Roles table
8
+ export const roles = pgTable('rbac_roles', {
9
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
10
+ name: text('name').notNull().unique(), // Role name, e.g.: admin, user, guest
11
+ displayName: text('display_name').notNull(), // Display name
12
+ description: text('description'), // Role description
13
+ isSystem: boolean('is_system').default(false).notNull(), // Whether it's a system role
14
+ isActive: boolean('is_active').default(true).notNull(), // Whether it's active
15
+
16
+ ...timestamps,
17
+ });
18
+
19
+ export type NewRole = typeof roles.$inferInsert;
20
+ export type RoleItem = typeof roles.$inferSelect;
21
+
22
+ // Permissions table
23
+ export const permissions = pgTable('rbac_permissions', {
24
+ id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
25
+ code: text('code').notNull().unique(), // Permission code, e.g.: chat:create, file:upload
26
+ name: text('name').notNull(), // Permission name
27
+ description: text('description'), // Permission description
28
+ category: text('category').notNull(), // Category it belongs to, e.g.: message, knowledge_base, agent
29
+ isActive: boolean('is_active').default(true).notNull(), // Whether it's active
30
+
31
+ ...timestamps,
32
+ });
33
+
34
+ export type NewPermission = typeof permissions.$inferInsert;
35
+ export type PermissionItem = typeof permissions.$inferSelect;
36
+
37
+ // Role-permission association table
38
+ export const rolePermissions = pgTable(
39
+ 'rbac_role_permissions',
40
+ {
41
+ roleId: integer('role_id')
42
+ .references(() => roles.id, { onDelete: 'cascade' })
43
+ .notNull(),
44
+ permissionId: integer('permission_id')
45
+ .references(() => permissions.id, { onDelete: 'cascade' })
46
+ .notNull(),
47
+
48
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
49
+ },
50
+ (self) => [
51
+ primaryKey({ columns: [self.roleId, self.permissionId] }),
52
+ index('rbac_role_permissions_role_id_idx').on(self.roleId),
53
+ index('rbac_role_permissions_permission_id_idx').on(self.permissionId),
54
+ ],
55
+ );
56
+
57
+ export type NewRolePermission = typeof rolePermissions.$inferInsert;
58
+ export type RolePermissionItem = typeof rolePermissions.$inferSelect;
59
+
60
+ // User-role association table
61
+ export const userRoles = pgTable(
62
+ 'rbac_user_roles',
63
+ {
64
+ userId: text('user_id')
65
+ .references(() => users.id, { onDelete: 'cascade' })
66
+ .notNull(),
67
+ roleId: integer('role_id')
68
+ .references(() => roles.id, { onDelete: 'cascade' })
69
+ .notNull(),
70
+
71
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
72
+ expiresAt: timestamp('expires_at', { withTimezone: true }), // Support for temporary roles
73
+ },
74
+ (self) => [
75
+ primaryKey({ columns: [self.userId, self.roleId] }),
76
+ index('rbac_user_roles_user_id_idx').on(self.userId),
77
+ index('rbac_user_roles_role_id_idx').on(self.roleId),
78
+ ],
79
+ );
80
+
81
+ export type NewUserRole = typeof userRoles.$inferInsert;
82
+ export type UserRoleItem = typeof userRoles.$inferSelect;
@@ -5,7 +5,14 @@ import { systemToUserModels } from '@/const/models';
5
5
 
6
6
  import { LobeRuntimeAI } from '../BaseAI';
7
7
  import { AgentRuntimeErrorType } from '../error';
8
- import { ChatMethodOptions, ChatStreamPayload, ModelProvider } from '../types';
8
+ import {
9
+ ChatMethodOptions,
10
+ ChatStreamPayload,
11
+ Embeddings,
12
+ EmbeddingsOptions,
13
+ EmbeddingsPayload,
14
+ ModelProvider,
15
+ } from '../types';
9
16
  import { AgentRuntimeError } from '../utils/createError';
10
17
  import { debugStream } from '../utils/debugStream';
11
18
  import { transformResponseToStream } from '../utils/openaiCompatibleFactory';
@@ -75,33 +82,50 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
75
82
  });
76
83
  }
77
84
  } catch (e) {
78
- let error = e as { [key: string]: any; code: string; message: string };
85
+ return this.handleError(e, model);
86
+ }
87
+ }
88
+
89
+ async embeddings(payload: EmbeddingsPayload, options?: EmbeddingsOptions): Promise<Embeddings[]> {
90
+ try {
91
+ const res = await this.client.embeddings.create(
92
+ { ...payload, encoding_format: 'float', user: options?.user },
93
+ { headers: options?.headers, signal: options?.signal },
94
+ );
95
+
96
+ return res.data.map((item) => item.embedding);
97
+ } catch (error) {
98
+ return this.handleError(error, payload.model);
99
+ }
100
+ }
101
+
102
+ protected handleError(e: any, model?: string): never {
103
+ let error = e as { [key: string]: any; code: string; message: string };
79
104
 
80
- if (error.code) {
81
- switch (error.code) {
82
- case 'DeploymentNotFound': {
83
- error = { ...error, deployId: model };
84
- }
105
+ if (error.code) {
106
+ switch (error.code) {
107
+ case 'DeploymentNotFound': {
108
+ error = { ...error, deployId: model };
85
109
  }
86
- } else {
87
- error = {
88
- cause: error.cause,
89
- message: error.message,
90
- name: error.name,
91
- } as any;
92
110
  }
111
+ } else {
112
+ error = {
113
+ cause: error.cause,
114
+ message: error.message,
115
+ name: error.name,
116
+ } as any;
117
+ }
93
118
 
94
- const errorType = error.code
95
- ? AgentRuntimeErrorType.ProviderBizError
96
- : AgentRuntimeErrorType.AgentRuntimeError;
119
+ const errorType = error.code
120
+ ? AgentRuntimeErrorType.ProviderBizError
121
+ : AgentRuntimeErrorType.AgentRuntimeError;
97
122
 
98
- throw AgentRuntimeError.chat({
99
- endpoint: this.maskSensitiveUrl(this.baseURL),
100
- error,
101
- errorType,
102
- provider: ModelProvider.Azure,
103
- });
104
- }
123
+ throw AgentRuntimeError.chat({
124
+ endpoint: this.maskSensitiveUrl(this.baseURL),
125
+ error,
126
+ errorType,
127
+ provider: ModelProvider.Azure,
128
+ });
105
129
  }
106
130
 
107
131
  // Convert object keys to camel case, copy from `@azure/openai` in `node_modules/@azure/openai/dist/index.cjs`
@@ -33,6 +33,82 @@ afterEach(() => {
33
33
  });
34
34
 
35
35
  describe('userProfileSelectors', () => {
36
+ describe('displayUserName', () => {
37
+ it('should return default username when auth is disabled and not desktop', () => {
38
+ enableAuth = false;
39
+ isDesktop = false;
40
+
41
+ const store: UserStore = {
42
+ isSignedIn: false,
43
+ user: null,
44
+ enableAuth: () => false,
45
+ } as unknown as UserStore;
46
+
47
+ expect(userProfileSelectors.displayUserName(store)).toBe('LobeChat');
48
+ });
49
+
50
+ it('should return user username when auth is disabled and is desktop', () => {
51
+ enableAuth = false;
52
+ isDesktop = true;
53
+
54
+ const store: UserStore = {
55
+ isSignedIn: false,
56
+ user: { username: 'johndoe' },
57
+ enableAuth: () => false,
58
+ } as unknown as UserStore;
59
+
60
+ expect(userProfileSelectors.displayUserName(store)).toBe('johndoe');
61
+ });
62
+
63
+ it('should return user username when signed in', () => {
64
+ const store: UserStore = {
65
+ isSignedIn: true,
66
+ user: { username: 'johndoe' },
67
+ enableAuth: () => true,
68
+ } as UserStore;
69
+
70
+ expect(userProfileSelectors.displayUserName(store)).toBe('johndoe');
71
+ });
72
+
73
+ it('should return email when signed in but username is not existed in UserStore', () => {
74
+ const store: UserStore = {
75
+ isSignedIn: true,
76
+ user: { email: 'demo@lobehub.com' },
77
+ enableAuth: () => true,
78
+ } as UserStore;
79
+
80
+ expect(userProfileSelectors.displayUserName(store)).toBe('demo@lobehub.com');
81
+ });
82
+
83
+ it('should return "anonymous" when not signed in', () => {
84
+ const store: UserStore = {
85
+ enableAuth: () => true,
86
+ isSignedIn: false,
87
+ user: null,
88
+ } as unknown as UserStore;
89
+
90
+ expect(userProfileSelectors.displayUserName(store)).toBe('anonymous');
91
+ });
92
+ });
93
+
94
+ describe('email', () => {
95
+ it('should return user email if exist', () => {
96
+ const store: UserStore = {
97
+ user: { email: 'demo@lobehub.com' },
98
+ } as UserStore;
99
+
100
+ expect(userProfileSelectors.email(store)).toBe('demo@lobehub.com');
101
+ });
102
+
103
+ it('should return empty string if not exist', () => {
104
+ const store: UserStore = {
105
+ user: { email: undefined },
106
+ } as UserStore;
107
+
108
+ expect(userProfileSelectors.email(store)).toBe('');
109
+ });
110
+ });
111
+
36
112
  describe('fullName', () => {
37
113
  it('should return user fullName if exist', () => {
38
114
  const store: UserStore = {
@@ -36,6 +36,8 @@ const username = (s: UserStore) => {
36
36
  };
37
37
 
38
38
  export const userProfileSelectors = {
39
+ displayUserName: (s: UserStore): string => username(s) || s.user?.email || '',
40
+ email: (s: UserStore): string => s.user?.email || '',
39
41
  fullName: (s: UserStore): string => s.user?.fullName || '',
40
42
  nickName,
41
43
  userAvatar: (s: UserStore): string => s.user?.avatar || '',
@@ -1,35 +1,36 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { parsePlaceholderVariablesMessages, VARIABLE_GENERATORS } from './parserPlaceholder';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { VARIABLE_GENERATORS, parsePlaceholderVariablesMessages } from './parserPlaceholder';
3
4
 
4
5
  // Mock dependencies
5
6
  vi.mock('@/utils/uuid', () => ({
6
- uuid: () => 'mocked-uuid-12345'
7
+ uuid: () => 'mocked-uuid-12345',
7
8
  }));
8
9
 
9
10
  vi.mock('@/store/user', () => ({
10
11
  useUserStore: {
11
- getState: () => ({})
12
- }
12
+ getState: () => ({}),
13
+ },
13
14
  }));
14
15
 
15
16
  vi.mock('@/store/user/selectors', () => ({
16
17
  userProfileSelectors: {
18
+ displayUserName: () => 'testuser',
17
19
  nickName: () => 'Test User',
18
- username: () => 'testuser',
19
- fullName: () => 'Test Full Name'
20
- }
20
+ fullName: () => 'Test Full Name',
21
+ },
21
22
  }));
22
23
 
23
24
  vi.mock('@/store/agent/store', () => ({
24
- getAgentStoreState: () => ({})
25
+ getAgentStoreState: () => ({}),
25
26
  }));
26
27
 
27
28
  vi.mock('@/store/agent/selectors', () => ({
28
29
  agentChatConfigSelectors: {
29
30
  currentChatConfig: () => ({
30
- inputTemplate: 'Hello {{username}}!'
31
- })
32
- }
31
+ inputTemplate: 'Hello {{username}}!',
32
+ }),
33
+ },
33
34
  }));
34
35
 
35
36
  describe('parsePlaceholderVariablesMessages', () => {
@@ -52,8 +53,8 @@ describe('parsePlaceholderVariablesMessages', () => {
52
53
  const messages = [
53
54
  {
54
55
  id: '1',
55
- content: 'Hello {{username}}, today is {{date}}'
56
- }
56
+ content: 'Hello {{username}}, today is {{date}}',
57
+ },
57
58
  ];
58
59
 
59
60
  const result = parsePlaceholderVariablesMessages(messages);
@@ -66,8 +67,8 @@ describe('parsePlaceholderVariablesMessages', () => {
66
67
  const messages = [
67
68
  {
68
69
  id: '1',
69
- content: 'Time: {{time}}, Date: {{date}}, User: {{nickname}}'
70
- }
70
+ content: 'Time: {{time}}, Date: {{date}}, User: {{nickname}}',
71
+ },
71
72
  ];
72
73
 
73
74
  const result = parsePlaceholderVariablesMessages(messages);
@@ -81,8 +82,8 @@ describe('parsePlaceholderVariablesMessages', () => {
81
82
  {
82
83
  id: '1',
83
84
  role: 'user',
84
- content: 'Hello {{username}}'
85
- }
85
+ content: 'Hello {{username}}',
86
+ },
86
87
  ];
87
88
 
88
89
  const result = parsePlaceholderVariablesMessages(messages);
@@ -90,7 +91,7 @@ describe('parsePlaceholderVariablesMessages', () => {
90
91
  expect(result[0]).toEqual({
91
92
  id: '1',
92
93
  role: 'user',
93
- content: 'Hello testuser'
94
+ content: 'Hello testuser',
94
95
  });
95
96
  });
96
97
  });
@@ -103,14 +104,14 @@ describe('parsePlaceholderVariablesMessages', () => {
103
104
  content: [
104
105
  {
105
106
  type: 'text',
106
- text: 'Hello {{username}}'
107
+ text: 'Hello {{username}}',
107
108
  },
108
109
  {
109
110
  type: 'image_url',
110
- image_url: 'image.jpg'
111
- }
112
- ]
113
- }
111
+ image_url: 'image.jpg',
112
+ },
113
+ ],
114
+ },
114
115
  ];
115
116
 
116
117
  const result = parsePlaceholderVariablesMessages(messages);
@@ -118,7 +119,7 @@ describe('parsePlaceholderVariablesMessages', () => {
118
119
  expect(result[0].content[0].text).toBe('Hello testuser');
119
120
  expect(result[0].content[1]).toEqual({
120
121
  type: 'image_url',
121
- image_url: 'image.jpg'
122
+ image_url: 'image.jpg',
122
123
  });
123
124
  });
124
125
 
@@ -129,18 +130,18 @@ describe('parsePlaceholderVariablesMessages', () => {
129
130
  content: [
130
131
  {
131
132
  type: 'text',
132
- text: 'Date: {{date}}'
133
+ text: 'Date: {{date}}',
133
134
  },
134
135
  {
135
136
  type: 'text',
136
- text: 'Time: {{time}}'
137
+ text: 'Time: {{time}}',
137
138
  },
138
139
  {
139
140
  type: 'image_url',
140
- image_url: 'test.jpg'
141
- }
142
- ]
143
- }
141
+ image_url: 'test.jpg',
142
+ },
143
+ ],
144
+ },
144
145
  ];
145
146
 
146
147
  const result = parsePlaceholderVariablesMessages(messages);
@@ -149,7 +150,7 @@ describe('parsePlaceholderVariablesMessages', () => {
149
150
  expect(result[0].content[1].text).toContain(new Date().toLocaleTimeString());
150
151
  expect(result[0].content[2]).toEqual({
151
152
  type: 'image_url',
152
- image_url: 'test.jpg'
153
+ image_url: 'test.jpg',
153
154
  });
154
155
  });
155
156
 
@@ -164,10 +165,10 @@ describe('parsePlaceholderVariablesMessages', () => {
164
165
  },
165
166
  {
166
167
  type: 'image_url',
167
- name: 'image2.jpg'
168
- }
169
- ]
170
- }
168
+ name: 'image2.jpg',
169
+ },
170
+ ],
171
+ },
171
172
  ];
172
173
 
173
174
  const result = parsePlaceholderVariablesMessages(messages);
@@ -175,12 +176,12 @@ describe('parsePlaceholderVariablesMessages', () => {
175
176
  expect(result[0].content).toEqual([
176
177
  {
177
178
  type: 'image_url',
178
- image_url: 'image.jpg'
179
+ image_url: 'image.jpg',
179
180
  },
180
181
  {
181
182
  type: 'image_url',
182
- name: 'image2.jpg'
183
- }
183
+ name: 'image2.jpg',
184
+ },
184
185
  ]);
185
186
  });
186
187
  });
@@ -192,25 +193,19 @@ describe('parsePlaceholderVariablesMessages', () => {
192
193
  });
193
194
 
194
195
  it('should handle messages without content', () => {
195
- const messages = [
196
- { id: '1' },
197
- { id: '2', content: null },
198
- { id: '3', content: undefined }
199
- ];
196
+ const messages = [{ id: '1' }, { id: '2', content: null }, { id: '3', content: undefined }];
200
197
 
201
198
  const result = parsePlaceholderVariablesMessages(messages);
202
199
 
203
200
  expect(result).toEqual([
204
201
  { id: '1' },
205
202
  { id: '2', content: null },
206
- { id: '3', content: undefined }
203
+ { id: '3', content: undefined },
207
204
  ]);
208
205
  });
209
206
 
210
207
  it('should handle empty string content', () => {
211
- const messages = [
212
- { id: '1', content: '' }
213
- ];
208
+ const messages = [{ id: '1', content: '' }];
214
209
 
215
210
  const result = parsePlaceholderVariablesMessages(messages);
216
211
 
@@ -220,13 +215,13 @@ describe('parsePlaceholderVariablesMessages', () => {
220
215
  it('should handle content without variables', () => {
221
216
  const messages = [
222
217
  { id: '1', content: 'Hello world!' },
223
- {
224
- id: '2',
218
+ {
219
+ id: '2',
225
220
  content: [
226
221
  { type: 'text', text: 'No variables here' },
227
- { type: 'image_url', image_url: 'test.jpg' }
228
- ]
229
- }
222
+ { type: 'image_url', image_url: 'test.jpg' },
223
+ ],
224
+ },
230
225
  ];
231
226
 
232
227
  const result = parsePlaceholderVariablesMessages(messages);
@@ -236,9 +231,7 @@ describe('parsePlaceholderVariablesMessages', () => {
236
231
  });
237
232
 
238
233
  it('should handle unknown variable types', () => {
239
- const messages = [
240
- { id: '1', content: 'Hello {{unknown_variable}}!' }
241
- ];
234
+ const messages = [{ id: '1', content: 'Hello {{unknown_variable}}!' }];
242
235
 
243
236
  const result = parsePlaceholderVariablesMessages(messages);
244
237
 
@@ -247,9 +240,7 @@ describe('parsePlaceholderVariablesMessages', () => {
247
240
  });
248
241
 
249
242
  it('should handle nested variables (input_template)', () => {
250
- const messages = [
251
- { id: '1', content: 'Template: {{input_template}}' }
252
- ];
243
+ const messages = [{ id: '1', content: 'Template: {{input_template}}' }];
253
244
 
254
245
  const result = parsePlaceholderVariablesMessages(messages);
255
246
 
@@ -261,10 +252,10 @@ describe('parsePlaceholderVariablesMessages', () => {
261
252
  describe('specific variable types', () => {
262
253
  it('should handle time variables', () => {
263
254
  const messages = [
264
- {
265
- id: '1',
266
- content: 'Year: {{year}}, Month: {{month}}, Day: {{day}}'
267
- }
255
+ {
256
+ id: '1',
257
+ content: 'Year: {{year}}, Month: {{month}}, Day: {{day}}',
258
+ },
268
259
  ];
269
260
 
270
261
  const result = parsePlaceholderVariablesMessages(messages);
@@ -276,10 +267,10 @@ describe('parsePlaceholderVariablesMessages', () => {
276
267
 
277
268
  it('should handle random variables', () => {
278
269
  const messages = [
279
- {
280
- id: '1',
281
- content: 'Random: {{random}}, Bool: {{random_bool}}, UUID: {{uuid}}'
282
- }
270
+ {
271
+ id: '1',
272
+ content: 'Random: {{random}}, Bool: {{random_bool}}, UUID: {{uuid}}',
273
+ },
283
274
  ];
284
275
 
285
276
  const result = parsePlaceholderVariablesMessages(messages);
@@ -291,10 +282,10 @@ describe('parsePlaceholderVariablesMessages', () => {
291
282
 
292
283
  it('should handle user variables', () => {
293
284
  const messages = [
294
- {
295
- id: '1',
296
- content: 'User: {{username}}, Nickname: {{nickname}}'
297
- }
285
+ {
286
+ id: '1',
287
+ content: 'User: {{username}}, Nickname: {{nickname}}',
288
+ },
298
289
  ];
299
290
 
300
291
  const result = parsePlaceholderVariablesMessages(messages);
@@ -307,13 +298,11 @@ describe('parsePlaceholderVariablesMessages', () => {
307
298
  it('should process multiple messages correctly', () => {
308
299
  const messages = [
309
300
  { id: '1', content: 'Hello {{username}}' },
310
- {
311
- id: '2',
312
- content: [
313
- { type: 'text', text: 'Today is {{date}}' }
314
- ]
301
+ {
302
+ id: '2',
303
+ content: [{ type: 'text', text: 'Today is {{date}}' }],
315
304
  },
316
- { id: '3', content: 'Time: {{time}}' }
305
+ { id: '3', content: 'Time: {{time}}' },
317
306
  ];
318
307
 
319
308
  const result = parsePlaceholderVariablesMessages(messages);
@@ -53,12 +53,14 @@ export const VARIABLE_GENERATORS = {
53
53
  *
54
54
  * | Value | Example |
55
55
  * |-------|---------|
56
+ * | `{{email}}` | demo@lobehub.com |
56
57
  * | `{{nickname}}` | 社区版用户 |
57
58
  * | `{{username}}` | LobeChat |
58
59
  *
59
60
  */
61
+ email: () => userProfileSelectors.email(useUserStore.getState()) ?? '',
60
62
  nickname: () => userProfileSelectors.nickName(useUserStore.getState()) ?? '',
61
- username: () => userProfileSelectors.username(useUserStore.getState()) ?? userProfileSelectors.fullName(useUserStore.getState()) ?? '',
63
+ username: () => userProfileSelectors.displayUserName(useUserStore.getState()) ?? userProfileSelectors.fullName(useUserStore.getState()) ?? '',
62
64
 
63
65
  /**
64
66
  * 随机值类模板变量