@lobehub/chat 1.92.1 → 1.92.3

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`
@@ -1,3 +1,4 @@
1
+ import OpenRouterModels from '@/config/aiModels/openrouter';
1
2
  import type { ChatModelCard } from '@/types/llm';
2
3
 
3
4
  import { ModelProvider } from '../types';
@@ -14,12 +15,27 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
14
15
  baseURL: 'https://openrouter.ai/api/v1',
15
16
  chatCompletion: {
16
17
  handlePayload: (payload) => {
17
- const { thinking } = payload;
18
+ const { thinking, model, max_tokens } = payload;
18
19
 
19
20
  let reasoning: OpenRouterReasoning = {};
21
+
20
22
  if (thinking?.type === 'enabled') {
23
+ const modelConfig = OpenRouterModels.find((m) => m.id === model);
24
+ const defaultMaxOutput = modelConfig?.maxOutput;
25
+
26
+ // 配置优先级:用户设置 > 模型配置 > 硬编码默认值
27
+ const getMaxTokens = () => {
28
+ if (max_tokens) return max_tokens;
29
+ if (defaultMaxOutput) return defaultMaxOutput;
30
+ return undefined;
31
+ };
32
+
33
+ const maxTokens = getMaxTokens() || 32_000; // Claude Opus 4 has minimum maxOutput
34
+
21
35
  reasoning = {
22
- max_tokens: thinking.budget_tokens,
36
+ max_tokens: thinking?.budget_tokens
37
+ ? Math.min(thinking.budget_tokens, maxTokens - 1)
38
+ : 1024,
23
39
  };
24
40
  }
25
41
 
@@ -43,7 +59,7 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
43
59
  models: async ({ client }) => {
44
60
  const modelsPage = (await client.models.list()) as any;
45
61
  const modelList: OpenRouterModelCard[] = modelsPage.data;
46
-
62
+
47
63
  const modelsExtraInfo: OpenRouterModelExtraInfo[] = [];
48
64
  try {
49
65
  const response = await fetch('https://openrouter.ai/api/frontend/models');
@@ -54,49 +70,45 @@ export const LobeOpenRouterAI = createOpenAICompatibleRuntime({
54
70
  } catch (error) {
55
71
  console.error('Failed to fetch OpenRouter frontend models:', error);
56
72
  }
57
-
73
+
58
74
  // 解析模型能力
59
75
  const baseModels = await processMultiProviderModelList(modelList);
60
-
76
+
61
77
  // 合并 OpenRouter 获取的模型信息
62
- return baseModels.map((baseModel) => {
63
- const model = modelList.find(m => m.id === baseModel.id);
64
- const extraInfo = modelsExtraInfo.find(
65
- (m) => m.slug.toLowerCase() === baseModel.id.toLowerCase(),
66
- );
67
-
68
- if (!model) return baseModel;
69
-
70
- return {
71
- ...baseModel,
72
- contextWindowTokens: model.context_length,
73
- description: model.description,
74
- displayName: model.name,
75
- functionCall:
76
- baseModel.functionCall ||
77
- model.description.includes('function calling') ||
78
- model.description.includes('tools') ||
79
- extraInfo?.endpoint?.supports_tool_parameters ||
80
- false,
81
- maxTokens:
82
- typeof model.top_provider.max_completion_tokens === 'number'
83
- ? model.top_provider.max_completion_tokens
84
- : undefined,
85
- pricing: {
86
- input: formatPrice(model.pricing.prompt),
87
- output: formatPrice(model.pricing.completion),
88
- },
89
- reasoning:
90
- baseModel.reasoning ||
91
- extraInfo?.endpoint?.supports_reasoning ||
92
- false,
93
- releasedAt: new Date(model.created * 1000).toISOString().split('T')[0],
94
- vision:
95
- baseModel.vision ||
96
- model.architecture.modality.includes('image') ||
97
- false,
98
- };
99
- }).filter(Boolean) as ChatModelCard[];
78
+ return baseModels
79
+ .map((baseModel) => {
80
+ const model = modelList.find((m) => m.id === baseModel.id);
81
+ const extraInfo = modelsExtraInfo.find(
82
+ (m) => m.slug.toLowerCase() === baseModel.id.toLowerCase(),
83
+ );
84
+
85
+ if (!model) return baseModel;
86
+
87
+ return {
88
+ ...baseModel,
89
+ contextWindowTokens: model.context_length,
90
+ description: model.description,
91
+ displayName: model.name,
92
+ functionCall:
93
+ baseModel.functionCall ||
94
+ model.description.includes('function calling') ||
95
+ model.description.includes('tools') ||
96
+ extraInfo?.endpoint?.supports_tool_parameters ||
97
+ false,
98
+ maxTokens:
99
+ typeof model.top_provider.max_completion_tokens === 'number'
100
+ ? model.top_provider.max_completion_tokens
101
+ : undefined,
102
+ pricing: {
103
+ input: formatPrice(model.pricing.prompt),
104
+ output: formatPrice(model.pricing.completion),
105
+ },
106
+ reasoning: baseModel.reasoning || extraInfo?.endpoint?.supports_reasoning || false,
107
+ releasedAt: new Date(model.created * 1000).toISOString().split('T')[0],
108
+ vision: baseModel.vision || model.architecture.modality.includes('image') || false,
109
+ };
110
+ })
111
+ .filter(Boolean) as ChatModelCard[];
100
112
  },
101
113
  provider: ModelProvider.OpenRouter,
102
114
  });
@@ -84,14 +84,14 @@ describe('BrowserS3Storage', () => {
84
84
  expect(result).toBeUndefined();
85
85
  });
86
86
 
87
- it('should throw error when get operation fails', async () => {
88
- const mockError = new Error('Storage error');
89
- (get as any).mockRejectedValue(mockError);
90
-
91
- await expect(storage.getObject('test-key')).rejects.toThrow(
92
- 'Failed to get object (key=test-key): Storage error',
93
- );
94
- });
87
+ // it('should throw error when get operation fails', async () => {
88
+ // const mockError = new Error('Storage error');
89
+ // (get as any).mockRejectedValue(mockError);
90
+ //
91
+ // await expect(storage.getObject('test-key')).rejects.toThrow(
92
+ // 'Failed to get object (key=test-key): Storage error',
93
+ // );
94
+ // });
95
95
  });
96
96
 
97
97
  describe('deleteObject', () => {
@@ -38,7 +38,8 @@ export class BrowserS3Storage {
38
38
 
39
39
  return new File([res.data], res!.name, { type: res?.type });
40
40
  } catch (e) {
41
- throw new Error(`Failed to get object (key=${key}): ${(e as Error).message}`);
41
+ console.log(`Failed to get object (key=${key}):`, e);
42
+ return undefined;
42
43
  }
43
44
  };
44
45