diff --git a/CHANGELOG.md b/CHANGELOG.md index f0f1da986a18..d87e041a8062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ # Changelog +## [Version 1.41.0](https://github.com/lobehub/lobe-chat/compare/v1.40.4...v1.41.0) + +Released on **2024-12-28** + +#### ✨ Features + +- **auth**: Add WeChat authentication support. +- **misc**: Support white list for discover assistant. + +
+ +
+Improvements and Fixes + +#### What's improved + +- **auth**: Add WeChat authentication support, closes [#5195](https://github.com/lobehub/lobe-chat/issues/5195) ([95153a4](https://github.com/lobehub/lobe-chat/commit/95153a4)) +- **misc**: Support white list for discover assistant, closes [#5216](https://github.com/lobehub/lobe-chat/issues/5216) ([90bb20d](https://github.com/lobehub/lobe-chat/commit/90bb20d)) + +
+ +
+ +[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) + +
+ ### [Version 1.40.4](https://github.com/lobehub/lobe-chat/compare/v1.40.3...v1.40.4) Released on **2024-12-28** diff --git a/changelog/v1.json b/changelog/v1.json index 1abccca2babd..7e5ca6ff3236 100644 --- a/changelog/v1.json +++ b/changelog/v1.json @@ -1,4 +1,11 @@ [ + { + "children": { + "features": ["Support white list for discover assistant."] + }, + "date": "2024-12-28", + "version": "1.41.0" + }, { "children": { "improvements": ["Update deepseek V3 model."] diff --git a/docs/self-hosting/advanced/auth/next-auth/wechat.mdx b/docs/self-hosting/advanced/auth/next-auth/wechat.mdx new file mode 100644 index 000000000000..031887045679 --- /dev/null +++ b/docs/self-hosting/advanced/auth/next-auth/wechat.mdx @@ -0,0 +1,46 @@ +--- +title: Configure Wechat Authentication Service in LobeChat +description: Learn how to configure Wechat authentication service in LobeChat, including creating a new Wechat App, setting permissions, and environment variables. +tags: + - Wechat Authentication + - Wechat App + - Environment Variable Configuration + - Single Sign-On + - LobeChat +--- + +# Configure Wechat Authentication Service + +## Wechat Configuration Process + + + ### Create a Wechat Application + +Click [here](https://open.weixin.qq.com/cgi-bin/index) and then click "Management Center", "Website Application", and "Create Website Application" in sequence. + +Fill in the information as required by the official website prompts and submit for review. + +After successful creation, click "Application Details" to obtain the AppID and AppSecret. + +### Configure Environment Variables + +When deploying LobeChat, you need to configure the following environment variables: + +| Environment Variable | Type | Description | +| --- | --- | --- | +| `NEXT_AUTH_SECRET` | Required | Key used to encrypt Auth.js session tokens. You can generate the key using the command: `openssl rand -base64 32` | +| `NEXT_AUTH_SSO_PROVIDERS` | Required | Select the Single Sign-On provider for LobeChat. Use `github` for Github. | +| `WECHAT_CLIENT_ID` | Required | Client ID from the Wechat website application details page | +| `WECHAT_CLIENT_SECRET` | Required | Client Secret from the Wechat website application details page | +| `NEXTAUTH_URL` | Required | This URL is used to specify the callback address for Auth.js when performing OAuth authentication. Only set it if the default generated redirect address is incorrect. `https://example.com/api/auth` | + + + Go to [📘 Environment Variables](/en/docs/self-hosting/environment-variables/auth#wechat) for more details about related variables. + + + + + + After successful deployment, users will be able to authenticate through the WeChat Open Platform + and use LobeChat. + diff --git a/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx b/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx new file mode 100644 index 000000000000..82c977319e19 --- /dev/null +++ b/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx @@ -0,0 +1,43 @@ +--- +title: 在 LobeChat 中配置微信身份验证服务 +description: 学习如何在 LobeChat 中配置微信身份验证服务,包括创建新的微信网站应用、设置权限和环境变量。 +tags: + -微信身份验证 + -微信网站应用 + - 环境变量配置 + - 单点登录 + - LobeChat +--- + +# 配置微信身份验证服务 + +##微信配置流程 + + + ### 创建微信网站应用 + +点击 [这里](https://open.weixin.qq.com/cgi-bin/index) 依次点击“管理中心”、“网站应用”、“创建网站应用” + +按照管网提示要求填写信息并提交审核。 + +创建成功后,点击“应用详情”,可获知AppID和AppSecret。 + +### 配置环境变量 + +在部署 LobeChat 时,你需要配置以下环境变量: + +| 环境变量 | 类型 | 描述 | +| --- | --- | --- | +| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令生成秘钥: `openssl rand -base64 32` | +| `NEXT_AUTH_SSO_PROVIDERS` | 必选 | 选择 LoboChat 的单点登录提供商。使用 Github 请填写 `github`。 | +| `WECHAT_CLIENT_ID` | 必选 |微信网站应用详情页的 客户端 ID | +| `WECHAT_CLIENT_SECRET` | 必选 |微信网站应用详情页的 客户端 Secret | +| `NEXTAUTH_URL` | 必选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` | + + + 前往 [📘 环境变量](/zh/docs/self-hosting/environment-variables/auth#wechat) 可查阅相关变量详情。 + + + + +部署成功后,用户将可以通过微信开放平台身份认证并使用 LobeChat。 diff --git a/package.json b/package.json index c5f4770925d5..305d4ee3889f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lobehub/chat", - "version": "1.40.4", + "version": "1.41.0", "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", diff --git a/src/app/(backend)/webapi/assistant/store/route.ts b/src/app/(backend)/webapi/assistant/store/route.ts index 54e696749236..036a7bac5ce4 100644 --- a/src/app/(backend)/webapi/assistant/store/route.ts +++ b/src/app/(backend)/webapi/assistant/store/route.ts @@ -1,6 +1,5 @@ import { NextResponse } from 'next/server'; -import { DEFAULT_LANG } from '@/const/locale'; import { AssistantStore } from '@/server/modules/AssistantStore'; export const runtime = 'edge'; @@ -11,18 +10,10 @@ export const GET = async (req: Request) => { const market = new AssistantStore(); - let res: Response; + const data = await market.getAgentIndex(locale as any); - res = await fetch(market.getAgentIndexUrl(locale as any)); - - if (res.status === 404) { - res = await fetch(market.getAgentIndexUrl(DEFAULT_LANG)); - } - - const data = await res.json(); return NextResponse.json(data); - } catch (e) { - console.error(e); + } catch { return new Response(`failed to fetch agent market index`, { headers: { 'Access-Control-Allow-Origin': '*', diff --git a/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx b/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx index 21df86c01343..0e14c3b49a4d 100644 --- a/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx +++ b/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx @@ -5,12 +5,11 @@ import { Button, Dropdown } from 'antd'; import { createStyles } from 'antd-style'; import { ChevronDownIcon, SquareArrowOutUpRight } from 'lucide-react'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { FlexboxProps } from 'react-layout-kit'; -import { useOpenSettings } from '@/hooks/useInterceptingRoutes'; -import { SettingsTabs } from '@/store/global/initialState'; import { DiscoverProviderItem } from '@/types/discover'; const useStyles = createStyles(({ css }) => ({ @@ -29,7 +28,11 @@ interface ProviderConfigProps extends FlexboxProps { const ProviderConfig = memo(({ data }) => { const { styles } = useStyles(); const { t } = useTranslation('discover'); - const openSettings = useOpenSettings(SettingsTabs.LLM); + + const router = useRouter(); + const openSettings = () => { + router.push('/settings/llm'); + }; const icon = ; @@ -56,7 +59,7 @@ const ProviderConfig = memo(({ data }) => { if (!items || items?.length === 0) return ( - ); diff --git a/src/app/@modal/(.)settings/modal/index.tsx b/src/app/@modal/(.)settings/modal/index.tsx deleted file mode 100644 index a8b4b064ebc3..000000000000 --- a/src/app/@modal/(.)settings/modal/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client'; - -import dynamic from 'next/dynamic'; -import { memo } from 'react'; - -import { useQuery } from '@/hooks/useQuery'; -import { SettingsTabs } from '@/store/global/initialState'; - -import Skeleton from './loading'; - -const loading = () => ; - -const Common = dynamic(() => import('@/app/(main)/settings/common'), { loading, ssr: false }); -const SystemAgent = dynamic(() => import('@/app/(main)/settings/system-agent'), { - loading, - ssr: false, -}); -const About = dynamic(() => import('@/app/(main)/settings/about'), { loading, ssr: false }); -const LLM = dynamic(() => import('@/app/(main)/settings/llm'), { loading, ssr: false }); -const TTS = dynamic(() => import('@/app/(main)/settings/tts'), { loading, ssr: false }); -const Agent = dynamic(() => import('@/app/(main)/settings/agent'), { loading, ssr: false }); -const Sync = dynamic(() => import('@/app/(main)/settings/sync'), { loading, ssr: false }); - -interface SettingsModalProps { - browser?: string; - mobile?: boolean; - os?: string; -} - -const SettingsModal = memo(({ browser, os, mobile }) => { - const { tab = SettingsTabs.Common } = useQuery(); - return ( - <> - {tab === SettingsTabs.Common && } - {tab === SettingsTabs.SystemAgent && } - {tab === SettingsTabs.Sync && } - {tab === SettingsTabs.LLM && } - {tab === SettingsTabs.TTS && } - {tab === SettingsTabs.Agent && } - {tab === SettingsTabs.About && } - - ); -}); - -export default SettingsModal; diff --git a/src/app/@modal/(.)settings/modal/layout.tsx b/src/app/@modal/(.)settings/modal/layout.tsx deleted file mode 100644 index e79ed5c985e1..000000000000 --- a/src/app/@modal/(.)settings/modal/layout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { Skeleton, Tag } from 'antd'; -import dynamic from 'next/dynamic'; -import { PropsWithChildren, memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useActiveSettingsKey } from '@/hooks/useActiveSettingsKey'; -import { SettingsTabs } from '@/store/global/initialState'; - -import ModalLayout from '../../_layout/ModalLayout'; -import SettingModalLayout from '../../_layout/SettingModalLayout'; - -const CategoryContent = dynamic( - () => import('@/app/(main)/settings/@category/features/CategoryContent'), - { loading: () => , ssr: false }, -); -const UpgradeAlert = dynamic(() => import('@/app/(main)/settings/features/UpgradeAlert'), { - ssr: false, -}); - -const Layout = memo(({ children }) => { - const { t } = useTranslation('setting'); - const activeKey = useActiveSettingsKey(); - return ( - - - {t(`tab.${activeKey}`)} - {activeKey === SettingsTabs.Sync && {t('tab.experiment')}} - - } - category={ - <> - - - - } - > - {children} - - - ); -}); - -export default Layout; diff --git a/src/app/@modal/(.)settings/modal/loading.tsx b/src/app/@modal/(.)settings/modal/loading.tsx deleted file mode 100644 index f99251a28e34..000000000000 --- a/src/app/@modal/(.)settings/modal/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Skeleton } from 'antd'; - -export default () => { - return ; -}; diff --git a/src/app/@modal/(.)settings/modal/page.tsx b/src/app/@modal/(.)settings/modal/page.tsx deleted file mode 100644 index 50e5aca23d48..000000000000 --- a/src/app/@modal/(.)settings/modal/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { gerServerDeviceInfo, isMobileDevice } from '@/utils/server/responsive'; - -import SettingsModal from './index'; - -/** - * @description: Settings Modal (intercepting route: /settings/modal ) - * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942 - */ - -const Page = async () => { - const isMobile = await isMobileDevice(); - const { os, browser } = await gerServerDeviceInfo(); - - return ; -}; - -Page.displayName = 'SettingModal'; - -export default Page; diff --git a/src/config/app.ts b/src/config/app.ts index fef3ca3a4b37..242a01151396 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -47,6 +47,8 @@ export const getAppConfig = () => { PLUGIN_SETTINGS: z.string().optional(), APP_URL: z.string().optional(), + VERCEL_EDGE_CONFIG: z.string().optional(), + CDN_USE_GLOBAL: z.boolean().optional(), CUSTOM_FONT_FAMILY: z.string().optional(), CUSTOM_FONT_URL: z.string().optional(), @@ -75,6 +77,8 @@ export const getAppConfig = () => { PLUGIN_SETTINGS: process.env.PLUGIN_SETTINGS, + VERCEL_EDGE_CONFIG: process.env.VERCEL_EDGE_CONFIG, + APP_URL, CUSTOM_FONT_FAMILY: process.env.CUSTOM_FONT_FAMILY, CUSTOM_FONT_URL: process.env.CUSTOM_FONT_URL, diff --git a/src/features/MobileTabBar/index.tsx b/src/features/MobileTabBar/index.tsx index e87935dffd3f..88398c711e93 100644 --- a/src/features/MobileTabBar/index.tsx +++ b/src/features/MobileTabBar/index.tsx @@ -6,7 +6,6 @@ import { rgba } from 'polished'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useOpenSettings } from '@/hooks/useInterceptingRoutes'; import { SidebarTabKey } from '@/store/global/initialState'; import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig'; @@ -26,8 +25,10 @@ interface Props { export default memo(({ className, tabBarKey }) => { const { t } = useTranslation('common'); const { styles } = useStyles(); - const openSettings = useOpenSettings(); const router = useRouter(); + const openSettings = () => { + router.push('/settings/llm'); + }; const { showMarket } = useServerConfigStore(featureFlagsSelectors); const items: MobileTabBarProps['items'] = useMemo( diff --git a/src/features/User/UserAvatar.tsx b/src/features/User/UserAvatar.tsx index 77e90bc283f7..f8e815405ceb 100644 --- a/src/features/User/UserAvatar.tsx +++ b/src/features/User/UserAvatar.tsx @@ -57,8 +57,8 @@ const UserAvatar = forwardRef( return ( void; showBadge?: boolean }) => { + }: PropsWithChildren & { onClick?: () => void; showBadge?: boolean }) => { const { t } = useTranslation('common'); if (!showBadge) return ( @@ -69,10 +64,8 @@ const NewVersionBadge = memo( ); export const useMenu = () => { - const router = useQueryRoute(); const { canInstall, install } = usePWAInstall(); const hasNewVersion = useNewVersion(); - const openSettings = useOpenSettings(); const { t } = useTranslation(['common', 'setting', 'auth']); const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors); const [isLogin, isLoginWithAuth, isLoginWithClerk, openUserProfile] = useUserStore((s) => [ @@ -93,20 +86,12 @@ export const useMenu = () => { const settings: MenuProps['items'] = [ { - extra: ( - router.push(urlJoin('/settings', SettingsTabs.Common))} - size={'small'} - title={t('fullscreen')} - /> - ), icon: , key: 'setting', label: ( - - {t('userPanel.setting')} - + + {t('userPanel.setting')} + ), }, { diff --git a/src/hooks/useInterceptingRoutes.test.ts b/src/hooks/useInterceptingRoutes.test.ts index 8cb3f9ebbcd8..74da683faecb 100644 --- a/src/hooks/useInterceptingRoutes.test.ts +++ b/src/hooks/useInterceptingRoutes.test.ts @@ -8,7 +8,7 @@ import { useGlobalStore } from '@/store/global'; import { ChatSettingsTabs, SettingsTabs, SidebarTabKey } from '@/store/global/initialState'; import { useSessionStore } from '@/store/session'; -import { useOpenChatSettings, useOpenSettings } from './useInterceptingRoutes'; +import { useOpenChatSettings } from './useInterceptingRoutes'; // Mocks vi.mock('next/navigation', () => ({ @@ -32,26 +32,12 @@ vi.mock('@/store/global', () => ({ }, })); -describe('useOpenSettings', () => { - it('should handle mobile route correctly', () => { - vi.mocked(useIsMobile).mockReturnValue(true); - const { result } = renderHook(() => useOpenSettings(SettingsTabs.Common)); - expect(result.current()).toBe('/settings/common'); - }); - - it('should handle desktop route correctly', () => { - vi.mocked(useIsMobile).mockReturnValue(false); - const { result } = renderHook(() => useOpenSettings(SettingsTabs.Agent)); - expect(result.current()).toBe('/settings/modal?tab=agent'); - }); -}); - describe('useOpenChatSettings', () => { it('should handle inbox session id correctly', () => { vi.mocked(useSessionStore).mockReturnValue(INBOX_SESSION_ID); const { result } = renderHook(() => useOpenChatSettings()); - expect(result.current()).toBe('/settings/modal?session=inbox&tab=agent'); // Assuming openSettings returns a function + expect(result.current()).toBe('/settings/agent'); // Assuming openSettings returns a function }); it('should handle mobile route for chat settings', () => { diff --git a/src/hooks/useInterceptingRoutes.ts b/src/hooks/useInterceptingRoutes.ts index 253519ac9ffb..855b8ea581cb 100644 --- a/src/hooks/useInterceptingRoutes.ts +++ b/src/hooks/useInterceptingRoutes.ts @@ -8,24 +8,8 @@ import { useGlobalStore } from '@/store/global'; import { ChatSettingsTabs, SettingsTabs, SidebarTabKey } from '@/store/global/initialState'; import { useSessionStore } from '@/store/session'; -export const useOpenSettings = (tab: SettingsTabs = SettingsTabs.Common) => { - const activeId = useSessionStore((s) => s.activeId); - const router = useQueryRoute(); - const mobile = useIsMobile(); - - return useMemo(() => { - if (mobile) { - return () => router.push(urlJoin('/settings', tab)); - } else { - // use Intercepting Routes on Desktop - return () => router.push('/settings/modal', { query: { session: activeId, tab } }); - } - }, [mobile, tab, activeId, router]); -}; - export const useOpenChatSettings = (tab: ChatSettingsTabs = ChatSettingsTabs.Meta) => { const activeId = useSessionStore((s) => s.activeId); - const openSettings = useOpenSettings(SettingsTabs.Agent); const router = useQueryRoute(); const mobile = useIsMobile(); @@ -34,7 +18,7 @@ export const useOpenChatSettings = (tab: ChatSettingsTabs = ChatSettingsTabs.Met useGlobalStore.setState({ sidebarKey: SidebarTabKey.Setting, }); - return openSettings; + return () => router.push(urlJoin('/settings', SettingsTabs.Agent)); } if (mobile) { return () => router.push('/chat/settings'); @@ -42,5 +26,5 @@ export const useOpenChatSettings = (tab: ChatSettingsTabs = ChatSettingsTabs.Met // use Intercepting Routes on Desktop return () => router.push('/chat/settings/modal', { query: { session: activeId, tab } }); } - }, [openSettings, mobile, activeId, router, tab]); + }, [mobile, activeId, router, tab]); }; diff --git a/src/libs/next-auth/sso-providers/index.ts b/src/libs/next-auth/sso-providers/index.ts index ec9e1682fb84..80e70e441b45 100644 --- a/src/libs/next-auth/sso-providers/index.ts +++ b/src/libs/next-auth/sso-providers/index.ts @@ -8,6 +8,7 @@ import GenericOIDC from './generic-oidc'; import Github from './github'; import Logto from './logto'; import MicrosoftEntraID from './microsoft-entra-id'; +import WeChat from './wechat'; import Zitadel from './zitadel'; export const ssoProviders = [ @@ -22,4 +23,5 @@ export const ssoProviders = [ CloudflareZeroTrust, Casdoor, MicrosoftEntraID, + WeChat, ]; diff --git a/src/libs/next-auth/sso-providers/wechat.ts b/src/libs/next-auth/sso-providers/wechat.ts new file mode 100644 index 000000000000..29727754f0b1 --- /dev/null +++ b/src/libs/next-auth/sso-providers/wechat.ts @@ -0,0 +1,24 @@ +import WeChat from '@auth/core/providers/wechat'; + +import { CommonProviderConfig } from './sso.config'; + +const provider = { + id: 'wechat', + provider: WeChat({ + ...CommonProviderConfig, + clientId: process.env.AUTH_WECHAT_ID, + clientSecret: process.env.AUTH_WECHAT_SECRET, + platformType: 'WebsiteApp', + profile: (profile) => { + return { + email: null, + id: profile.unionid, + image: profile.headimgurl, + name: profile.nickname, + providerAccountId: profile.unionid, + }; + }, + }), +}; + +export default provider; diff --git a/src/server/modules/AssistantStore/index.test.ts b/src/server/modules/AssistantStore/index.test.ts index b4fa94cabc86..3a4e07b18554 100644 --- a/src/server/modules/AssistantStore/index.test.ts +++ b/src/server/modules/AssistantStore/index.test.ts @@ -7,19 +7,19 @@ const baseURL = 'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/p describe('AssistantStore', () => { it('should return the default index URL when no language is provided', () => { const agentMarket = new AssistantStore(); - const url = agentMarket.getAgentIndexUrl(); + const url = agentMarket['getAgentIndexUrl'](); expect(url).toBe(`${baseURL}/index.en-US.json`); }); it('should return the index URL for a not supported language', () => { const agentMarket = new AssistantStore(); - const url = agentMarket.getAgentIndexUrl('xxx' as any); + const url = agentMarket['getAgentIndexUrl']('xxx' as any); expect(url).toBe('https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public'); }); it('should return the zh-CN URL for zh locale', () => { const agentMarket = new AssistantStore(); - const url = agentMarket.getAgentIndexUrl('zh' as any); + const url = agentMarket['getAgentIndexUrl']('zh' as any); expect(url).toBe( 'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public/index.zh-CN.json', ); @@ -27,7 +27,7 @@ describe('AssistantStore', () => { it('should return the default URL for en locale', () => { const agentMarket = new AssistantStore(); - const url = agentMarket.getAgentIndexUrl('en' as any); + const url = agentMarket['getAgentIndexUrl']('en' as any); expect(url).toBe( 'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public/index.en-US.json', ); @@ -35,7 +35,7 @@ describe('AssistantStore', () => { it('should return the base URL if the provided language is not supported', () => { const agentMarket = new AssistantStore(); - const url = agentMarket.getAgentIndexUrl('fr' as any); + const url = agentMarket['getAgentIndexUrl']('fr' as any); expect(url).toBe(baseURL); }); diff --git a/src/server/modules/AssistantStore/index.ts b/src/server/modules/AssistantStore/index.ts index 313dcece6f13..9abc44fe4e86 100644 --- a/src/server/modules/AssistantStore/index.ts +++ b/src/server/modules/AssistantStore/index.ts @@ -3,6 +3,8 @@ import urlJoin from 'url-join'; import { appEnv } from '@/config/app'; import { DEFAULT_LANG, isLocaleNotSupport } from '@/const/locale'; import { Locales, normalizeLocale } from '@/locales/resources'; +import { EdgeConfig } from '@/server/modules/EdgeConfig'; +import { AgentStoreIndex } from '@/types/discover'; export class AssistantStore { private readonly baseUrl: string; @@ -11,7 +13,7 @@ export class AssistantStore { this.baseUrl = baseUrl || appEnv.AGENTS_INDEX_URL; } - getAgentIndexUrl = (lang: Locales = DEFAULT_LANG) => { + private getAgentIndexUrl = (lang: Locales = DEFAULT_LANG) => { if (isLocaleNotSupport(lang)) return this.baseUrl; return urlJoin(this.baseUrl, `index.${normalizeLocale(lang)}.json`); @@ -22,4 +24,40 @@ export class AssistantStore { return urlJoin(this.baseUrl, `${identifier}.${normalizeLocale(lang)}.json`); }; + + getAgentIndex = async (locale: Locales = DEFAULT_LANG, revalidate?: number) => { + try { + let res: Response; + + res = await fetch(this.getAgentIndexUrl(locale as any), { next: { revalidate } }); + + if (res.status === 404) { + res = await fetch(this.getAgentIndexUrl(DEFAULT_LANG), { next: { revalidate } }); + } + + if (!res.ok) { + console.error('fetch agent index error:', await res.text()); + return []; + } + + const data: AgentStoreIndex = await res.json(); + + // Get the assistant whitelist from Edge Config + const edgeConfig = new EdgeConfig(); + + if (!!appEnv.VERCEL_EDGE_CONFIG) { + const assistantWhitelist = await edgeConfig.getAgentWhitelist(); + + if (assistantWhitelist && assistantWhitelist?.length > 0) { + data.agents = data.agents.filter((item) => assistantWhitelist.includes(item.identifier)); + } + } + + return data; + } catch (e) { + console.error('fetch agent index error:', e); + + throw e; + } + }; } diff --git a/src/server/modules/EdgeConfig/index.ts b/src/server/modules/EdgeConfig/index.ts new file mode 100644 index 000000000000..cdb620b5d0ff --- /dev/null +++ b/src/server/modules/EdgeConfig/index.ts @@ -0,0 +1,23 @@ +import { EdgeConfigClient, createClient } from '@vercel/edge-config'; + +import { appEnv } from '@/config/app'; + +enum EdgeConfigKeys { + /** + * Assistant whitelist + */ + AssistantWhitelist = 'assistant_whitelist', +} + +export class EdgeConfig { + get client(): EdgeConfigClient { + if (!appEnv.VERCEL_EDGE_CONFIG) { + throw new Error('VERCEL_EDGE_CONFIG is not set'); + } + return createClient(appEnv.VERCEL_EDGE_CONFIG); + } + + getAgentWhitelist = async (): Promise => { + return this.client.get(EdgeConfigKeys.AssistantWhitelist); + }; +} diff --git a/src/server/services/discover/index.ts b/src/server/services/discover/index.ts index e4001ee1d60d..c38949a92497 100644 --- a/src/server/services/discover/index.ts +++ b/src/server/services/discover/index.ts @@ -50,20 +50,9 @@ export class DiscoverService { }; getAssistantList = async (locale: Locales): Promise => { - let res = await fetch(this.assistantStore.getAgentIndexUrl(locale), { - next: { revalidate }, - }); - - if (!res.ok) { - res = await fetch(this.assistantStore.getAgentIndexUrl(DEFAULT_LANG), { - next: { revalidate }, - }); - } - - if (!res.ok) return []; - - const json = await res.json(); + const json = await this.assistantStore.getAgentIndex(locale, revalidate); + // @ts-expect-error 目前类型不一致,未来要统一 return json.agents; }; diff --git a/src/types/discover.ts b/src/types/discover.ts index 50fe3b30a12f..8ec0809ef9d3 100644 --- a/src/types/discover.ts +++ b/src/types/discover.ts @@ -154,3 +154,23 @@ export interface FilterBy { token?: number; vision?: boolean; } + +interface AgentIndexItem { + author: string; + createAt: string; + createdAt: string; + homepage: string; + identifier: string; + meta: { + avatar: string; + category: string; + description: string; + tags: string[]; + title: string; + }; +} + +export interface AgentStoreIndex { + agents: AgentIndexItem[]; + schemaVersion: number; +}