@lobehub/chat 0.102.2 → 0.102.4

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 (34) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/Development-Guide/Data-Store/Selectors.zh-CN.md +68 -0
  3. package/package.json +1 -1
  4. package/src/app/api/openai/errorResponse.test.ts +55 -0
  5. package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +3 -2
  6. package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +2 -1
  7. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +2 -1
  8. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +2 -1
  9. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +2 -1
  10. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +2 -1
  11. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/utils/postMessage.ts +1 -1
  12. package/src/app/chat/features/Conversation/index.tsx +3 -3
  13. package/src/app/chat/features/Conversation/usePluginsInit.ts +14 -0
  14. package/src/app/settings/llm/LLM/Checker.tsx +2 -2
  15. package/src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx +4 -3
  16. package/src/features/AgentSetting/AgentPlugin/MarketList.tsx +4 -3
  17. package/src/features/AgentSetting/AgentPlugin/PluginSettings.tsx +3 -2
  18. package/src/features/AgentSetting/store/action.ts +5 -5
  19. package/src/features/PluginDevModal/MetaForm.tsx +2 -1
  20. package/src/features/PluginSettingsModal/index.tsx +2 -1
  21. package/src/services/chat.ts +84 -0
  22. package/src/services/plugin.ts +15 -33
  23. package/src/store/plugin/index.ts +0 -1
  24. package/src/store/plugin/selectors.test.ts +185 -0
  25. package/src/store/plugin/selectors.ts +6 -3
  26. package/src/store/plugin/slices/customPlugin/action.ts +3 -3
  27. package/src/store/plugin/slices/plugin/action.ts +44 -15
  28. package/src/store/session/slices/chat/actions/message.ts +2 -2
  29. package/src/store/session/slices/chat/actions/plugin.ts +2 -2
  30. package/src/store/session/slices/chat/actions/topic.ts +2 -2
  31. package/src/store/session/slices/chat/actions/translate.ts +9 -7
  32. package/src/utils/fetch.ts +0 -3
  33. package/src/services/chatModel.ts +0 -48
  34. package/src/services/pluginMarket.ts +0 -13
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.102.4](https://github.com/lobehub/lobe-chat/compare/v0.102.3...v0.102.4)
6
+
7
+ <sup>Released on **2023-11-20**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **plugin**: Fix plugin can't get settings from lobe-chat.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **plugin**: Fix plugin can't get settings from lobe-chat, closes [#488](https://github.com/lobehub/lobe-chat/issues/488) ([1555140](https://github.com/lobehub/lobe-chat/commit/1555140))
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
+
30
+ ### [Version 0.102.3](https://github.com/lobehub/lobe-chat/compare/v0.102.2...v0.102.3)
31
+
32
+ <sup>Released on **2023-11-20**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix plugin not work correct when adding agent from market.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix plugin not work correct when adding agent from market, closes [#394](https://github.com/lobehub/lobe-chat/issues/394) ([7c99816](https://github.com/lobehub/lobe-chat/commit/7c99816))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.102.2](https://github.com/lobehub/lobe-chat/compare/v0.102.1...v0.102.2)
6
56
 
7
57
  <sup>Released on **2023-11-20**</sup>
@@ -0,0 +1,68 @@
1
+ ## Selector
2
+
3
+ selectors 是 LobeChat 数据流研发框架下的取数模块,它的作用是从 store 中以特定特务逻辑取出数据,供组件消费使用。
4
+
5
+ 以 `src/store/plugin/selectors.ts` 为例:
6
+
7
+ 这个 TypeScript 代码段定义了一个名为 `pluginSelectors` 的对象,该对象包含一系列用于从插件存储状态中检索数据的选择器函数。选择器是一种从 Redux store(或类似的状态管理库)中提取和派生数据的函数。这个特定的例子是为了管理与前端应用程序的插件系统相关的状态。
8
+
9
+ 下面是一些关键点的说明:
10
+
11
+ - `enabledSchema`: 一个函数,返回一个基于启用插件列表 `enabledPlugins` 过滤后的 `ChatCompletionFunctions` 数组。它将插件标识符作为前缀添加到 API 名称,以确保唯一性,并使用 `uniqBy` 函数从 Lodash 库中删除重复项。
12
+ - `onlinePluginStore`: 返回当前在线插件列表。
13
+ - `pluginList`: 返回插件列表,包括自定义插件和标准插件。
14
+ - `getPluginMetaById`: 根据插件 ID 返回插件元数据。
15
+ - `getDevPluginById`: 返回开发中的自定义插件信息。
16
+ - `getPluginManifestById`: 根据插件 ID 返回插件清单。
17
+ - `getPluginSettingsById`: 根据插件 ID 返回插件设置。
18
+ - `getPluginManifestLoadingStatus`: 根据插件 ID 返回插件清单的加载状态(加载中、成功或错误)。
19
+ - `isCustomPlugin`: 检查给定 ID 的插件是否为自定义插件。
20
+ - `displayPluginList`: 返回一个处理过的插件列表,包括作者、头像、创建时间、描述、首页 URL、标识符和标题。
21
+ - `hasPluginUI`: 根据插件 ID 确定插件是否有 UI 组件。
22
+
23
+ selectors 是高度模块化和可维护的,它通过将复杂的状态选择逻辑封装在单独的函数中,使得在应用程序的其他部分调用状态数据时,代码更加简洁和直观。此外,由于使用了 TypeScript,每个函数都可以具有明确的输入和输出类型,这有助于提高代码的可靠性和开发效率。
24
+
25
+ 以 `displayPluginList` 方法为例。其代码如下:
26
+
27
+ ```ts
28
+ const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
29
+
30
+ const displayPluginList = (s: PluginStoreState) =>
31
+ pluginList(s).map((p) => ({
32
+ author: p.author,
33
+ avatar: p.meta?.avatar,
34
+ createAt: p.createAt,
35
+ desc: pluginHelpers.getPluginDesc(p.meta),
36
+ homepage: p.homepage,
37
+ identifier: p.identifier,
38
+ title: pluginHelpers.getPluginTitle(p.meta),
39
+ }));
40
+ ```
41
+
42
+ - `pluginList` 方法:用于从插件状态存储 PluginStoreState 中获取所有插件的列表。 它通过结合两个数组:pluginList 和 customPluginList 来创建一个新的插件列表;
43
+ - `displayPluginList` 方法:调用 pluginList 方法来获取合并后的插件列表,转换其中的 title 和 desc 变成显示在 UI 上的文本。
44
+
45
+ 在组件中,只需引入便可直接获取最终消费的数据:
46
+
47
+ ```tsx | pure
48
+ import { usePluginStore } from '@/store/plugin';
49
+ import { pluginSelectors } from '@/store/plugin/selectors';
50
+
51
+ const Render = ({ plugins }) => {
52
+ const list = usePluginStore(pluginSelectors.displayPluginList);
53
+
54
+ return <> ... </>;
55
+ };
56
+ ```
57
+
58
+ 这样实现的好处在于:
59
+
60
+ 1. **解耦和重用**:通过将选择器独立于组件,我们可以在多个组件之间复用这些选择器而不需要重写取数逻辑。这减少了重复代码,提高了开发效率,并且使得代码库更加干净和易于维护。
61
+ 2. **性能优化**:选择器可以用来计算派生数据,这样可以避免在每个组件中重复计算相同的数据。当状态发生变化时,只有依赖于这部分状态的选择器才会重新计算,从而减少不必要的渲染和计算。
62
+ 3. **易于测试**:选择器是纯函数,它们仅依赖于传入的参数。这意味着它们可以在隔离的环境中进行测试,无需模拟整个 store 或组件树。
63
+ 4. **类型安全**:由于 LobeChat 使用 TypeScript,每个选择器都有明确的输入和输出类型定义。这为开发者提供了自动完成和编译时检查的优势,减少了运行时错误。
64
+ 5. **可维护性**:选择器集中了状态的读取逻辑,使得跟踪状态的变化和管理更加直观。如果状态结构发生变化,我们只需要更新相应的选择器,而不是搜索和替换整个代码库中的多个位置。
65
+ 6. **可组合性**:选择器可以组合其他选择器,以创建更复杂的选择逻辑。这种模式允许开发者构建一个选择器层次结构,使得状态选择更加灵活和强大。
66
+ 7. **简化组件逻辑**:组件不需要知道状态的结构或如何获取和计算需要的数据。组件只需调用选择器即可获取渲染所需的数据,这使得组件逻辑变得更简单和清晰。
67
+
68
+ 通过这样的设计,LobeChat 的开发者可以更专注于构建用户界面和业务逻辑,而不必担心数据的获取和处理细节。这种模式也为未来可能的状态结构变更提供了更好的适应性和扩展性。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.102.2",
3
+ "version": "0.102.4",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ChatErrorType, ErrorResponse, ErrorType } from '@/types/fetch';
4
+
5
+ import { createErrorResponse } from './errorResponse';
6
+
7
+ describe('createErrorResponse', () => {
8
+ // 测试各种错误类型的状态码
9
+ it('returns a 401 status for NoAPIKey error type', () => {
10
+ const errorType = ChatErrorType.NoAPIKey;
11
+ const response = createErrorResponse(errorType);
12
+ expect(response.status).toBe(401);
13
+ });
14
+
15
+ it('returns a 401 status for InvalidAccessCode error type', () => {
16
+ const errorType = ChatErrorType.InvalidAccessCode;
17
+ const response = createErrorResponse(errorType);
18
+ expect(response.status).toBe(401);
19
+ });
20
+
21
+ it('returns a 577 status for OpenAIBizError error type', () => {
22
+ const errorType = ChatErrorType.OpenAIBizError;
23
+ const response = createErrorResponse(errorType);
24
+ expect(response.status).toBe(577);
25
+ });
26
+
27
+ // 测试默认情况
28
+ it('returns the same error type as status for unknown error types', () => {
29
+ const errorType = 500; // 假设500是一个未知的错误类型
30
+ const response = createErrorResponse(errorType);
31
+ expect(response.status).toBe(errorType);
32
+ });
33
+
34
+ // 测试返回的Response对象是否包含正确的body和errorType
35
+ it('returns a Response object with the correct body and errorType', () => {
36
+ const errorType = ChatErrorType.NoAPIKey;
37
+ const body = { message: 'No API key provided' };
38
+ const response = createErrorResponse(errorType, body);
39
+ return response.json().then((data) => {
40
+ expect(data).toEqual({
41
+ body,
42
+ errorType,
43
+ });
44
+ });
45
+ });
46
+
47
+ // 测试没有提供body时,返回的Response对象的body是否为undefined
48
+ it('returns a Response object with an undefined body when no body is provided', () => {
49
+ const errorType = ChatErrorType.NoAPIKey;
50
+ const response = createErrorResponse(errorType);
51
+ return response.json().then((data) => {
52
+ expect(data.body).toBeUndefined();
53
+ });
54
+ });
55
+ });
@@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import ManifestPreviewer from '@/components/ManifestPreviewer';
9
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
9
+ import { usePluginStore } from '@/store/plugin';
10
+ import { pluginSelectors } from '@/store/plugin/selectors';
10
11
  import { useSessionStore } from '@/store/session';
11
12
 
12
13
  interface PluginStatusProps {
@@ -19,7 +20,7 @@ const PluginStatus = memo<PluginStatusProps>(({ title, id, deprecated }) => {
19
20
  const [status, isCustom, fetchPluginManifest] = usePluginStore((s) => [
20
21
  pluginSelectors.getPluginManifestLoadingStatus(id)(s),
21
22
  pluginSelectors.isCustomPlugin(id)(s),
22
- s.fetchPluginManifest,
23
+ s.installPlugin,
23
24
  ]);
24
25
 
25
26
  const manifest = usePluginStore(pluginSelectors.getPluginManifestById(id));
@@ -5,7 +5,8 @@ import isEqual from 'fast-deep-equal';
5
5
  import { LucideToyBrick } from 'lucide-react';
6
6
  import { memo } from 'react';
7
7
 
8
- import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
8
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
9
+ import { pluginSelectors } from '@/store/plugin/selectors';
9
10
 
10
11
  import PluginStatus from './PluginStatus';
11
12
 
@@ -7,7 +7,8 @@ import { useTranslation } from 'react-i18next';
7
7
  import { Center, Flexbox } from 'react-layout-kit';
8
8
 
9
9
  import PluginSettingsConfig from '@/features/PluginSettings';
10
- import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
10
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
11
+ import { pluginSelectors } from '@/store/plugin/selectors';
11
12
  import { useSessionStore } from '@/store/session';
12
13
 
13
14
  import { ErrorActionContainer, useStyles } from '../style';
@@ -4,7 +4,8 @@ import { memo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import PluginSettingsModal from '@/features/PluginSettingsModal';
7
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
7
+ import { usePluginStore } from '@/store/plugin';
8
+ import { pluginSelectors } from '@/store/plugin/selectors';
8
9
 
9
10
  const Settings = memo<{ id: string }>(({ id }) => {
10
11
  const item = usePluginStore(pluginSelectors.getPluginManifestById(id));
@@ -14,7 +14,8 @@ import { memo, useState } from 'react';
14
14
  import { useTranslation } from 'react-i18next';
15
15
  import { Flexbox } from 'react-layout-kit';
16
16
 
17
- import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
17
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
18
+ import { pluginSelectors } from '@/store/plugin/selectors';
18
19
 
19
20
  import PluginResult from './PluginResultJSON';
20
21
  import Settings from './Settings';
@@ -2,7 +2,8 @@ import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
2
  import { Skeleton } from 'antd';
3
3
  import { memo, useRef, useState } from 'react';
4
4
 
5
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
5
+ import { usePluginStore } from '@/store/plugin';
6
+ import { pluginSelectors } from '@/store/plugin/selectors';
6
7
  import { useSessionStore } from '@/store/session';
7
8
  import { chatSelectors } from '@/store/session/selectors';
8
9
 
@@ -13,5 +13,5 @@ export const sendPluginStateToPlugin = (window: Window, key: string, value: any)
13
13
  };
14
14
 
15
15
  export const sendPluginSettingsToPlugin = (window: Window, settings: any) => {
16
- window.postMessage({ type: PluginChannel.renderPluginState, value: settings }, '*');
16
+ window.postMessage({ type: PluginChannel.renderPluginSettings, value: settings }, '*');
17
17
  };
@@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import SafeSpacing from '@/components/SafeSpacing';
8
- import { usePluginStore } from '@/store/plugin';
9
8
 
10
9
  import ChatList from './ChatList';
11
10
  import ChatScrollAnchor from './ScrollAnchor';
11
+ import { usePluginsInit } from './usePluginsInit';
12
12
 
13
13
  const useStyles = createStyles(
14
14
  ({ css, responsive, stylish }) => css`
@@ -31,8 +31,8 @@ const Conversation = memo<ConversationProps>(({ mobile, chatInput }) => {
31
31
  const { t } = useTranslation('chat');
32
32
  const { styles } = useStyles();
33
33
 
34
- const useFetchPluginList = usePluginStore((s) => s.useFetchPluginList);
35
- useFetchPluginList();
34
+ // fetch plugin list and init plugins
35
+ usePluginsInit();
36
36
 
37
37
  return (
38
38
  <Flexbox flex={1} style={{ position: 'relative' }}>
@@ -0,0 +1,14 @@
1
+ import { usePluginStore } from '@/store/plugin';
2
+ import { useSessionStore } from '@/store/session';
3
+ import { agentSelectors } from '@/store/session/slices/agentConfig';
4
+
5
+ export const usePluginsInit = () => {
6
+ const [plugins] = useSessionStore((s) => [agentSelectors.currentAgentPlugins(s)]);
7
+
8
+ const useFetchPluginStore = usePluginStore((s) => s.useFetchPluginStore);
9
+ useFetchPluginStore();
10
+
11
+ const checkPluginsIsInstalled = usePluginStore((s) => s.useCheckPluginsIsInstalled);
12
+
13
+ checkPluginsIsInstalled(plugins);
14
+ };
@@ -6,8 +6,8 @@ import { memo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
+ import { chatService } from '@/services/chat';
9
10
  import { ChatMessageError } from '@/types/chatMessage';
10
- import { fetchPresetTaskResult } from '@/utils/fetch';
11
11
 
12
12
  import { getModelList } from './getModelList';
13
13
 
@@ -28,7 +28,7 @@ const Checker = memo<CheckerProps>(({ checkModel }) => {
28
28
  getModelList();
29
29
  }
30
30
 
31
- const data = await fetchPresetTaskResult({
31
+ const data = await chatService.fetchPresetTaskResult({
32
32
  onError: (_, rawError) => {
33
33
  setError(rawError);
34
34
  },
@@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import DevModal from '@/features/PluginDevModal';
8
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
8
+ import { usePluginStore } from '@/store/plugin';
9
+ import { pluginSelectors } from '@/store/plugin/selectors';
9
10
 
10
11
  import { useStore } from '../store';
11
12
 
@@ -19,8 +20,8 @@ const MarketList = memo<{ id: string }>(({ id }) => {
19
20
 
20
21
  const [useFetchPluginList, fetchPluginManifest, deleteCustomPlugin, updateCustomPlugin] =
21
22
  usePluginStore((s) => [
22
- s.useFetchPluginList,
23
- s.fetchPluginManifest,
23
+ s.useFetchPluginStore,
24
+ s.installPlugin,
24
25
  s.deleteCustomPlugin,
25
26
  s.updateCustomPlugin,
26
27
  ]);
@@ -9,7 +9,8 @@ import { Flexbox } from 'react-layout-kit';
9
9
  import DevModal from 'src/features/PluginDevModal';
10
10
 
11
11
  import { FORM_STYLE } from '@/const/layoutTokens';
12
- import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
12
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
13
+ import { pluginSelectors } from '@/store/plugin/selectors';
13
14
 
14
15
  import { useStore } from '../store';
15
16
  import LocalPluginItem from './LocalPluginItem';
@@ -47,8 +48,8 @@ const MarketList = memo(() => {
47
48
 
48
49
  const [useFetchPluginList, fetchPluginManifest, saveToDevList, updateNewDevPlugin] =
49
50
  usePluginStore((s) => [
50
- s.useFetchPluginList,
51
- s.fetchPluginManifest,
51
+ s.useFetchPluginStore,
52
+ s.installPlugin,
52
53
  s.saveToCustomPluginList,
53
54
  s.updateNewCustomPlugin,
54
55
  ]);
@@ -8,7 +8,8 @@ import { useTranslation } from 'react-i18next';
8
8
  import { FORM_STYLE } from '@/const/layoutTokens';
9
9
  import { transformPluginSettings } from '@/features/PluginSettings';
10
10
  import PluginSettingRender from '@/features/PluginSettings/PluginSettingRender';
11
- import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
11
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
12
+ import { pluginSelectors } from '@/store/plugin/selectors';
12
13
 
13
14
  import { useStore } from '../store';
14
15
 
@@ -30,7 +31,7 @@ const PluginSettings = memo(() => {
30
31
  const [plugins] = useStore((s) => [s.config.plugins || [], !!s.config.plugins]);
31
32
 
32
33
  const [useFetchPluginList, updatePluginSettings] = usePluginStore((s) => [
33
- s.useFetchPluginList,
34
+ s.useFetchPluginStore,
34
35
  s.updatePluginSettings,
35
36
  ]);
36
37
  const pluginList = usePluginStore(pluginSelectors.pluginList);
@@ -4,9 +4,9 @@ import { chainPickEmoji } from '@/chains/pickEmoji';
4
4
  import { chainSummaryAgentName } from '@/chains/summaryAgentName';
5
5
  import { chainSummaryDescription } from '@/chains/summaryDescription';
6
6
  import { chainSummaryTags } from '@/chains/summaryTags';
7
+ import { chatService } from '@/services/chat';
7
8
  import { MetaData } from '@/types/meta';
8
9
  import { LobeAgentConfig } from '@/types/session';
9
- import { fetchPresetTaskResult } from '@/utils/fetch';
10
10
  import { setNamespace } from '@/utils/storeDebug';
11
11
 
12
12
  import { SessionLoadingState } from '../store/initialState';
@@ -74,7 +74,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
74
74
 
75
75
  const systemRole = config.systemRole;
76
76
 
77
- const emoji = await fetchPresetTaskResult({
77
+ const emoji = await chatService.fetchPresetTaskResult({
78
78
  onLoadingChange: (loading) => {
79
79
  get().updateLoadingState('avatar', loading);
80
80
  },
@@ -97,7 +97,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
97
97
  // 替换为 ...
98
98
  dispatchMeta({ type: 'update', value: { description: '...' } });
99
99
 
100
- fetchPresetTaskResult({
100
+ chatService.fetchPresetTaskResult({
101
101
  onError: () => {
102
102
  dispatchMeta({ type: 'update', value: { description: preValue } });
103
103
  },
@@ -120,7 +120,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
120
120
  // 替换为 ...
121
121
  dispatchMeta({ type: 'update', value: { tags: ['...'] } });
122
122
 
123
- fetchPresetTaskResult({
123
+ chatService.fetchPresetTaskResult({
124
124
  onError: () => {
125
125
  dispatchMeta({ type: 'update', value: { tags: preValue } });
126
126
  },
@@ -145,7 +145,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
145
145
  // 替换为 ...
146
146
  dispatchMeta({ type: 'update', value: { title: '...' } });
147
147
 
148
- fetchPresetTaskResult({
148
+ chatService.fetchPresetTaskResult({
149
149
  onError: () => {
150
150
  dispatchMeta({ type: 'update', value: { title: previousTitle } });
151
151
  },
@@ -5,7 +5,8 @@ import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { settingsSelectors, useGlobalStore } from '@/store/global';
8
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
8
+ import { usePluginStore } from '@/store/plugin';
9
+ import { pluginSelectors } from '@/store/plugin/selectors';
9
10
 
10
11
  const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false });
11
12
 
@@ -7,7 +7,8 @@ import { useTranslation } from 'react-i18next';
7
7
  import { Center, Flexbox } from 'react-layout-kit';
8
8
 
9
9
  import PluginSettingsConfig from '@/features/PluginSettings';
10
- import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';
10
+ import { pluginHelpers, usePluginStore } from '@/store/plugin';
11
+ import { pluginSelectors } from '@/store/plugin/selectors';
11
12
 
12
13
  interface PluginSettingsModalProps {
13
14
  id: string;
@@ -0,0 +1,84 @@
1
+ import { PluginRequestPayload, createHeadersWithPluginSettings } from '@lobehub/chat-plugin-sdk';
2
+ import { merge } from 'lodash-es';
3
+
4
+ import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
5
+ import { usePluginStore } from '@/store/plugin';
6
+ import { pluginSelectors } from '@/store/plugin/selectors';
7
+ import { initialLobeAgentConfig } from '@/store/session/initialState';
8
+ import type { OpenAIChatStreamPayload } from '@/types/openai/chat';
9
+ import { fetchAIFactory, getMessageError } from '@/utils/fetch';
10
+
11
+ import { createHeaderWithOpenAI } from './_header';
12
+ import { OPENAI_URLS, URLS } from './_url';
13
+
14
+ interface FetchOptions {
15
+ signal?: AbortSignal | undefined;
16
+ }
17
+
18
+ class ChatService {
19
+ getChatCompletion = (
20
+ { plugins: enabledPlugins, ...params }: Partial<OpenAIChatStreamPayload>,
21
+ options?: FetchOptions,
22
+ ) => {
23
+ const payload = merge(
24
+ {
25
+ model: initialLobeAgentConfig.model,
26
+ stream: true,
27
+ ...initialLobeAgentConfig.params,
28
+ },
29
+ params,
30
+ );
31
+ // ============ preprocess tools ============ //
32
+
33
+ const filterTools = pluginSelectors.enabledSchema(enabledPlugins)(usePluginStore.getState());
34
+
35
+ // the rule that model can use tools:
36
+ // 1. tools is not empty
37
+ // 2. model is not in vision white list, because vision model can't use tools
38
+ // TODO: we need to find some method to let vision model use tools
39
+ const shouldUseTools =
40
+ filterTools.length > 0 && !VISION_MODEL_WHITE_LIST.includes(payload.model);
41
+
42
+ const functions = shouldUseTools ? filterTools : undefined;
43
+
44
+ return fetch(OPENAI_URLS.chat, {
45
+ body: JSON.stringify({ ...payload, functions }),
46
+ headers: createHeaderWithOpenAI({ 'Content-Type': 'application/json' }),
47
+ method: 'POST',
48
+ signal: options?.signal,
49
+ });
50
+ };
51
+
52
+ /**
53
+ * run the plugin api to get result
54
+ * @param params
55
+ * @param options
56
+ */
57
+ runPluginApi = async (params: PluginRequestPayload, options?: FetchOptions) => {
58
+ const { usePluginStore } = await import('@/store/plugin');
59
+
60
+ const s = usePluginStore.getState();
61
+
62
+ const settings = pluginSelectors.getPluginSettingsById(params.identifier)(s);
63
+ const manifest = pluginSelectors.getPluginManifestById(params.identifier)(s);
64
+
65
+ const gatewayURL = manifest?.gateway;
66
+
67
+ const res = await fetch(gatewayURL ?? URLS.plugins, {
68
+ body: JSON.stringify({ ...params, manifest }),
69
+ headers: createHeadersWithPluginSettings(settings),
70
+ method: 'POST',
71
+ signal: options?.signal,
72
+ });
73
+
74
+ if (!res.ok) {
75
+ throw await getMessageError(res);
76
+ }
77
+
78
+ return await res.text();
79
+ };
80
+
81
+ fetchPresetTaskResult = fetchAIFactory(this.getChatCompletion);
82
+ }
83
+
84
+ export const chatService = new ChatService();
@@ -1,39 +1,21 @@
1
- import { PluginRequestPayload, createHeadersWithPluginSettings } from '@lobehub/chat-plugin-sdk';
1
+ import { LobeChatPluginsMarketIndex } from '@lobehub/chat-plugin-sdk';
2
2
 
3
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
4
- import { getMessageError } from '@/utils/fetch';
3
+ import { getPluginIndexJSON } from '@/const/url';
4
+ import { getCurrentLanguage } from '@/store/global/helpers';
5
5
 
6
- import { URLS } from './_url';
6
+ class PluginService {
7
+ /**
8
+ * get plugin list from store
9
+ */
10
+ getPluginList = async () => {
11
+ const url = getPluginIndexJSON(getCurrentLanguage());
7
12
 
8
- interface FetchChatModelOptions {
9
- signal?: AbortSignal | undefined;
10
- withPlugin?: boolean;
11
- }
12
-
13
- /**
14
- * 请求插件结果´
15
- */
16
- export const fetchPlugin = async (
17
- params: PluginRequestPayload,
18
- options?: FetchChatModelOptions,
19
- ) => {
20
- const s = usePluginStore.getState();
21
-
22
- const settings = pluginSelectors.getPluginSettingsById(params.identifier)(s);
23
- const manifest = pluginSelectors.getPluginManifestById(params.identifier)(s);
13
+ const res = await fetch(url);
24
14
 
25
- const gatewayURL = manifest?.gateway;
15
+ const data: LobeChatPluginsMarketIndex = await res.json();
26
16
 
27
- const res = await fetch(gatewayURL ?? URLS.plugins, {
28
- body: JSON.stringify({ ...params, manifest }),
29
- headers: createHeadersWithPluginSettings(settings),
30
- method: 'POST',
31
- signal: options?.signal,
32
- });
33
-
34
- if (!res.ok) {
35
- throw await getMessageError(res);
36
- }
17
+ return data;
18
+ };
19
+ }
37
20
 
38
- return await res.text();
39
- };
21
+ export const pluginService = new PluginService();
@@ -1,3 +1,2 @@
1
1
  export * from './helpers';
2
- export * from './selectors';
3
2
  export { usePluginStore } from './store';
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { PluginStoreState } from './initialState';
4
+ import { pluginSelectors } from './selectors';
5
+
6
+ const mockState = {
7
+ pluginManifestMap: {
8
+ 'plugin-1': {
9
+ identifier: 'plugin-1',
10
+ api: [{ name: 'api-1' }],
11
+ type: 'default',
12
+ },
13
+ 'plugin-2': {
14
+ identifier: 'plugin-2',
15
+ api: [{ name: 'api-2' }],
16
+ type: 'default',
17
+ },
18
+ },
19
+ pluginManifestLoading: {
20
+ 'plugin-1': false,
21
+ 'plugin-2': true,
22
+ },
23
+ pluginList: [
24
+ {
25
+ identifier: 'plugin-1',
26
+ author: 'Author 1',
27
+ createAt: '2021-01-01',
28
+ meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
29
+ homepage: 'http://homepage-1.com',
30
+ },
31
+ {
32
+ identifier: 'plugin-2',
33
+ author: 'Author 2',
34
+ createAt: '2022-02-02',
35
+ meta: { avatar: 'avatar-url-2', title: 'Plugin 2' },
36
+ homepage: 'http://homepage-2.com',
37
+ },
38
+ ],
39
+ customPluginList: [],
40
+ pluginsSettings: {},
41
+ } as unknown as PluginStoreState;
42
+
43
+ describe('pluginSelectors', () => {
44
+ describe('enabledSchema', () => {
45
+ it('enabledSchema should return correct ChatCompletionFunctions array', () => {
46
+ const result = pluginSelectors.enabledSchema(['plugin-1'])(mockState);
47
+ expect(result).toEqual([{ name: 'plugin-1____api-1____default' }]);
48
+ });
49
+ it('enabledSchema should return empty', () => {
50
+ const result = pluginSelectors.enabledSchema([])(mockState);
51
+ expect(result).toEqual([]);
52
+ });
53
+ });
54
+
55
+ describe('getPluginManifestById', () => {
56
+ it('getPluginManifestById should return the correct manifest', () => {
57
+ const result = pluginSelectors.getPluginManifestById('plugin-1')(mockState);
58
+ expect(result).toEqual(mockState.pluginManifestMap['plugin-1']);
59
+ });
60
+ });
61
+ describe('getPluginManifestLoadingStatus', () => {
62
+ it('success', () => {
63
+ const result = pluginSelectors.getPluginManifestLoadingStatus('plugin-1')(mockState);
64
+ expect(result).toBe('success');
65
+ });
66
+ it('loading', () => {
67
+ let result = pluginSelectors.getPluginManifestLoadingStatus('plugin-2')(mockState);
68
+ expect(result).toBe('loading');
69
+ });
70
+ it('error', () => {
71
+ let result = pluginSelectors.getPluginManifestLoadingStatus('plugin-3')(mockState);
72
+ expect(result).toBe('error');
73
+ });
74
+ });
75
+
76
+ describe('onlinePluginStore', () => {
77
+ it('should return the online plugin list', () => {
78
+ const result = pluginSelectors.onlinePluginStore(mockState);
79
+ expect(result).toEqual(mockState.pluginList);
80
+ });
81
+ });
82
+
83
+ describe('pluginList', () => {
84
+ it('should return the combined list of pluginList and customPluginList', () => {
85
+ const result = pluginSelectors.pluginList(mockState);
86
+ expect(result).toEqual([...mockState.pluginList, ...mockState.customPluginList]);
87
+ });
88
+ });
89
+
90
+ describe('getPluginMetaById', () => {
91
+ it('should return the plugin metadata by id', () => {
92
+ const result = pluginSelectors.getPluginMetaById('plugin-1')(mockState);
93
+ expect(result).toEqual(mockState.pluginList[0]);
94
+ });
95
+ });
96
+
97
+ describe('getDevPluginById', () => {
98
+ it('should return undefined for non-existing custom plugin', () => {
99
+ const result = pluginSelectors.getDevPluginById('non-existing')(mockState);
100
+ expect(result).toBeUndefined();
101
+ });
102
+
103
+ it('should return custom plugin by id if exists', () => {
104
+ const customPlugin = {
105
+ identifier: 'custom-plugin',
106
+ author: 'Custom Author',
107
+ createAt: '2023-03-03',
108
+ meta: { avatar: 'avatar-url-custom', title: 'Custom Plugin' },
109
+ homepage: 'http://homepage-custom.com',
110
+ };
111
+ const stateWithCustomPlugin = {
112
+ ...mockState,
113
+ customPluginList: [customPlugin],
114
+ } as PluginStoreState;
115
+ const result = pluginSelectors.getDevPluginById('custom-plugin')(stateWithCustomPlugin);
116
+ expect(result).toEqual(customPlugin);
117
+ });
118
+ });
119
+
120
+ describe('getPluginSettingsById', () => {
121
+ it('should return the plugin settings by id', () => {
122
+ const mockSettings = { setting1: 'value1' };
123
+ const stateWithSettings = {
124
+ ...mockState,
125
+ pluginsSettings: { 'plugin-1': mockSettings },
126
+ };
127
+ const result = pluginSelectors.getPluginSettingsById('plugin-1')(stateWithSettings);
128
+ expect(result).toEqual(mockSettings);
129
+ });
130
+ });
131
+
132
+ describe('isCustomPlugin', () => {
133
+ it('should return false for a non-custom plugin', () => {
134
+ const result = pluginSelectors.isCustomPlugin('plugin-1')(mockState);
135
+ expect(result).toBe(false);
136
+ });
137
+
138
+ it('should return true for a custom plugin', () => {
139
+ const stateWithCustomPlugin = {
140
+ ...mockState,
141
+ customPluginList: [{ identifier: 'custom-plugin' }],
142
+ } as PluginStoreState;
143
+ const result = pluginSelectors.isCustomPlugin('custom-plugin')(stateWithCustomPlugin);
144
+ expect(result).toBe(true);
145
+ });
146
+ });
147
+
148
+ describe('displayPluginList', () => {
149
+ it('should return a list of plugins with display information', () => {
150
+ const result = pluginSelectors.displayPluginList(mockState);
151
+ const expected = mockState.pluginList.map((p) => ({
152
+ author: p.author,
153
+ avatar: p.meta?.avatar,
154
+ createAt: p.createAt,
155
+ desc: p.meta.description,
156
+ homepage: p.homepage,
157
+ identifier: p.identifier,
158
+ title: p.meta.title,
159
+ }));
160
+ expect(result).toEqual(expected);
161
+ });
162
+ });
163
+
164
+ describe('hasPluginUI', () => {
165
+ it('should return false when the plugin does not have a UI component', () => {
166
+ const result = pluginSelectors.hasPluginUI('non-ui-plugin')(mockState);
167
+ expect(result).toBe(false);
168
+ });
169
+
170
+ it('should return true when the plugin has a UI component', () => {
171
+ const stateWithUIPlugin = {
172
+ ...mockState,
173
+ pluginManifestMap: {
174
+ ...mockState.pluginManifestMap,
175
+ 'ui-plugin': {
176
+ ...mockState.pluginManifestMap['plugin-1'],
177
+ ui: true,
178
+ },
179
+ },
180
+ } as unknown as PluginStoreState;
181
+ const result = pluginSelectors.hasPluginUI('ui-plugin')(stateWithUIPlugin);
182
+ expect(result).toBe(true);
183
+ });
184
+ });
185
+ });
@@ -1,10 +1,10 @@
1
1
  import { uniqBy } from 'lodash-es';
2
2
 
3
3
  import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
4
- import { pluginHelpers } from '@/store/plugin/helpers';
5
4
  import { ChatCompletionFunctions } from '@/types/openai/chat';
6
5
 
7
- import { PluginStoreState } from './initialState';
6
+ import { pluginHelpers } from './helpers';
7
+ import type { PluginStoreState } from './initialState';
8
8
 
9
9
  const enabledSchema =
10
10
  (enabledPlugins: string[] = []) =>
@@ -34,6 +34,8 @@ const enabledSchema =
34
34
  return uniqBy(list, 'name');
35
35
  };
36
36
 
37
+ const onlinePluginStore = (s: PluginStoreState) => s.pluginList;
38
+
37
39
  const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
38
40
 
39
41
  const getPluginMetaById = (id: string) => (s: PluginStoreState) =>
@@ -47,7 +49,7 @@ const getPluginSettingsById = (id: string) => (s: PluginStoreState) => s.plugins
47
49
 
48
50
  // 获取插件 manifest 加载状态
49
51
  const getPluginManifestLoadingStatus = (id: string) => (s: PluginStoreState) => {
50
- const manifest = getPluginManifestById(id);
52
+ const manifest = getPluginManifestById(id)(s);
51
53
 
52
54
  if (s.pluginManifestLoading[id]) return 'loading';
53
55
 
@@ -86,5 +88,6 @@ export const pluginSelectors = {
86
88
  getPluginSettingsById,
87
89
  hasPluginUI,
88
90
  isCustomPlugin,
91
+ onlinePluginStore,
89
92
  pluginList,
90
93
  };
@@ -47,11 +47,11 @@ export const createCustomPluginSlice: StateCreator<
47
47
  set({ newCustomPlugin: defaultCustomPlugin }, false, t('saveToCustomPluginList'));
48
48
  },
49
49
  updateCustomPlugin: (id, value) => {
50
- const { dispatchCustomPluginList, fetchPluginManifest } = get();
50
+ const { dispatchCustomPluginList, installPlugin } = get();
51
51
  // 1. 更新 list 项信息
52
52
  dispatchCustomPluginList({ id, plugin: value, type: 'updateItem' });
53
- // 2. 更新 重新拉取 manifest
54
- fetchPluginManifest(id);
53
+ // 2. 重新安装插件
54
+ installPlugin(id);
55
55
  },
56
56
 
57
57
  updateNewCustomPlugin: (newCustomPlugin) => {
@@ -9,7 +9,7 @@ import { merge, uniq } from 'lodash-es';
9
9
  import useSWR, { SWRResponse } from 'swr';
10
10
  import { StateCreator } from 'zustand/vanilla';
11
11
 
12
- import { getPluginList } from '@/services/pluginMarket';
12
+ import { pluginService } from '@/services/plugin';
13
13
  import { pluginSelectors } from '@/store/plugin/selectors';
14
14
  import { LobeSessions } from '@/types/session';
15
15
  import { setNamespace } from '@/utils/storeDebug';
@@ -24,13 +24,17 @@ const t = setNamespace('plugin');
24
24
  */
25
25
  export interface PluginAction {
26
26
  checkLocalEnabledPlugins: (sessions: LobeSessions) => void;
27
+ checkPluginsIsInstalled: (plugins: string[]) => void;
27
28
  deletePluginSettings: (id: string) => void;
28
29
  dispatchPluginManifest: (payload: PluginDispatch) => void;
29
- fetchPluginManifest: (identifier: string) => Promise<void>;
30
+ installPlugin: (identifier: string) => Promise<void>;
31
+ installPlugins: (plugins: string[]) => Promise<void>;
32
+ loadPluginStore: () => Promise<LobeChatPluginsMarketIndex>;
30
33
  resetPluginSettings: () => void;
31
34
  updateManifestLoadingState: (key: string, value: boolean | undefined) => void;
32
35
  updatePluginSettings: <T>(id: string, settings: Partial<T>) => void;
33
- useFetchPluginList: () => SWRResponse<LobeChatPluginsMarketIndex>;
36
+ useCheckPluginsIsInstalled: (plugins: string[]) => SWRResponse;
37
+ useFetchPluginStore: () => SWRResponse<LobeChatPluginsMarketIndex>;
34
38
  }
35
39
 
36
40
  export const createPluginSlice: StateCreator<
@@ -40,7 +44,7 @@ export const createPluginSlice: StateCreator<
40
44
  PluginAction
41
45
  > = (set, get) => ({
42
46
  checkLocalEnabledPlugins: async (sessions) => {
43
- const { fetchPluginManifest } = get();
47
+ const { checkPluginsIsInstalled } = get();
44
48
 
45
49
  let enabledPlugins: string[] = [];
46
50
 
@@ -53,9 +57,23 @@ export const createPluginSlice: StateCreator<
53
57
 
54
58
  const plugins = uniq(enabledPlugins);
55
59
 
56
- await Promise.all(plugins.map((identifier) => fetchPluginManifest(identifier)));
60
+ await checkPluginsIsInstalled(plugins);
61
+ },
62
+ checkPluginsIsInstalled: async (plugins) => {
63
+ // if there is no plugins, just skip.
64
+ if (plugins.length === 0) return;
65
+
66
+ const { loadPluginStore, installPlugins } = get();
67
+
68
+ // check if the store is empty
69
+ // if it is, we need to load the plugin store
70
+ if (pluginSelectors.onlinePluginStore(get()).length === 0) {
71
+ await loadPluginStore();
72
+ }
73
+
74
+ await installPlugins(plugins);
57
75
 
58
- set({ manifestPrepared: true }, false, t('checkLocalEnabledPlugins'));
76
+ set({ manifestPrepared: true }, false, t('checkPluginsIsInstalled'));
59
77
  },
60
78
  deletePluginSettings: (id) => {
61
79
  set(
@@ -72,9 +90,10 @@ export const createPluginSlice: StateCreator<
72
90
 
73
91
  set({ pluginManifestMap: nextManifest }, false, t('dispatchPluginManifest', payload));
74
92
  },
75
- fetchPluginManifest: async (name) => {
93
+ installPlugin: async (name) => {
76
94
  const plugin = pluginSelectors.getPluginMetaById(name)(get());
77
- // 1. 校验文件
95
+
96
+ // 1. valid plugin
78
97
 
79
98
  if (!plugin) return;
80
99
 
@@ -98,7 +117,7 @@ export const createPluginSlice: StateCreator<
98
117
 
99
118
  get().updateManifestLoadingState(name, undefined);
100
119
  if (!data) {
101
- message.error('插件描述文件请求失败');
120
+ message.error(`插件 ${plugin.meta.title} 描述文件请求失败`);
102
121
  return;
103
122
  }
104
123
 
@@ -113,6 +132,18 @@ export const createPluginSlice: StateCreator<
113
132
  // 4. 存储 manifest 信息
114
133
  get().dispatchPluginManifest({ id: plugin.identifier, plugin: data, type: 'addManifest' });
115
134
  },
135
+ installPlugins: async (plugins) => {
136
+ const { installPlugin } = get();
137
+
138
+ await Promise.all(plugins.map((identifier) => installPlugin(identifier)));
139
+ },
140
+ loadPluginStore: async () => {
141
+ const pluginMarketIndex = await pluginService.getPluginList();
142
+
143
+ set({ pluginList: pluginMarketIndex.plugins }, false, t('loadPluginList'));
144
+
145
+ return pluginMarketIndex;
146
+ },
116
147
  resetPluginSettings: () => {
117
148
  set({ pluginsSettings: {} }, false, t('resetPluginSettings'));
118
149
  },
@@ -135,10 +166,8 @@ export const createPluginSlice: StateCreator<
135
166
  );
136
167
  },
137
168
 
138
- useFetchPluginList: () =>
139
- useSWR<LobeChatPluginsMarketIndex>('fetchPluginList', getPluginList, {
140
- onSuccess: (pluginMarketIndex) => {
141
- set({ pluginList: pluginMarketIndex.plugins }, false, t('useFetchPluginList'));
142
- },
143
- }),
169
+ useCheckPluginsIsInstalled: (plugins) => useSWR(plugins, get().checkPluginsIsInstalled),
170
+
171
+ useFetchPluginStore: () =>
172
+ useSWR<LobeChatPluginsMarketIndex>('loadPluginStore', get().loadPluginStore),
144
173
  });
@@ -4,7 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
4
4
  import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
5
5
  import { LOADING_FLAT } from '@/const/message';
6
6
  import { VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/settings';
7
- import { fetchChatModel } from '@/services/chatModel';
7
+ import { chatService } from '@/services/chat';
8
8
  import { filesSelectors, useFileStore } from '@/store/files';
9
9
  import { SessionStore } from '@/store/session';
10
10
  import { ChatMessage } from '@/types/chatMessage';
@@ -263,7 +263,7 @@ export const chatMessage: StateCreator<
263
263
  }
264
264
 
265
265
  const fetcher = () =>
266
- fetchChatModel(
266
+ chatService.getChatCompletion(
267
267
  {
268
268
  messages: postMessages,
269
269
  model: config.model,
@@ -2,7 +2,7 @@ import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
2
  import { StateCreator } from 'zustand/vanilla';
3
3
 
4
4
  import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
5
- import { fetchPlugin } from '@/services/plugin';
5
+ import { chatService } from '@/services/chat';
6
6
  import { SessionStore } from '@/store/session';
7
7
  import { OpenAIFunctionCall } from '@/types/chatMessage';
8
8
  import { setNamespace } from '@/utils/storeDebug';
@@ -41,7 +41,7 @@ export const chatPlugin: StateCreator<
41
41
  let data: string;
42
42
  try {
43
43
  const abortController = toggleChatLoading(true, id, t('fetchPlugin') as string);
44
- data = await fetchPlugin(payload, { signal: abortController?.signal });
44
+ data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
45
45
  } catch (error) {
46
46
  dispatchMessage({ id, key: 'error', type: 'updateMessage', value: error });
47
47
 
@@ -2,8 +2,8 @@ import { StateCreator } from 'zustand/vanilla';
2
2
 
3
3
  import { chainSummaryTitle } from '@/chains/summaryTitle';
4
4
  import { LOADING_FLAT } from '@/const/message';
5
+ import { chatService } from '@/services/chat';
5
6
  import { SessionStore } from '@/store/session';
6
- import { fetchPresetTaskResult } from '@/utils/fetch';
7
7
  import { setNamespace } from '@/utils/storeDebug';
8
8
  import { nanoid } from '@/utils/uuid';
9
9
 
@@ -145,7 +145,7 @@ export const chatTopic: StateCreator<
145
145
  let output = '';
146
146
 
147
147
  // 自动总结话题标题
148
- fetchPresetTaskResult({
148
+ chatService.fetchPresetTaskResult({
149
149
  onError: () => {
150
150
  dispatchTopic({ id: topicId, key: 'title', type: 'updateChatTopic', value: defaultTitle });
151
151
  },
@@ -4,8 +4,8 @@ import { StateCreator } from 'zustand/vanilla';
4
4
  import { chainLangDetect } from '@/chains/langDetect';
5
5
  import { chainTranslate } from '@/chains/translate';
6
6
  import { supportLocales } from '@/locales/options';
7
+ import { chatService } from '@/services/chat';
7
8
  import { SessionStore } from '@/store/session';
8
- import { fetchPresetTaskResult } from '@/utils/fetch';
9
9
  import { setNamespace } from '@/utils/storeDebug';
10
10
 
11
11
  import { sessionSelectors } from '../../session/selectors';
@@ -72,14 +72,16 @@ export const chatTranslate: StateCreator<
72
72
  toggleChatLoading(true, id, t('translateMessage(start)', { id }) as string);
73
73
 
74
74
  // detect from language
75
- fetchPresetTaskResult({
76
- params: chainLangDetect(message.content),
77
- }).then((data) => {
78
- if (data && supportLocales.includes(data)) from = data;
79
- });
75
+ chatService
76
+ .fetchPresetTaskResult({
77
+ params: chainLangDetect(message.content),
78
+ })
79
+ .then((data) => {
80
+ if (data && supportLocales.includes(data)) from = data;
81
+ });
80
82
 
81
83
  // translate to target language
82
- await fetchPresetTaskResult({
84
+ await chatService.fetchPresetTaskResult({
83
85
  onMessageHandle: (text) => {
84
86
  dispatchMessage({
85
87
  id,
@@ -1,6 +1,5 @@
1
1
  import { t } from 'i18next';
2
2
 
3
- import { fetchChatModel } from '@/services/chatModel';
4
3
  import { ChatMessageError } from '@/types/chatMessage';
5
4
  import { ErrorResponse, ErrorType } from '@/types/fetch';
6
5
 
@@ -122,5 +121,3 @@ export const fetchAIFactory =
122
121
 
123
122
  return await data?.text();
124
123
  };
125
-
126
- export const fetchPresetTaskResult = fetchAIFactory(fetchChatModel);
@@ -1,48 +0,0 @@
1
- import { merge } from 'lodash-es';
2
-
3
- import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
4
- import { pluginSelectors, usePluginStore } from '@/store/plugin';
5
- import { initialLobeAgentConfig } from '@/store/session/initialState';
6
- import type { OpenAIChatStreamPayload } from '@/types/openai/chat';
7
-
8
- import { createHeaderWithOpenAI } from './_header';
9
- import { OPENAI_URLS } from './_url';
10
-
11
- interface FetchChatModelOptions {
12
- signal?: AbortSignal | undefined;
13
- }
14
-
15
- /**
16
- * 专门用于对话的 fetch
17
- */
18
- export const fetchChatModel = (
19
- { plugins: enabledPlugins, ...params }: Partial<OpenAIChatStreamPayload>,
20
- options?: FetchChatModelOptions,
21
- ) => {
22
- const payload = merge(
23
- {
24
- model: initialLobeAgentConfig.model,
25
- stream: true,
26
- ...initialLobeAgentConfig.params,
27
- },
28
- params,
29
- );
30
- // ============ preprocess tools ============ //
31
-
32
- const filterTools = pluginSelectors.enabledSchema(enabledPlugins)(usePluginStore.getState());
33
-
34
- // the rule that model can use tools:
35
- // 1. tools is not empty
36
- // 2. model is not in vision white list, because vision model can't use tools
37
- // TODO: we need to find some method to let vision model use tools
38
- const shouldUseTools = filterTools.length > 0 && !VISION_MODEL_WHITE_LIST.includes(payload.model);
39
-
40
- const functions = shouldUseTools ? filterTools : undefined;
41
-
42
- return fetch(OPENAI_URLS.chat, {
43
- body: JSON.stringify({ ...payload, functions }),
44
- headers: createHeaderWithOpenAI({ 'Content-Type': 'application/json' }),
45
- method: 'POST',
46
- signal: options?.signal,
47
- });
48
- };
@@ -1,13 +0,0 @@
1
- import { getPluginIndexJSON } from '@/const/url';
2
- import { getCurrentLanguage } from '@/store/global/helpers';
3
-
4
- /**
5
- * fetch Plugin Market List
6
- */
7
- export const getPluginList = async () => {
8
- const url = getPluginIndexJSON(getCurrentLanguage());
9
-
10
- const res = await fetch(url);
11
-
12
- return res.json();
13
- };