diff --git a/.env.example b/.env.example index b102cf93c13e..4ec9439d91c5 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ ACCESS_CODE=lobe66 # add your custom model name, multi model separate by comma. for example gpt-3.5-1106,gpt-4-1106 -# NEXT_PUBLIC_CUSTOM_MODELS=model1,model2,model3 +# CUSTOM_MODELS=model1,model2,model3 # ---- only choose one from OpenAI Service and Azure OpenAI Service ---- # diff --git a/Dockerfile b/Dockerfile index 03f16e8fe1af..c04632819311 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,7 @@ ENV PORT=3210 # General Variables ENV ACCESS_CODE "lobe66" -ENV NEXT_PUBLIC_CUSTOM_MODELS "" +ENV CUSTOM_MODELS "" # OpenAI ENV OPENAI_API_KEY "" diff --git a/README.md b/README.md index 29e4910d8620..eb454c1ad15e 100644 --- a/README.md +++ b/README.md @@ -391,12 +391,12 @@ $ docker run -d -p 3210:3210 \ This project provides some additional configuration items set with environment variables: -| Environment Variable | Required | Description | Example | -| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | Yes | This is the API key you apply on the OpenAI account page | `sk-xxxxxx...xxxxxx` | -| `OPENAI_PROXY_URL` | No | If you manually configure the OpenAI interface proxy, you can use this configuration item to override the default OpenAI API request base URL | `https://api.chatanywhere.cn/v1`
The default value is
`https://api.openai.com/v1` | -| `OPENAI_FUNCTION_REGIONS` | No | When you deploy Lobe-Chat using Vercel and need to specify the region for the Edge Function that handles requests to the OpenAI API, you can use this configuration. The value should be a comma-separated array of strings. | `iad1,sfo1` | -| `ACCESS_CODE` | No | Add a password to access this service; you can set a long password to avoid leaking. If this value contains a comma, it is a password array. | `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` | +| Environment Variable | Required | Description | Example | +| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | Yes | This is the API key you apply on the OpenAI account page | `sk-xxxxxx...xxxxxx` | +| `OPENAI_PROXY_URL` | No | If you manually configure the OpenAI interface proxy, you can use this configuration item to override the default OpenAI API request base URL | `https://api.chatanywhere.cn/v1`
The default value is
`https://api.openai.com/v1` | +| `ACCESS_CODE` | No | Add a password to access this service; you can set a long password to avoid leaking. If this value contains a comma, it is a password array. | `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` | +| `CUSTOM_MODELS` | No | Used to control the model list. Use `+` to add a model, `-` to hide a model, and `model_name=display_name` to customize the display name of a model, separated by commas. | `qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` | > \[!NOTE] > diff --git a/README.zh-CN.md b/README.zh-CN.md index f4079698e5f9..2bd91f05b65c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -363,12 +363,12 @@ $ docker run -d -p 3210:3210 \ 本项目提供了一些额外的配置项,使用环境变量进行设置: -| 环境变量 | 类型 | 描述 | 示例 | -| ------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | 必选 | 这是你在 OpenAI 账户页面申请的 API 密钥 | `sk-xxxxxx...xxxxxx` | -| `OPENAI_PROXY_URL` | 可选 | 如果你手动配置了 OpenAI 接口代理,可以使用此配置项来覆盖默认的 OpenAI API 请求基础 URL | `https://api.chatanywhere.cn/v1`
默认值:
`https://api.openai.com/v1` | -| `OPENAI_FUNCTION_REGIONS` | 可选 | 当你使用 Vercel 部署 Lobe-Chat,而且有需求指定响应调用 OpenAI 接口的请求的 Edge Function 的 Region 时,可以使用该配置进行配置,该值的类型为逗号分隔的字符串数组 | `iad1,sfo1` | -| `ACCESS_CODE` | 可选 | 添加访问此服务的密码,你可以设置一个长密码以防被爆破,该值用逗号分隔时为密码数组 | `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` | +| 环境变量 | 类型 | 描述 | 示例 | +| ------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | 必选 | 这是你在 OpenAI 账户页面申请的 API 密钥 | `sk-xxxxxx...xxxxxx` | +| `OPENAI_PROXY_URL` | 可选 | 如果你手动配置了 OpenAI 接口代理,可以使用此配置项来覆盖默认的 OpenAI API 请求基础 URL | `https://api.chatanywhere.cn/v1`
默认值:
`https://api.openai.com/v1` | +| `ACCESS_CODE` | 可选 | 添加访问此服务的密码,你可以设置一个长密码以防被爆破,该值用逗号分隔时为密码数组 | `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` | +| `CUSTOM_MODELS` | 可选 | 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 | `qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` | > \[!NOTE] > diff --git a/docs/Deployment/Environment-Variable.md b/docs/Deployment/Environment-Variable.md index ce195e617d9c..79b26fae84c8 100644 --- a/docs/Deployment/Environment-Variable.md +++ b/docs/Deployment/Environment-Variable.md @@ -6,7 +6,7 @@ LobeChat provides additional configuration options during deployment, which can - [General Variables](#general-variables) - [`ACCESS_CODE`](#access_code) - - [`NEXT_PUBLIC_CUSTOM_MODELS`](#next_public_custom_models) + - [`CUSTOM_MODELS`](#custom_models) - [OpenAI](#openai) - [`OPENAI_API_KEY`](#openai_api_key) - [`OPENAI_PROXY_URL`](#openai_proxy_url) @@ -31,7 +31,7 @@ LobeChat provides additional configuration options during deployment, which can - Default: `-` - Example: `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` -### `NEXT_PUBLIC_CUSTOM_MODELS` +### `CUSTOM_MODELS` - Type: Optional - Description: Used to control the model list. Use `+` to add a model, `-` to hide a model, and `model_name=display_name` to customize the display name of a model, separated by commas. diff --git a/docs/Deployment/Environment-Variable.zh-CN.md b/docs/Deployment/Environment-Variable.zh-CN.md index 9def8293e5f5..24011fcfc6d6 100644 --- a/docs/Deployment/Environment-Variable.zh-CN.md +++ b/docs/Deployment/Environment-Variable.zh-CN.md @@ -6,7 +6,7 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进 - [通用变量](#通用变量) - [`ACCESS_CODE`](#access_code) - - [`NEXT_PUBLIC_CUSTOM_MODELS`](#next_public_custom_models) + - [`CUSTOM_MODELS`](#custom_models) - [OpenAI](#openai) - [`OPENAI_API_KEY`](#openai_api_key) - [`OPENAI_PROXY_URL`](#openai_proxy_url) @@ -31,7 +31,7 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进 - 默认值:- - 示例:`awCTe)re_r74` or `rtrt_ewee3@09!` -### `NEXT_PUBLIC_CUSTOM_MODELS` +### `CUSTOM_MODELS` - 类型:可选 - 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 diff --git a/next.config.mjs b/next.config.mjs index aace97764136..1c60e8d3d6b7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -19,10 +19,6 @@ const withPWA = nextPWA({ /** @type {import('next').NextConfig} */ const nextConfig = { compress: isProd, - env: { - AGENTS_INDEX_URL: process.env.AGENTS_INDEX_URL ?? '', - PLUGINS_INDEX_URL: process.env.PLUGINS_INDEX_URL ?? '', - }, experimental: { forceSwcTransforms: true, optimizePackageImports: [ diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts new file mode 100644 index 000000000000..61db882bc305 --- /dev/null +++ b/src/app/api/config/route.ts @@ -0,0 +1,15 @@ +import { getServerConfig } from '@/config/server'; +import { GlobalServerConfig } from '@/types/settings'; + +export const runtime = 'edge'; + +/** + * get Server config to client + */ +export const GET = async () => { + const { CUSTOM_MODELS } = getServerConfig(); + + const config: GlobalServerConfig = { customModelName: CUSTOM_MODELS }; + + return new Response(JSON.stringify(config)); +}; diff --git a/src/app/api/market/AgentMarket.ts b/src/app/api/market/AgentMarket.ts new file mode 100644 index 000000000000..35eaf2087828 --- /dev/null +++ b/src/app/api/market/AgentMarket.ts @@ -0,0 +1,25 @@ +import urlJoin from 'url-join'; + +import { getServerConfig } from '@/config/server'; +import { DEFAULT_LANG, checkLang } from '@/const/locale'; +import { Locales } from '@/locales/resources'; + +export class AgentMarket { + private readonly baseUrl: string; + + constructor(baseUrl?: string) { + this.baseUrl = baseUrl || getServerConfig().AGENTS_INDEX_URL; + } + + getAgentIndexUrl = (lang: Locales = DEFAULT_LANG) => { + if (checkLang(lang)) return this.baseUrl; + + return urlJoin(this.baseUrl, `index.${lang}.json`); + }; + + getAgentUrl = (identifier: string, lang: Locales = DEFAULT_LANG) => { + if (checkLang(lang)) return urlJoin(this.baseUrl, `${identifier}.json`); + + return urlJoin(this.baseUrl, `${identifier}.${lang}.json`); + }; +} diff --git a/src/app/api/market/[id]/route.ts b/src/app/api/market/[id]/route.ts index e8185561f0d4..943d6f688352 100644 --- a/src/app/api/market/[id]/route.ts +++ b/src/app/api/market/[id]/route.ts @@ -1,5 +1,6 @@ import { DEFAULT_LANG } from '@/const/locale'; -import { getAgentJSON } from '@/const/url'; + +import { AgentMarket } from '../AgentMarket'; export const runtime = 'edge'; @@ -8,11 +9,13 @@ export const GET = async (req: Request, { params }: { params: { id: string } }) const locale = searchParams.get('locale'); + const market = new AgentMarket(); + let res: Response; - res = await fetch(getAgentJSON(params.id, locale as any)); + res = await fetch(market.getAgentUrl(params.id, locale as any)); if (res.status === 404) { - res = await fetch(getAgentJSON(params.id, DEFAULT_LANG)); + res = await fetch(market.getAgentUrl(params.id, DEFAULT_LANG)); } return res; diff --git a/src/app/api/market/route.ts b/src/app/api/market/route.ts index 933e262b4e5d..4ac0e8868ea4 100644 --- a/src/app/api/market/route.ts +++ b/src/app/api/market/route.ts @@ -1,17 +1,20 @@ import { DEFAULT_LANG } from '@/const/locale'; -import { getAgentIndexJSON } from '@/const/url'; + +import { AgentMarket } from './AgentMarket'; export const runtime = 'edge'; export const GET = async (req: Request) => { const locale = new URL(req.url).searchParams.get('locale'); + const market = new AgentMarket(); + let res: Response; - res = await fetch(getAgentIndexJSON(locale as any)); + res = await fetch(market.getAgentIndexUrl(locale as any)); if (res.status === 404) { - res = await fetch(getAgentIndexJSON(DEFAULT_LANG)); + res = await fetch(market.getAgentIndexUrl(DEFAULT_LANG)); } return res; diff --git a/src/components/FileList/EditableFileList.tsx b/src/components/FileList/EditableFileList.tsx index 37d9193240ad..6fd738bbe83a 100644 --- a/src/components/FileList/EditableFileList.tsx +++ b/src/components/FileList/EditableFileList.tsx @@ -3,7 +3,7 @@ import { useResponsive } from 'antd-style'; import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; -import ImageFileItem from '@/components/FileList/ImageFileItem'; +import ImageFileItem from './ImageFileItem'; interface EditableFileListProps { alwaysShowClose?: boolean; diff --git a/src/components/FileList/FileGrid.tsx b/src/components/FileList/FileGrid.tsx deleted file mode 100644 index 4239d6d93415..000000000000 --- a/src/components/FileList/FileGrid.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { CSSProperties, ReactNode, memo } from 'react'; -import { Flexbox } from 'react-layout-kit'; - -import { MAX_SIZE_DESKTOP, MIN_IMAGE_SIZE, useStyles } from '@/components/FileList/style'; - -interface FileGridProps { - children: ReactNode; - className?: string; - col?: number; - gap?: number; - max?: number; - min?: number; - style?: CSSProperties; -} - -const FileGrid = memo( - ({ - gap = 4, - col = 3, - max = MAX_SIZE_DESKTOP, - min = MIN_IMAGE_SIZE, - children, - className, - style, - }) => { - const { styles, cx } = useStyles({ col, gap, max, min }); - - return ( - - {children} - - ); - }, -); - -export default FileGrid; diff --git a/src/components/FileList/index.tsx b/src/components/FileList/index.tsx index 197c9582dd66..ce83e9e2cd4c 100644 --- a/src/components/FileList/index.tsx +++ b/src/components/FileList/index.tsx @@ -1,11 +1,8 @@ import { ImageGallery } from '@lobehub/ui'; -import { useResponsive } from 'antd-style'; -import { memo, useMemo } from 'react'; -import { Flexbox } from 'react-layout-kit'; +import { memo } from 'react'; -import { MAX_SIZE_DESKTOP, MAX_SIZE_MOBILE } from '@/components/FileList/style'; +import GalleyGrid from '@/components/GalleyGrid'; -import FileGrid from './FileGrid'; import ImageFileItem from './ImageFileItem'; interface FileListProps { @@ -13,48 +10,9 @@ interface FileListProps { } const FileList = memo(({ items }) => { - const { mobile } = useResponsive(); - - const { firstRow, lastRow } = useMemo(() => { - if (items.length === 4) { - return { - firstRow: items.slice(0, 2), - lastRow: items.slice(2, 4), - }; - } - - const firstCol = items.length % 3 === 0 ? 3 : items.length % 3; - - return { - firstRow: items.slice(0, firstCol), - lastRow: items.slice(firstCol, items.length), - }; - }, [items]); - - const { gap, max } = useMemo( - () => ({ - gap: mobile ? 4 : 6, - max: mobile ? MAX_SIZE_MOBILE : MAX_SIZE_DESKTOP, - }), - [mobile], - ); - return ( - - - {firstRow.map((i) => ( - - ))} - - {lastRow.length > 0 && ( - 2 ? 3 : lastRow.length} gap={gap} max={max}> - {lastRow.map((i) => ( - - ))} - - )} - + ); }); diff --git a/src/components/FileList/style.ts b/src/components/FileList/style.ts index 1f8e5f1601e1..1bb5a495a4f7 100644 --- a/src/components/FileList/style.ts +++ b/src/components/FileList/style.ts @@ -1,24 +1 @@ -import { createStyles } from 'antd-style'; - export const MIN_IMAGE_SIZE = 64; -export const MAX_SIZE_DESKTOP = 640; -export const MAX_SIZE_MOBILE = 280; -export const useStyles = createStyles( - ({ css }, { col, gap, max, min }: { col: number; gap: number; max: number; min: number }) => ({ - container: css` - display: grid; - grid-gap: ${gap}px; - grid-template-columns: repeat(${col}, 1fr); - - width: 100%; - max-width: ${max}px; - - & > div { - width: 100%; - min-width: ${min}px; - min-height: ${min}px; - max-height: ${(max - gap * (col - 1)) / col}px; - } - `, - }), -); diff --git a/src/config/__tests__/client.test.ts b/src/config/__tests__/client.test.ts index 84ce92b4943e..0399e2723509 100644 --- a/src/config/__tests__/client.test.ts +++ b/src/config/__tests__/client.test.ts @@ -9,29 +9,6 @@ vi.stubGlobal('process', { }); describe('getClientConfig', () => { - it('should return default URLs when no environment variables are set', () => { - const config = getClientConfig(); - expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com'); - expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com'); - }); - - it('should return custom URLs when environment variables are set', () => { - process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com'; - process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com'; - const config = getClientConfig(); - expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com'); - expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com'); - }); - - it('should return default URLs when environment variables are empty string', () => { - process.env.AGENTS_INDEX_URL = ''; - process.env.PLUGINS_INDEX_URL = ''; - - const config = getClientConfig(); - expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com'); - expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com'); - }); - it('should correctly reflect boolean values for analytics flags', () => { process.env.NEXT_PUBLIC_ANALYTICS_VERCEL = '1'; process.env.NEXT_PUBLIC_VERCEL_DEBUG = '1'; diff --git a/src/config/__tests__/server.test.ts b/src/config/__tests__/server.test.ts index bda70cfcace4..21c31bec6903 100644 --- a/src/config/__tests__/server.test.ts +++ b/src/config/__tests__/server.test.ts @@ -21,7 +21,7 @@ describe('getServerConfig', () => { global.process = undefined; expect(() => getServerConfig()).toThrow( - '[Server Config] you are importing a nodejs-only module outside of nodejs', + '[Server Config] you are importing a server-only module outside of server', ); global.process = originalProcess; // Restore the original process object @@ -55,4 +55,29 @@ describe('getServerConfig', () => { const config = getServerConfig(); expect(config.IMGUR_CLIENT_ID).toBe('custom-client-id'); }); + + describe('index url', () => { + it('should return default URLs when no environment variables are set', () => { + const config = getServerConfig(); + expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com'); + expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com'); + }); + + it('should return custom URLs when environment variables are set', () => { + process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com'; + process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com'; + const config = getServerConfig(); + expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com'); + expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com'); + }); + + it('should return default URLs when environment variables are empty string', () => { + process.env.AGENTS_INDEX_URL = ''; + process.env.PLUGINS_INDEX_URL = ''; + + const config = getServerConfig(); + expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com'); + expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com'); + }); + }); }); diff --git a/src/config/client.ts b/src/config/client.ts index f770496a5356..abc9348edf0b 100644 --- a/src/config/client.ts +++ b/src/config/client.ts @@ -1,14 +1,13 @@ +/** + * the client config is only used in Vercel deployment + */ + /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace NodeJS { interface ProcessEnv { - AGENTS_INDEX_URL?: string; - PLUGINS_INDEX_URL?: string; - - NEXT_PUBLIC_CUSTOM_MODELS?: string; - NEXT_PUBLIC_ANALYTICS_VERCEL?: string; NEXT_PUBLIC_VERCEL_DEBUG?: string; @@ -35,16 +34,6 @@ declare global { } export const getClientConfig = () => ({ - AGENTS_INDEX_URL: !!process.env.AGENTS_INDEX_URL - ? process.env.AGENTS_INDEX_URL - : 'https://chat-agents.lobehub.com', - PLUGINS_INDEX_URL: !!process.env.PLUGINS_INDEX_URL - ? process.env.PLUGINS_INDEX_URL - : 'https://chat-plugins.lobehub.com', - - // custom model names - CUSTOM_MODELS: process.env.NEXT_PUBLIC_CUSTOM_MODELS, - // Vercel Analytics ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL === '1', VERCEL_DEBUG: process.env.NEXT_PUBLIC_VERCEL_DEBUG === '1', diff --git a/src/config/server.ts b/src/config/server.ts index d068f182a0b6..673d3f9efb78 100644 --- a/src/config/server.ts +++ b/src/config/server.ts @@ -5,6 +5,7 @@ declare global { namespace NodeJS { interface ProcessEnv { ACCESS_CODE?: string; + CUSTOM_MODELS?: string; OPENAI_API_KEY?: string; OPENAI_PROXY_URL?: string; @@ -14,6 +15,9 @@ declare global { USE_AZURE_OPENAI?: string; IMGUR_CLIENT_ID?: string; + + AGENTS_INDEX_URL?: string; + PLUGINS_INDEX_URL?: string; } } } @@ -24,7 +28,7 @@ const DEFAULT_IMAGUR_CLIENT_ID = 'e415f320d6e24f9'; export const getServerConfig = () => { if (typeof process === 'undefined') { - throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs'); + throw new Error('[Server Config] you are importing a server-only module outside of server'); } // region format: iad1,sfo1 @@ -37,6 +41,7 @@ export const getServerConfig = () => { return { ACCESS_CODES, + CUSTOM_MODELS: process.env.CUSTOM_MODELS, SHOW_ACCESS_CODE_CONFIG: !!ACCESS_CODES.length, @@ -49,5 +54,13 @@ export const getServerConfig = () => { USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1', IMGUR_CLIENT_ID: process.env.IMGUR_CLIENT_ID || DEFAULT_IMAGUR_CLIENT_ID, + + AGENTS_INDEX_URL: !!process.env.AGENTS_INDEX_URL + ? process.env.AGENTS_INDEX_URL + : 'https://chat-agents.lobehub.com', + + PLUGINS_INDEX_URL: !!process.env.PLUGINS_INDEX_URL + ? process.env.PLUGINS_INDEX_URL + : 'https://chat-plugins.lobehub.com', }; }; diff --git a/src/const/settings.ts b/src/const/settings.ts index 6fd1eca950ba..e53cd9e6e081 100644 --- a/src/const/settings.ts +++ b/src/const/settings.ts @@ -1,4 +1,3 @@ -import { getClientConfig } from '@/config/client'; import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm'; import { DEFAULT_AGENT_META } from '@/const/meta'; import { LobeAgentConfig, LobeAgentTTSConfig } from '@/types/agent'; @@ -52,8 +51,6 @@ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = { export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = { openAI: { OPENAI_API_KEY: '', - // support user custom model names with env var - customModelName: getClientConfig().CUSTOM_MODELS, models: DEFAULT_OPENAI_MODEL_LIST, }, }; diff --git a/src/const/url.ts b/src/const/url.ts index 4b5ec0298f05..8847bb4491ce 100644 --- a/src/const/url.ts +++ b/src/const/url.ts @@ -1,6 +1,5 @@ import urlJoin from 'url-join'; -import { getClientConfig } from '@/config/client'; import { Locales } from '@/locales/resources'; import pkg from '../../package.json'; @@ -15,7 +14,7 @@ export const ABOUT = pkg.homepage; export const FEEDBACK = pkg.bugs.url; export const DISCORD = 'https://discord.gg/AYFPHvv2jT'; -export const { PLUGINS_INDEX_URL, AGENTS_INDEX_URL } = getClientConfig(); +export const PLUGINS_INDEX_URL = 'https://chat-plugins.lobehub.com'; export const getPluginIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = PLUGINS_INDEX_URL) => { if (checkLang(lang)) return baseUrl; @@ -23,22 +22,6 @@ export const getPluginIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = PLUGI return urlJoin(baseUrl, `index.${lang}.json`); }; -export const getAgentIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = AGENTS_INDEX_URL) => { - if (checkLang(lang)) return baseUrl; - - return urlJoin(baseUrl, `index.${lang}.json`); -}; - -export const getAgentJSON = ( - identifier: string, - lang: Locales = DEFAULT_LANG, - baseUrl = AGENTS_INDEX_URL, -) => { - if (checkLang(lang)) return urlJoin(baseUrl, `${identifier}.json`); - - return urlJoin(baseUrl, `${identifier}.${lang}.json`); -}; - export const AGENTS_INDEX_GITHUB = 'https://github.com/lobehub/lobe-chat-agents'; export const AGENTS_INDEX_GITHUB_ISSUE = urlJoin(AGENTS_INDEX_GITHUB, 'issues/new'); diff --git a/src/layout/GlobalLayout/StoreHydration.tsx b/src/layout/GlobalLayout/StoreHydration.tsx index 89d87b97f2d9..f58bdc6488d2 100644 --- a/src/layout/GlobalLayout/StoreHydration.tsx +++ b/src/layout/GlobalLayout/StoreHydration.tsx @@ -8,12 +8,16 @@ import { useEffectAfterSessionHydrated, useSessionStore } from '@/store/session' const StoreHydration = memo(() => { const router = useRouter(); + const useFetchGlobalConfig = useGlobalStore((s) => s.useFetchGlobalConfig); + useEffect(() => { // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated useSessionStore.persist.rehydrate(); useGlobalStore.persist.rehydrate(); }, []); + useFetchGlobalConfig(); + const { mobile } = useResponsive(); useEffectAfterSessionHydrated( @@ -29,6 +33,7 @@ const StoreHydration = memo(() => { }, [router], ); + useEffect(() => { router.prefetch('/chat'); router.prefetch('/market'); diff --git a/src/services/__tests__/chat.test.ts b/src/services/__tests__/chat.test.ts index 6fe0874c1c2c..4d137bef2e1e 100644 --- a/src/services/__tests__/chat.test.ts +++ b/src/services/__tests__/chat.test.ts @@ -1,6 +1,6 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; import { act } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { VISION_MODEL_WHITE_LIST } from '@/const/llm'; import { DEFAULT_AGENT_CONFIG } from '@/const/settings'; diff --git a/src/services/__tests__/global.test.ts b/src/services/__tests__/global.test.ts index 897b9ff79691..41d49aa6b0f4 100644 --- a/src/services/__tests__/global.test.ts +++ b/src/services/__tests__/global.test.ts @@ -9,49 +9,66 @@ beforeEach(() => { }); describe('GlobalService', () => { - it('should return the latest version when fetch is successful', async () => { - // Arrange - const mockVersion = '1.0.0'; - (fetch as Mock).mockResolvedValue({ - json: () => Promise.resolve({ 'dist-tags': { latest: mockVersion } }), + describe('getLatestVersion', () => { + it('should return the latest version when fetch is successful', async () => { + // Arrange + const mockVersion = '1.0.0'; + (fetch as Mock).mockResolvedValue({ + json: () => Promise.resolve({ 'dist-tags': { latest: mockVersion } }), + }); + + // Act + const version = await globalService.getLatestVersion(); + + // Assert + expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat'); + expect(version).toBe(mockVersion); }); - // Act - const version = await globalService.getLatestVersion(); + it('should return undefined if the latest version is not found in the response', async () => { + // Arrange + (fetch as Mock).mockResolvedValue({ + json: () => Promise.resolve({}), + }); - // Assert - expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat'); - expect(version).toBe(mockVersion); - }); + // Act + const version = await globalService.getLatestVersion(); - it('should return undefined if the latest version is not found in the response', async () => { - // Arrange - (fetch as Mock).mockResolvedValue({ - json: () => Promise.resolve({}), + // Assert + expect(version).toBeUndefined(); }); - // Act - const version = await globalService.getLatestVersion(); + it('should throw an error when the fetch call fails', async () => { + // Arrange + (fetch as Mock).mockRejectedValue(new Error('Network error')); - // Assert - expect(version).toBeUndefined(); - }); + // Act & Assert + await expect(globalService.getLatestVersion()).rejects.toThrow('Network error'); + }); - it('should throw an error when the fetch call fails', async () => { - // Arrange - (fetch as Mock).mockRejectedValue(new Error('Network error')); + it('should handle non-JSON responses gracefully', async () => { + // Arrange + (fetch as Mock).mockResolvedValue({ + json: () => Promise.reject(new SyntaxError('Unexpected token < in JSON at position 0')), + }); - // Act & Assert - await expect(globalService.getLatestVersion()).rejects.toThrow('Network error'); + // Act & Assert + await expect(globalService.getLatestVersion()).rejects.toThrow(SyntaxError); + }); }); - it('should handle non-JSON responses gracefully', async () => { - // Arrange - (fetch as Mock).mockResolvedValue({ - json: () => Promise.reject(new SyntaxError('Unexpected token < in JSON at position 0')), - }); + describe('ServerConfig', () => { + it('should return the serverConfig when fetch is successful', async () => { + // Arrange + (fetch as Mock).mockResolvedValue({ + json: () => Promise.resolve({ customModelName: 'abc' }), + }); - // Act & Assert - await expect(globalService.getLatestVersion()).rejects.toThrow(SyntaxError); + // Act + const config = await globalService.getGlobalConfig(); + + // Assert + expect(config).toEqual({ customModelName: 'abc' }); + }); }); }); diff --git a/src/services/_url.ts b/src/services/_url.ts index 8aad8796e47d..a827f89b2059 100644 --- a/src/services/_url.ts +++ b/src/services/_url.ts @@ -1,4 +1,5 @@ export const URLS = { + config: '/api/config', market: '/api/market', plugins: '/api/plugins', }; diff --git a/src/services/agentMarket.ts b/src/services/agentMarket.ts deleted file mode 100644 index f9e6d4271fdb..000000000000 --- a/src/services/agentMarket.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { URLS } from '@/services/_url'; -import { LobeChatAgentsMarketIndex } from '@/types/market'; - -/** - * 请求助手列表 - */ -export const getAgentList = async (locale: string): Promise => { - const res = await fetch(`${URLS.market}?locale=${locale}`); - - return res.json(); -}; - -/** - * 请求助手 manifest - */ -export const getAgentManifest = async (identifier: string, locale: string) => { - if (!identifier) return; - const res = await fetch(`${URLS.market}/${identifier}?locale=${locale}`); - - return res.json(); -}; diff --git a/src/services/global.ts b/src/services/global.ts index 182f49a7899f..a661728c522f 100644 --- a/src/services/global.ts +++ b/src/services/global.ts @@ -1,3 +1,6 @@ +import { URLS } from '@/services/_url'; +import { GlobalServerConfig } from '@/types/settings'; + const VERSION_URL = 'https://registry.npmmirror.com/@lobehub/chat'; class GlobalService { @@ -10,6 +13,12 @@ class GlobalService { return data['dist-tags']?.latest; }; + + getGlobalConfig = async (): Promise => { + const res = await fetch(URLS.config); + + return res.json(); + }; } export const globalService = new GlobalService(); diff --git a/src/services/market.ts b/src/services/market.ts new file mode 100644 index 000000000000..5374767131b8 --- /dev/null +++ b/src/services/market.ts @@ -0,0 +1,21 @@ +import { URLS } from '@/services/_url'; +import { LobeChatAgentsMarketIndex } from '@/types/market'; + +class MarketService { + getAgentList = async (locale: string): Promise => { + const res = await fetch(`${URLS.market}?locale=${locale}`); + + return res.json(); + }; + + /** + * 请求助手 manifest + */ + getAgentManifest = async (identifier: string, locale: string) => { + if (!identifier) return; + const res = await fetch(`${URLS.market}/${identifier}?locale=${locale}`); + + return res.json(); + }; +} +export const marketService = new MarketService(); diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 136510f30ad8..0ab951914b07 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -1,5 +1,5 @@ import { DEFAULT_SETTINGS } from '@/const/settings'; -import type { GlobalSettings } from '@/types/settings'; +import type { GlobalServerConfig, GlobalSettings } from '@/types/settings'; export enum SidebarTabKey { Chat = 'chat', @@ -38,6 +38,7 @@ export interface GlobalState { * @localStorage */ preference: GlobalPreference; + serverConfig: GlobalServerConfig; /** * @localStorage * 用户设置 @@ -58,6 +59,7 @@ export const initialState: GlobalState = { showSessionPanel: true, showSystemRole: false, }, + serverConfig: {}, settings: DEFAULT_SETTINGS, settingsTab: SettingsTabs.Common, sidebarKey: SidebarTabKey.Chat, diff --git a/src/store/global/selectors/__snapshots__/settings.test.ts.snap b/src/store/global/selectors/__snapshots__/settings.test.ts.snap index 6ab0d61aaece..5c50b28de2f0 100644 --- a/src/store/global/selectors/__snapshots__/settings.test.ts.snap +++ b/src/store/global/selectors/__snapshots__/settings.test.ts.snap @@ -136,7 +136,6 @@ exports[`settingsSelectors > currentSettings > should merge DEFAULT_SETTINGS and "languageModel": { "openAI": { "OPENAI_API_KEY": "openai-api-key", - "customModelName": undefined, "endpoint": "https://openai-endpoint.com", "models": [ "gpt-3.5-turbo", diff --git a/src/store/global/selectors/settings.test.ts b/src/store/global/selectors/settings.test.ts index fb9aba4d40a1..079ce97a1a85 100644 --- a/src/store/global/selectors/settings.test.ts +++ b/src/store/global/selectors/settings.test.ts @@ -61,12 +61,13 @@ describe('settingsSelectors', () => { describe('CUSTOM_MODELS', () => { it('custom deletion, addition, and renaming of models', () => { const s = { + serverConfig: { + customModelName: + '-all,+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo,gpt-4-1106-preview=gpt-4-32k', + }, settings: { languageModel: { - openAI: { - customModelName: - '-all,+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo,gpt-4-1106-preview=gpt-4-32k', - }, + openAI: {}, }, }, } as unknown as GlobalStore; @@ -78,6 +79,7 @@ describe('settingsSelectors', () => { it('duplicate naming model', () => { const s = { + serverConfig: {}, settings: { languageModel: { openAI: { @@ -94,6 +96,7 @@ describe('settingsSelectors', () => { it('only add the model', () => { const s = { + serverConfig: {}, settings: { languageModel: { openAI: { diff --git a/src/store/global/selectors/settings.ts b/src/store/global/selectors/settings.ts index 3979a25085d6..4cd939664300 100644 --- a/src/store/global/selectors/settings.ts +++ b/src/store/global/selectors/settings.ts @@ -34,6 +34,7 @@ const modelListSelectors = (s: GlobalStore) => { const modelNames = [ ...DEFAULT_OPENAI_MODEL_LIST, + ...(s.serverConfig.customModelName || '').split(/[,,]/).filter(Boolean), ...(s.settings.languageModel.openAI.customModelName || '').split(/[,,]/).filter(Boolean), ]; diff --git a/src/store/global/slices/common.ts b/src/store/global/slices/common.ts index 309d64bfe4fa..d0c1b6fb788e 100644 --- a/src/store/global/slices/common.ts +++ b/src/store/global/slices/common.ts @@ -5,6 +5,7 @@ import type { StateCreator } from 'zustand/vanilla'; import { CURRENT_VERSION } from '@/const/version'; import { globalService } from '@/services/global'; +import type { GlobalServerConfig } from '@/types/settings'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; @@ -28,6 +29,7 @@ export interface CommonAction { updateGuideState: (guide: Partial) => void; updatePreference: (preference: Partial, action?: string) => void; useCheckLatestVersion: () => SWRResponse; + useFetchGlobalConfig: () => SWRResponse; } export const createCommonSlice: StateCreator< @@ -78,4 +80,11 @@ export const createCommonSlice: StateCreator< set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion')); }, }), + useFetchGlobalConfig: () => + useSWR('fetchGlobalConfig', globalService.getGlobalConfig, { + onSuccess: (data) => { + if (data) set({ serverConfig: data }); + }, + revalidateOnFocus: false, + }), }); diff --git a/src/store/market/action.ts b/src/store/market/action.ts index 4e5a55dbaee2..3ced8e9947fb 100644 --- a/src/store/market/action.ts +++ b/src/store/market/action.ts @@ -3,7 +3,7 @@ import { produce } from 'immer'; import useSWR, { SWRResponse } from 'swr'; import type { StateCreator } from 'zustand/vanilla'; -import { getAgentList, getAgentManifest } from '@/services/agentMarket'; +import { marketService } from '@/services/market'; import { globalHelpers } from '@/store/global/helpers'; import { AgentsMarketItem, LobeChatAgentsMarketIndex } from '@/types/market'; @@ -47,7 +47,7 @@ export const createMarketAction: StateCreator< useFetchAgent: (identifier) => useSWR( [identifier, globalHelpers.getCurrentLanguage()], - ([id, locale]) => getAgentManifest(id, locale as string), + ([id, locale]) => marketService.getAgentManifest(id, locale as string), { onError: () => { get().deactivateAgent(); @@ -58,13 +58,17 @@ export const createMarketAction: StateCreator< }, ), useFetchAgentList: () => - useSWR(globalHelpers.getCurrentLanguage(), getAgentList, { - onSuccess: (agentMarketIndex) => { - set( - { agentList: agentMarketIndex.agents, tagList: agentMarketIndex.tags }, - false, - 'useFetchAgentList', - ); + useSWR( + globalHelpers.getCurrentLanguage(), + marketService.getAgentList, + { + onSuccess: (agentMarketIndex) => { + set( + { agentList: agentMarketIndex.agents, tagList: agentMarketIndex.tags }, + false, + 'useFetchAgentList', + ); + }, }, - }), + ), }); diff --git a/src/types/settings.ts b/src/types/settings.ts index 8b3b469743fc..771c746b3888 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -91,3 +91,7 @@ export interface GlobalSettings extends GlobalBaseSettings { } export type ConfigKeys = keyof GlobalSettings; + +export interface GlobalServerConfig { + customModelName?: string; +}