@lobehub/chat 1.49.10 → 1.49.12

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 (95) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/components.json +24 -0
  4. package/locales/ar/modelProvider.json +0 -24
  5. package/locales/ar/models.json +15 -0
  6. package/locales/bg-BG/components.json +24 -0
  7. package/locales/bg-BG/modelProvider.json +0 -24
  8. package/locales/bg-BG/models.json +15 -0
  9. package/locales/de-DE/components.json +24 -0
  10. package/locales/de-DE/modelProvider.json +0 -24
  11. package/locales/de-DE/models.json +15 -0
  12. package/locales/en-US/components.json +24 -0
  13. package/locales/en-US/modelProvider.json +0 -24
  14. package/locales/en-US/models.json +15 -0
  15. package/locales/es-ES/components.json +24 -0
  16. package/locales/es-ES/modelProvider.json +0 -24
  17. package/locales/es-ES/models.json +15 -0
  18. package/locales/fa-IR/components.json +24 -0
  19. package/locales/fa-IR/modelProvider.json +0 -24
  20. package/locales/fa-IR/models.json +15 -0
  21. package/locales/fr-FR/components.json +24 -0
  22. package/locales/fr-FR/modelProvider.json +0 -24
  23. package/locales/fr-FR/models.json +15 -0
  24. package/locales/it-IT/components.json +24 -0
  25. package/locales/it-IT/modelProvider.json +0 -24
  26. package/locales/it-IT/models.json +15 -0
  27. package/locales/ja-JP/components.json +24 -0
  28. package/locales/ja-JP/modelProvider.json +0 -24
  29. package/locales/ja-JP/models.json +15 -0
  30. package/locales/ko-KR/components.json +24 -0
  31. package/locales/ko-KR/modelProvider.json +0 -24
  32. package/locales/ko-KR/models.json +4 -0
  33. package/locales/nl-NL/components.json +24 -0
  34. package/locales/nl-NL/modelProvider.json +0 -24
  35. package/locales/nl-NL/models.json +15 -0
  36. package/locales/pl-PL/components.json +24 -0
  37. package/locales/pl-PL/modelProvider.json +0 -24
  38. package/locales/pl-PL/models.json +15 -0
  39. package/locales/pt-BR/components.json +24 -0
  40. package/locales/pt-BR/modelProvider.json +0 -24
  41. package/locales/pt-BR/models.json +15 -0
  42. package/locales/ru-RU/components.json +24 -0
  43. package/locales/ru-RU/modelProvider.json +0 -24
  44. package/locales/ru-RU/models.json +15 -0
  45. package/locales/tr-TR/components.json +24 -0
  46. package/locales/tr-TR/modelProvider.json +0 -24
  47. package/locales/tr-TR/models.json +15 -0
  48. package/locales/vi-VN/components.json +24 -0
  49. package/locales/vi-VN/modelProvider.json +0 -24
  50. package/locales/vi-VN/models.json +15 -0
  51. package/locales/zh-CN/components.json +24 -0
  52. package/locales/zh-CN/modelProvider.json +0 -24
  53. package/locales/zh-CN/models.json +16 -1
  54. package/locales/zh-TW/components.json +24 -0
  55. package/locales/zh-TW/modelProvider.json +0 -24
  56. package/locales/zh-TW/models.json +15 -0
  57. package/package.json +1 -1
  58. package/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx +1 -0
  59. package/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +26 -2
  60. package/src/app/(main)/settings/provider/(detail)/[id]/page.tsx +10 -3
  61. package/src/app/(main)/settings/provider/(detail)/ollama/CheckError.tsx +70 -0
  62. package/src/app/(main)/settings/provider/(detail)/ollama/Container.tsx +57 -0
  63. package/src/app/(main)/settings/provider/(detail)/ollama/OllamaModelDownloader/index.tsx +127 -0
  64. package/src/app/(main)/settings/provider/(detail)/ollama/OllamaModelDownloader/useDownloadMonitor.ts +29 -0
  65. package/src/app/(main)/settings/provider/(detail)/ollama/page.tsx +2 -7
  66. package/src/app/(main)/settings/provider/features/ProviderConfig/Checker.tsx +90 -69
  67. package/src/app/(main)/settings/provider/features/ProviderConfig/index.tsx +6 -6
  68. package/src/components/FormAction/index.tsx +66 -0
  69. package/src/components/OllamaSetupGuide/index.tsx +217 -0
  70. package/src/components/Thinking/index.tsx +14 -16
  71. package/src/config/aiModels/ollama.ts +12 -19
  72. package/src/config/modelProviders/ollama.ts +1 -0
  73. package/src/config/modelProviders/siliconcloud.ts +2 -2
  74. package/src/database/repositories/aiInfra/index.ts +33 -2
  75. package/src/database/server/models/aiProvider.ts +5 -1
  76. package/src/features/Conversation/Error/OllamaBizError/SetupGuide.tsx +2 -209
  77. package/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx +7 -58
  78. package/src/libs/agent-runtime/ollama/index.ts +1 -1
  79. package/src/libs/agent-runtime/siliconcloud/index.ts +33 -1
  80. package/src/locales/default/components.ts +26 -0
  81. package/src/locales/default/modelProvider.ts +0 -26
  82. package/src/server/routers/lambda/aiProvider.ts +2 -10
  83. package/src/services/aiProvider/client.ts +2 -8
  84. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +10 -10
  85. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +4 -3
  86. package/src/store/chat/slices/aiChat/initialState.ts +1 -1
  87. package/src/store/chat/slices/message/action.ts +4 -3
  88. package/src/store/global/initialState.ts +2 -0
  89. package/src/store/global/selectors.ts +2 -0
  90. package/src/store/serverConfig/selectors.test.ts +3 -0
  91. package/src/store/serverConfig/store.test.ts +3 -2
  92. package/src/store/serverConfig/store.ts +1 -1
  93. package/src/store/user/slices/common/action.test.ts +1 -0
  94. package/src/types/serverConfig.ts +1 -1
  95. package/src/app/(main)/settings/provider/(detail)/ollama/Checker.tsx +0 -73
