@lobehub/chat 0.115.1 → 0.115.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.env.example +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/Dockerfile +1 -1
  4. package/README.md +6 -6
  5. package/README.zh-CN.md +6 -6
  6. package/docs/Deployment/Environment-Variable.md +2 -2
  7. package/docs/Deployment/Environment-Variable.zh-CN.md +2 -2
  8. package/next.config.mjs +0 -4
  9. package/package.json +1 -1
  10. package/src/app/api/config/route.ts +15 -0
  11. package/src/app/api/market/AgentMarket.ts +25 -0
  12. package/src/app/api/market/[id]/route.ts +6 -3
  13. package/src/app/api/market/route.ts +6 -3
  14. package/src/components/FileList/EditableFileList.tsx +1 -1
  15. package/src/components/FileList/index.tsx +3 -45
  16. package/src/components/FileList/style.ts +0 -23
  17. package/src/config/__tests__/client.test.ts +0 -23
  18. package/src/config/__tests__/server.test.ts +26 -1
  19. package/src/config/client.ts +4 -15
  20. package/src/config/server.ts +14 -1
  21. package/src/const/settings.ts +0 -3
  22. package/src/const/url.ts +1 -18
  23. package/src/layout/GlobalLayout/StoreHydration.tsx +5 -0
  24. package/src/services/__tests__/chat.test.ts +1 -1
  25. package/src/services/__tests__/global.test.ts +49 -32
  26. package/src/services/_url.ts +1 -0
  27. package/src/services/global.ts +9 -0
  28. package/src/services/market.ts +21 -0
  29. package/src/store/global/initialState.ts +3 -1
  30. package/src/store/global/selectors/__snapshots__/settings.test.ts.snap +0 -1
  31. package/src/store/global/selectors/settings.test.ts +7 -4
  32. package/src/store/global/selectors/settings.ts +1 -0
  33. package/src/store/global/slices/common.ts +9 -0
  34. package/src/store/market/action.ts +14 -10
  35. package/src/types/settings.ts +4 -0
  36. package/src/components/FileList/FileGrid.tsx +0 -36
  37. package/src/services/agentMarket.ts +0 -21
package/.env.example CHANGED
@@ -2,7 +2,7 @@
2
2
  ACCESS_CODE=lobe66
3
3
 
4
4
  # add your custom model name, multi model separate by comma. for example gpt-3.5-1106,gpt-4-1106
5
- # NEXT_PUBLIC_CUSTOM_MODELS=model1,model2,model3
5
+ # CUSTOM_MODELS=model1,model2,model3
6
6
 
7
7
  # ---- only choose one from OpenAI Service and Azure OpenAI Service ---- #
