@lobehub/chat 1.36.38 → 1.36.40

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +7 -3
  4. package/locales/bg-BG/common.json +7 -3
  5. package/locales/de-DE/common.json +7 -3
  6. package/locales/en-US/common.json +7 -3
  7. package/locales/es-ES/common.json +7 -3
  8. package/locales/fa-IR/common.json +7 -3
  9. package/locales/fr-FR/common.json +7 -3
  10. package/locales/it-IT/common.json +7 -3
  11. package/locales/ja-JP/common.json +7 -3
  12. package/locales/ko-KR/common.json +7 -3
  13. package/locales/nl-NL/common.json +7 -3
  14. package/locales/pl-PL/common.json +7 -3
  15. package/locales/pt-BR/common.json +7 -3
  16. package/locales/ru-RU/common.json +7 -3
  17. package/locales/tr-TR/common.json +7 -3
  18. package/locales/vi-VN/common.json +7 -3
  19. package/locales/zh-CN/common.json +8 -4
  20. package/locales/zh-TW/common.json +7 -3
  21. package/package.json +1 -2
  22. package/src/app/(main)/(mobile)/me/(home)/page.tsx +2 -2
  23. package/src/app/(main)/(mobile)/me/data/page.tsx +2 -2
  24. package/src/app/(main)/(mobile)/me/profile/page.tsx +2 -2
  25. package/src/app/(main)/(mobile)/me/settings/page.tsx +2 -2
  26. package/src/app/(main)/chat/(workspace)/@conversation/default.tsx +3 -3
  27. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/index.tsx +1 -3
  28. package/src/app/(main)/chat/(workspace)/@portal/default.tsx +2 -2
  29. package/src/app/(main)/chat/(workspace)/@topic/default.tsx +2 -2
  30. package/src/app/(main)/chat/(workspace)/page.tsx +1 -1
  31. package/src/app/(main)/discover/(detail)/assistant/[slug]/page.tsx +1 -1
  32. package/src/app/(main)/discover/(detail)/model/[...slugs]/page.tsx +1 -1
  33. package/src/app/(main)/discover/(detail)/plugin/[slug]/page.tsx +1 -1
  34. package/src/app/(main)/discover/(detail)/provider/[slug]/page.tsx +1 -1
  35. package/src/app/(main)/discover/(list)/assistants/[slug]/page.tsx +1 -1
  36. package/src/app/(main)/discover/(list)/assistants/page.tsx +1 -1
  37. package/src/app/(main)/discover/(list)/models/[slug]/page.tsx +1 -1
  38. package/src/app/(main)/discover/(list)/models/page.tsx +1 -1
  39. package/src/app/(main)/discover/(list)/plugins/[slug]/page.tsx +1 -1
  40. package/src/app/(main)/discover/(list)/plugins/page.tsx +1 -1
  41. package/src/app/(main)/discover/(list)/providers/page.tsx +1 -1
  42. package/src/app/(main)/discover/search/page.tsx +1 -1
  43. package/src/app/(main)/profile/[[...slugs]]/page.tsx +3 -2
  44. package/src/app/(main)/profile/layout.tsx +3 -2
  45. package/src/app/(main)/profile/loading.tsx +2 -2
  46. package/src/app/(main)/settings/about/page.tsx +2 -2
  47. package/src/app/(main)/settings/sync/page.tsx +3 -3
  48. package/src/app/@modal/(.)settings/modal/page.tsx +3 -3
  49. package/src/app/layout.tsx +2 -2
  50. package/src/components/server/ServerLayout.tsx +2 -2
  51. package/src/database/schemas/user.ts +1 -0
  52. package/src/database/server/models/__tests__/user.test.ts +23 -19
  53. package/src/database/server/models/session.ts +3 -1
  54. package/src/database/server/models/user.ts +25 -58
  55. package/src/layout/GlobalProvider/index.tsx +1 -1
  56. package/src/locales/default/common.ts +6 -3
  57. package/src/server/modules/KeyVaultsEncrypt/index.ts +23 -0
  58. package/src/server/routers/lambda/user.ts +19 -2
  59. package/src/server/services/user/index.ts +5 -0
  60. package/src/utils/server/responsive.ts +4 -5
@@ -3,14 +3,18 @@ import { eq } from 'drizzle-orm/expressions';
3
3
  import { DeepPartial } from 'utility-types';