@@ -155,32 +155,6 @@ export default {
155
155
  desc: '必须包含http(s)://,本地未额外指定可留空',
156
156
  title: 'Ollama 服务地址',
157
157
  },
158
- setup: {
159
- cors: {
160
- description: '因浏览器安全限制,你需要为 Ollama 进行跨域配置后方可正常使用。',
161
- linux: {
162
- env: '在 [Service] 部分下添加 `Environment`,添加 OLLAMA_ORIGINS 环境变量:',
163
- reboot: '重载 systemd 并重启 Ollama',
164
- systemd: '调用 systemd 编辑 ollama 服务:',
165
- },
166
- macos: '请打开「终端」应用程序,并粘贴以下指令,并按回车运行',
167
- reboot: '请在执行完成后重启 Ollama 服务',
168
- title: '配置 Ollama 允许跨域访问',
169
- windows:
170
- '在 Windows 上,点击「控制面板」,进入编辑系统环境变量。为您的用户账户新建名为 「OLLAMA_ORIGINS」 的环境变量,值为 * ,点击 「OK/应用」 保存',
171
- },
172
- install: {
173
- description: '请确认你已经开启 Ollama ,如果没有下载 Ollama ,请前往官网<1>下载</1>',
174
- docker:
175
- '如果你更倾向于使用 Docker,Ollama 也提供了官方 Docker 镜像,你可以通过以下命令拉取:',
176
- linux: {
177
- command: '通过以下命令安装:',
178
- manual: '或者,你也可以参考 <1>Linux 手动安装指南</1> 自行安装',
179
- },
180
- title: '在本地安装并开启 Ollama 应用',
181
- windowsTab: 'Windows (预览版)',
182
- },
183
- },
184
158
  title: 'Ollama',
