@lobehub/chat 0.161.11 → 0.161.13

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.
@@ -3,10 +3,29 @@ description: 'Report an bug'
3
3
  title: '[Bug] '
4
4
  labels: ['🐛 Bug']
5
5
  body:
6
+ - type: dropdown
7
+ attributes:
8
+ label: '📦 Environment'
9
+ options:
10
+ - Unselected
11
+ - Official
12
+ - Official Preview
13
+ - Vercel / Zeabur / Sealos
14
+ - Docker
15
+ - Other
16
+ validations:
17
+ required: true
18
+ - type: input
19
+ attributes:
20
+ label: '📌 Version'
21
+ validations:
22
+ required: true
23
+
6
24
  - type: dropdown
7
25
  attributes:
8
26
  label: '💻 Operating System'
9
27
  options:
28
+ - Unselected
10
29
  - Windows
11
30
  - macOS
12
31
  - Ubuntu
@@ -16,21 +35,11 @@ body:
16
35
  - Other
17
36
  validations:
18
37
  required: true
19
- - type: dropdown
20
- attributes:
21
- label: '📦 Environment'
22
- options:
23
- - Official Preview
24
- - Vercel / Zeabur / Sealos
25
- - Docker
26
- - Other
27
- validations:
28
- required: true
29
-
30
38
  - type: dropdown
31
39
  attributes:
32
40
  label: '🌐 Browser'
33
41
  options:
42
+ - Unselected
34
43
  - Chrome
35
44
  - Edge
36
45
  - Safari
@@ -41,17 +50,17 @@ body:
41
50
  - type: textarea
42
51
  attributes:
43
52
  label: '🐛 Bug Description'
44
- description: A clear and concise description of the bug.
53
+ description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
45
54
  validations:
46
55
  required: true
47
- - type: textarea
48
- attributes:
49
- label: '🚦 Expected Behavior'
50
- description: A clear and concise description of what you expected to happen.
51
56
  - type: textarea
52
57
  attributes:
53
58
  label: '📷 Recurrence Steps'
54
59
  description: A clear and concise description of how to recurrence.
60
+ - type: textarea
61
+ attributes:
62
+ label: '🚦 Expected Behavior'
63
+ description: A clear and concise description of what you expected to happen.
55
64
  - type: textarea
56
65
  attributes:
57
66
  label: '📝 Additional Information'
@@ -5,33 +5,41 @@ labels: ['🐛 Bug']
5
5
  body:
6
6
  - type: dropdown
7
7
  attributes:
8
- label: '💻 系统环境'
8
+ label: '📦 部署环境'
9
9
  options:
10
- - Windows
11
- - macOS
12
- - Ubuntu
13
- - Other Linux
14
- - iOS
15
- - Android
10
+ - 待选择
11
+ - Official
12
+ - Official Preview
13
+ - Vercel / Zeabur / Sealos
14
+ - Docker
16
15
  - Other
17
16
  validations:
18
17
  required: true
18
+ - type: input
19
+ attributes:
20
+ label: '📌 软件版本'
21
+ validations:
22
+ required: true
19
23
 
20
24
  - type: dropdown
21
25
  attributes:
22
- label: '📦 部署环境'
26
+ label: '💻 系统环境'
23
27
  options:
24
- - Official Preview
25
- - Vercel / Zeabur / Sealos
26
- - Docker
28
+ - 待选择
29
+ - Windows
30
+ - macOS
31
+ - Ubuntu
32
+ - Other Linux
33
+ - iOS
34
+ - Android
27
35
  - Other
28
36
  validations:
29
37
  required: true
30
-
31
38
  - type: dropdown
32
39
  attributes:
33
40
  label: '🌐 浏览器'
34
41
  options:
42
+ - 待选择
35
43
  - Chrome
36
44
  - Edge
37
45
  - Safari
@@ -42,17 +50,17 @@ body:
42
50
  - type: textarea
43
51
  attributes:
44
52
  label: '🐛 问题描述'
45
- description: 请提供一个清晰且简洁的问题描述。
53
+ description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
46
54
  validations:
47
55
  required: true
48
- - type: textarea
49
- attributes:
50
- label: '🚦 期望结果'
51
- description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
52
56
  - type: textarea
53
57
  attributes:
54
58
  label: '📷 复现步骤'
55
59
  description: 请提供一个清晰且简洁的描述,说明如何复现问题。