8
8
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.115.2](https://github.com/lobehub/lobe-chat/compare/v0.115.1...v0.115.2)
6
+
7
+ <sup>Released on **2023-12-23**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix envs like `CUSTOM_MODELS` don't work with docker deployment.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix envs like `CUSTOM_MODELS` don't work with docker deployment, closes [#773](https://github.com/lobehub/lobe-chat/issues/773) ([54dc18b](https://github.com/lobehub/lobe-chat/commit/54dc18b))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 0.115.1](https://github.com/lobehub/lobe-chat/compare/v0.115.0...v0.115.1)
6
31
 
7
32
  <sup>Released on **2023-12-22**</sup>
package/Dockerfile CHANGED
@@ -58,7 +58,7 @@ ENV PORT=3210
58
58
 
59
59
  # General Variables
60
60
  ENV ACCESS_CODE "lobe66"
61
- ENV NEXT_PUBLIC_CUSTOM_MODELS ""
61
+ ENV CUSTOM_MODELS ""
62
62
 
63
63
  # OpenAI
64
64
  ENV OPENAI_API_KEY ""
package/README.md CHANGED
@@ -391,12 +391,12 @@ $ docker run -d -p 3210:3210 \
391
391
 
392
392
  This project provides some additional configuration items set with environment variables:
393
393
 
394
- | Environment Variable | Required | Description | Example |
395
- | ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
396
- | `OPENAI_API_KEY` | Yes | This is the API key you apply on the OpenAI account page | `sk-xxxxxx...xxxxxx` |
397
- | `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`<br/>The default value is<br/>`https://api.openai.com/v1` |
398
- | `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` |
399
- | `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` |
394
+ | Environment Variable | Required | Description | Example |
395
+ | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
396
+ | `OPENAI_API_KEY` | Yes | This is the API key you apply on the OpenAI account page | `sk-xxxxxx...xxxxxx` |
397
+ | `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`<br/>The default value is<br/>`https://api.openai.com/v1` |
398
+ | `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` |
399
+ | `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` |
400
400
 
401
401
  > \[!NOTE]
402
402
  >
package/README.zh-CN.md CHANGED
@@ -363,12 +363,12 @@ $ docker run -d -p 3210:3210 \
363
363
 
364
364
  本项目提供了一些额外的配置项,使用环境变量进行设置:
365
365
 
366
- | 环境变量 | 类型 | 描述 | 示例 |
367
- | ------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
368
- | `OPENAI_API_KEY` | 必选 | 这是你在 OpenAI 账户页面申请的 API 密钥 | `sk-xxxxxx...xxxxxx` |
369
- | `OPENAI_PROXY_URL` | 可选 | 如果你手动配置了 OpenAI 接口代理,可以使用此配置项来覆盖默认的 OpenAI API 请求基础 URL | `https://api.chatanywhere.cn/v1`<br/>默认值:<br/>`https://api.openai.com/v1` |
370
- | `OPENAI_FUNCTION_REGIONS` | 可选 | 当你使用 Vercel 部署 Lobe-Chat,而且有需求指定响应调用 OpenAI 接口的请求的 Edge Function 的 Region 时,可以使用该配置进行配置,该值的类型为逗号分隔的字符串数组 | `iad1,sfo1` |
371
- | `ACCESS_CODE` | 可选 | 添加访问此服务的密码,你可以设置一个长密码以防被爆破,该值用逗号分隔时为密码数组 | `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` |
366
+ | 环境变量 | 类型 | 描述 | 示例 |
367
+ | ------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
368
+ | `OPENAI_API_KEY` | 必选 | 这是你在 OpenAI 账户页面申请的 API 密钥 | `sk-xxxxxx...xxxxxx` |
369
+ | `OPENAI_PROXY_URL` | 可选 | 如果你手动配置了 OpenAI 接口代理,可以使用此配置项来覆盖默认的 OpenAI API 请求基础 URL | `https://api.chatanywhere.cn/v1`<br/>默认值:<br/>`https://api.openai.com/v1` |
370
+ | `ACCESS_CODE` | 可选 | 添加访问此服务的密码,你可以设置一个长密码以防被爆破,该值用逗号分隔时为密码数组 | `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3` |
371
+ | `CUSTOM_MODELS` | 可选 | 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 | `qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` |
372
372
 
373
373
  > \[!NOTE]
374
374
  >
@@ -6,7 +6,7 @@ LobeChat provides additional configuration options during deployment, which can
6
6
 
7
7
  - [General Variables](#general-variables)
8
8
  - [`ACCESS_CODE`](#access_code)
9
- - [`NEXT_PUBLIC_CUSTOM_MODELS`](#next_public_custom_models)
9
+ - [`CUSTOM_MODELS`](#custom_models)
10
10
  - [OpenAI](#openai)
11
11
  - [`OPENAI_API_KEY`](#openai_api_key)
12
12
  - [`OPENAI_PROXY_URL`](#openai_proxy_url)
@@ -31,7 +31,7 @@ LobeChat provides additional configuration options during deployment, which can
31
31
  - Default: `-`
32
32
  - Example: `awCTe)re_r74` or `rtrt_ewee3@09!` or `code1,code2,code3`
33
33
 
34
- ### `NEXT_PUBLIC_CUSTOM_MODELS`
34
+ ### `CUSTOM_MODELS`
35
35
 
36
36
  - Type: Optional
37
37
  - 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.
@@ -6,7 +6,7 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进
6
6
 
7
7
  - [通用变量](#通用变量)
8
8
  - [`ACCESS_CODE`](#access_code)
9
- - [`NEXT_PUBLIC_CUSTOM_MODELS`](#next_public_custom_models)
9
+ - [`CUSTOM_MODELS`](#custom_models)
10
10
  - [OpenAI](#openai)
11
11
  - [`OPENAI_API_KEY`](#openai_api_key)
12
12
  - [`OPENAI_PROXY_URL`](#openai_proxy_url)
@@ -31,7 +31,7 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进
31
31
  - 默认值:-
32
32
  - 示例:`awCTe)re_r74` or `rtrt_ewee3@09!`
33
33
 
34
- ### `NEXT_PUBLIC_CUSTOM_MODELS`
34
+ ### `CUSTOM_MODELS`
35
35
 
36
36
  - 类型:可选
37
37
  - 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
package/next.config.mjs CHANGED
@@ -19,10 +19,6 @@ const withPWA = nextPWA({
19
19
  /** @type {import('next').NextConfig} */
20
20
  const nextConfig = {
21
21
  compress: isProd,
22
- env: {
23
- AGENTS_INDEX_URL: process.env.AGENTS_INDEX_URL ?? '',
24
- PLUGINS_INDEX_URL: process.env.PLUGINS_INDEX_URL ?? '',
25
- },
26
22
  experimental: {
27
23
  forceSwcTransforms: true,
28
24
  optimizePackageImports: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.115.1",
3
+ "version": "0.115.2",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,15 @@
1
+ import { getServerConfig } from '@/config/server';
2
+ import { GlobalServerConfig } from '@/types/settings';
3
+
4
+ export const runtime = 'edge';
5
+
6
+ /**
7
+ * get Server config to client
8
+ */
9
+ export const GET = async () => {
10
+ const { CUSTOM_MODELS } = getServerConfig();
11
+
12
+ const config: GlobalServerConfig = { customModelName: CUSTOM_MODELS };
13
+
14
+ return new Response(JSON.stringify(config));
15
+ };
@@ -0,0 +1,25 @@
1
+ import urlJoin from 'url-join';
2
+
3
+ import { getServerConfig } from '@/config/server';
4
+ import { DEFAULT_LANG, checkLang } from '@/const/locale';
5
+ import { Locales } from '@/locales/resources';
6
+
7
+ export class AgentMarket {
8
+ private readonly baseUrl: string;
9
+
10
+ constructor(baseUrl?: string) {
11
+ this.baseUrl = baseUrl || getServerConfig().AGENTS_INDEX_URL;
12
+ }
13
+
14
+ getAgentIndexUrl = (lang: Locales = DEFAULT_LANG) => {
15
+ if (checkLang(lang)) return this.baseUrl;
16
+
17
+ return urlJoin(this.baseUrl, `index.${lang}.json`);
18
+ };
19
+
20
+ getAgentUrl = (identifier: string, lang: Locales = DEFAULT_LANG) => {
21
+ if (checkLang(lang)) return urlJoin(this.baseUrl, `${identifier}.json`);
22
+
23
+ return urlJoin(this.baseUrl, `${identifier}.${lang}.json`);
24
+ };
25
+ }
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_LANG } from '@/const/locale';
2
- import { getAgentJSON } from '@/const/url';
2
+
3
+ import { AgentMarket } from '../AgentMarket';
3
4
 
4
5
  export const runtime = 'edge';
5
6
 
@@ -8,11 +9,13 @@ export const GET = async (req: Request, { params }: { params: { id: string } })
8
9
 
9
10
  const locale = searchParams.get('locale');
10
11
 
12
+ const market = new AgentMarket();
13
+
11
14
  let res: Response;
12
15
 
13
- res = await fetch(getAgentJSON(params.id, locale as any));
16
+ res = await fetch(market.getAgentUrl(params.id, locale as any));
14
17
  if (res.status === 404) {
15
- res = await fetch(getAgentJSON(params.id, DEFAULT_LANG));
18
+ res = await fetch(market.getAgentUrl(params.id, DEFAULT_LANG));
16
19
  }
17
20
 
18
21
  return res;
@@ -1,17 +1,20 @@
1
1
  import { DEFAULT_LANG } from '@/const/locale';
2
- import { getAgentIndexJSON } from '@/const/url';
2
+
3
+ import { AgentMarket } from './AgentMarket';
3
4
 
4
5
  export const runtime = 'edge';
5
6
 
6
7
  export const GET = async (req: Request) => {
7
8
  const locale = new URL(req.url).searchParams.get('locale');
8
9
 
10
+ const market = new AgentMarket();
11
+
9
12
  let res: Response;
10
13
 
11
- res = await fetch(getAgentIndexJSON(locale as any));
14
+ res = await fetch(market.getAgentIndexUrl(locale as any));
12
15
 
13
16
  if (res.status === 404) {
14
- res = await fetch(getAgentIndexJSON(DEFAULT_LANG));
17
+ res = await fetch(market.getAgentIndexUrl(DEFAULT_LANG));
15
18
  }
16
19
 
17
20
  return res;
@@ -3,7 +3,7 @@ import { useResponsive } from 'antd-style';
3
3
  import { memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
- import ImageFileItem from '@/components/FileList/ImageFileItem';
6
+ import ImageFileItem from './ImageFileItem';
7
7
 
8
8
  interface EditableFileListProps {
9
9
  alwaysShowClose?: boolean;
@@ -1,11 +1,8 @@
1
1
  import { ImageGallery } from '@lobehub/ui';
2
- import { useResponsive } from 'antd-style';
3
- import { memo, useMemo } from 'react';
4
- import { Flexbox } from 'react-layout-kit';
2
+ import { memo } from 'react';
5
3
 
6
- import { MAX_SIZE_DESKTOP, MAX_SIZE_MOBILE } from '@/components/FileList/style';
4
+ import GalleyGrid from '@/components/GalleyGrid';
7
5
 
8
- import FileGrid from './FileGrid';
9
6
  import ImageFileItem from './ImageFileItem';
10
7
 
11
8
  interface FileListProps {
@@ -13,48 +10,9 @@ interface FileListProps {
13
10
  }
14
11
 
15
12
  const FileList = memo<FileListProps>(({ items }) => {
16
- const { mobile } = useResponsive();
17
-
18
- const { firstRow, lastRow } = useMemo(() => {
19
- if (items.length === 4) {
20
- return {
21
- firstRow: items.slice(0, 2),
22
- lastRow: items.slice(2, 4),
23
- };
24
- }
25
-
26
- const firstCol = items.length % 3 === 0 ? 3 : items.length % 3;
27
-
28
- return {
29
- firstRow: items.slice(0, firstCol),
30
- lastRow: items.slice(firstCol, items.length),
31
- };
32
- }, [items]);
33
-
34
- const { gap, max } = useMemo(
35
- () => ({
36
- gap: mobile ? 4 : 6,
37
- max: mobile ? MAX_SIZE_MOBILE : MAX_SIZE_DESKTOP,
38
- }),
39
- [mobile],
40
- );
41
-
42
13
  return (
43
14
  <ImageGallery>
44
- <Flexbox gap={gap}>
45
- <FileGrid col={firstRow.length} gap={gap} max={max}>
46
- {firstRow.map((i) => (
47
- <ImageFileItem id={i} key={i} />
48
- ))}
49
- </FileGrid>
50
- {lastRow.length > 0 && (
51
- <FileGrid col={lastRow.length > 2 ? 3 : lastRow.length} gap={gap} max={max}>
52
- {lastRow.map((i) => (
53
- <ImageFileItem id={i} key={i} />
54
- ))}
55
- </FileGrid>
56
- )}
57
- </Flexbox>
15
+ <GalleyGrid items={items} renderItem={ImageFileItem} />
58
16
  </ImageGallery>
59
17
  );
60
18
  });
@@ -1,24 +1 @@
1
- import { createStyles } from 'antd-style';
2
-
3
1
  export const MIN_IMAGE_SIZE = 64;
4
- export const MAX_SIZE_DESKTOP = 640;
5
- export const MAX_SIZE_MOBILE = 280;
6
- export const useStyles = createStyles(
7
- ({ css }, { col, gap, max, min }: { col: number; gap: number; max: number; min: number }) => ({
8
- container: css`
9
- display: grid;
10
- grid-gap: ${gap}px;
11
- grid-template-columns: repeat(${col}, 1fr);
12
-
13
- width: 100%;
14
- max-width: ${max}px;
15
-
16
- & > div {
17
- width: 100%;
18
- min-width: ${min}px;
19
- min-height: ${min}px;
20
- max-height: ${(max - gap * (col - 1)) / col}px;
21
- }
22
- `,
23
- }),
24
- );
@@ -9,29 +9,6 @@ vi.stubGlobal('process', {
9
9
  });
10
10
 
11
11
  describe('getClientConfig', () => {
12
- it('should return default URLs when no environment variables are set', () => {
13
- const config = getClientConfig();
14
- expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
15
- expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
16
- });
17
-
18
- it('should return custom URLs when environment variables are set', () => {
19
- process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com';
20
- process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com';
21
- const config = getClientConfig();
22
- expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com');
23
- expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com');
24
- });
25
-
26
- it('should return default URLs when environment variables are empty string', () => {
27
- process.env.AGENTS_INDEX_URL = '';
28
- process.env.PLUGINS_INDEX_URL = '';
29
-
30
- const config = getClientConfig();
31
- expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
32
- expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
33
- });
34
-
35
12
  it('should correctly reflect boolean values for analytics flags', () => {
36
13
  process.env.NEXT_PUBLIC_ANALYTICS_VERCEL = '1';
37
14
  process.env.NEXT_PUBLIC_VERCEL_DEBUG = '1';
@@ -21,7 +21,7 @@ describe('getServerConfig', () => {
21
21
  global.process = undefined;
22
22
 
23
23
  expect(() => getServerConfig()).toThrow(
24
- '[Server Config] you are importing a nodejs-only module outside of nodejs',
24
+ '[Server Config] you are importing a server-only module outside of server',
25
25
  );
26
26
 
27
27
  global.process = originalProcess; // Restore the original process object
@@ -55,4 +55,29 @@ describe('getServerConfig', () => {
55
55
  const config = getServerConfig();
56
56
  expect(config.IMGUR_CLIENT_ID).toBe('custom-client-id');
57
57
  });
58
+
59
+ describe('index url', () => {
60
+ it('should return default URLs when no environment variables are set', () => {
61
+ const config = getServerConfig();
62
+ expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
63
+ expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
64
+ });
65
+
66
+ it('should return custom URLs when environment variables are set', () => {
67
+ process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com';
68
+ process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com';
69
+ const config = getServerConfig();
70
+ expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com');
71
+ expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com');
72
+ });
73
+
74
+ it('should return default URLs when environment variables are empty string', () => {
75
+ process.env.AGENTS_INDEX_URL = '';
76
+ process.env.PLUGINS_INDEX_URL = '';
77
+
78
+ const config = getServerConfig();
79
+ expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
80
+ expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
81
+ });
82
+ });
58
83
  });
@@ -1,14 +1,13 @@
1
+ /**
2
+ * the client config is only used in Vercel deployment
3
+ */
4
+
1
5
  /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
2
6
 
3
7
  declare global {
4
8
  // eslint-disable-next-line @typescript-eslint/no-namespace
5
9
  namespace NodeJS {
6
10
  interface ProcessEnv {
7
- AGENTS_INDEX_URL?: string;
8
- PLUGINS_INDEX_URL?: string;
9
-
10
- NEXT_PUBLIC_CUSTOM_MODELS?: string;
11
-
12
11
  NEXT_PUBLIC_ANALYTICS_VERCEL?: string;
13
12
  NEXT_PUBLIC_VERCEL_DEBUG?: string;
14
13
 
@@ -35,16 +34,6 @@ declare global {
35
34
  }
36
35
 
37
36
  export const getClientConfig = () => ({
38
- AGENTS_INDEX_URL: !!process.env.AGENTS_INDEX_URL
39
- ? process.env.AGENTS_INDEX_URL
40
- : 'https://chat-agents.lobehub.com',
41
- PLUGINS_INDEX_URL: !!process.env.PLUGINS_INDEX_URL
42
- ? process.env.PLUGINS_INDEX_URL
43
- : 'https://chat-plugins.lobehub.com',
44
-
45
- // custom model names
46
- CUSTOM_MODELS: process.env.NEXT_PUBLIC_CUSTOM_MODELS,
47
-
48
37
  // Vercel Analytics
49
38
  ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL === '1',
50
39
  VERCEL_DEBUG: process.env.NEXT_PUBLIC_VERCEL_DEBUG === '1',
@@ -5,6 +5,7 @@ declare global {
5
5
  namespace NodeJS {
6
6
  interface ProcessEnv {
7
7
  ACCESS_CODE?: string;
8
+ CUSTOM_MODELS?: string;
8
9
 
9
10
  OPENAI_API_KEY?: string;
10
11
  OPENAI_PROXY_URL?: string;
@@ -14,6 +15,9 @@ declare global {
14
15
  USE_AZURE_OPENAI?: string;
15
16
 
16
17
  IMGUR_CLIENT_ID?: string;
18
+
19
+ AGENTS_INDEX_URL?: string;
20
+ PLUGINS_INDEX_URL?: string;
17
21
  }
18
22
  }
19
23
  }
@@ -24,7 +28,7 @@ const DEFAULT_IMAGUR_CLIENT_ID = 'e415f320d6e24f9';
24
28
 
25
29
  export const getServerConfig = () => {
26
30
  if (typeof process === 'undefined') {
27
- throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs');
31
+ throw new Error('[Server Config] you are importing a server-only module outside of server');
28
32
  }
29
33
 
30
34
  // region format: iad1,sfo1
@@ -37,6 +41,7 @@ export const getServerConfig = () => {
37
41
 
38
42
  return {
39
43
  ACCESS_CODES,
44
+ CUSTOM_MODELS: process.env.CUSTOM_MODELS,
40
45
 
41
46
  SHOW_ACCESS_CODE_CONFIG: !!ACCESS_CODES.length,
42
47
 
@@ -49,5 +54,13 @@ export const getServerConfig = () => {
49
54
  USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1',
50
55
 
51
56
  IMGUR_CLIENT_ID: process.env.IMGUR_CLIENT_ID || DEFAULT_IMAGUR_CLIENT_ID,
57
+
58
+ AGENTS_INDEX_URL: !!process.env.AGENTS_INDEX_URL
59
+ ? process.env.AGENTS_INDEX_URL
60
+ : 'https://chat-agents.lobehub.com',
61
+
62
+ PLUGINS_INDEX_URL: !!process.env.PLUGINS_INDEX_URL
63
+ ? process.env.PLUGINS_INDEX_URL
64
+ : 'https://chat-plugins.lobehub.com',
52
65
  };
53
66
  };
@@ -1,4 +1,3 @@
1
- import { getClientConfig } from '@/config/client';
2
1
  import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm';
3
2
  import { DEFAULT_AGENT_META } from '@/const/meta';
4
3
  import { LobeAgentConfig, LobeAgentTTSConfig } from '@/types/agent';
@@ -52,8 +51,6 @@ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
52
51
  export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = {
53
52
  openAI: {
54
53
  OPENAI_API_KEY: '',
55
- // support user custom model names with env var
56
- customModelName: getClientConfig().CUSTOM_MODELS,
57
54
  models: DEFAULT_OPENAI_MODEL_LIST,
58
55
  },
59
56
  };
package/src/const/url.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import urlJoin from 'url-join';
2
2
 
3
- import { getClientConfig } from '@/config/client';
4
3
  import { Locales } from '@/locales/resources';
5
4
 
6
5
  import pkg from '../../package.json';
@@ -15,7 +14,7 @@ export const ABOUT = pkg.homepage;
15
14
  export const FEEDBACK = pkg.bugs.url;
16
15
  export const DISCORD = 'https://discord.gg/AYFPHvv2jT';
17
16
 
18
- export const { PLUGINS_INDEX_URL, AGENTS_INDEX_URL } = getClientConfig();
17
+ export const PLUGINS_INDEX_URL = 'https://chat-plugins.lobehub.com';
19
18
 
20
19
  export const getPluginIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = PLUGINS_INDEX_URL) => {
21
20
  if (checkLang(lang)) return baseUrl;
@@ -23,22 +22,6 @@ export const getPluginIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = PLUGI
23
22
  return urlJoin(baseUrl, `index.${lang}.json`);
24
23
  };
25
24
 
26
- export const getAgentIndexJSON = (lang: Locales = DEFAULT_LANG, baseUrl = AGENTS_INDEX_URL) => {
27
- if (checkLang(lang)) return baseUrl;
28
-
29
- return urlJoin(baseUrl, `index.${lang}.json`);
30
- };
31
-
32
- export const getAgentJSON = (
33
- identifier: string,
34
- lang: Locales = DEFAULT_LANG,
35
- baseUrl = AGENTS_INDEX_URL,
36
- ) => {
37
- if (checkLang(lang)) return urlJoin(baseUrl, `${identifier}.json`);
38
-
39
- return urlJoin(baseUrl, `${identifier}.${lang}.json`);
40
- };
41
-
42
25
  export const AGENTS_INDEX_GITHUB = 'https://github.com/lobehub/lobe-chat-agents';
43
26
  export const AGENTS_INDEX_GITHUB_ISSUE = urlJoin(AGENTS_INDEX_GITHUB, 'issues/new');
44
27
 
@@ -8,12 +8,16 @@ import { useEffectAfterSessionHydrated, useSessionStore } from '@/store/session'
8
8
  const StoreHydration = memo(() => {
9
9
  const router = useRouter();
10
10
 
11
+ const useFetchGlobalConfig = useGlobalStore((s) => s.useFetchGlobalConfig);
12
+
11
13
  useEffect(() => {
12
14
  // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
13
15
  useSessionStore.persist.rehydrate();
14
16
  useGlobalStore.persist.rehydrate();
15
17
  }, []);
16
18
 
19
+ useFetchGlobalConfig();
20
+
17
21
  const { mobile } = useResponsive();
18
22
 
19
23
  useEffectAfterSessionHydrated(
@@ -29,6 +33,7 @@ const StoreHydration = memo(() => {
29
33
  },
30
34
  [router],
31
35
  );
36
+
32
37
  useEffect(() => {
33
38
  router.prefetch('/chat');
34
39
  router.prefetch('/market');
@@ -1,6 +1,6 @@
1
1
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
2
  import { act } from '@testing-library/react';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
6
6
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
@@ -9,49 +9,66 @@ beforeEach(() => {
9
9
  });
10
10
 
11
11
  describe('GlobalService', () => {
12
- it('should return the latest version when fetch is successful', async () => {
13
- // Arrange
14
- const mockVersion = '1.0.0';
15
- (fetch as Mock).mockResolvedValue({
16
- json: () => Promise.resolve({ 'dist-tags': { latest: mockVersion } }),
12
+ describe('getLatestVersion', () => {
13
+ it('should return the latest version when fetch is successful', async () => {
14
+ // Arrange
15
+ const mockVersion = '1.0.0';
16
+ (fetch as Mock).mockResolvedValue({
17
+ json: () => Promise.resolve({ 'dist-tags': { latest: mockVersion } }),
18
+ });
19
+
20
+ // Act
21
+ const version = await globalService.getLatestVersion();
22
+
23
+ // Assert
24
+ expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat');
25
+ expect(version).toBe(mockVersion);
17
26
  });
18
27
 
19
- // Act
20
- const version = await globalService.getLatestVersion();
28
+ it('should return undefined if the latest version is not found in the response', async () => {
29
+ // Arrange
30
+ (fetch as Mock).mockResolvedValue({
31
+ json: () => Promise.resolve({}),
32
+ });
21
33
 
22
- // Assert
23
- expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat');
24
- expect(version).toBe(mockVersion);
25
- });
34
+ // Act
35
+ const version = await globalService.getLatestVersion();
26
36
 
27
- it('should return undefined if the latest version is not found in the response', async () => {
28
- // Arrange
29
- (fetch as Mock).mockResolvedValue({
30
- json: () => Promise.resolve({}),
37
+ // Assert
38
+ expect(version).toBeUndefined();
31
39
  });
32
40
 
33
- // Act
34
- const version = await globalService.getLatestVersion();
41
+ it('should throw an error when the fetch call fails', async () => {
42
+ // Arrange
43
+ (fetch as Mock).mockRejectedValue(new Error('Network error'));
35
44
 
36
- // Assert
37
- expect(version).toBeUndefined();
38
- });
45
+ // Act & Assert
46
+ await expect(globalService.getLatestVersion()).rejects.toThrow('Network error');
47
+ });
39
48
 
40
- it('should throw an error when the fetch call fails', async () => {
41
- // Arrange
42
- (fetch as Mock).mockRejectedValue(new Error('Network error'));
49
+ it('should handle non-JSON responses gracefully', async () => {
50
+ // Arrange
51
+ (fetch as Mock).mockResolvedValue({
52
+ json: () => Promise.reject(new SyntaxError('Unexpected token < in JSON at position 0')),
53
+ });
43
54
 
44
- // Act & Assert
45
- await expect(globalService.getLatestVersion()).rejects.toThrow('Network error');
55
+ // Act & Assert
56
+ await expect(globalService.getLatestVersion()).rejects.toThrow(SyntaxError);
57
+ });
46
58
  });
47
59
 
48
- it('should handle non-JSON responses gracefully', async () => {
49
- // Arrange
50
- (fetch as Mock).mockResolvedValue({
51
- json: () => Promise.reject(new SyntaxError('Unexpected token < in JSON at position 0')),
52
- });
60
+ describe('ServerConfig', () => {
61
+ it('should return the serverConfig when fetch is successful', async () => {
62
+ // Arrange
63
+ (fetch as Mock).mockResolvedValue({
64
+ json: () => Promise.resolve({ customModelName: 'abc' }),
65
+ });
53
66
 
54
- // Act & Assert
55
- await expect(globalService.getLatestVersion()).rejects.toThrow(SyntaxError);
67
+ // Act
68
+ const config = await globalService.getGlobalConfig();
69
+
70
+ // Assert
71
+ expect(config).toEqual({ customModelName: 'abc' });
72
+ });
56
73
  });
57
74
  });
@@ -1,4 +1,5 @@
1
1
  export const URLS = {
2
+ config: '/api/config',
2
3
  market: '/api/market',
3
4
  plugins: '/api/plugins',
4
5
  };
@@ -1,3 +1,6 @@
1
+ import { URLS } from '@/services/_url';
2
+ import { GlobalServerConfig } from '@/types/settings';
3
+
1
4
  const VERSION_URL = 'https://registry.npmmirror.com/@lobehub/chat';
2
5
 
3
6
  class GlobalService {
@@ -10,6 +13,12 @@ class GlobalService {
10
13
 
11
14
  return data['dist-tags']?.latest;
12
15
  };
16
+
17
+ getGlobalConfig = async (): Promise<GlobalServerConfig> => {
18
+ const res = await fetch(URLS.config);
19
+
20
+ return res.json();
21
+ };
13
22
  }
14
23
 
15
24
  export const globalService = new GlobalService();
@@ -0,0 +1,21 @@
1
+ import { URLS } from '@/services/_url';
2
+ import { LobeChatAgentsMarketIndex } from '@/types/market';
3
+
4
+ class MarketService {
5
+ getAgentList = async (locale: string): Promise<LobeChatAgentsMarketIndex> => {
6
+ const res = await fetch(`${URLS.market}?locale=${locale}`);
7
+
8
+ return res.json();
9
+ };
10
+
11
+ /**
12
+ * 请求助手 manifest
13
+ */
14
+ getAgentManifest = async (identifier: string, locale: string) => {
15
+ if (!identifier) return;
16
+ const res = await fetch(`${URLS.market}/${identifier}?locale=${locale}`);
17
+
18
+ return res.json();
19
+ };
20
+ }
21
+ export const marketService = new MarketService();
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_SETTINGS } from '@/const/settings';
2
- import type { GlobalSettings } from '@/types/settings';
2
+ import type { GlobalServerConfig, GlobalSettings } from '@/types/settings';
3
3
 
4
4
  export enum SidebarTabKey {
5
5
  Chat = 'chat',
@@ -38,6 +38,7 @@ export interface GlobalState {
38
38
  * @localStorage
39
39
  */
40
40
  preference: GlobalPreference;
41
+ serverConfig: GlobalServerConfig;
41
42
  /**
42
43
  * @localStorage
43
44
  * 用户设置
@@ -58,6 +59,7 @@ export const initialState: GlobalState = {
58
59
  showSessionPanel: true,
59
60
  showSystemRole: false,
60
61
  },
62
+ serverConfig: {},
61
63
  settings: DEFAULT_SETTINGS,
62
64
  settingsTab: SettingsTabs.Common,
63
65
  sidebarKey: SidebarTabKey.Chat,
@@ -136,7 +136,6 @@ exports[`settingsSelectors > currentSettings > should merge DEFAULT_SETTINGS and
136
136
  "languageModel": {
137
137
  "openAI": {
138
138
  "OPENAI_API_KEY": "openai-api-key",
139
- "customModelName": undefined,
140
139
  "endpoint": "https://openai-endpoint.com",
141
140
  "models": [
142
141
  "gpt-3.5-turbo",
@@ -61,12 +61,13 @@ describe('settingsSelectors', () => {
61
61
  describe('CUSTOM_MODELS', () => {
62
62
  it('custom deletion, addition, and renaming of models', () => {
63
63
  const s = {
64
+ serverConfig: {
65
+ customModelName:
66
+ '-all,+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo,gpt-4-1106-preview=gpt-4-32k',
67
+ },
64
68
  settings: {
65
69
  languageModel: {
66
- openAI: {
67
- customModelName:
68
- '-all,+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo,gpt-4-1106-preview=gpt-4-32k',
69
- },
70
+ openAI: {},
70
71
  },
71
72
  },
72
73
  } as unknown as GlobalStore;
@@ -78,6 +79,7 @@ describe('settingsSelectors', () => {
78
79
 
79
80
  it('duplicate naming model', () => {
80
81
  const s = {
82
+ serverConfig: {},
81
83
  settings: {
82
84
  languageModel: {
83
85
  openAI: {
@@ -94,6 +96,7 @@ describe('settingsSelectors', () => {
94
96
 
95
97
  it('only add the model', () => {
96
98
  const s = {
99
+ serverConfig: {},
97
100
  settings: {
98
101
  languageModel: {
99
102
  openAI: {
@@ -34,6 +34,7 @@ const modelListSelectors = (s: GlobalStore) => {
34
34
 
35
35
  const modelNames = [
36
36
  ...DEFAULT_OPENAI_MODEL_LIST,
37
+ ...(s.serverConfig.customModelName || '').split(/[,,]/).filter(Boolean),
37
38
  ...(s.settings.languageModel.openAI.customModelName || '').split(/[,,]/).filter(Boolean),
38
39
  ];
39
40
 
@@ -5,6 +5,7 @@ import type { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { CURRENT_VERSION } from '@/const/version';
7
7
  import { globalService } from '@/services/global';
8
+ import type { GlobalServerConfig } from '@/types/settings';
8
9
  import { merge } from '@/utils/merge';
9
10
  import { setNamespace } from '@/utils/storeDebug';
10
11
 
@@ -28,6 +29,7 @@ export interface CommonAction {
28
29
  updateGuideState: (guide: Partial<Guide>) => void;
29
30
  updatePreference: (preference: Partial<GlobalPreference>, action?: string) => void;
30
31
  useCheckLatestVersion: () => SWRResponse<string>;
32
+ useFetchGlobalConfig: () => SWRResponse;
31
33
  }
32
34
 
33
35
  export const createCommonSlice: StateCreator<
@@ -78,4 +80,11 @@ export const createCommonSlice: StateCreator<
78
80
  set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion'));
79
81
  },
80
82
  }),
83
+ useFetchGlobalConfig: () =>
84
+ useSWR<GlobalServerConfig>('fetchGlobalConfig', globalService.getGlobalConfig, {
85
+ onSuccess: (data) => {
86
+ if (data) set({ serverConfig: data });
87
+ },
88
+ revalidateOnFocus: false,
89
+ }),
81
90
  });
@@ -3,7 +3,7 @@ import { produce } from 'immer';
3
3
  import useSWR, { SWRResponse } from 'swr';
4
4
  import type { StateCreator } from 'zustand/vanilla';
5
5
 
6
- import { getAgentList, getAgentManifest } from '@/services/agentMarket';
6
+ import { marketService } from '@/services/market';
7
7
  import { globalHelpers } from '@/store/global/helpers';
8
8
  import { AgentsMarketItem, LobeChatAgentsMarketIndex } from '@/types/market';
9
9
 
@@ -47,7 +47,7 @@ export const createMarketAction: StateCreator<
47
47
  useFetchAgent: (identifier) =>
48
48
  useSWR<AgentsMarketItem>(
49
49
  [identifier, globalHelpers.getCurrentLanguage()],
50
- ([id, locale]) => getAgentManifest(id, locale as string),
50
+ ([id, locale]) => marketService.getAgentManifest(id, locale as string),
51
51
  {
52
52
  onError: () => {
53
53
  get().deactivateAgent();
@@ -58,13 +58,17 @@ export const createMarketAction: StateCreator<
58
58
  },
59
59
  ),
60
60
  useFetchAgentList: () =>
61
- useSWR<LobeChatAgentsMarketIndex>(globalHelpers.getCurrentLanguage(), getAgentList, {
62
- onSuccess: (agentMarketIndex) => {
63
- set(
64
- { agentList: agentMarketIndex.agents, tagList: agentMarketIndex.tags },
65
- false,
66
- 'useFetchAgentList',
67
- );
61
+ useSWR<LobeChatAgentsMarketIndex>(
62
+ globalHelpers.getCurrentLanguage(),
63
+ marketService.getAgentList,
64
+ {
65
+ onSuccess: (agentMarketIndex) => {
66
+ set(
67
+ { agentList: agentMarketIndex.agents, tagList: agentMarketIndex.tags },
68
+ false,
69
+ 'useFetchAgentList',
70
+ );
71
+ },
68
72
  },
69
- }),
73
+ ),
70
74
  });
@@ -91,3 +91,7 @@ export interface GlobalSettings extends GlobalBaseSettings {
91
91
  }
92
92
 
93
93
  export type ConfigKeys = keyof GlobalSettings;
94
+
95
+ export interface GlobalServerConfig {
96
+ customModelName?: string;
97
+ }
@@ -1,36 +0,0 @@
1
- import { CSSProperties, ReactNode, memo } from 'react';
2
- import { Flexbox } from 'react-layout-kit';
3
-
4
- import { MAX_SIZE_DESKTOP, MIN_IMAGE_SIZE, useStyles } from '@/components/FileList/style';
5
-
6
- interface FileGridProps {
7
- children: ReactNode;
8
- className?: string;
9
- col?: number;
10
- gap?: number;
11
- max?: number;
12
- min?: number;
13
- style?: CSSProperties;
14
- }
15
-
16
- const FileGrid = memo<FileGridProps>(
17
- ({
18
- gap = 4,
19
- col = 3,
20
- max = MAX_SIZE_DESKTOP,
21
- min = MIN_IMAGE_SIZE,
22
- children,
23
- className,
24
- style,
25
- }) => {
26
- const { styles, cx } = useStyles({ col, gap, max, min });
27
-
28
- return (
29
- <Flexbox className={cx(styles.container, className)} gap={gap} horizontal style={style}>
30
- {children}
31
- </Flexbox>
32
- );
33
- },
34
- );
35
-
36
- export default FileGrid;
@@ -1,21 +0,0 @@
1
- import { URLS } from '@/services/_url';
2
- import { LobeChatAgentsMarketIndex } from '@/types/market';
3
-
4
- /**
5
- * 请求助手列表
6
- */
7
- export const getAgentList = async (locale: string): Promise<LobeChatAgentsMarketIndex> => {
8
- const res = await fetch(`${URLS.market}?locale=${locale}`);
9
-
10
- return res.json();
11
- };
12
-
13
- /**
14
- * 请求助手 manifest
15
- */
16
- export const getAgentManifest = async (identifier: string, locale: string) => {
17
- if (!identifier) return;
18
- const res = await fetch(`${URLS.market}/${identifier}?locale=${locale}`);
19
-
20
- return res.json();
21
- };