185
159
  unlock: {
186
160
  cancel: '取消下载',
@@ -48,7 +48,7 @@ export const aiProviderRouter = router({
48
48
  .input(z.object({ id: z.string() }))
49
49
 
50
50
  .query(async ({ input, ctx }): Promise<AiProviderDetailItem | undefined> => {
51
- return ctx.aiProviderModel.getAiProviderById(input.id, KeyVaultsGateKeeper.getUserKeyVaults);
51
+ return ctx.aiInfraRepos.getAiProviderDetail(input.id, KeyVaultsGateKeeper.getUserKeyVaults);
52
52
  }),
53
53
 
54
54
  getAiProviderList: aiProviderProcedure.query(async ({ ctx }) => {
@@ -58,15 +58,7 @@ export const aiProviderRouter = router({
58
58
  getAiProviderRuntimeState: aiProviderProcedure
59
59
  .input(z.object({ isLogin: z.boolean().optional() }))
60
60
  .query(async ({ ctx }): Promise<AiProviderRuntimeState> => {
61
- const runtimeConfig = await ctx.aiProviderModel.getAiProviderRuntimeConfig(
62
- KeyVaultsGateKeeper.getUserKeyVaults,
63
- );
64
-
65
- const enabledAiProviders = await ctx.aiInfraRepos.getUserEnabledProviderList();
66
-
67
- const enabledAiModels = await ctx.aiInfraRepos.getEnabledModels();
68
-
69
- return { enabledAiModels, enabledAiProviders, runtimeConfig };
61
+ return ctx.aiInfraRepos.getAiProviderRuntimeState(KeyVaultsGateKeeper.getUserKeyVaults);
70
62
  }),
71
63
 
72
64
  removeAiProvider: aiProviderProcedure
@@ -25,7 +25,7 @@ export class ClientService extends BaseClientService implements IAiProviderServi
25
25
  };
26
26
 
27
27
  getAiProviderById: IAiProviderService['getAiProviderById'] = async (id) => {
28
- return this.aiProviderModel.getAiProviderById(id);
28
+ return this.aiInfraRepos.getAiProviderDetail(id);
29
29
  };
30
30
 
31
31
  getAiProviderList: IAiProviderService['getAiProviderList'] = async () => {
@@ -33,13 +33,7 @@ export class ClientService extends BaseClientService implements IAiProviderServi
33
33
  };
34
34
 
35
35
  getAiProviderRuntimeState: IAiProviderService['getAiProviderRuntimeState'] = async () => {
36
- const runtimeConfig = await this.aiProviderModel.getAiProviderRuntimeConfig();
37
-
38
- const enabledAiProviders = await this.aiInfraRepos.getUserEnabledProviderList();
39
-
40
- const enabledAiModels = await this.aiInfraRepos.getEnabledModels();
41
-
42
- return { enabledAiModels, enabledAiProviders, runtimeConfig };
36
+ return await this.aiInfraRepos.getAiProviderRuntimeState();
43
37
  };
44
38
 
45
39
  toggleProviderEnabled: IAiProviderService['toggleProviderEnabled'] = async (id, enabled) => {
@@ -576,7 +576,7 @@ describe('chatMessage actions', () => {
576
576
  const abortController = new AbortController();
577
577
 
578
578
  act(() => {
579
- useChatStore.setState({ abortController });
579
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
580
580
  });
581
581
 
582
582
  await act(async () => {
@@ -596,18 +596,18 @@ describe('chatMessage actions', () => {
596
596
 
597
597
  await act(async () => {
598
598
  // 确保没有设置 abortController
599
- useChatStore.setState({ abortController: undefined });
599
+ useChatStore.setState({ chatLoadingIdsAbortController: undefined });
600
600
 
601
601
  result.current.stopGenerateMessage();
602
602
  });
603
603
 
604
604
  // 由于没有 abortController,不应调用任何方法
605
- expect(result.current.abortController).toBeUndefined();
605
+ expect(result.current.chatLoadingIdsAbortController).toBeUndefined();
606
606
  });
607
607
 
608
608
  it('should return early if abortController is undefined', () => {
609
609
  act(() => {
610
- useChatStore.setState({ abortController: undefined });
610
+ useChatStore.setState({ chatLoadingIdsAbortController: undefined });
611
611
  });
612
612
 
613
613
  const { result } = renderHook(() => useChatStore());
@@ -625,7 +625,7 @@ describe('chatMessage actions', () => {
625
625
  const abortMock = vi.fn();
626
626
  const abortController = { abort: abortMock } as unknown as AbortController;
627
627
  act(() => {
628
- useChatStore.setState({ abortController });
628
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
629
629
  });
630
630
  const { result } = renderHook(() => useChatStore());
631
631
 
@@ -639,7 +639,7 @@ describe('chatMessage actions', () => {
639
639
  it('should call internal_toggleChatLoading with correct parameters', () => {
640
640
  const abortController = new AbortController();
641
641
  act(() => {
642
- useChatStore.setState({ abortController });
642
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
643
643
  });
644
644
  const { result } = renderHook(() => useChatStore());
645
645
  const spy = vi.spyOn(result.current, 'internal_toggleChatLoading');
@@ -868,7 +868,7 @@ describe('chatMessage actions', () => {
868
868
  });
869
869
 
870
870
  const state = useChatStore.getState();
871
- expect(state.abortController).toBeInstanceOf(AbortController);
871
+ expect(state.chatLoadingIdsAbortController).toBeInstanceOf(AbortController);
872
872
  expect(state.chatLoadingIds).toEqual(['message-id']);
873
873
  });
874
874
 
@@ -887,7 +887,7 @@ describe('chatMessage actions', () => {
887
887
  });
888
888
 
889
889
  const state = useChatStore.getState();
890
- expect(state.abortController).toBeUndefined();
890
+ expect(state.chatLoadingIdsAbortController).toBeUndefined();
891
891
  expect(state.chatLoadingIds).toEqual([]);
892
892
  });
893
893
 
@@ -920,12 +920,12 @@ describe('chatMessage actions', () => {
920
920
  const abortController = new AbortController();
921
921
 
922
922
  act(() => {
923
- useChatStore.setState({ abortController });
923
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
924
924
  result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
925
925
  });
926
926
 
927
927
  const state = useChatStore.getState();
928
- expect(state.abortController).toEqual(abortController);
928
+ expect(state.chatLoadingIdsAbortController).toEqual(abortController);
929
929
  });
930
930
  });
931
931
 
@@ -267,10 +267,11 @@ export const generateAIChat: StateCreator<
267
267
  await Promise.all([summaryTitle(), addFilesToAgent()]);
268
268
  },
269
269
  stopGenerateMessage: () => {
270
- const { abortController, internal_toggleChatLoading } = get();
271
- if (!abortController) return;
270
+ const { chatLoadingIdsAbortController, internal_toggleChatLoading } = get();
272
271
 
273
- abortController.abort(MESSAGE_CANCEL_FLAT);
272
+ if (!chatLoadingIdsAbortController) return;
273
+
274
+ chatLoadingIdsAbortController.abort(MESSAGE_CANCEL_FLAT);
274
275
 
275
276
  internal_toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
276
277
  },
@@ -1,9 +1,9 @@
1
1
  export interface ChatAIChatState {
2
- abortController?: AbortController;
3
2
  /**
4
3
  * is the AI message is generating
5
4
  */
6
5
  chatLoadingIds: string[];
6
+ chatLoadingIdsAbortController?: AbortController;
7
7
  inputFiles: File[];
8
8
  inputMessage: string;
9
9
  /**
@@ -368,13 +368,14 @@ export const chatMessage: StateCreator<
368
368
  );
369
369
  },
370
370
  internal_toggleLoadingArrays: (key, loading, id, action) => {
371
+ const abortControllerKey = `${key}AbortController`;
371
372
  if (loading) {
372
373
  window.addEventListener('beforeunload', preventLeavingFn);
373
374
 
374
375
  const abortController = new AbortController();
375
376
  set(
376
377
  {
377
- abortController,
378
+ [abortControllerKey]: abortController,
378
379
  [key]: toggleBooleanList(get()[key] as string[], id!, loading),
379
380
  },
380
381
  false,
@@ -384,11 +385,11 @@ export const chatMessage: StateCreator<
384
385
  return abortController;
385
386
  } else {
386
387
  if (!id) {
387
- set({ abortController: undefined, [key]: [] }, false, action);
388
+ set({ [abortControllerKey]: undefined, [key]: [] }, false, action);
388
389
  } else
389
390
  set(
390
391
  {
391
- abortController: undefined,
392
+ [abortControllerKey]: undefined,
392
393
  [key]: toggleBooleanList(get()[key] as string[], id, loading),
393
394
  },
394
395
  false,
@@ -52,6 +52,7 @@ export interface SystemStatus {
52
52
  latestChangelogId?: string;
53
53
  mobileShowPortal?: boolean;
54
54
  mobileShowTopic?: boolean;
55
+ portalWidth: number;
55
56
  sessionsWidth: number;
56
57
  showChatSideBar?: boolean;
57
58
  showFilePanel?: boolean;
@@ -86,6 +87,7 @@ export const INITIAL_STATUS = {
86
87
  hideThreadLimitAlert: false,
87
88
  inputHeight: 200,
88
89
  mobileShowTopic: false,
90
+ portalWidth: 400,
89
91
  sessionsWidth: 320,
90
92
  showChatSideBar: true,
91
93
  showFilePanel: true,
@@ -20,6 +20,7 @@ const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller;
20
20
  const showChatHeader = (s: GlobalStore) => !s.status.zenMode;
21
21
  const inZenMode = (s: GlobalStore) => s.status.zenMode;
22
22
  const sessionWidth = (s: GlobalStore) => s.status.sessionsWidth;
23
+ const portalWidth = (s: GlobalStore) => s.status.portalWidth || 400;
23
24
  const filePanelWidth = (s: GlobalStore) => s.status.filePanelWidth;
24
25
  const inputHeight = (s: GlobalStore) => s.status.inputHeight;
25
26
  const threadInputHeight = (s: GlobalStore) => s.status.threadInputHeight;
@@ -59,6 +60,7 @@ export const systemStatusSelectors = {
59
60
  isPgliteNotInited,
60
61
  mobileShowPortal,
61
62
  mobileShowTopic,
63
+ portalWidth,
62
64
  sessionGroupKeys,
63
65
  sessionWidth,
64
66
  showChatHeader,
@@ -49,6 +49,7 @@ describe('serverConfigSelectors', () => {
49
49
  serverConfig: {
50
50
  enabledOAuthSSO: true,
51
51
  telemetry: {},
52
+ aiProvider: {},
52
53
  },
53
54
  });
54
55
 
@@ -63,6 +64,7 @@ describe('serverConfigSelectors', () => {
63
64
  const store = initServerConfigStore({
64
65
  serverConfig: {
65
66
  telemetry: { langfuse: true },
67
+ aiProvider: {},
66
68
  },
67
69
  });
68
70
 
@@ -75,6 +77,7 @@ describe('serverConfigSelectors', () => {
75
77
  const store = initServerConfigStore({
76
78
  serverConfig: {
77
79
  telemetry: {},
80
+ aiProvider: {},
78
81
  },
79
82
  });
80
83
 
@@ -23,14 +23,14 @@ describe('createServerConfigStore', () => {
23
23
 
24
24
  expect(store.getState()).toEqual({
25
25
  featureFlags: DEFAULT_FEATURE_FLAGS,
26
- serverConfig: { telemetry: {} },
26
+ serverConfig: { telemetry: {}, aiProvider: {} },
27
27
  });
28
28
  });
29
29
 
30
30
  it('should initialize store with custom initial state', () => {
31
31
  const initialState: Partial<ServerConfigStore> = {
32
32
  featureFlags: { edit_agent: false },
33
- serverConfig: { telemetry: { langfuse: true } },
33
+ serverConfig: { telemetry: { langfuse: true }, aiProvider: {} },
34
34
  };
35
35
 
36
36
  const store = initServerConfigStore(initialState);
@@ -38,6 +38,7 @@ describe('createServerConfigStore', () => {
38
38
  expect(store.getState().featureFlags.edit_agent).toBeFalsy();
39
39
  expect(store.getState().serverConfig).toEqual({
40
40
  telemetry: { langfuse: true },
41
+ aiProvider: {},
41
42
  });
42
43
  });
43
44
 
@@ -12,7 +12,7 @@ import { StoreApiWithSelector } from '@/utils/zustand';
12
12
 
13
13
  const initialState: ServerConfigStore = {
14
14
  featureFlags: DEFAULT_FEATURE_FLAGS,
15
- serverConfig: { telemetry: {} },
15
+ serverConfig: { aiProvider: {}, telemetry: {} },
16
16
  };
17
17
 
18
18
  // =============== 聚合 createStoreFn ============ //
@@ -55,6 +55,7 @@ describe('createCommonSlice', () => {
55
55
  defaultAgent: 'agent1',
56
56
  languageModel: 'model1',
57
57
  telemetry: {},
58
+ aiProvider: {},
58
59
  } as GlobalServerConfig;
59
60
 
60
61
  it('should not fetch user state if user is not login', async () => {
@@ -20,7 +20,7 @@ export interface ServerModelProviderConfig {
20
20
  export type ServerLanguageModel = Partial<Record<GlobalLLMProviderKey, ServerModelProviderConfig>>;
21
21
 
22
22
  export interface GlobalServerConfig {
23
- aiProvider?: ServerLanguageModel;
23
+ aiProvider: ServerLanguageModel;
24
24
  defaultAgent?: DeepPartial<UserDefaultAgent>;
25
25
  enableUploadFileToServer?: boolean;
26
26
  enabledAccessCode?: boolean;
@@ -1,73 +0,0 @@
1
- import { CheckCircleFilled } from '@ant-design/icons';
2
- import { Alert, Highlighter } from '@lobehub/ui';
3
- import { Button } from 'antd';
4
- import { useTheme } from 'antd-style';
5
- import { ListResponse } from 'ollama/browser';
6
- import { memo } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
- import { Flexbox } from 'react-layout-kit';
9
- import useSWR from 'swr';
10
-
11
- import { useIsMobile } from '@/hooks/useIsMobile';
12
- import { ollamaService } from '@/services/ollama';
13
-
14
- const OllamaChecker = memo(() => {
15
- const { t } = useTranslation('setting');
16
-
17
- const theme = useTheme();
18
-
19
- const { data, error, isLoading, mutate } = useSWR<ListResponse>(
20
- 'ollama.list',
21
- ollamaService.getModels,
22
- {
23
- revalidateOnFocus: false,
24
- revalidateOnMount: false,
25
- revalidateOnReconnect: false,
26
- },
27
- );
28
-
29
- const checkConnection = () => {
30
- mutate().catch();
31
- };
32
-
33
- const isMobile = useIsMobile();
34
-
35
- return (
36
- <Flexbox align={isMobile ? 'flex-start' : 'flex-end'} gap={8}>
37
- <Flexbox align={'center'} direction={isMobile ? 'horizontal-reverse' : 'horizontal'} gap={12}>
38
- {!error && data?.models && (
39
- <Flexbox gap={4} horizontal>
40
- <CheckCircleFilled
41
- style={{
42
- color: theme.colorSuccess,
43
- }}
44
- />
45
- {t('llm.checker.pass')}
46
- </Flexbox>
47
- )}
48
- <Button loading={isLoading} onClick={checkConnection}>
49
- {t('llm.checker.button')}
50
- </Button>
51
- </Flexbox>
52
- {error && (
53
- <Flexbox gap={8} style={{ maxWidth: '600px', width: '100%' }}>
54
- <Alert
55
- banner
56
- extra={
57
- <Flexbox>
58
- <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
59
- {JSON.stringify(error.body || error, null, 2)}
60
- </Highlighter>
61
- </Flexbox>
62
- }
63
- message={t(`response.${error.type}` as any, { ns: 'error' })}
64
- showIcon
65
- type={'error'}
66
- />
67
- </Flexbox>
68
- )}
69
- </Flexbox>
70
- );
71
- });
72
-
73
- export default OllamaChecker;