60
+ - type: textarea
61
+ attributes:
62
+ label: '🚦 期望结果'
63
+ description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
56
64
  - type: textarea
57
65
  attributes:
58
66
  label: '📝 补充信息'
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.161.13](https://github.com/lobehub/lobe-chat/compare/v0.161.12...v0.161.13)
6
+
7
+ <sup>Released on **2024-05-24**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 0.161.12](https://github.com/lobehub/lobe-chat/compare/v0.161.11...v0.161.12)
23
+
24
+ <sup>Released on **2024-05-23**</sup>
25
+
26
+ #### ♻ Code Refactoring
27
+
28
+ - **misc**: Refactor the home redirect implement.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### Code refactoring
36
+
37
+ - **misc**: Refactor the home redirect implement, closes [#2626](https://github.com/lobehub/lobe-chat/issues/2626) ([ab4216e](https://github.com/lobehub/lobe-chat/commit/ab4216e))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 0.161.11](https://github.com/lobehub/lobe-chat/compare/v0.161.10...v0.161.11)
6
48
 
7
49
  <sup>Released on **2024-05-23**</sup>
@@ -15,6 +15,13 @@ When deploying LobeChat, a rich set of environment variables related to model se
15
15
 
16
16
  ## OpenAI
17
17
 
18
+ ### `ENABLED_OPENAI`
19
+
20
+ - Type:Optional
21
+ - Description:Enables OpenAI as a model provider by default, turns off the OpenAI service when set to `0`
22
+ - Default:`1`
23
+ - Example:`0`
24
+
18
25
  ### `OPENAI_API_KEY`
19
26
 
20
27
  - Type: Required
@@ -189,6 +196,13 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
189
196
 
190
197
  ## Ollama
191
198
 
199
+ ### `ENABLED_OLLAMA`
200
+
201
+ - Type:Optional
202
+ - Description:Enables Ollama as a model provider by default, turns off the Ollama service when set to `0`
203
+ - Default:`1`
204
+ - Example:`0`
205
+
192
206
  ### `OLLAMA_PROXY_URL`
193
207
 
194
208
  - Type: Required
@@ -15,6 +15,13 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
15
15
 
16
16
  ## OpenAI
17
17
 
18
+ ### `ENABLED_OPENAI`
19
+
20
+ - 类型:可选
21
+ - 描述:默认启用 OpenAI 作为模型供应商,当设为 0 时关闭 OpenAI 服务
22
+ - 默认值:`1`
23
+ - 示例:`0`
24
+
18
25
  ### `OPENAI_API_KEY`
19
26
 
20
27
  - 类型:必选
@@ -187,6 +194,13 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
187
194
 
188
195
  ## Ollama
189
196
 
197
+ ### `ENABLED_OLLAMA`
198
+
199
+ - 类型:可选
200
+ - 描述:默认启用 Ollama 作为模型供应商,当设为 0 时关闭 Ollama 服务
201
+ - 默认值:`1`
202
+ - 示例:`0`
203
+
190
204
  ### `OLLAMA_PROXY_URL`
191
205
 
192
206
  - 类型:必选
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.161.11",
3
+ "version": "0.161.13",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -158,6 +158,7 @@
158
158
  "remark": "^14.0.3",
159
159
  "remark-gfm": "^3.0.1",
160
160
  "remark-html": "^15.0.2",
161
+ "resolve-accept-language": "^3.1.4",
161
162
  "rtl-detect": "^1.1.2",
162
163
  "semver": "^7.6.2",
163
164
  "sharp": "^0.33.4",
@@ -3,35 +3,47 @@
3
3
  import { useRouter } from 'next/navigation';
4
4
  import { memo, useEffect } from 'react';
5
5
 
6
- import { messageService } from '@/services/message';
7
- import { sessionService } from '@/services/session';
8
6
  import { useUserStore } from '@/store/user';
9
7
  import { authSelectors } from '@/store/user/selectors';
10
8
 
11
- const checkHasConversation = async () => {
12
- const hasMessages = await messageService.hasMessages();
13
- const hasAgents = await sessionService.hasSessions();
14
- return hasMessages || hasAgents;
15
- };
16
-
17
9
  const Redirect = memo(() => {
18
10
  const router = useRouter();
19
- const isLogin = useUserStore(authSelectors.isLogin);
11
+ const [isLogin, isLoaded, isUserStateInit, isUserHasConversation, isOnboard] = useUserStore(
12
+ (s) => [
13
+ authSelectors.isLogin(s),
14
+ authSelectors.isLoaded(s),
15
+ s.isUserStateInit,
16
+ s.isUserHasConversation,
17
+ s.isOnboard,
18
+ ],
19
+ );
20
20
 
21
21
  useEffect(() => {
22
+ // if user auth state is not ready, wait for loading
23
+ if (!isLoaded) return;
24
+
25
+ // this mean user is definitely not login
22
26
  if (!isLogin) {
23
27
  router.replace('/welcome');
24
28
  return;
25
29
  }
26
30
 
27
- checkHasConversation().then((hasData) => {
28
- if (hasData) {
29
- router.replace('/chat');
30
- } else {
31
- router.replace('/welcome');
32
- }
33
- });
34
- }, []);
31
+ // if user state not init, wait for loading
32
+ if (!isUserStateInit) return;
33
+
34
+ // user need to onboard
35
+ if (!isOnboard) {
36
+ router.replace('/onboard');
37
+ return;
38
+ }
39
+
40
+ // finally check the conversation status
41
+ if (isUserHasConversation) {
42
+ router.replace('/chat');
43
+ } else {
44
+ router.replace('/welcome');
45
+ }
46
+ }, [isUserStateInit, isLoaded, isUserHasConversation, isOnboard, isLogin]);
35
47
 
36
48
  return null;
37
49
  });
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { PropsWithChildren, memo } from 'react';
4
+ import { createStoreUpdater } from 'zustand-utils';
5
+
6
+ import { useUserStore } from '@/store/user';
7
+
8
+ const NoAuthProvider = memo<PropsWithChildren>(({ children }) => {
9
+ const useStoreUpdater = createStoreUpdater(useUserStore);
10
+
11
+ useStoreUpdater('isLoaded', true);
12
+
13
+ return children;
14
+ });
15
+
16
+ export default NoAuthProvider;
@@ -4,13 +4,14 @@ import { authEnv } from '@/config/auth';
4
4
 