4
4
 
5
5
  import { LobeChatDatabase } from '@/database/type';
6
- import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
7
6
  import { UserGuide, UserPreference } from '@/types/user';
8
7
  import { UserKeyVaults, UserSettings } from '@/types/user/settings';
9
8
  import { merge } from '@/utils/merge';
10
9
 
11
- import { NewUser, UserItem, userSettings, users } from '../../schemas';
10
+ import { NewUser, UserItem, UserSettingsItem, userSettings, users } from '../../schemas';
12
11
  import { SessionModel } from './session';
13
12
 
13
+ type DecryptUserKeyVaults = (
14
+ encryptKeyVaultsStr: string | null,
15
+ userId?: string,
16
+ ) => Promise<UserKeyVaults>;
17
+
14
18
  export class UserNotFoundError extends TRPCError {
15
19
  constructor() {
16
20
  super({ code: 'UNAUTHORIZED', message: 'user not found' });
@@ -26,7 +30,7 @@ export class UserModel {
26
30
  this.db = db;
27
31
  }
28
32
 
29
- getUserState = async () => {
33
+ getUserState = async (decryptor: DecryptUserKeyVaults) => {
30
34
  const result = await this.db
31
35
  .select({
32
36
  isOnboarded: users.isOnboarded,
@@ -51,19 +55,7 @@ export class UserModel {
51
55
  const state = result[0];
52
56
 
53
57
  // Decrypt keyVaults
54
- let decryptKeyVaults = {};
55
- if (state.settingsKeyVaults) {
56
- const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
57
- const { wasAuthentic, plaintext } = await gateKeeper.decrypt(state.settingsKeyVaults);
58
-
59
- if (wasAuthentic) {
60
- try {
61
- decryptKeyVaults = JSON.parse(plaintext);
62
- } catch (e) {
63
- console.error(`Failed to parse keyVaults ,userId: ${this.userId}. Error:`, e);
64
- }
65
- }
66
- }
58
+ const decryptKeyVaults = await decryptor(state.settingsKeyVaults, this.userId);
67
59
 
68
60
  const settings: DeepPartial<UserSettings> = {
69
61
  defaultAgent: state.settingsDefaultAgent || {},
@@ -94,32 +86,17 @@ export class UserModel {
94
86
  return this.db.delete(userSettings).where(eq(userSettings.id, this.userId));
95
87
  };
96
88
 
97
- updateSetting = async (value: Partial<UserSettings>) => {
98
- const { keyVaults, ...res } = value;
99
-
100
- // Encrypt keyVaults
101
- let encryptedKeyVaults: string | null = null;
102
-
103
- if (keyVaults) {
104
- // TODO: better to add a validation
105
- const data = JSON.stringify(keyVaults);
106
- const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
107
-
108
- encryptedKeyVaults = await gateKeeper.encrypt(data);
109
- }
110
-
111
- const newValue = { ...res, keyVaults: encryptedKeyVaults };
112
-
113
- // update or create user settings
114
- const settings = await this.db.query.userSettings.findFirst({
115
- where: eq(users.id, this.userId),
116
- });
117
- if (!settings) {
118
- await this.db.insert(userSettings).values({ id: this.userId, ...newValue });
119
- return;
120
- }
121
-
122
- return this.db.update(userSettings).set(newValue).where(eq(userSettings.id, this.userId));
89
+ updateSetting = async (value: Partial<UserSettingsItem>) => {
90
+ return this.db
91
+ .insert(userSettings)
92
+ .values({
93
+ id: this.userId,
94
+ ...value,
95
+ })
96
+ .onConflictDoUpdate({
97
+ set: value,
98
+ target: userSettings.id,
99
+ });
123
100
  };
124
101
 
125
102
  updatePreference = async (value: Partial<UserPreference>) => {
@@ -175,7 +152,11 @@ export class UserModel {
175
152
  return db.query.users.findFirst({ where: eq(users.email, email) });
176
153
  };
177
154
 
178
- static getUserApiKeys = async (db: LobeChatDatabase, id: string) => {
155
+ static getUserApiKeys = async (
156
+ db: LobeChatDatabase,
157
+ id: string,
158
+ decryptor: DecryptUserKeyVaults,
159
+ ) => {
179
160
  const result = await db
180
161
  .select({
181
162
  settingsKeyVaults: userSettings.keyVaults,
@@ -190,20 +171,6 @@ export class UserModel {
190
171
  const state = result[0];
191
172
 
192
173
  // Decrypt keyVaults
193
- let decryptKeyVaults = {};
194
- if (state.settingsKeyVaults) {
195
- const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
196
- const { wasAuthentic, plaintext } = await gateKeeper.decrypt(state.settingsKeyVaults);
197
-
198
- if (wasAuthentic) {
199
- try {
200
- decryptKeyVaults = JSON.parse(plaintext);
201
- } catch (e) {
202
- console.error(`Failed to parse keyVaults ,userId: ${id}. Error:`, e);
203
- }
204
- }
205
- }
206
-
207
- return decryptKeyVaults as UserKeyVaults;
174
+ return await decryptor(state.settingsKeyVaults, id);
208
175
  };
209
176
  }
@@ -68,7 +68,7 @@ const GlobalLayout = async ({ children }: PropsWithChildren) => {
68
68
  // get default feature flags to use with ssr
69
69
  const serverFeatureFlags = getServerFeatureFlagsValue();
70
70
  const serverConfig = getServerGlobalConfig();
71
- const isMobile = isMobileDevice();
71
+ const isMobile = await isMobileDevice();
72
72
  return (
73
73
  <StyleRegistry>
74
74
  <Locale antdLocale={antdLocale} defaultLang={userLocale}>
@@ -13,6 +13,7 @@ export default {
13
13
  appLoading: {
14
14
  appIdle: '准备启动',
15
15
  appInitializing: '应用启动中...',
16
+ failed: '很抱歉,应用初始化失败,请查看详情进行排查',
16
17
  finished: '数据库初始化完成',
17
18
  goToChat: '对话页面加载中...',
18
19
  initAuth: '鉴权服务初始化...',
@@ -22,6 +23,7 @@ export default {
22
23
  loadingWasm: '加载 WASM 模块...',
23
24
  migrating: '执行数据表迁移...',
24
25
  ready: '数据库已就绪',
26
+ showDetail: '查看详情',
25
27
  },
26
28
  autoGenerate: '自动补全',
27
29
  autoGenerateTooltip: '基于提示词自动补全助手描述',
@@ -39,12 +41,13 @@ export default {
39
41
  title: '初始化 PGlite 数据库',
40
42
  },
41
43
  error: {
42
- desc: '非常抱歉,Pglite 数据库初始化过程发生异常。请点击 「重试」按钮。<br><br> 如仍然出错,请 <1>提交问题</1> ,我们将会第一时间帮你排查',
44
+ desc: '非常抱歉,Pglite 数据库初始化过程发生异常。请点击按钮重试。如多次重试后仍重复出错,请 <1>提交问题</1> ,我们将会第一时间帮你排查',
45
+ detail: '错误原因:[{{type}}] {{message}},明细如下:',
43
46
  retry: '重试',
44
- title: '数据库升级失败',
47
+ title: '数据库初始化失败',
45
48
  },
46
49
  initing: {
47
- error: '发生错误,请重试',
50
+ error: '数据库初始化出错,点击查看详情',
48
51
  idle: '等待初始化...',
49
52
  initializing: '正在初始化...',
50
53
  loadingDependencies: '加载依赖中...',
@@ -1,4 +1,5 @@
1
1
  import { getServerDBConfig } from '@/config/db';
2
+ import { UserKeyVaults } from '@/types/user/settings';
2
3
 
3
4
  interface DecryptionResult {
4
5
  plaintext: string;
@@ -90,4 +91,26 @@ If you don't have it, please run \`openssl rand -base64 32\` to create one.
90
91
  };
91
92
  }
92
93
  };
94
+
95
+ static getUserKeyVaults = async (
96
+ encryptedKeyVaults: string | null,
97
+ userId?: string,
98
+ ): Promise<UserKeyVaults> => {
99
+ if (!encryptedKeyVaults) return {};
100
+ // Decrypt keyVaults
101
+ let decryptKeyVaults = {};
102
+
103
+ const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
104
+ const { wasAuthentic, plaintext } = await gateKeeper.decrypt(encryptedKeyVaults);
105
+
106
+ if (wasAuthentic) {
107
+ try {
108
+ decryptKeyVaults = JSON.parse(plaintext);
109
+ } catch (e) {
110
+ console.error(`Failed to parse keyVaults, userId: ${userId}. Error:`, e);
111
+ }
112
+ }
113
+
114
+ return decryptKeyVaults as UserKeyVaults;
115
+ };
93
116
  }
@@ -8,8 +8,10 @@ import { MessageModel } from '@/database/server/models/message';
8
8
  import { SessionModel } from '@/database/server/models/session';
9
9
  import { UserModel, UserNotFoundError } from '@/database/server/models/user';
10
10
  import { authedProcedure, router } from '@/libs/trpc';
11
+ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
11
12
  import { UserService } from '@/server/services/user';
12
13
  import { UserGuideSchema, UserInitializationState, UserPreference } from '@/types/user';
14
+ import { UserSettings } from '@/types/user/settings';
13
15
 
14
16
  const userProcedure = authedProcedure.use(async (opts) => {
15
17
  return opts.next({
@@ -24,7 +26,7 @@ export const userRouter = router({
24
26
  // get or create first-time user
25
27
  while (!state) {
26
28
  try {
27
- state = await ctx.userModel.getUserState();
29
+ state = await ctx.userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
28
30
  } catch (error) {
29
31
  if (enableClerk && error instanceof UserNotFoundError) {
30
32
  const user = await currentUser();
@@ -97,7 +99,22 @@ export const userRouter = router({
97
99
  updateSettings: userProcedure
98
100
  .input(z.object({}).passthrough())
99
101
  .mutation(async ({ ctx, input }) => {
100
- return ctx.userModel.updateSetting(input);
102
+ const { keyVaults, ...res } = input as Partial<UserSettings>;
103
+
104
+ // Encrypt keyVaults
105
+ let encryptedKeyVaults: string | null = null;
106
+
107
+ if (keyVaults) {
108
+ // TODO: better to add a validation
109
+ const data = JSON.stringify(keyVaults);
110
+ const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
111
+
112
+ encryptedKeyVaults = await gateKeeper.encrypt(data);
113
+ }
114
+
115
+ const nextValue = { ...res, keyVaults: encryptedKeyVaults };
116
+
117
+ return ctx.userModel.updateSetting(nextValue);
101
118
  }),
102
119
  });
103
120
 
@@ -3,6 +3,7 @@ import { UserJSON } from '@clerk/backend';
3
3
  import { serverDB } from '@/database/server';
4
4
  import { UserModel } from '@/database/server/models/user';
5
5
  import { pino } from '@/libs/logger';
6
+ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
6
7
 
7
8
  export class UserService {
8
9
  createUser = async (id: string, params: UserJSON) => {
@@ -84,4 +85,8 @@ export class UserService {
84
85
 
85
86
  return { message: 'user updated', success: true };
86
87
  };
88
+
89
+ getUserApiKeys = async (id: string) => {
90
+ return UserModel.getUserApiKeys(serverDB, id, KeyVaultsGateKeeper.getUserKeyVaults);
91
+ };
87
92
  }
@@ -4,12 +4,12 @@ import { UAParser } from 'ua-parser-js';
4
4
  /**
5
5
  * check mobile device in server
6
6
  */
7
- export const isMobileDevice = () => {
7
+ export const isMobileDevice = async () => {
8
8
  if (typeof process === 'undefined') {
9
9
  throw new Error('[Server method] you are importing a server-only module outside of server');
10
10
  }
11
11
 
12
- const { get } = headers();
12
+ const { get } = await headers();
13
13
  const ua = get('user-agent');
14
14
 
15
15
  // console.debug(ua);
@@ -21,15 +21,14 @@ export const isMobileDevice = () => {
21
21
  /**
22
22
  * check mobile device in server
23
23
  */
24
- export const gerServerDeviceInfo = () => {
24
+ export const gerServerDeviceInfo = async () => {
25
25
  if (typeof process === 'undefined') {
26
26
  throw new Error('[Server method] you are importing a server-only module outside of server');
27
27
  }
28
28
 
29
- const { get } = headers();
29
+ const { get } = await headers();
30
30
  const ua = get('user-agent');
31
31
 
32
- // console.debug(ua);
33
32
  const parser = new UAParser(ua || '');
34
33
 
35
34
  return {