From 3957ac2f46f02244a5ef6acb45192c8bd4ba26a8 Mon Sep 17 00:00:00 2001 From: lds Date: Mon, 10 Feb 2025 20:26:57 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Update=20`docs/sel?= =?UTF-8?q?f-hosting/environment-variables/auth.zh-CN.mdx`=20(#5976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/self-hosting/environment-variables/auth.zh-CN.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/self-hosting/environment-variables/auth.zh-CN.mdx b/docs/self-hosting/environment-variables/auth.zh-CN.mdx index 3aa77411dae5e..bf54c62ba08fc 100644 --- a/docs/self-hosting/environment-variables/auth.zh-CN.mdx +++ b/docs/self-hosting/environment-variables/auth.zh-CN.mdx @@ -47,7 +47,7 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相 - 默认值: `-` - 示例: `evCnOJP1UX8FMnXR9Xkj5t0NyFn5p70P` -#### `AUTH_AUTH_SECRET` +#### `AUTH_AUTH0_SECRET` - 类型:必选 - 描述: Auth0 应用程序的 Client Secret @@ -280,4 +280,4 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相 - 类型:必选 - 描述: Clerk 应用程序的 Secret key。您可以在[这里](https://dashboard.clerk.com)访问,并导航到 API Keys 以查看。 - 默认值:`-` -- 示例: `sk_test_513Ma0P7IAWM1XMv4waxZjRYRajWTaCfJLjpEO3SD2` (测试环境) / `sk_live_eMMlHjwJvZFUfczFljSKqZdwQtLvmczmsJSNmdrpeZ`(生产环境) +- 示例: `sk_test_513Ma0P7IAWM1XMv4waxZjRYRajWTaCfJLjpEO3SD2` (测试环境) / `sk_live_eMMlHjwJvZFUfczFljSKqZdwQtLvmczmsJSNmdrpeZ`(生产环境) \ No newline at end of file From 91912cf725f3616e8077c616d9723ff82066679e Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Mon, 10 Feb 2025 20:58:20 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20language=20in?= =?UTF-8?q?correct=20on=20page=20hydration=20(#5970)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix language issue * refactor the language part * fix tests --- .../InboxWelcome/AgentsSuggest.tsx | 6 +- .../SessionListContent/DefaultMode.tsx | 2 +- .../SubmitAgentButton/SubmitAgentModal.tsx | 6 +- .../settings/common/features/Theme/index.tsx | 9 +- src/chains/__tests__/summaryAgentName.test.ts | 4 +- .../__tests__/summaryDescription.test.ts | 4 +- src/chains/__tests__/summaryHistory.test.ts | 2 +- src/chains/__tests__/summaryTags.test.ts | 4 +- src/chains/__tests__/summaryTitle.test.ts | 4 +- src/chains/summaryAgentName.ts | 2 +- src/chains/summaryDescription.ts | 2 +- src/chains/summaryTags.ts | 2 +- src/chains/summaryTitle.ts | 2 +- src/const/settings/common.ts | 1 - .../AgentMeta/AutoGenerateAvatar.tsx | 6 +- src/features/AgentSetting/AgentTTS/index.tsx | 8 +- src/features/ChatInput/STT/browser.tsx | 6 +- src/features/ChatInput/STT/openai.tsx | 6 +- .../Conversation/Extras/TTS/index.tsx | 6 +- src/features/PluginDevModal/LocalForm.tsx | 6 +- src/features/User/UserPanel/LangButton.tsx | 18 ++-- src/hooks/useTTS.ts | 6 +- src/layout/GlobalProvider/AppTheme.tsx | 2 +- src/middleware.ts | 7 +- src/services/__tests__/assistant.test.ts | 4 +- src/services/__tests__/tool.test.ts | 4 +- src/services/assistant.ts | 2 +- src/services/tool.ts | 2 +- .../global/{action.ts => actions/general.ts} | 73 +++------------- src/store/global/actions/workspacePane.ts | 73 ++++++++++++++++ src/store/global/helpers.ts | 6 ++ src/store/global/initialState.ts | 2 + src/store/global/selectors/general.test.ts | 18 ++++ src/store/global/selectors/general.ts | 25 ++++++ src/store/global/selectors/index.ts | 2 + .../systemStatus.ts} | 4 +- src/store/global/store.ts | 14 ++- src/store/user/helpers.ts | 9 -- src/store/user/slices/common/action.test.ts | 18 +--- src/store/user/slices/common/action.ts | 8 -- src/store/user/slices/settings/action.ts | 8 -- .../slices/settings/selectors/general.test.ts | 14 --- .../user/slices/settings/selectors/general.ts | 19 ----- src/types/user/settings/general.ts | 3 - src/utils/client/cookie.test.ts | 85 +++++++++++++++++++ src/utils/client/cookie.ts | 22 +++++ src/utils/client/switchLang.ts | 5 ++ src/utils/cookie.ts | 10 --- 48 files changed, 336 insertions(+), 215 deletions(-) rename src/store/global/{action.ts => actions/general.ts} (51%) create mode 100644 src/store/global/actions/workspacePane.ts create mode 100644 src/store/global/helpers.ts create mode 100644 src/store/global/selectors/general.test.ts create mode 100644 src/store/global/selectors/general.ts create mode 100644 src/store/global/selectors/index.ts rename src/store/global/{selectors.ts => selectors/systemStatus.ts} (95%) delete mode 100644 src/store/user/helpers.ts create mode 100644 src/utils/client/cookie.test.ts create mode 100644 src/utils/client/cookie.ts delete mode 100644 src/utils/cookie.ts diff --git a/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx b/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx index 78fd1728bb0f6..f4422780c9ce2 100644 --- a/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +++ b/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx @@ -12,8 +12,8 @@ import useSWR from 'swr'; import urlJoin from 'url-join'; import { assistantService } from '@/services/assistant'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { DiscoverAssistantItem } from '@/types/discover'; const { Paragraph } = Typography; @@ -60,7 +60,7 @@ const useStyles = createStyles(({ css, token, responsive }) => ({ const AgentsSuggest = memo<{ mobile?: boolean }>(({ mobile }) => { const { t } = useTranslation('welcome'); - const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); const [sliceStart, setSliceStart] = useState(0); const { data: assistantList, isLoading } = useSWR( diff --git a/src/app/[variants]/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx b/src/app/[variants]/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx index aa37a8c104e8a..cdc4fe8a88898 100644 --- a/src/app/[variants]/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx +++ b/src/app/[variants]/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx @@ -68,7 +68,7 @@ const DefaultMode = memo(() => { label: t('defaultList'), }, ].filter(Boolean) as CollapseProps['items'], - [customSessionGroups, pinnedSessions, defaultSessions], + [t, customSessionGroups, pinnedSessions, defaultSessions], ); return ( diff --git a/src/app/[variants]/(main)/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx b/src/app/[variants]/(main)/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx index b3b1e8df75362..6bafb87044bb9 100644 --- a/src/app/[variants]/(main)/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +++ b/src/app/[variants]/(main)/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx @@ -14,10 +14,10 @@ import { AGENTS_INDEX_GITHUB_ISSUE } from '@/const/url'; import AgentInfo from '@/features/AgentInfo'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; import { sessionMetaSelectors } from '@/store/session/selectors'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; const SubmitAgentModal = memo(({ open, onCancel }) => { const { t } = useTranslation('setting'); @@ -25,7 +25,7 @@ const SubmitAgentModal = memo(({ open, onCancel }) => { const systemRole = useAgentStore(agentSelectors.currentAgentSystemRole); const theme = useTheme(); const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); - const language = useUserStore((s) => userGeneralSettingsSelectors.currentLanguage(s)); + const language = useGlobalStore(globalGeneralSelectors.currentLanguage); const isMetaPass = Boolean( meta && meta.title && meta.description && (meta.tags as string[])?.length > 0 && meta.avatar, diff --git a/src/app/[variants]/(main)/settings/common/features/Theme/index.tsx b/src/app/[variants]/(main)/settings/common/features/Theme/index.tsx index 10414f3b5d22d..9e93fe93cfb45 100644 --- a/src/app/[variants]/(main)/settings/common/features/Theme/index.tsx +++ b/src/app/[variants]/(main)/settings/common/features/Theme/index.tsx @@ -4,7 +4,6 @@ import { Form, type ItemGroup, SelectWithImg, SliderWithInput } from '@lobehub/u import { Select } from 'antd'; import isEqual from 'fast-deep-equal'; import { Monitor, Moon, Sun } from 'lucide-react'; -import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,9 +11,9 @@ import { useSyncSettings } from '@/app/[variants]/(main)/settings/hooks/useSyncS import { FORM_STYLE } from '@/const/layoutTokens'; import { imageUrl } from '@/const/url'; import { Locales, localeOptions } from '@/locales/resources'; +import { useGlobalStore } from '@/store/global'; import { useUserStore } from '@/store/user'; import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors'; -import { switchLang } from '@/utils/client/switchLang'; import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from './ThemeSwatches'; @@ -22,17 +21,17 @@ type SettingItemGroup = ItemGroup; const Theme = memo(() => { const { t } = useTranslation('setting'); - const router = useRouter(); + const [form] = Form.useForm(); const settings = useUserStore(settingsSelectors.currentSettings, isEqual); const themeMode = useUserStore(userGeneralSettingsSelectors.currentThemeMode); const [setThemeMode, setSettings] = useUserStore((s) => [s.switchThemeMode, s.setSettings]); useSyncSettings(form); + const [switchLocale] = useGlobalStore((s) => [s.switchLocale]); const handleLangChange = (value: Locales) => { - switchLang(value); - router.refresh(); + switchLocale(value); }; const theme: SettingItemGroup = { diff --git a/src/chains/__tests__/summaryAgentName.test.ts b/src/chains/__tests__/summaryAgentName.test.ts index 3c91662b77380..4e562e6c709b6 100644 --- a/src/chains/__tests__/summaryAgentName.test.ts +++ b/src/chains/__tests__/summaryAgentName.test.ts @@ -1,11 +1,11 @@ import { Mock, describe, expect, it } from 'vitest'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { chainSummaryAgentName } from '../summaryAgentName'; // Mock the getCurrentLanguage function -vi.mock('@/store/user/helpers', () => ({ +vi.mock('@/store/global/helpers', () => ({ globalHelpers: { getCurrentLanguage: vi.fn(), }, diff --git a/src/chains/__tests__/summaryDescription.test.ts b/src/chains/__tests__/summaryDescription.test.ts index 4fb16ae659881..20dfaea522396 100644 --- a/src/chains/__tests__/summaryDescription.test.ts +++ b/src/chains/__tests__/summaryDescription.test.ts @@ -1,11 +1,11 @@ import { Mock, describe, expect, it, vi } from 'vitest'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { chainSummaryDescription } from '../summaryDescription'; // Mock the globalHelpers.getCurrentLanguage function -vi.mock('@/store/user/helpers', () => ({ +vi.mock('@/store/global/helpers', () => ({ globalHelpers: { getCurrentLanguage: vi.fn(() => 'en-US'), }, diff --git a/src/chains/__tests__/summaryHistory.test.ts b/src/chains/__tests__/summaryHistory.test.ts index 51b2b16f8e0b1..157aaaf3e0edb 100644 --- a/src/chains/__tests__/summaryHistory.test.ts +++ b/src/chains/__tests__/summaryHistory.test.ts @@ -1,7 +1,7 @@ import { Mock, describe, expect, it, vi } from 'vitest'; import { chatHelpers } from '@/store/chat/helpers'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { ChatMessage } from '@/types/message'; import { OpenAIChatMessage } from '@/types/openai/chat'; diff --git a/src/chains/__tests__/summaryTags.test.ts b/src/chains/__tests__/summaryTags.test.ts index ac722456850a1..8b5b790076ff6 100644 --- a/src/chains/__tests__/summaryTags.test.ts +++ b/src/chains/__tests__/summaryTags.test.ts @@ -1,11 +1,11 @@ import { Mock, describe, expect, it } from 'vitest'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { chainSummaryTags } from '../summaryTags'; // Mock the getCurrentLanguage function -vi.mock('@/store/user/helpers', () => ({ +vi.mock('@/store/global/helpers', () => ({ globalHelpers: { getCurrentLanguage: vi.fn(), }, diff --git a/src/chains/__tests__/summaryTitle.test.ts b/src/chains/__tests__/summaryTitle.test.ts index aab96c940e7d8..e644b24c95b2a 100644 --- a/src/chains/__tests__/summaryTitle.test.ts +++ b/src/chains/__tests__/summaryTitle.test.ts @@ -1,13 +1,13 @@ import { Mock, describe, expect, it, vi } from 'vitest'; import { chatHelpers } from '@/store/chat/helpers'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { OpenAIChatMessage } from '@/types/openai/chat'; import { chainSummaryTitle } from '../summaryTitle'; // Mock the getCurrentLanguage function -vi.mock('@/store/user/helpers', () => ({ +vi.mock('@/store/global/helpers', () => ({ globalHelpers: { getCurrentLanguage: vi.fn(), }, diff --git a/src/chains/summaryAgentName.ts b/src/chains/summaryAgentName.ts index 19acedd4cf243..987b529747368 100644 --- a/src/chains/summaryAgentName.ts +++ b/src/chains/summaryAgentName.ts @@ -1,4 +1,4 @@ -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { ChatStreamPayload } from '@/types/openai/chat'; /** diff --git a/src/chains/summaryDescription.ts b/src/chains/summaryDescription.ts index 7cd3bb0d73f52..662de30d5c229 100644 --- a/src/chains/summaryDescription.ts +++ b/src/chains/summaryDescription.ts @@ -1,4 +1,4 @@ -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { ChatStreamPayload } from '@/types/openai/chat'; export const chainSummaryDescription = (content: string): Partial => ({ diff --git a/src/chains/summaryTags.ts b/src/chains/summaryTags.ts index bbe7833a65dd6..403418cb0d151 100644 --- a/src/chains/summaryTags.ts +++ b/src/chains/summaryTags.ts @@ -1,4 +1,4 @@ -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { ChatStreamPayload } from '@/types/openai/chat'; export const chainSummaryTags = (content: string): Partial => ({ diff --git a/src/chains/summaryTitle.ts b/src/chains/summaryTitle.ts index 8bd92c2128f2c..4e25cfb447874 100644 --- a/src/chains/summaryTitle.ts +++ b/src/chains/summaryTitle.ts @@ -1,4 +1,4 @@ -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat'; export const chainSummaryTitle = (messages: OpenAIChatMessage[]): Partial => { diff --git a/src/const/settings/common.ts b/src/const/settings/common.ts index f3644c0cdd250..1258b9fa2afcf 100644 --- a/src/const/settings/common.ts +++ b/src/const/settings/common.ts @@ -2,6 +2,5 @@ import { UserGeneralConfig } from '@/types/user/settings'; export const DEFAULT_COMMON_SETTINGS: UserGeneralConfig = { fontSize: 14, - language: 'auto', themeMode: 'auto', }; diff --git a/src/features/AgentSetting/AgentMeta/AutoGenerateAvatar.tsx b/src/features/AgentSetting/AgentMeta/AutoGenerateAvatar.tsx index c459bf2ede293..a12f8e1aa9972 100644 --- a/src/features/AgentSetting/AgentMeta/AutoGenerateAvatar.tsx +++ b/src/features/AgentSetting/AgentMeta/AutoGenerateAvatar.tsx @@ -6,8 +6,8 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false }); @@ -24,7 +24,7 @@ const AutoGenerateAvatar = memo( ({ loading, background, value, onChange, onGenerate, canAutoGenerate }) => { const { t } = useTranslation('common'); const theme = useTheme(); - const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); return ( diff --git a/src/features/AgentSetting/AgentTTS/index.tsx b/src/features/AgentSetting/AgentTTS/index.tsx index 9c7d3211d0fa2..ac8fea6f8332f 100644 --- a/src/features/AgentSetting/AgentTTS/index.tsx +++ b/src/features/AgentSetting/AgentTTS/index.tsx @@ -9,8 +9,8 @@ import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { FORM_STYLE } from '@/const/layoutTokens'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { useStore } from '../store'; import { useAgentSyncSettings } from '../useSyncAgemtSettings'; @@ -23,8 +23,8 @@ const { openaiVoiceOptions, localeOptions } = VoiceList; const AgentTTS = memo(() => { const { t } = useTranslation('setting'); const [form] = Form.useForm(); - const voiceList = useUserStore((s) => { - const locale = userGeneralSettingsSelectors.currentLanguage(s); + const voiceList = useGlobalStore((s) => { + const locale = globalGeneralSelectors.currentLanguage(s); return (all?: boolean) => new VoiceList(all ? undefined : locale); }); const [showAllLocaleVoice, ttsService, updateConfig] = useStore((s) => [ diff --git a/src/features/ChatInput/STT/browser.tsx b/src/features/ChatInput/STT/browser.tsx index 6419f98cfa0f2..5139d63ba5eb8 100644 --- a/src/features/ChatInput/STT/browser.tsx +++ b/src/features/ChatInput/STT/browser.tsx @@ -8,8 +8,10 @@ import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/slices/chat'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { useUserStore } from '@/store/user'; -import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { settingsSelectors } from '@/store/user/selectors'; import { ChatMessageError } from '@/types/message'; import { getMessageError } from '@/utils/fetch'; @@ -22,7 +24,7 @@ interface STTConfig extends SWRConfiguration { const useBrowserSTT = (config: STTConfig) => { const ttsSettings = useUserStore(settingsSelectors.currentTTS, isEqual); const ttsAgentSettings = useAgentStore(agentSelectors.currentAgentTTS, isEqual); - const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); const autoStop = ttsSettings.sttAutoStop; const sttLocale = diff --git a/src/features/ChatInput/STT/openai.tsx b/src/features/ChatInput/STT/openai.tsx index 660632ad2c6b6..a78b916cc9fea 100644 --- a/src/features/ChatInput/STT/openai.tsx +++ b/src/features/ChatInput/STT/openai.tsx @@ -11,8 +11,10 @@ import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/slices/message/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { useUserStore } from '@/store/user'; -import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { settingsSelectors } from '@/store/user/selectors'; import { ChatMessageError } from '@/types/message'; import { getMessageError } from '@/utils/fetch'; @@ -25,7 +27,7 @@ interface STTConfig extends SWRConfiguration { const useOpenaiSTT = (config: STTConfig) => { const ttsSettings = useUserStore(settingsSelectors.currentTTS, isEqual); const ttsAgentSettings = useAgentStore(agentSelectors.currentAgentTTS, isEqual); - const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); const autoStop = ttsSettings.sttAutoStop; const sttLocale = diff --git a/src/features/Conversation/Extras/TTS/index.tsx b/src/features/Conversation/Extras/TTS/index.tsx index 3e895164621a5..4542ddc76ed7a 100644 --- a/src/features/Conversation/Extras/TTS/index.tsx +++ b/src/features/Conversation/Extras/TTS/index.tsx @@ -3,8 +3,8 @@ import { Md5 } from 'ts-md5'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/slices/chat'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import FilePlayer from './FilePlayer'; import InitPlayer, { TTSProps } from './InitPlayer'; @@ -12,7 +12,7 @@ import InitPlayer, { TTSProps } from './InitPlayer'; const TTS = memo( (props) => { const { file, voice, content, contentMd5 } = props; - const lang = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const lang = useGlobalStore(globalGeneralSelectors.currentLanguage); const currentVoice = useAgentStore(agentSelectors.currentAgentTTSVoice(lang)); const md5 = useMemo(() => Md5.hashStr(content).toString(), [content]); diff --git a/src/features/PluginDevModal/LocalForm.tsx b/src/features/PluginDevModal/LocalForm.tsx index 1ad29ea62f4ae..a94e2966bd4fd 100644 --- a/src/features/PluginDevModal/LocalForm.tsx +++ b/src/features/PluginDevModal/LocalForm.tsx @@ -4,16 +4,16 @@ import dynamic from 'next/dynamic'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { useToolStore } from '@/store/tool'; import { pluginSelectors } from '@/store/tool/selectors'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false }); const LocalForm = memo<{ form: FormInstance; mode?: 'edit' | 'create' }>(({ form, mode }) => { const isEditMode = mode === 'edit'; - const locale = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const locale = useGlobalStore(globalGeneralSelectors.currentLanguage); const { t } = useTranslation('plugin'); const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList); diff --git a/src/features/User/UserPanel/LangButton.tsx b/src/features/User/UserPanel/LangButton.tsx index 1e380f8d951b7..b1935a4c25f25 100644 --- a/src/features/User/UserPanel/LangButton.tsx +++ b/src/features/User/UserPanel/LangButton.tsx @@ -7,16 +7,22 @@ import { useTranslation } from 'react-i18next'; import Menu, { type MenuProps } from '@/components/Menu'; import { localeOptions } from '@/locales/resources'; -import { useUserStore } from '@/store/user'; -import { userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; +import { LocaleMode } from '@/types/locale'; const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => { const theme = useTheme(); - const [language, switchLocale] = useUserStore((s) => [ - userGeneralSettingsSelectors.language(s), + + const [language, switchLocale] = useGlobalStore((s) => [ + globalGeneralSelectors.language(s), s.switchLocale, ]); + const handleLangChange = (value: LocaleMode) => { + switchLocale(value); + }; + const { t } = useTranslation('setting'); const items: MenuProps['items'] = useMemo( @@ -24,12 +30,12 @@ const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement { key: 'auto', label: t('settingTheme.lang.autoMode'), - onClick: () => switchLocale('auto'), + onClick: () => handleLangChange('auto'), }, ...localeOptions.map((item) => ({ key: item.value, label: item.label, - onClick: () => switchLocale(item.value), + onClick: () => handleLangChange(item.value), })), ], [t], diff --git a/src/hooks/useTTS.ts b/src/hooks/useTTS.ts index b864a094cb0d5..fcc9c906c89a6 100644 --- a/src/hooks/useTTS.ts +++ b/src/hooks/useTTS.ts @@ -13,8 +13,10 @@ import { createHeaderWithOpenAI } from '@/services/_header'; import { API_ENDPOINTS } from '@/services/_url'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/slices/chat'; +import { useGlobalStore } from '@/store/global'; +import { globalGeneralSelectors } from '@/store/global/selectors'; import { useUserStore } from '@/store/user'; -import { settingsSelectors, userGeneralSettingsSelectors } from '@/store/user/selectors'; +import { settingsSelectors } from '@/store/user/selectors'; import { TTSServer } from '@/types/agent'; interface TTSConfig extends TTSOptions { @@ -26,7 +28,7 @@ interface TTSConfig extends TTSOptions { export const useTTS = (content: string, config?: TTSConfig) => { const ttsSettings = useUserStore(settingsSelectors.currentTTS, isEqual); const ttsAgentSettings = useAgentStore(agentSelectors.currentAgentTTS, isEqual); - const lang = useUserStore(userGeneralSettingsSelectors.currentLanguage); + const lang = useGlobalStore(globalGeneralSelectors.currentLanguage); const voice = useAgentStore(agentSelectors.currentAgentTTSVoice(lang)); let useSelectedTTS; let options: any = {}; diff --git a/src/layout/GlobalProvider/AppTheme.tsx b/src/layout/GlobalProvider/AppTheme.tsx index 36c4c4268aca2..70c0b960634a6 100644 --- a/src/layout/GlobalProvider/AppTheme.tsx +++ b/src/layout/GlobalProvider/AppTheme.tsx @@ -22,7 +22,7 @@ import { import { useUserStore } from '@/store/user'; import { userGeneralSettingsSelectors } from '@/store/user/selectors'; import { GlobalStyle } from '@/styles'; -import { setCookie } from '@/utils/cookie'; +import { setCookie } from '@/utils/client/cookie'; const useStyles = createStyles(({ css, token }) => ({ app: css` diff --git a/src/middleware.ts b/src/middleware.ts index 4d7020d0d1a96..97e2c271e73e9 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,6 +5,7 @@ import urlJoin from 'url-join'; import { appEnv } from '@/config/app'; import { authEnv } from '@/config/auth'; +import { LOBE_LOCALE_COOKIE } from '@/const/locale'; import { LOBE_THEME_APPEARANCE } from '@/const/theme'; import NextAuthEdge from '@/libs/next-auth/edge'; import { Locales } from '@/locales/resources'; @@ -54,10 +55,8 @@ const defaultMiddleware = (request: NextRequest) => { // if it's a new user, there's no cookie // So we need to use the fallback language parsed by accept-language - const locale = parseBrowserLanguage(request.headers) as Locales; - // const locale = - // request.cookies.get(LOBE_LOCALE_COOKIE)?.value || - // browserLanguage; + const browserLanguage = parseBrowserLanguage(request.headers); + const locale = (request.cookies.get(LOBE_LOCALE_COOKIE)?.value || browserLanguage) as Locales; const ua = request.headers.get('user-agent'); diff --git a/src/services/__tests__/assistant.test.ts b/src/services/__tests__/assistant.test.ts index 6c7eaaaf95f0f..7642e5fe770bd 100644 --- a/src/services/__tests__/assistant.test.ts +++ b/src/services/__tests__/assistant.test.ts @@ -1,12 +1,12 @@ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { assistantService } from '../assistant'; // Mocking modules and functions -vi.mock('@/store/user/helpers', () => ({ +vi.mock('@/store/global/helpers', () => ({ globalHelpers: { getCurrentLanguage: vi.fn(), }, diff --git a/src/services/__tests__/tool.test.ts b/src/services/__tests__/tool.test.ts index d52e2106e5454..0f5a4599619f4 100644 --- a/src/services/__tests__/tool.test.ts +++ b/src/services/__tests__/tool.test.ts @@ -1,6 +1,6 @@ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { toolService } from '../tool'; import openAPIV3 from './openai/OpenAPI_V3.json'; @@ -8,7 +8,7 @@ import OpenAIPlugin from './openai/plugin.json'; // Mocking modules and functions -vi.mock('@/store/user/helpers', () => ({ +vi.mock('@/store/global/helpers', () => ({ globalHelpers: { getCurrentLanguage: vi.fn(), }, diff --git a/src/services/assistant.ts b/src/services/assistant.ts index 2d7fe0956d93f..c46a7bc4ffb6f 100644 --- a/src/services/assistant.ts +++ b/src/services/assistant.ts @@ -1,7 +1,7 @@ import { cloneDeep, merge } from 'lodash-es'; import { DEFAULT_DISCOVER_ASSISTANT_ITEM } from '@/const/discover'; -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { DiscoverAssistantItem } from '@/types/discover'; import { API_ENDPOINTS } from './_url'; diff --git a/src/services/tool.ts b/src/services/tool.ts index b7e4352728ca4..316ef14239cbb 100644 --- a/src/services/tool.ts +++ b/src/services/tool.ts @@ -1,4 +1,4 @@ -import { globalHelpers } from '@/store/user/helpers'; +import { globalHelpers } from '@/store/global/helpers'; import { DiscoverPlugintem } from '@/types/discover'; import { convertOpenAIManifestToLobeManifest, getToolManifest } from '@/utils/toolManifest'; diff --git a/src/store/global/action.ts b/src/store/global/actions/general.ts similarity index 51% rename from src/store/global/action.ts rename to src/store/global/actions/general.ts index 721cf54f5d2e0..ab8514988e1c2 100644 --- a/src/store/global/action.ts +++ b/src/store/global/actions/general.ts @@ -1,89 +1,38 @@ import isEqual from 'fast-deep-equal'; -import { produce } from 'immer'; import { gt, parse, valid } from 'semver'; import { SWRResponse } from 'swr'; import type { StateCreator } from 'zustand/vanilla'; -import { INBOX_SESSION_ID } from '@/const/session'; -import { SESSION_CHAT_URL } from '@/const/url'; import { CURRENT_VERSION } from '@/const/version'; import { useOnlyFetchOnceSWR } from '@/libs/swr'; import { globalService } from '@/services/global'; -import type { GlobalStore } from '@/store/global/index'; +import type { SystemStatus } from '@/store/global/initialState'; +import { LocaleMode } from '@/types/locale'; +import { switchLang } from '@/utils/client/switchLang'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; -import type { SystemStatus } from './initialState'; +import type { GlobalStore } from '../store'; const n = setNamespace('g'); -/** - * 设置操作 - */ -export interface GlobalStoreAction { - switchBackToChat: (sessionId?: string) => void; - toggleChatSideBar: (visible?: boolean) => void; - toggleExpandSessionGroup: (id: string, expand: boolean) => void; - toggleMobilePortal: (visible?: boolean) => void; - toggleMobileTopic: (visible?: boolean) => void; - toggleSystemRole: (visible?: boolean) => void; - toggleZenMode: () => void; +export interface GlobalGeneralAction { + switchLocale: (locale: LocaleMode) => void; updateSystemStatus: (status: Partial, action?: any) => void; useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse; useInitSystemStatus: () => SWRResponse; } -export const globalActionSlice: StateCreator< +export const generalActionSlice: StateCreator< GlobalStore, [['zustand/devtools', never]], [], - GlobalStoreAction + GlobalGeneralAction > = (set, get) => ({ - switchBackToChat: (sessionId) => { - get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile)); - }, - - toggleChatSideBar: (newValue) => { - const showChatSideBar = - typeof newValue === 'boolean' ? newValue : !get().status.showChatSideBar; - - get().updateSystemStatus({ showChatSideBar }, n('toggleAgentPanel', newValue)); - }, - toggleExpandSessionGroup: (id, expand) => { - const { status } = get(); - const nextExpandSessionGroup = produce(status.expandSessionGroupKeys, (draft: string[]) => { - if (expand) { - if (draft.includes(id)) return; - draft.push(id); - } else { - const index = draft.indexOf(id); - if (index !== -1) draft.splice(index, 1); - } - }); - get().updateSystemStatus({ expandSessionGroupKeys: nextExpandSessionGroup }); - }, - toggleMobilePortal: (newValue) => { - const mobileShowPortal = - typeof newValue === 'boolean' ? newValue : !get().status.mobileShowPortal; - - get().updateSystemStatus({ mobileShowPortal }, n('toggleMobilePortal', newValue)); - }, - toggleMobileTopic: (newValue) => { - const mobileShowTopic = - typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic; - - get().updateSystemStatus({ mobileShowTopic }, n('toggleMobileTopic', newValue)); - }, - toggleSystemRole: (newValue) => { - const showSystemRole = typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic; - - get().updateSystemStatus({ showSystemRole }, n('toggleMobileTopic', newValue)); - }, - toggleZenMode: () => { - const { status } = get(); - const nextZenMode = !status.zenMode; + switchLocale: (locale) => { + get().updateSystemStatus({ language: locale }); - get().updateSystemStatus({ zenMode: nextZenMode }, n('toggleZenMode')); + switchLang(locale); }, updateSystemStatus: (status, action) => { // Status cannot be modified when it is not initialized diff --git a/src/store/global/actions/workspacePane.ts b/src/store/global/actions/workspacePane.ts new file mode 100644 index 0000000000000..5e329099a3716 --- /dev/null +++ b/src/store/global/actions/workspacePane.ts @@ -0,0 +1,73 @@ +import { produce } from 'immer'; +import type { StateCreator } from 'zustand/vanilla'; + +import { INBOX_SESSION_ID } from '@/const/session'; +import { SESSION_CHAT_URL } from '@/const/url'; +import type { GlobalStore } from '@/store/global'; +import { setNamespace } from '@/utils/storeDebug'; + +const n = setNamespace('w'); + +export interface GlobalWorkspacePaneAction { + switchBackToChat: (sessionId?: string) => void; + toggleChatSideBar: (visible?: boolean) => void; + toggleExpandSessionGroup: (id: string, expand: boolean) => void; + toggleMobilePortal: (visible?: boolean) => void; + toggleMobileTopic: (visible?: boolean) => void; + toggleSystemRole: (visible?: boolean) => void; + toggleZenMode: () => void; +} + +export const globalWorkspaceSlice: StateCreator< + GlobalStore, + [['zustand/devtools', never]], + [], + GlobalWorkspacePaneAction +> = (set, get) => ({ + switchBackToChat: (sessionId) => { + get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile)); + }, + + toggleChatSideBar: (newValue) => { + const showChatSideBar = + typeof newValue === 'boolean' ? newValue : !get().status.showChatSideBar; + + get().updateSystemStatus({ showChatSideBar }, n('toggleAgentPanel', newValue)); + }, + toggleExpandSessionGroup: (id, expand) => { + const { status } = get(); + const nextExpandSessionGroup = produce(status.expandSessionGroupKeys, (draft: string[]) => { + if (expand) { + if (draft.includes(id)) return; + draft.push(id); + } else { + const index = draft.indexOf(id); + if (index !== -1) draft.splice(index, 1); + } + }); + get().updateSystemStatus({ expandSessionGroupKeys: nextExpandSessionGroup }); + }, + toggleMobilePortal: (newValue) => { + const mobileShowPortal = + typeof newValue === 'boolean' ? newValue : !get().status.mobileShowPortal; + + get().updateSystemStatus({ mobileShowPortal }, n('toggleMobilePortal', newValue)); + }, + toggleMobileTopic: (newValue) => { + const mobileShowTopic = + typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic; + + get().updateSystemStatus({ mobileShowTopic }, n('toggleMobileTopic', newValue)); + }, + toggleSystemRole: (newValue) => { + const showSystemRole = typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic; + + get().updateSystemStatus({ showSystemRole }, n('toggleMobileTopic', newValue)); + }, + toggleZenMode: () => { + const { status } = get(); + const nextZenMode = !status.zenMode; + + get().updateSystemStatus({ zenMode: nextZenMode }, n('toggleZenMode')); + }, +}); diff --git a/src/store/global/helpers.ts b/src/store/global/helpers.ts new file mode 100644 index 0000000000000..f4bd54421e321 --- /dev/null +++ b/src/store/global/helpers.ts @@ -0,0 +1,6 @@ +import { useGlobalStore } from '@/store/global/index'; +import { globalGeneralSelectors } from '@/store/global/selectors'; + +const getCurrentLanguage = () => globalGeneralSelectors.currentLanguage(useGlobalStore.getState()); + +export const globalHelpers = { getCurrentLanguage }; diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 11e67eb7c6d50..2e04535fafa1b 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -1,6 +1,7 @@ import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; import { DatabaseLoadingState } from '@/types/clientDB'; +import { LocaleMode } from '@/types/locale'; import { SessionDefaultGroup } from '@/types/session'; import { AsyncLocalStorage } from '@/utils/localStorage'; @@ -49,6 +50,7 @@ export interface SystemStatus { * 应用初始化时不启用 PGLite,只有当用户手动开启时才启用 */ isEnablePglite?: boolean; + language?: LocaleMode; latestChangelogId?: string; mobileShowPortal?: boolean; mobileShowTopic?: boolean; diff --git a/src/store/global/selectors/general.test.ts b/src/store/global/selectors/general.test.ts new file mode 100644 index 0000000000000..982ede0c4ceb5 --- /dev/null +++ b/src/store/global/selectors/general.test.ts @@ -0,0 +1,18 @@ +import { GlobalState, initialState } from '@/store/global/initialState'; +import { merge } from '@/utils/merge'; + +import { globalGeneralSelectors } from './general'; + +describe('settingsSelectors', () => { + describe('currentLanguage', () => { + it('should return the correct language setting', () => { + const s: GlobalState = merge(initialState, { + status: { language: 'fr' }, + }); + + const result = globalGeneralSelectors.currentLanguage(s); + + expect(result).toBe('fr'); + }); + }); +}); diff --git a/src/store/global/selectors/general.ts b/src/store/global/selectors/general.ts new file mode 100644 index 0000000000000..f37cc309fe61c --- /dev/null +++ b/src/store/global/selectors/general.ts @@ -0,0 +1,25 @@ +import { DEFAULT_LANG } from '@/const/locale'; +import { Locales } from '@/locales/resources'; +import { isOnServerSide } from '@/utils/env'; + +import { GlobalState } from '../initialState'; +import { systemStatus } from './systemStatus'; + +const language = (s: GlobalState) => systemStatus(s).language || 'auto'; + +const currentLanguage = (s: GlobalState) => { + const locale = language(s); + + if (locale === 'auto') { + if (isOnServerSide) return DEFAULT_LANG; + + return navigator.language as Locales; + } + + return locale as Locales; +}; + +export const globalGeneralSelectors = { + currentLanguage, + language, +}; diff --git a/src/store/global/selectors/index.ts b/src/store/global/selectors/index.ts new file mode 100644 index 0000000000000..552edde7b419a --- /dev/null +++ b/src/store/global/selectors/index.ts @@ -0,0 +1,2 @@ +export * from './general'; +export * from './systemStatus'; diff --git a/src/store/global/selectors.ts b/src/store/global/selectors/systemStatus.ts similarity index 95% rename from src/store/global/selectors.ts rename to src/store/global/selectors/systemStatus.ts index 0f75cbda4625f..491db7c504230 100644 --- a/src/store/global/selectors.ts +++ b/src/store/global/selectors/systemStatus.ts @@ -2,9 +2,9 @@ import { isServerMode, isUsePgliteDB } from '@/const/version'; import { GlobalStore } from '@/store/global'; import { DatabaseLoadingState } from '@/types/clientDB'; -import { INITIAL_STATUS } from './initialState'; +import { GlobalState, INITIAL_STATUS } from '../initialState'; -const systemStatus = (s: GlobalStore) => s.status; +export const systemStatus = (s: GlobalState) => s.status; const sessionGroupKeys = (s: GlobalStore): string[] => s.status.expandSessionGroupKeys || INITIAL_STATUS.expandSessionGroupKeys; diff --git a/src/store/global/store.ts b/src/store/global/store.ts index ed3f28e9389a9..3a910138b8165 100644 --- a/src/store/global/store.ts +++ b/src/store/global/store.ts @@ -4,18 +4,26 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { StateCreator } from 'zustand/vanilla'; import { createDevtools } from '../middleware/createDevtools'; -import { type GlobalStoreAction, globalActionSlice } from './action'; import { type GlobalClientDBAction, clientDBSlice } from './actions/clientDb'; +import { type GlobalGeneralAction, generalActionSlice } from './actions/general'; +import { type GlobalWorkspacePaneAction, globalWorkspaceSlice } from './actions/workspacePane'; import { type GlobalState, initialState } from './initialState'; // =============== 聚合 createStoreFn ============ // -export type GlobalStore = GlobalState & GlobalStoreAction & GlobalClientDBAction; +export interface GlobalStore + extends GlobalState, + GlobalWorkspacePaneAction, + GlobalClientDBAction, + GlobalGeneralAction { + /* empty */ +} const createStore: StateCreator = (...parameters) => ({ ...initialState, - ...globalActionSlice(...parameters), + ...globalWorkspaceSlice(...parameters), ...clientDBSlice(...parameters), + ...generalActionSlice(...parameters), }); // =============== 实装 useStore ============ // diff --git a/src/store/user/helpers.ts b/src/store/user/helpers.ts deleted file mode 100644 index b1065856f4fc1..0000000000000 --- a/src/store/user/helpers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { userGeneralSettingsSelectors } from './slices/settings/selectors'; -import { useUserStore } from './store'; - -const getCurrentLanguage = () => - userGeneralSettingsSelectors.currentLanguage(useUserStore.getState()); - -export const globalHelpers = { - getCurrentLanguage, -}; diff --git a/src/store/user/slices/common/action.test.ts b/src/store/user/slices/common/action.test.ts index 0130c88ebea1e..e7b308096290f 100644 --- a/src/store/user/slices/common/action.test.ts +++ b/src/store/user/slices/common/action.test.ts @@ -1,5 +1,4 @@ import { act, renderHook, waitFor } from '@testing-library/react'; -import { mutate } from 'swr'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { withSWR } from '~test-utils'; @@ -10,14 +9,9 @@ import { useUserStore } from '@/store/user'; import { preferenceSelectors } from '@/store/user/selectors'; import { GlobalServerConfig } from '@/types/serverConfig'; import { UserInitializationState, UserPreference } from '@/types/user'; -import { switchLang } from '@/utils/client/switchLang'; vi.mock('zustand/traditional'); -vi.mock('@/utils/client/switchLang', () => ({ - switchLang: vi.fn(), -})); - vi.mock('swr', async (importOriginal) => { const modules = await importOriginal(); return { @@ -87,7 +81,7 @@ describe('createCommonSlice', () => { telemetry: true, }, settings: { - general: { language: 'en-US' }, + general: { fontSize: 14 }, }, }; @@ -111,9 +105,6 @@ describe('createCommonSlice', () => { expect(useUserStore.getState().user?.avatar).toBe(mockUserState.avatar); expect(useUserStore.getState().settings).toEqual(mockUserState.settings); expect(successCallback).toHaveBeenCalledWith(mockUserState); - - // 验证是否正确处理了语言设置 - expect(switchLang).not.toHaveBeenCalledWith('auto'); }); it('should call switch language when language is auto', async () => { @@ -134,9 +125,6 @@ describe('createCommonSlice', () => { // 等待 SWR 完成数据获取 await waitFor(() => expect(result.current.data).toEqual(mockUserState)); - - // 验证是否正确处理了语言设置 - expect(switchLang).toHaveBeenCalledWith('auto'); }); it('should fetch use server config correctly', async () => { @@ -169,7 +157,7 @@ describe('createCommonSlice', () => { isOnboard: true, preference: savedPreference, settings: { - general: { language: 'en-US' }, + general: { fontSize: 14 }, }, }; vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState); @@ -219,7 +207,7 @@ describe('createCommonSlice', () => { isOnboard: true, preference: {} as any, settings: { - general: { language: 'en-US' }, + general: { fontSize: 12 }, }, }; diff --git a/src/store/user/slices/common/action.ts b/src/store/user/slices/common/action.ts index f9fda9f6b4c28..b9a3a2906696a 100644 --- a/src/store/user/slices/common/action.ts +++ b/src/store/user/slices/common/action.ts @@ -9,12 +9,10 @@ import type { UserStore } from '@/store/user'; import type { GlobalServerConfig } from '@/types/serverConfig'; import { UserInitializationState } from '@/types/user'; import type { UserSettings } from '@/types/user/settings'; -import { switchLang } from '@/utils/client/switchLang'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; import { preferenceSelectors } from '../preference/selectors'; -import { userGeneralSettingsSelectors } from '../settings/selectors'; const n = setNamespace('common'); @@ -114,12 +112,6 @@ export const createCommonSlice: StateCreator< ); get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' }); - - // auto switch language - const language = userGeneralSettingsSelectors.config(get()).language; - if (language === 'auto') { - switchLang('auto'); - } } }, }, diff --git a/src/store/user/slices/settings/action.ts b/src/store/user/slices/settings/action.ts index fdf675f105f3f..01c889401fca8 100644 --- a/src/store/user/slices/settings/action.ts +++ b/src/store/user/slices/settings/action.ts @@ -7,7 +7,6 @@ import { MESSAGE_CANCEL_FLAT } from '@/const/message'; import { shareService } from '@/services/share'; import { userService } from '@/services/user'; import type { UserStore } from '@/store/user'; -import { LocaleMode } from '@/types/locale'; import { LobeAgentSettings } from '@/types/session'; import { SystemAgentItem, @@ -16,7 +15,6 @@ import { UserSettings, UserSystemAgentConfigKey, } from '@/types/user/settings'; -import { switchLang } from '@/utils/client/switchLang'; import { difference } from '@/utils/difference'; import { merge } from '@/utils/merge'; @@ -26,7 +24,6 @@ export interface UserSettingsAction { internal_createSignal: () => AbortController; resetSettings: () => Promise; setSettings: (settings: DeepPartial) => Promise; - switchLocale: (locale: LocaleMode) => Promise; switchThemeMode: (themeMode: ThemeMode) => Promise; updateDefaultAgent: (agent: DeepPartial) => Promise; updateGeneralConfig: (settings: Partial) => Promise; @@ -95,11 +92,6 @@ export const createSettingsSlice: StateCreator< await userService.updateUserSettings(diffs, abortController.signal); await get().refreshUserState(); }, - switchLocale: async (locale) => { - await get().updateGeneralConfig({ language: locale }); - - switchLang(locale); - }, switchThemeMode: async (themeMode) => { await get().updateGeneralConfig({ themeMode }); }, diff --git a/src/store/user/slices/settings/selectors/general.test.ts b/src/store/user/slices/settings/selectors/general.test.ts index ea1f798520ca8..c84d6a5133250 100644 --- a/src/store/user/slices/settings/selectors/general.test.ts +++ b/src/store/user/slices/settings/selectors/general.test.ts @@ -5,20 +5,6 @@ import { merge } from '@/utils/merge'; import { userGeneralSettingsSelectors } from './general'; describe('settingsSelectors', () => { - describe('currentLanguage', () => { - it('should return the correct language setting', () => { - const s: UserState = merge(initialState, { - settings: { - general: { language: 'fr' }, - }, - }); - - const result = userGeneralSettingsSelectors.currentLanguage(s as UserStore); - - expect(result).toBe('fr'); - }); - }); - describe('currentThemeMode', () => { it('should return the correct theme', () => { const s: UserState = merge(initialState, { diff --git a/src/store/user/slices/settings/selectors/general.ts b/src/store/user/slices/settings/selectors/general.ts index 7646e5cbc955b..86791d0a23382 100644 --- a/src/store/user/slices/settings/selectors/general.ts +++ b/src/store/user/slices/settings/selectors/general.ts @@ -1,24 +1,8 @@ -import { DEFAULT_LANG } from '@/const/locale'; -import { Locales } from '@/locales/resources'; -import { isOnServerSide } from '@/utils/env'; - import { UserStore } from '../../../store'; import { currentSettings } from './settings'; const generalConfig = (s: UserStore) => currentSettings(s).general || {}; -const currentLanguage = (s: UserStore) => { - const locale = generalConfig(s).language; - - if (locale === 'auto') { - if (isOnServerSide) return DEFAULT_LANG; - - return navigator.language as Locales; - } - - return locale; -}; - const currentThemeMode = (s: UserStore) => { const themeMode = generalConfig(s).themeMode; return themeMode || 'auto'; @@ -27,14 +11,11 @@ const currentThemeMode = (s: UserStore) => { const neutralColor = (s: UserStore) => generalConfig(s).neutralColor; const primaryColor = (s: UserStore) => generalConfig(s).primaryColor; const fontSize = (s: UserStore) => generalConfig(s).fontSize; -const language = (s: UserStore) => generalConfig(s).language; export const userGeneralSettingsSelectors = { config: generalConfig, - currentLanguage, currentThemeMode, fontSize, - language, neutralColor, primaryColor, }; diff --git a/src/types/user/settings/general.ts b/src/types/user/settings/general.ts index 3fd4e201e6c90..1180c4bbe5a18 100644 --- a/src/types/user/settings/general.ts +++ b/src/types/user/settings/general.ts @@ -1,11 +1,8 @@ import type { NeutralColors, PrimaryColors } from '@lobehub/ui'; import type { ThemeMode } from 'antd-style'; -import { LocaleMode } from '@/types/locale'; - export interface UserGeneralConfig { fontSize: number; - language: LocaleMode; neutralColor?: NeutralColors; primaryColor?: PrimaryColors; themeMode: ThemeMode; diff --git a/src/utils/client/cookie.test.ts b/src/utils/client/cookie.test.ts new file mode 100644 index 0000000000000..622419d74f728 --- /dev/null +++ b/src/utils/client/cookie.test.ts @@ -0,0 +1,85 @@ +import dayjs from 'dayjs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { COOKIE_CACHE_DAYS } from '@/const/settings'; + +import { setCookie } from './cookie'; + +describe('setCookie', () => { + // Mock document.cookie since we're in a test environment + beforeEach(() => { + // Create a getter/setter for document.cookie + Object.defineProperty(document, 'cookie', { + writable: true, + value: '', + }); + }); + + it('should set a cookie with default expiration', () => { + const key = 'testKey'; + const value = 'testValue'; + + // Mock the current date + const mockDate = new Date('2024-01-01T00:00:00Z'); + vi.setSystemTime(mockDate); + + // Calculate expected expiration date + const expectedExpires = dayjs(mockDate).add(COOKIE_CACHE_DAYS, 'day').toDate().toUTCString(); + + setCookie(key, value); + + expect(document.cookie).toBe(`${key}=${value};expires=${expectedExpires};path=/;`); + + // Reset system time + vi.useRealTimers(); + }); + + it('should set a cookie with custom expiration days', () => { + const key = 'testKey'; + const value = 'testValue'; + const customDays = 7; + + // Mock the current date + const mockDate = new Date('2024-01-01T00:00:00Z'); + vi.setSystemTime(mockDate); + + // Calculate expected expiration date + const expectedExpires = dayjs(mockDate).add(customDays, 'day').toDate().toUTCString(); + + setCookie(key, value, customDays); + + expect(document.cookie).toBe(`${key}=${value};expires=${expectedExpires};path=/;`); + + // Reset system time + vi.useRealTimers(); + }); + + it('should remove cookie when value is undefined', () => { + const key = 'testKey'; + + // Expected expiration date for removal (1970-01-01T00:00:00Z) + const expectedExpires = new Date(0).toUTCString(); + + setCookie(key, undefined); + + expect(document.cookie).toBe(`${key}=; expires=${expectedExpires}; path=/;`); + }); + + it('should handle special characters in key and value', () => { + const key = 'test@Key'; + const value = 'test Value with spaces'; + + // Mock the current date + const mockDate = new Date('2024-01-01T00:00:00Z'); + vi.setSystemTime(mockDate); + + const expectedExpires = dayjs(mockDate).add(COOKIE_CACHE_DAYS, 'day').toDate().toUTCString(); + + setCookie(key, value); + + expect(document.cookie).toBe(`${key}=${value};expires=${expectedExpires};path=/;`); + + // Reset system time + vi.useRealTimers(); + }); +}); diff --git a/src/utils/client/cookie.ts b/src/utils/client/cookie.ts new file mode 100644 index 0000000000000..4af72a035a14b --- /dev/null +++ b/src/utils/client/cookie.ts @@ -0,0 +1,22 @@ +import dayjs from 'dayjs'; + +import { COOKIE_CACHE_DAYS } from '@/const/settings'; + +export const setCookie = ( + key: string, + value: string | undefined, + expireDays = COOKIE_CACHE_DAYS, +) => { + if (typeof value === 'undefined') { + // Set the expiration time to yesterday (expire immediately) + const expiredDate = new Date(0).toUTCString(); // 1970-01-01T00:00:00Z + + // eslint-disable-next-line unicorn/no-document-cookie + document.cookie = `${key}=; expires=${expiredDate}; path=/;`; + } else { + const expires = dayjs().add(expireDays, 'day').toDate().toUTCString(); + + // eslint-disable-next-line unicorn/no-document-cookie + document.cookie = `${key}=${value};expires=${expires};path=/;`; + } +}; diff --git a/src/utils/client/switchLang.ts b/src/utils/client/switchLang.ts index 15e95104b02c4..f808c38cccf7c 100644 --- a/src/utils/client/switchLang.ts +++ b/src/utils/client/switchLang.ts @@ -1,10 +1,15 @@ import { changeLanguage } from 'i18next'; +import { LOBE_LOCALE_COOKIE } from '@/const/locale'; import { LocaleMode } from '@/types/locale'; +import { setCookie } from './cookie'; + export const switchLang = (locale: LocaleMode) => { const lang = locale === 'auto' ? navigator.language : locale; changeLanguage(lang); document.documentElement.lang = lang; + + setCookie(LOBE_LOCALE_COOKIE, locale === 'auto' ? undefined : locale, 365); }; diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts deleted file mode 100644 index dab605b21f7aa..0000000000000 --- a/src/utils/cookie.ts +++ /dev/null @@ -1,10 +0,0 @@ -import dayjs from 'dayjs'; - -import { COOKIE_CACHE_DAYS } from '@/const/settings'; - -export const setCookie = (key: string, value: string | undefined) => { - const expires = dayjs().add(COOKIE_CACHE_DAYS, 'day').toISOString(); - - // eslint-disable-next-line unicorn/no-document-cookie - document.cookie = `${key}=${value};expires=${expires};path=/;`; -}; From ac317ded63dc772572c1c8197fef300f29806727 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 10 Feb 2025 13:06:11 +0000 Subject: [PATCH 03/18] :bookmark: chore(release): v1.52.12 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### [Version 1.52.12](https://github.com/lobehub/lobe-chat/compare/v1.52.11...v1.52.12) Released on **2025-02-10** #### 🐛 Bug Fixes - **misc**: Fix language incorrect on page hydration.
Improvements and Fixes #### What's fixed * **misc**: Fix language incorrect on page hydration, closes [#5970](https://github.com/lobehub/lobe-chat/issues/5970) ([91912cf](https://github.com/lobehub/lobe-chat/commit/91912cf))
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
--- CHANGELOG.md | 25 +++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9a2c1926ebb..b5450d7e24c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ # Changelog +### [Version 1.52.12](https://github.com/lobehub/lobe-chat/compare/v1.52.11...v1.52.12) + +Released on **2025-02-10** + +#### 🐛 Bug Fixes + +- **misc**: Fix language incorrect on page hydration. + +
+ +
+Improvements and Fixes + +#### What's fixed + +- **misc**: Fix language incorrect on page hydration, closes [#5970](https://github.com/lobehub/lobe-chat/issues/5970) ([91912cf](https://github.com/lobehub/lobe-chat/commit/91912cf)) + +
+ +
+ +[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) + +
+ ### [Version 1.52.11](https://github.com/lobehub/lobe-chat/compare/v1.52.10...v1.52.11) Released on **2025-02-10** diff --git a/package.json b/package.json index d8365a08bdfed..45a9af5bf9406 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lobehub/chat", - "version": "1.52.11", + "version": "1.52.12", "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.", "keywords": [ "framework", From 0d7e364bc5099221ca9acc259d800e06c8ea9e30 Mon Sep 17 00:00:00 2001 From: lobehubbot Date: Mon, 10 Feb 2025 13:07:14 +0000 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=93=9D=20docs(bot):=20Auto=20sync?= =?UTF-8?q?=20agents=20&=20plugin=20to=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog/v1.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog/v1.json b/changelog/v1.json index f1185c7f0df11..b6efa59d56669 100644 --- a/changelog/v1.json +++ b/changelog/v1.json @@ -1,4 +1,11 @@ [ + { + "children": { + "fixes": ["Fix language incorrect on page hydration."] + }, + "date": "2025-02-10", + "version": "1.52.12" + }, { "children": { "improvements": ["Support Mermaid in Artifacts."] From ae4aa0e2e18807bb58059d977392665dc05d2d22 Mon Sep 17 00:00:00 2001 From: Rylan Cai <67412196+cy948@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:28:42 +0800 Subject: [PATCH 05/18] :memo: docs: Add warning to caching in reverse proxy (#5985) * :memo: docs: add warning * :memo: docs: add links --- docs/self-hosting/platform/btpanel.mdx | 4 ++++ docs/self-hosting/platform/btpanel.zh-CN.mdx | 4 ++++ docs/self-hosting/server-database/docker-compose.mdx | 4 ++++ docs/self-hosting/server-database/docker-compose.zh-CN.mdx | 3 +++ 4 files changed, 15 insertions(+) diff --git a/docs/self-hosting/platform/btpanel.mdx b/docs/self-hosting/platform/btpanel.mdx index 53cebc1e5e7e5..c155fa6e10960 100644 --- a/docs/self-hosting/platform/btpanel.mdx +++ b/docs/self-hosting/platform/btpanel.mdx @@ -36,6 +36,10 @@ To install aaPanel, go to the [aaPanel](https://www.aapanel.com/new/download.htm 6. After submission, the panel will automatically initialize the application, which will take about `1-3` minutes. It can be accessed after the initialization is completed. + + ⚠️ Do not enable any form of cache in the reverse proxy settings of the panel to avoid affecting the normal operation of the service. Read more at https://github.com/lobehub/lobe-chat/discussions/5986 + + ## Visit LobeChat - If you have set a domain name, please directly enter the domain name in the browser address bar, such as `http://demo.lobechat`, to access the `LobeChat` console. diff --git a/docs/self-hosting/platform/btpanel.zh-CN.mdx b/docs/self-hosting/platform/btpanel.zh-CN.mdx index 8f088d2fca41e..8497c914e0da9 100644 --- a/docs/self-hosting/platform/btpanel.zh-CN.mdx +++ b/docs/self-hosting/platform/btpanel.zh-CN.mdx @@ -40,6 +40,10 @@ tags: 5. 提交后面板会自动进行应用初始化,大概需要`1-3`分钟,初始化完成后即可访问。 + + ⚠️ 请不要在面板的反向代理设置中开启任何形式的缓存,以免影响服务的正常运行。详情请见 https://github.com/lobehub/lobe-chat/discussions/5986 + + ## 访问 LobeChat - 如果您填写域名,请在浏览器输入您的域名访问,如`http://demo.lobechat`,即可访问 `LobeChat` 页面。 diff --git a/docs/self-hosting/server-database/docker-compose.mdx b/docs/self-hosting/server-database/docker-compose.mdx index 53d8276c2d469..051a2d4924b03 100644 --- a/docs/self-hosting/server-database/docker-compose.mdx +++ b/docs/self-hosting/server-database/docker-compose.mdx @@ -226,6 +226,10 @@ The script supports the following deployment modes; please choose the appropriat proxy_set_header X-Forwarded-Proto $scheme; # Keep the request protocol } ``` + + ⚠️ If you are using such panel software, + please do not enable any form of caching in the reverse proxy settings of such panel software to avoid affecting the normal operation of the service. + Read more at https://github.com/lobehub/lobe-chat/discussions/5986 ### Complete Remaining Configuration in Interactive Script diff --git a/docs/self-hosting/server-database/docker-compose.zh-CN.mdx b/docs/self-hosting/server-database/docker-compose.zh-CN.mdx index 763b7c4fd0f09..4f6b1d1402c33 100644 --- a/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +++ b/docs/self-hosting/server-database/docker-compose.zh-CN.mdx @@ -224,6 +224,9 @@ bash <(curl -fsSL https://lobe.li/setup.sh) -l zh_CN proxy_set_header X-Forwarded-Proto $scheme; # 保留请求协议 } ``` + + ⚠️ 请不要在此类面板软件的反向代理设置中开启任何形式的缓存,以免影响服务的正常运行。 + 详情请见 https://github.com/lobehub/lobe-chat/discussions/5986 ### 在交互式脚本中完成剩余配置 From 0d7e6651d7d137400f774569e0670bc5779ed31d Mon Sep 17 00:00:00 2001 From: DeepWzh Date: Mon, 10 Feb 2025 22:24:33 +0800 Subject: [PATCH 06/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20Aliyun=20deep?= =?UTF-8?q?seek-r1=20reasoning=20parsing=20with=20oneapi=20(#5964)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: ensure that reasoning_content is a non-empty string when dealing with Aliyun Bailian * fix: improve handling of reasoning_content and content in OpenAI stream transformation * test: add openai reasoning test in aliyun bailian api --- .../utils/streams/openai.test.ts | 219 ++++++++++++++++++ .../agent-runtime/utils/streams/openai.ts | 37 +-- 2 files changed, 243 insertions(+), 13 deletions(-) diff --git a/src/libs/agent-runtime/utils/streams/openai.test.ts b/src/libs/agent-runtime/utils/streams/openai.test.ts index fc056db56c64e..14629cfe601bf 100644 --- a/src/libs/agent-runtime/utils/streams/openai.test.ts +++ b/src/libs/agent-runtime/utils/streams/openai.test.ts @@ -755,6 +755,225 @@ describe('OpenAIStream', () => { ); }); + it('should handle reasoning event in aliyun bailian api', async () => { + const data = [ + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { role: 'assistant', content: '', reasoning_content: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '', reasoning_content: '您好' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '', reasoning_content: '!' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '', reasoning_content: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '你好', reasoning_content: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '很高兴', reasoning_cont: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '为您', reasoning_content: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '提供', reasoning_content: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '帮助。', reasoning_content: '' }, + logprobs: null, + finish_reason: null, + }, + ], + }, + { + id: '1', + object: 'chat.completion.chunk', + created: 1737563070, + model: 'deepseek-reasoner', + system_fingerprint: 'fp_1c5d8833bc', + choices: [ + { + index: 0, + delta: { content: '', reasoning_content: '' }, + logprobs: null, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 6, + completion_tokens: 104, + total_tokens: 110, + prompt_tokens_details: { cached_tokens: 0 }, + completion_tokens_details: { reasoning_tokens: 70 }, + prompt_cache_hit_tokens: 0, + prompt_cache_miss_tokens: 6, + }, + }, + ]; + + const mockOpenAIStream = new ReadableStream({ + start(controller) { + data.forEach((chunk) => { + controller.enqueue(chunk); + }); + + controller.close(); + }, + }); + + const protocolStream = OpenAIStream(mockOpenAIStream); + + const decoder = new TextDecoder(); + const chunks = []; + + // @ts-ignore + for await (const chunk of protocolStream) { + chunks.push(decoder.decode(chunk, { stream: true })); + } + + expect(chunks).toEqual( + [ + 'id: 1', + 'event: reasoning', + `data: ""\n`, + 'id: 1', + 'event: reasoning', + `data: "您好"\n`, + 'id: 1', + 'event: reasoning', + `data: "!"\n`, + 'id: 1', + 'event: reasoning', + `data: ""\n`, + 'id: 1', + 'event: text', + `data: "你好"\n`, + 'id: 1', + 'event: text', + `data: "很高兴"\n`, + 'id: 1', + 'event: text', + `data: "为您"\n`, + 'id: 1', + 'event: text', + `data: "提供"\n`, + 'id: 1', + 'event: text', + `data: "帮助。"\n`, + 'id: 1', + 'event: stop', + `data: "stop"\n`, + ].map((i) => `${i}\n`), + ); + }); + it('should handle reasoning in litellm', async () => { const data = [ { diff --git a/src/libs/agent-runtime/utils/streams/openai.ts b/src/libs/agent-runtime/utils/streams/openai.ts index 3ec8b1f7f4543..1a1124875e4b1 100644 --- a/src/libs/agent-runtime/utils/streams/openai.ts +++ b/src/libs/agent-runtime/utils/streams/openai.ts @@ -87,20 +87,31 @@ export const transformOpenAIStream = ( return { data: item.finish_reason, id: chunk.id, type: 'stop' }; } - // DeepSeek reasoner will put thinking in the reasoning_content field - // litellm will not set content = null when processing reasoning content - // en: siliconflow has encountered a situation where both content and reasoning_content are present, so the parsing order go ahead - // refs: https://github.com/lobehub/lobe-chat/issues/5681 - if ( - item.delta && - 'reasoning_content' in item.delta && - typeof item.delta.reasoning_content === 'string' - ) { - return { data: item.delta.reasoning_content, id: chunk.id, type: 'reasoning' }; - } + if (item.delta) { + let reasoning_content = + 'reasoning_content' in item.delta ? item.delta.reasoning_content : null; + let content = 'content' in item.delta ? item.delta.content : null; + + // DeepSeek reasoner will put thinking in the reasoning_content field + // litellm and not set content = null when processing reasoning content + // en: siliconflow and aliyun bailian has encountered a situation where both content and reasoning_content are present, so need to handle it + // refs: https://github.com/lobehub/lobe-chat/issues/5681 (siliconflow) + // refs: https://github.com/lobehub/lobe-chat/issues/5956 (aliyun bailian) + if (typeof content === 'string' && typeof reasoning_content === 'string') { + if (content === '' && reasoning_content === '') { + content = null; + } else if (reasoning_content === '') { + reasoning_content = null; + } + } - if (typeof item.delta?.content === 'string') { - return { data: item.delta.content, id: chunk.id, type: 'text' }; + if (typeof reasoning_content === 'string') { + return { data: reasoning_content, id: chunk.id, type: 'reasoning' }; + } + + if (typeof content === 'string') { + return { data: content, id: chunk.id, type: 'text' }; + } } // 无内容情况 From cf7a2d642e23d58c10cbc961c660673d253207cd Mon Sep 17 00:00:00 2001 From: sxjeru Date: Mon, 10 Feb 2025 22:25:05 +0800 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Support=20Aliyun=20?= =?UTF-8?q?deepseek-r1=20reasoning=20(#5954)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update qwen.ts * Update qwen.ts --- src/libs/agent-runtime/utils/streams/qwen.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/agent-runtime/utils/streams/qwen.ts b/src/libs/agent-runtime/utils/streams/qwen.ts index f0cc613b25e2b..bbc906d16228a 100644 --- a/src/libs/agent-runtime/utils/streams/qwen.ts +++ b/src/libs/agent-runtime/utils/streams/qwen.ts @@ -61,6 +61,16 @@ export const transformQwenStream = (chunk: OpenAI.ChatCompletionChunk): StreamPr } as StreamProtocolToolCallChunk; } + // DeepSeek reasoner will put thinking in the reasoning_content field + if ( + item.delta && + 'reasoning_content' in item.delta && + typeof item.delta.reasoning_content === 'string' && + item.delta.reasoning_content !== '' + ) { + return { data: item.delta.reasoning_content, id: chunk.id, type: 'reasoning' }; + } + if (typeof item.delta?.content === 'string') { return { data: item.delta.content, id: chunk.id, type: 'text' }; } From 500dca322341b40edbd5da67cf0be8d0da6ac817 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 10 Feb 2025 14:33:08 +0000 Subject: [PATCH 08/18] :bookmark: chore(release): v1.52.13 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### [Version 1.52.13](https://github.com/lobehub/lobe-chat/compare/v1.52.12...v1.52.13) Released on **2025-02-10** #### 🐛 Bug Fixes - **misc**: Fix Aliyun deepseek-r1 reasoning parsing with oneapi, Support Aliyun deepseek-r1 reasoning.
Improvements and Fixes #### What's fixed * **misc**: Fix Aliyun deepseek-r1 reasoning parsing with oneapi, closes [#5964](https://github.com/lobehub/lobe-chat/issues/5964) ([0d7e665](https://github.com/lobehub/lobe-chat/commit/0d7e665)) * **misc**: Support Aliyun deepseek-r1 reasoning, closes [#5954](https://github.com/lobehub/lobe-chat/issues/5954) ([cf7a2d6](https://github.com/lobehub/lobe-chat/commit/cf7a2d6))
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
--- CHANGELOG.md | 26 ++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5450d7e24c61..2f53ccf8269c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ # Changelog +### [Version 1.52.13](https://github.com/lobehub/lobe-chat/compare/v1.52.12...v1.52.13) + +Released on **2025-02-10** + +#### 🐛 Bug Fixes + +- **misc**: Fix Aliyun deepseek-r1 reasoning parsing with oneapi, Support Aliyun deepseek-r1 reasoning. + +
+ +
+Improvements and Fixes + +#### What's fixed + +- **misc**: Fix Aliyun deepseek-r1 reasoning parsing with oneapi, closes [#5964](https://github.com/lobehub/lobe-chat/issues/5964) ([0d7e665](https://github.com/lobehub/lobe-chat/commit/0d7e665)) +- **misc**: Support Aliyun deepseek-r1 reasoning, closes [#5954](https://github.com/lobehub/lobe-chat/issues/5954) ([cf7a2d6](https://github.com/lobehub/lobe-chat/commit/cf7a2d6)) + +
+ +
+ +[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) + +
+ ### [Version 1.52.12](https://github.com/lobehub/lobe-chat/compare/v1.52.11...v1.52.12) Released on **2025-02-10** diff --git a/package.json b/package.json index 45a9af5bf9406..ca769ef0e4956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lobehub/chat", - "version": "1.52.12", + "version": "1.52.13", "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.", "keywords": [ "framework", From d8b522d338280a4eb6d6c20191c167da1a52ee6b Mon Sep 17 00:00:00 2001 From: lobehubbot Date: Mon, 10 Feb 2025 14:34:22 +0000 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=93=9D=20docs(bot):=20Auto=20sync?= =?UTF-8?q?=20agents=20&=20plugin=20to=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog/v1.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog/v1.json b/changelog/v1.json index b6efa59d56669..038924c8679ea 100644 --- a/changelog/v1.json +++ b/changelog/v1.json @@ -1,4 +1,13 @@ [ + { + "children": { + "fixes": [ + "Fix Aliyun deepseek-r1 reasoning parsing with oneapi, Support Aliyun deepseek-r1 reasoning." + ] + }, + "date": "2025-02-10", + "version": "1.52.13" + }, { "children": { "fixes": ["Fix language incorrect on page hydration."] From 6482f8a33253ee8c722cc9f49aa0d71ea970c80f Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Mon, 10 Feb 2025 23:25:19 +0800 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=92=84=20style:=20refactor=20agent?= =?UTF-8?q?=20settings=20modal=20(#5987)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove setting modal * refactor the agent settings modal * fix tests * fix tests --- .../AgentSettings/CategoryContent/index.tsx} | 13 +- .../CategoryContent}/useCategory.tsx | 0 .../features/AgentSettings/index.tsx | 114 ++++++++++++++++++ .../(workspace)/features/SettingButton.tsx | 20 ++- .../(main)/chat/settings/modal/page.tsx | 23 ---- .../@modal/chat/(.)settings/modal/layout.tsx | 61 ---------- .../@modal/chat/(.)settings/modal/loading.tsx | 5 - .../@modal/chat/(.)settings/modal/page.tsx | 56 --------- src/hooks/useChatSettingsTab.ts | 12 -- src/hooks/useInterceptingRoutes.test.ts | 15 ++- src/hooks/useInterceptingRoutes.ts | 18 +-- src/store/agent/slices/chat/initialState.ts | 2 + 12 files changed, 157 insertions(+), 182 deletions(-) rename src/app/[variants]/{@modal/chat/(.)settings/modal/features/CategoryContent.tsx => (main)/chat/(workspace)/features/AgentSettings/CategoryContent/index.tsx} (68%) rename src/app/[variants]/{@modal/chat/(.)settings/modal/features => (main)/chat/(workspace)/features/AgentSettings/CategoryContent}/useCategory.tsx (100%) create mode 100644 src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx delete mode 100644 src/app/[variants]/(main)/chat/settings/modal/page.tsx delete mode 100644 src/app/[variants]/@modal/chat/(.)settings/modal/layout.tsx delete mode 100644 src/app/[variants]/@modal/chat/(.)settings/modal/loading.tsx delete mode 100644 src/app/[variants]/@modal/chat/(.)settings/modal/page.tsx delete mode 100644 src/hooks/useChatSettingsTab.ts diff --git a/src/app/[variants]/@modal/chat/(.)settings/modal/features/CategoryContent.tsx b/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/index.tsx similarity index 68% rename from src/app/[variants]/@modal/chat/(.)settings/modal/features/CategoryContent.tsx rename to src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/index.tsx index fac656b4fe921..1560849079722 100644 --- a/src/app/[variants]/@modal/chat/(.)settings/modal/features/CategoryContent.tsx +++ b/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/index.tsx @@ -5,22 +5,23 @@ import { Flexbox } from 'react-layout-kit'; import HeaderContent from '@/app/[variants]/(main)/chat/settings/features/HeaderContent'; import Menu from '@/components/Menu'; -import { useChatSettingsTab } from '@/hooks/useChatSettingsTab'; -import { useQueryRoute } from '@/hooks/useQueryRoute'; +import { ChatSettingsTabs } from '@/store/global/initialState'; import { useCategory } from './useCategory'; -const CategoryContent = memo(() => { +interface CategoryContentProps { + setTab: (tab: ChatSettingsTabs) => void; + tab: string; +} +const CategoryContent = memo(({ setTab, tab }) => { const cateItems = useCategory(); - const tab = useChatSettingsTab(); - const router = useQueryRoute(); return ( <> { - router.replace('/chat/settings/modal', { query: { tab: key } }); + setTab(key as ChatSettingsTabs); }} selectable selectedKeys={[tab as any]} diff --git a/src/app/[variants]/@modal/chat/(.)settings/modal/features/useCategory.tsx b/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/useCategory.tsx similarity index 100% rename from src/app/[variants]/@modal/chat/(.)settings/modal/features/useCategory.tsx rename to src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/useCategory.tsx diff --git a/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx b/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx new file mode 100644 index 0000000000000..6958aafeec15c --- /dev/null +++ b/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { Drawer } from 'antd'; +import { useResponsive, useTheme } from 'antd-style'; +import isEqual from 'fast-deep-equal'; +import { memo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import Header from '@/app/[variants]/(main)/settings/_layout/Desktop/Header'; +import AgentChat from '@/features/AgentSetting/AgentChat'; +import AgentMeta from '@/features/AgentSetting/AgentMeta'; +import AgentModal from '@/features/AgentSetting/AgentModal'; +import AgentPlugin from '@/features/AgentSetting/AgentPlugin'; +import AgentPrompt from '@/features/AgentSetting/AgentPrompt'; +import AgentTTS from '@/features/AgentSetting/AgentTTS'; +import StoreUpdater from '@/features/AgentSetting/StoreUpdater'; +import { Provider, createStore } from '@/features/AgentSetting/store'; +import Footer from '@/features/Setting/Footer'; +import { useAgentStore } from '@/store/agent'; +import { agentSelectors } from '@/store/agent/slices/chat'; +import { ChatSettingsTabs } from '@/store/global/initialState'; +import { useSessionStore } from '@/store/session'; +import { sessionMetaSelectors } from '@/store/session/selectors'; + +import CategoryContent from './CategoryContent'; + +const AgentSettings = memo(() => { + const { t } = useTranslation('setting'); + const id = useSessionStore((s) => s.activeId); + const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual); + const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); + const [showAgentSetting, updateAgentConfig] = useAgentStore((s) => [ + s.showAgentSetting, + s.updateAgentConfig, + ]); + const [updateAgentMeta] = useSessionStore((s) => [ + s.updateSessionMeta, + sessionMetaSelectors.currentAgentTitle(s), + ]); + + const [tab, setTab] = useState(ChatSettingsTabs.Meta); + + const ref = useRef(null); + const theme = useTheme(); + const { md = true, mobile = false } = useResponsive(); + + const category = ; + return ( + + + { + useAgentStore.setState({ showAgentSetting: false }); + }} + open={showAgentSetting} + placement={'bottom'} + styles={{ + body: { padding: 0 }, + content: { + background: theme.colorBgContainer, + }, + }} + title={t('header.session')} + > + + {md ? ( + {category} + ) : ( +
ref.current} + title={t(`agentTab.${tab as ChatSettingsTabs}`)} + > + {category} +
+ )} + + {tab === ChatSettingsTabs.Meta && } + {tab === ChatSettingsTabs.Prompt && } + {tab === ChatSettingsTabs.Chat && } + {tab === ChatSettingsTabs.Modal && } + {tab === ChatSettingsTabs.TTS && } + {tab === ChatSettingsTabs.Plugin && }