5
5
  import Clerk from './Clerk';
6
6
  import NextAuth from './NextAuth';
7
+ import NoAuth from './NoAuth';
7
8
 
8
9
  const AuthProvider = ({ children }: PropsWithChildren) => {
9
10
  if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH) return <Clerk>{children}</Clerk>;
10
11
 
11
12
  if (authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH) return <NextAuth>{children}</NextAuth>;
12
13
 
13
- return children;
14
+ return <NoAuth>{children}</NoAuth>;
14
15
  };
15
16
 
16
17
  export default AuthProvider;
@@ -1,6 +1,7 @@
1
1
  import dynamic from 'next/dynamic';
2
- import { cookies } from 'next/headers';
3
- import { FC, ReactNode } from 'react';
2
+ import { cookies, headers } from 'next/headers';
3
+ import { FC, PropsWithChildren } from 'react';
4
+ import { resolveAcceptLanguage } from 'resolve-accept-language';
4
5
 
5
6
  import { getDebugConfig } from '@/config/debug';
6
7
  import { getServerFeatureFlagsValue } from '@/config/featureFlags';
@@ -10,6 +11,7 @@ import {
10
11
  LOBE_THEME_NEUTRAL_COLOR,
11
12
  LOBE_THEME_PRIMARY_COLOR,
12
13
  } from '@/const/theme';
14
+ import { locales } from '@/locales/resources';
13
15
  import { getServerGlobalConfig } from '@/server/globalConfig';
14
16
  import { ServerConfigStoreProvider } from '@/store/serverConfig';
15
17
  import { getAntdLocale } from '@/utils/locale';
@@ -31,11 +33,27 @@ if (process.env.NODE_ENV === 'development') {
31
33
  }
32
34
  }
33
35
 
34
- interface GlobalLayoutProps {
35
- children: ReactNode;
36
- }
36
+ const parserFallbackLang = () => {
37
+ /**
38
+ * The arguments are as follows:
39
+ *
40
+ * 1) The HTTP accept-language header.
41
+ * 2) The available locales (they must contain the default locale).
42
+ * 3) The default locale.
43
+ */
44
+ let fallbackLang: string = resolveAcceptLanguage(
45
+ headers().get('accept-language') || '',
46
+ // Invalid locale identifier 'ar'. A valid locale should follow the BCP 47 'language-country' format.
47
+ locales.map((locale) => (locale === 'ar' ? 'ar-EG' : locale)),
48
+ 'en-US',
49
+ );
50
+ // if match the ar-EG then fallback to ar
51
+ if (fallbackLang === 'ar-EG') fallbackLang = 'ar';
52
+
53
+ return fallbackLang;
54
+ };
37
55
 
38
- const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
56
+ const GlobalLayout = async ({ children }: PropsWithChildren) => {
39
57
  // get default theme config to use with ssr
40
58
  const cookieStore = cookies();
41
59
  const appearance = cookieStore.get(LOBE_THEME_APPEARANCE);
@@ -44,7 +62,13 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
44
62
 
45
63
  // get default locale config to use with ssr
46
64
  const defaultLang = cookieStore.get(LOBE_LOCALE_COOKIE);
47
- const antdLocale = await getAntdLocale(defaultLang?.value);
65
+ const fallbackLang = parserFallbackLang();
66
+
67
+ // if it's a new user, there's no cookie
68
+ // So we need to use the fallback language parsed by accept-language
69
+ const userLocale = defaultLang?.value || fallbackLang;
70
+
71
+ const antdLocale = await getAntdLocale(userLocale);
48
72
 
49
73
  // get default feature flags to use with ssr
50
74
  const serverFeatureFlags = getServerFeatureFlagsValue();
@@ -52,7 +76,7 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
52
76
  const isMobile = isMobileDevice();
53
77
  return (
54
78
  <StyleRegistry>
55
- <Locale antdLocale={antdLocale} defaultLang={defaultLang?.value}>
79
+ <Locale antdLocale={antdLocale} defaultLang={userLocale}>
56
80
  <AppTheme
57
81
  defaultAppearance={appearance?.value}
58
82
  defaultNeutralColor={neutralColor?.value as any}
package/src/middleware.ts CHANGED
@@ -41,16 +41,7 @@ const nextAuthMiddleware = auth((req) => {
41
41
  });
42
42
 
43
43
  export default authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH
44
- ? // can't lift to a function because if there is no clerk public key, it will throw error
45
- clerkMiddleware((auth, request) => {
46
- // if user is logged in and on the home page, redirect to chat
47
- if (auth().userId && request.nextUrl.pathname === '/') {
48
- request.nextUrl.pathname = '/chat';
49
- return NextResponse.redirect(request.nextUrl);
50
- }
51
-
52
- return NextResponse.next();
53
- })
44
+ ? clerkMiddleware()
54
45
  : authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH
55
46
  ? nextAuthMiddleware
56
47
  : defaultMiddleware;
@@ -46,6 +46,8 @@ describe('ClientService', () => {
46
46
  expect(userState).toEqual({
47
47
  avatar: mockUser.avatar,
48
48
  isOnboard: true,
49
+ canEnablePWAGuide: false,
50
+ hasConversation: false,
49
51
  canEnableTrace: false,
50
52
  preference: mockPreference,
51
53
  settings: mockUser.settings,
@@ -1,6 +1,7 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
3
  import { MessageModel } from '@/database/client/models/message';
4
+ import { SessionModel } from '@/database/client/models/session';
4
5
  import { UserModel } from '@/database/client/models/user';
5
6
  import { GlobalSettings } from '@/types/settings';
6
7
  import { UserInitializationState, UserPreference } from '@/types/user';
@@ -18,10 +19,13 @@ export class ClientService implements IUserService {
18
19
  async getUserState(): Promise<UserInitializationState> {
19
20
  const user = await UserModel.getUser();
20
21
  const messageCount = await MessageModel.count();
22
+ const sessionCount = await SessionModel.count();
21
23
 
22
24
  return {
23
25
  avatar: user.avatar,
26
+ canEnablePWAGuide: messageCount >= 2,
24
27
  canEnableTrace: messageCount >= 4,
28
+ hasConversation: messageCount > 0 || sessionCount > 0,
25
29
  isOnboard: true,
26
30
  preference: await this.preferenceStorage.getFromLocalStorage(),
27
31
  settings: user.settings as GlobalSettings,
@@ -6,7 +6,6 @@ import { UserInitializationState, UserPreference } from '@/types/user';
6
6
  export interface IUserService {
7
7
  getUserState: () => Promise<UserInitializationState>;
8
8
  resetUserSettings: () => Promise<any>;
9
- updateAvatar: (avatar: string) => Promise<any>;
10
9
  updatePreference: (preference: UserPreference) => Promise<any>;
11
10
  updateUserSettings: (patch: DeepPartial<GlobalSettings>) => Promise<any>;
12
11
  }
@@ -41,6 +41,7 @@ const isLogin = (s: UserStore) => {
41
41
  };
42
42
 
43
43
  export const authSelectors = {
44
+ isLoaded: (s: UserStore) => s.isLoaded,
44
45
  isLogin,
45
46
  isLoginWithAuth: (s: UserStore) => s.isSignedIn,
46
47
  isLoginWithClerk: (s: UserStore): boolean => (s.isSignedIn && enableClerk) || false,
@@ -5,6 +5,7 @@ import { withSWR } from '~test-utils';
5
5
 
6
6
  import { DEFAULT_PREFERENCE } from '@/const/user';
7
7
  import { userService } from '@/services/user';
8
+ import { ClientService } from '@/services/user/client';
8
9
  import { useUserStore } from '@/store/user';
9
10
  import { preferenceSelectors } from '@/store/user/selectors';
10
11
  import { GlobalServerConfig } from '@/types/serverConfig';
@@ -36,7 +37,7 @@ describe('createCommonSlice', () => {
36
37
  const avatar = 'new-avatar';
37
38
 
38
39
  const spyOn = vi.spyOn(result.current, 'refreshUserState');
39
- const updateAvatarSpy = vi.spyOn(userService, 'updateAvatar');
40
+ const updateAvatarSpy = vi.spyOn(ClientService.prototype, 'updateAvatar');
40
41
 
41
42
  await act(async () => {
42
43
  await result.current.updateAvatar(avatar);
@@ -4,6 +4,7 @@ import type { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { DEFAULT_PREFERENCE } from '@/const/user';
6
6
  import { userService } from '@/services/user';
7
+ import { ClientService } from '@/services/user/client';
7
8
  import type { UserStore } from '@/store/user';
8
9
  import type { GlobalServerConfig } from '@/types/serverConfig';
9
10
  import type { GlobalSettings } from '@/types/settings';
@@ -45,7 +46,9 @@ export const createCommonSlice: StateCreator<
45
46
  await mutate(GET_USER_STATE_KEY);
46
47
  },
47
48
  updateAvatar: async (avatar) => {
48
- await userService.updateAvatar(avatar);
49
+ const clientService = new ClientService();
50
+
51
+ await clientService.updateAvatar(avatar);
49
52
  await get().refreshUserState();
50
53
  },
51
54
 
@@ -89,8 +92,12 @@ export const createCommonSlice: StateCreator<
89
92
  {
90
93
  defaultSettings,
91
94
  enabledNextAuth: serverConfig.enabledOAuthSSO,
95
+ isOnboard: data.isOnboard,
96
+ isShowPWAGuide: data.canEnablePWAGuide,
92
97
  isUserCanEnableTrace: data.canEnableTrace,
98
+ isUserHasConversation: data.hasConversation,
93
99
  isUserStateInit: true,
100
+
94
101
  preference,
95
102
  serverLanguageModel: serverConfig.languageModel,
96
103
  settings: data.settings || {},
@@ -1,9 +1,15 @@
1
1
  export interface CommonState {
2
+ isOnboard: boolean;
3
+ isShowPWAGuide: boolean;
2
4
  isUserCanEnableTrace: boolean;
5
+ isUserHasConversation: boolean;
3
6
  isUserStateInit: boolean;
4
7
  }
5
8
 
6
9
  export const initialCommonState: CommonState = {
10
+ isOnboard: false,
11
+ isShowPWAGuide: false,
7
12
  isUserCanEnableTrace: false,
13
+ isUserHasConversation: false,
8
14
  isUserStateInit: false,
9
15
  };
@@ -34,7 +34,9 @@ export interface UserPreference {
34
34
 
35
35
  export interface UserInitializationState {
36
36
  avatar?: string;
37
+ canEnablePWAGuide?: boolean;
37
38
  canEnableTrace?: boolean;
39
+ hasConversation?: boolean;
38
40
  isOnboard?: boolean;
39
41
  preference: UserPreference;
40
42
  settings: DeepPartial<GlobalSettings>;