@lobehub/chat 1.77.15 → 1.77.17
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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docker-compose/local/docker-compose.yml +2 -1
- package/locales/ar/components.json +4 -0
- package/locales/ar/modelProvider.json +1 -0
- package/locales/ar/models.json +8 -5
- package/locales/bg-BG/components.json +4 -0
- package/locales/bg-BG/modelProvider.json +1 -0
- package/locales/bg-BG/models.json +8 -5
- package/locales/de-DE/components.json +4 -0
- package/locales/de-DE/modelProvider.json +1 -0
- package/locales/de-DE/models.json +8 -5
- package/locales/en-US/components.json +4 -0
- package/locales/en-US/modelProvider.json +1 -0
- package/locales/en-US/models.json +8 -5
- package/locales/es-ES/components.json +4 -0
- package/locales/es-ES/modelProvider.json +1 -0
- package/locales/es-ES/models.json +7 -4
- package/locales/fa-IR/components.json +4 -0
- package/locales/fa-IR/modelProvider.json +1 -0
- package/locales/fa-IR/models.json +7 -4
- package/locales/fr-FR/components.json +4 -0
- package/locales/fr-FR/modelProvider.json +1 -0
- package/locales/fr-FR/models.json +8 -5
- package/locales/it-IT/components.json +4 -0
- package/locales/it-IT/modelProvider.json +1 -0
- package/locales/it-IT/models.json +7 -4
- package/locales/ja-JP/components.json +4 -0
- package/locales/ja-JP/modelProvider.json +1 -0
- package/locales/ja-JP/models.json +8 -5
- package/locales/ko-KR/components.json +4 -0
- package/locales/ko-KR/modelProvider.json +1 -0
- package/locales/ko-KR/models.json +8 -5
- package/locales/nl-NL/components.json +4 -0
- package/locales/nl-NL/modelProvider.json +1 -0
- package/locales/nl-NL/models.json +8 -5
- package/locales/pl-PL/components.json +4 -0
- package/locales/pl-PL/modelProvider.json +1 -0
- package/locales/pl-PL/models.json +8 -5
- package/locales/pt-BR/components.json +4 -0
- package/locales/pt-BR/modelProvider.json +1 -0
- package/locales/pt-BR/models.json +7 -4
- package/locales/ru-RU/components.json +4 -0
- package/locales/ru-RU/modelProvider.json +1 -0
- package/locales/ru-RU/models.json +7 -4
- package/locales/tr-TR/components.json +4 -0
- package/locales/tr-TR/modelProvider.json +1 -0
- package/locales/tr-TR/models.json +8 -5
- package/locales/vi-VN/components.json +4 -0
- package/locales/vi-VN/modelProvider.json +1 -0
- package/locales/vi-VN/models.json +8 -5
- package/locales/zh-CN/components.json +4 -0
- package/locales/zh-CN/modelProvider.json +1 -0
- package/locales/zh-CN/models.json +9 -6
- package/locales/zh-TW/components.json +4 -0
- package/locales/zh-TW/modelProvider.json +1 -0
- package/locales/zh-TW/models.json +7 -4
- package/package.json +1 -1
- package/src/app/(backend)/webapi/models/[provider]/pull/route.ts +34 -0
- package/src/app/(backend)/webapi/{chat/models → models}/[provider]/route.ts +1 -2
- package/src/app/[variants]/(main)/settings/llm/ProviderList/Ollama/index.tsx +0 -7
- package/src/app/[variants]/(main)/settings/provider/(detail)/ollama/CheckError.tsx +1 -1
- package/src/components/FormAction/index.tsx +1 -1
- package/src/database/models/__tests__/aiProvider.test.ts +100 -0
- package/src/database/models/aiProvider.ts +11 -1
- package/src/features/Conversation/Error/OllamaBizError/InvalidOllamaModel.tsx +43 -0
- package/src/features/Conversation/Error/OllamaDesktopSetupGuide/index.tsx +61 -0
- package/src/features/Conversation/Error/index.tsx +7 -0
- package/src/features/DevPanel/SystemInspector/ServerConfig.tsx +18 -2
- package/src/features/DevPanel/SystemInspector/index.tsx +25 -6
- package/src/features/OllamaModelDownloader/index.tsx +149 -0
- package/src/libs/agent-runtime/AgentRuntime.ts +6 -0
- package/src/libs/agent-runtime/BaseAI.ts +7 -0
- package/src/libs/agent-runtime/ollama/index.ts +84 -2
- package/src/libs/agent-runtime/openrouter/__snapshots__/index.test.ts.snap +24 -3263
- package/src/libs/agent-runtime/openrouter/fixtures/frontendModels.json +25 -0
- package/src/libs/agent-runtime/openrouter/fixtures/models.json +0 -3353
- package/src/libs/agent-runtime/openrouter/index.test.ts +56 -1
- package/src/libs/agent-runtime/openrouter/index.ts +9 -4
- package/src/libs/agent-runtime/types/index.ts +1 -0
- package/src/libs/agent-runtime/types/model.ts +44 -0
- package/src/libs/agent-runtime/utils/streams/index.ts +1 -0
- package/src/libs/agent-runtime/utils/streams/model.ts +110 -0
- package/src/locales/default/components.ts +4 -0
- package/src/locales/default/modelProvider.ts +1 -0
- package/src/server/routers/async/file.ts +3 -4
- package/src/server/routers/lambda/file.ts +8 -11
- package/src/server/routers/lambda/importer.ts +3 -4
- package/src/server/routers/lambda/message.ts +9 -3
- package/src/server/routers/lambda/ragEval.ts +5 -6
- package/src/server/services/file/impls/index.ts +12 -0
- package/src/server/services/file/impls/s3.test.ts +110 -0
- package/src/server/services/file/impls/s3.ts +60 -0
- package/src/server/services/file/impls/type.ts +44 -0
- package/src/server/services/file/index.ts +65 -0
- package/src/services/__tests__/models.test.ts +21 -0
- package/src/services/_url.ts +4 -1
- package/src/services/chat.ts +1 -1
- package/src/services/electron/__tests__/devtools.test.ts +34 -0
- package/src/services/models.ts +153 -7
- package/src/store/aiInfra/slices/aiModel/action.ts +1 -1
- package/src/store/aiInfra/slices/aiProvider/action.ts +2 -1
- package/src/store/user/slices/modelList/action.test.ts +2 -2
- package/src/store/user/slices/modelList/action.ts +1 -1
- package/src/app/[variants]/(main)/settings/llm/ProviderList/Ollama/Checker.tsx +0 -73
- package/src/app/[variants]/(main)/settings/provider/(detail)/ollama/OllamaModelDownloader/index.tsx +0 -127
- package/src/features/Conversation/Error/OllamaBizError/InvalidOllamaModel/index.tsx +0 -154
- package/src/features/Conversation/Error/OllamaBizError/InvalidOllamaModel/useDownloadMonitor.ts +0 -29
- package/src/server/utils/files.test.ts +0 -37
- package/src/server/utils/files.ts +0 -20
- package/src/services/__tests__/ollama.test.ts +0 -28
- package/src/services/ollama.ts +0 -83
- /package/src/{app/[variants]/(main)/settings/provider/(detail)/ollama → features}/OllamaModelDownloader/useDownloadMonitor.ts +0 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
import urlJoin from 'url-join';
|
2
|
+
|
3
|
+
import { fileEnv } from '@/config/file';
|
4
|
+
import { S3 } from '@/server/modules/S3';
|
5
|
+
|
6
|
+
import { FileServiceImpl } from './type';
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 基于S3的文件服务实现
|
10
|
+
*/
|
11
|
+
export class S3StaticFileImpl implements FileServiceImpl {
|
12
|
+
private readonly s3: S3;
|
13
|
+
|
14
|
+
constructor() {
|
15
|
+
this.s3 = new S3();
|
16
|
+
}
|
17
|
+
|
18
|
+
async deleteFile(key: string) {
|
19
|
+
return this.s3.deleteFile(key);
|
20
|
+
}
|
21
|
+
|
22
|
+
async deleteFiles(keys: string[]) {
|
23
|
+
return this.s3.deleteFiles(keys);
|
24
|
+
}
|
25
|
+
|
26
|
+
async getFileContent(key: string): Promise<string> {
|
27
|
+
return this.s3.getFileContent(key);
|
28
|
+
}
|
29
|
+
|
30
|
+
async getFileByteArray(key: string): Promise<Uint8Array> {
|
31
|
+
return this.s3.getFileByteArray(key);
|
32
|
+
}
|
33
|
+
|
34
|
+
async createPreSignedUrl(key: string): Promise<string> {
|
35
|
+
return this.s3.createPreSignedUrl(key);
|
36
|
+
}
|
37
|
+
|
38
|
+
async createPreSignedUrlForPreview(key: string, expiresIn?: number): Promise<string> {
|
39
|
+
return this.s3.createPreSignedUrlForPreview(key, expiresIn);
|
40
|
+
}
|
41
|
+
|
42
|
+
async uploadContent(path: string, content: string) {
|
43
|
+
return this.s3.uploadContent(path, content);
|
44
|
+
}
|
45
|
+
|
46
|
+
async getFullFileUrl(url?: string | null, expiresIn?: number): Promise<string> {
|
47
|
+
if (!url) return '';
|
48
|
+
|
49
|
+
// If bucket is not set public read, the preview address needs to be regenerated each time
|
50
|
+
if (!fileEnv.S3_SET_ACL) {
|
51
|
+
return await this.createPreSignedUrlForPreview(url, expiresIn);
|
52
|
+
}
|
53
|
+
|
54
|
+
if (fileEnv.S3_ENABLE_PATH_STYLE) {
|
55
|
+
return urlJoin(fileEnv.S3_PUBLIC_DOMAIN!, fileEnv.S3_BUCKET!, url);
|
56
|
+
}
|
57
|
+
|
58
|
+
return urlJoin(fileEnv.S3_PUBLIC_DOMAIN!, url);
|
59
|
+
}
|
60
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
/**
|
2
|
+
* S3文件服务实现
|
3
|
+
*/
|
4
|
+
export interface FileServiceImpl {
|
5
|
+
/**
|
6
|
+
* 创建预签名上传URL
|
7
|
+
*/
|
8
|
+
createPreSignedUrl(key: string): Promise<string>;
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 创建预签名预览URL
|
12
|
+
*/
|
13
|
+
createPreSignedUrlForPreview(key: string, expiresIn?: number): Promise<string>;
|
14
|
+
|
15
|
+
/**
|
16
|
+
* 删除文件
|
17
|
+
*/
|
18
|
+
deleteFile(key: string): Promise<any>;
|
19
|
+
|
20
|
+
/**
|
21
|
+
* 批量删除文件
|
22
|
+
*/
|
23
|
+
deleteFiles(keys: string[]): Promise<any>;
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 获取文件字节数组
|
27
|
+
*/
|
28
|
+
getFileByteArray(key: string): Promise<Uint8Array>;
|
29
|
+
|
30
|
+
/**
|
31
|
+
* 获取文件内容
|
32
|
+
*/
|
33
|
+
getFileContent(key: string): Promise<string>;
|
34
|
+
|
35
|
+
/**
|
36
|
+
* 获取完整文件URL
|
37
|
+
*/
|
38
|
+
getFullFileUrl(url?: string | null, expiresIn?: number): Promise<string>;
|
39
|
+
|
40
|
+
/**
|
41
|
+
* 上传内容
|
42
|
+
*/
|
43
|
+
uploadContent(path: string, content: string): Promise<any>;
|
44
|
+
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import { FileServiceImpl, createFileServiceModule } from './impls';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* 文件服务类
|
5
|
+
* 使用模块化实现方式,提供文件操作服务
|
6
|
+
*/
|
7
|
+
export class FileService {
|
8
|
+
private impl: FileServiceImpl = createFileServiceModule();
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 删除文件
|
12
|
+
*/
|
13
|
+
public async deleteFile(key: string) {
|
14
|
+
return this.impl.deleteFile(key);
|
15
|
+
}
|
16
|
+
|
17
|
+
/**
|
18
|
+
* 批量删除文件
|
19
|
+
*/
|
20
|
+
public async deleteFiles(keys: string[]) {
|
21
|
+
return this.impl.deleteFiles(keys);
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* 获取文件内容
|
26
|
+
*/
|
27
|
+
public async getFileContent(key: string): Promise<string> {
|
28
|
+
return this.impl.getFileContent(key);
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* 获取文件字节数组
|
33
|
+
*/
|
34
|
+
public async getFileByteArray(key: string): Promise<Uint8Array> {
|
35
|
+
return this.impl.getFileByteArray(key);
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* 创建预签名上传URL
|
40
|
+
*/
|
41
|
+
public async createPreSignedUrl(key: string): Promise<string> {
|
42
|
+
return this.impl.createPreSignedUrl(key);
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* 创建预签名预览URL
|
47
|
+
*/
|
48
|
+
public async createPreSignedUrlForPreview(key: string, expiresIn?: number): Promise<string> {
|
49
|
+
return this.impl.createPreSignedUrlForPreview(key, expiresIn);
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* 上传内容
|
54
|
+
*/
|
55
|
+
public async uploadContent(path: string, content: string) {
|
56
|
+
return this.impl.uploadContent(path, content);
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* 获取完整文件URL
|
61
|
+
*/
|
62
|
+
public async getFullFileUrl(url?: string | null, expiresIn?: number): Promise<string> {
|
63
|
+
return this.impl.getFullFileUrl(url, expiresIn);
|
64
|
+
}
|
65
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { Mock, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { ModelsService } from '../models';
|
4
|
+
|
5
|
+
vi.stubGlobal('fetch', vi.fn());
|
6
|
+
|
7
|
+
// 创建一个测试用的 ModelsService 实例
|
8
|
+
|
9
|
+
const modelsService = new ModelsService();
|
10
|
+
|
11
|
+
describe('ModelsService', () => {
|
12
|
+
describe('getModels', () => {
|
13
|
+
it('should call the appropriate endpoint for a generic provider', async () => {
|
14
|
+
(fetch as Mock).mockResolvedValueOnce(new Response(JSON.stringify({ models: [] })));
|
15
|
+
|
16
|
+
await modelsService.getModels('openai');
|
17
|
+
|
18
|
+
expect(fetch).toHaveBeenCalled();
|
19
|
+
});
|
20
|
+
});
|
21
|
+
});
|
package/src/services/_url.ts
CHANGED
@@ -32,7 +32,10 @@ export const API_ENDPOINTS = mapWithBasePath({
|
|
32
32
|
|
33
33
|
// chat
|
34
34
|
chat: (provider: string) => withBasePath(`/webapi/chat/${provider}`),
|
35
|
-
|
35
|
+
|
36
|
+
// models
|
37
|
+
models: (provider: string) => withBasePath(`/webapi/models/${provider}`),
|
38
|
+
modelPull: (provider: string) => withBasePath(`/webapi/models/${provider}/pull`),
|
36
39
|
|
37
40
|
// image
|
38
41
|
images: (provider: string) => `/webapi/text-to-image/${provider}`,
|
package/src/services/chat.ts
CHANGED
@@ -133,7 +133,7 @@ interface CreateAssistantMessageStream extends FetchSSEOptions {
|
|
133
133
|
*
|
134
134
|
* **Note**: if you try to fetch directly, use `fetchOnClient` instead.
|
135
135
|
*/
|
136
|
-
export function initializeWithClientStore(provider: string, payload
|
136
|
+
export function initializeWithClientStore(provider: string, payload?: any) {
|
137
137
|
/**
|
138
138
|
* Since #5267, we map parameters for client-fetch in function `getProviderAuthPayload`
|
139
139
|
* which called by `createPayloadWithKeyVaults` below.
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import { dispatch } from '@lobechat/electron-client-ipc';
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { electronDevtoolsService } from '../devtools';
|
5
|
+
|
6
|
+
vi.mock('@lobechat/electron-client-ipc', () => ({
|
7
|
+
dispatch: vi.fn(),
|
8
|
+
}));
|
9
|
+
|
10
|
+
describe('DevtoolsService', () => {
|
11
|
+
beforeEach(() => {
|
12
|
+
vi.clearAllMocks();
|
13
|
+
});
|
14
|
+
|
15
|
+
describe('openDevtools', () => {
|
16
|
+
it('should call dispatch with openDevtools', async () => {
|
17
|
+
await electronDevtoolsService.openDevtools();
|
18
|
+
expect(dispatch).toHaveBeenCalledWith('openDevtools');
|
19
|
+
});
|
20
|
+
|
21
|
+
it('should return void when dispatch succeeds', async () => {
|
22
|
+
vi.mocked(dispatch).mockResolvedValueOnce();
|
23
|
+
const result = await electronDevtoolsService.openDevtools();
|
24
|
+
expect(result).toBeUndefined();
|
25
|
+
});
|
26
|
+
|
27
|
+
it('should throw error when dispatch fails', async () => {
|
28
|
+
const error = new Error('Failed to open devtools');
|
29
|
+
vi.mocked(dispatch).mockRejectedValueOnce(error);
|
30
|
+
|
31
|
+
await expect(electronDevtoolsService.openDevtools()).rejects.toThrow(error);
|
32
|
+
});
|
33
|
+
});
|
34
|
+
});
|
package/src/services/models.ts
CHANGED
@@ -1,13 +1,42 @@
|
|
1
|
+
import { isDeprecatedEdition } from '@/const/version';
|
1
2
|
import { createHeaderWithAuth } from '@/services/_auth';
|
3
|
+
import { aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
|
2
4
|
import { useUserStore } from '@/store/user';
|
3
5
|
import { modelConfigSelectors } from '@/store/user/selectors';
|
4
6
|
import { ChatModelCard } from '@/types/llm';
|
7
|
+
import { getMessageError } from '@/utils/fetch';
|
5
8
|
|
6
9
|
import { API_ENDPOINTS } from './_url';
|
7
10
|
import { initializeWithClientStore } from './chat';
|
8
11
|
|
9
|
-
|
10
|
-
|
12
|
+
const isEnableFetchOnClient = (provider: string) => {
|
13
|
+
// TODO: remove this condition in V2.0
|
14
|
+
if (isDeprecatedEdition) {
|
15
|
+
return modelConfigSelectors.isProviderFetchOnClient(provider)(useUserStore.getState());
|
16
|
+
} else {
|
17
|
+
return aiProviderSelectors.isProviderFetchOnClient(provider)(getAiInfraStoreState());
|
18
|
+
}
|
19
|
+
};
|
20
|
+
|
21
|
+
// 进度信息接口
|
22
|
+
export interface ModelProgressInfo {
|
23
|
+
completed?: number;
|
24
|
+
digest?: string;
|
25
|
+
model?: string;
|
26
|
+
status?: string;
|
27
|
+
total?: number;
|
28
|
+
}
|
29
|
+
|
30
|
+
// 进度回调函数类型
|
31
|
+
export type ProgressCallback = (progress: ModelProgressInfo) => void;
|
32
|
+
export type ErrorCallback = (error: { message: string }) => void;
|
33
|
+
|
34
|
+
export class ModelsService {
|
35
|
+
// 用于中断下载的控制器
|
36
|
+
private _abortController: AbortController | null = null;
|
37
|
+
|
38
|
+
// 获取模型列表
|
39
|
+
getModels = async (provider: string): Promise<ChatModelCard[] | undefined> => {
|
11
40
|
const headers = await createHeaderWithAuth({
|
12
41
|
headers: { 'Content-Type': 'application/json' },
|
13
42
|
provider,
|
@@ -16,15 +45,13 @@ class ModelsService {
|
|
16
45
|
/**
|
17
46
|
* Use browser agent runtime
|
18
47
|
*/
|
19
|
-
const enableFetchOnClient =
|
20
|
-
useUserStore.getState(),
|
21
|
-
);
|
48
|
+
const enableFetchOnClient = isEnableFetchOnClient(provider);
|
22
49
|
if (enableFetchOnClient) {
|
23
|
-
const agentRuntime = await initializeWithClientStore(provider
|
50
|
+
const agentRuntime = await initializeWithClientStore(provider);
|
24
51
|
return agentRuntime.models();
|
25
52
|
}
|
26
53
|
|
27
|
-
const res = await fetch(API_ENDPOINTS.
|
54
|
+
const res = await fetch(API_ENDPOINTS.models(provider), { headers });
|
28
55
|
if (!res.ok) return;
|
29
56
|
|
30
57
|
return res.json();
|
@@ -32,6 +59,125 @@ class ModelsService {
|
|
32
59
|
return;
|
33
60
|
}
|
34
61
|
};
|
62
|
+
|
63
|
+
/**
|
64
|
+
* 下载模型并通过回调函数返回进度信息
|
65
|
+
*/
|
66
|
+
downloadModel = async (
|
67
|
+
{ model, provider }: { model: string; provider: string },
|
68
|
+
{ onProgress }: { onError?: ErrorCallback; onProgress?: ProgressCallback } = {},
|
69
|
+
): Promise<void> => {
|
70
|
+
try {
|
71
|
+
// 创建一个新的 AbortController
|
72
|
+
this._abortController = new AbortController();
|
73
|
+
const signal = this._abortController.signal;
|
74
|
+
|
75
|
+
const headers = await createHeaderWithAuth({
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
77
|
+
provider,
|
78
|
+
});
|
79
|
+
|
80
|
+
const enableFetchOnClient = isEnableFetchOnClient(provider);
|
81
|
+
|
82
|
+
console.log('enableFetchOnClient:', enableFetchOnClient);
|
83
|
+
let res: Response;
|
84
|
+
if (enableFetchOnClient) {
|
85
|
+
const agentRuntime = await initializeWithClientStore(provider);
|
86
|
+
res = (await agentRuntime.pullModel({ model }, { signal }))!;
|
87
|
+
} else {
|
88
|
+
res = await fetch(API_ENDPOINTS.modelPull(provider), {
|
89
|
+
body: JSON.stringify({ model }),
|
90
|
+
headers,
|
91
|
+
method: 'POST',
|
92
|
+
signal,
|
93
|
+
});
|
94
|
+
}
|
95
|
+
|
96
|
+
if (!res.ok) {
|
97
|
+
throw await getMessageError(res);
|
98
|
+
}
|
99
|
+
|
100
|
+
// 处理响应流
|
101
|
+
if (res.body) {
|
102
|
+
await this.processModelPullStream(res, { onProgress });
|
103
|
+
}
|
104
|
+
} catch (error) {
|
105
|
+
// 如果是取消操作,不需要继续抛出错误
|
106
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
|
110
|
+
console.error('download model error:', error);
|
111
|
+
throw error;
|
112
|
+
} finally {
|
113
|
+
// 清理 AbortController
|
114
|
+
this._abortController = null;
|
115
|
+
}
|
116
|
+
};
|
117
|
+
|
118
|
+
// 中断模型下载
|
119
|
+
abortPull = () => {
|
120
|
+
// 使用 AbortController 中断下载
|
121
|
+
if (this._abortController) {
|
122
|
+
this._abortController.abort();
|
123
|
+
this._abortController = null;
|
124
|
+
}
|
125
|
+
};
|
126
|
+
|
127
|
+
/**
|
128
|
+
* 处理模型下载流,解析进度信息并通过回调函数返回
|
129
|
+
* @param response 响应对象
|
130
|
+
* @param onProgress 进度回调函数
|
131
|
+
* @returns Promise<void>
|
132
|
+
*/
|
133
|
+
private processModelPullStream = async (
|
134
|
+
response: Response,
|
135
|
+
{ onProgress, onError }: { onError?: ErrorCallback; onProgress?: ProgressCallback },
|
136
|
+
): Promise<void> => {
|
137
|
+
// 处理响应流
|
138
|
+
const reader = response.body?.getReader();
|
139
|
+
if (!reader) return;
|
140
|
+
|
141
|
+
// 读取和处理流数据
|
142
|
+
// eslint-disable-next-line no-constant-condition
|
143
|
+
while (true) {
|
144
|
+
const { done, value } = await reader.read();
|
145
|
+
if (done) break;
|
146
|
+
|
147
|
+
// 解析进度数据
|
148
|
+
const progressText = new TextDecoder().decode(value);
|
149
|
+
// 一行可能包含多个进度更新
|
150
|
+
const progressUpdates = progressText.trim().split('\n');
|
151
|
+
|
152
|
+
for (const update of progressUpdates) {
|
153
|
+
let progress;
|
154
|
+
try {
|
155
|
+
progress = JSON.parse(update);
|
156
|
+
} catch (e) {
|
157
|
+
console.error('Error parsing progress update:', e);
|
158
|
+
console.error('raw data', update);
|
159
|
+
}
|
160
|
+
|
161
|
+
if (progress.status === 'canceled') {
|
162
|
+
console.log('progress:', progress);
|
163
|
+
// const abortError = new Error('abort');
|
164
|
+
// abortError.name = 'AbortError';
|
165
|
+
//
|
166
|
+
// throw abortError;
|
167
|
+
}
|
168
|
+
|
169
|
+
if (progress.status === 'error') {
|
170
|
+
onError?.({ message: progress.error });
|
171
|
+
throw new Error(progress.error);
|
172
|
+
}
|
173
|
+
|
174
|
+
// 调用进度回调
|
175
|
+
if (progress.completed !== undefined || progress.status) {
|
176
|
+
onProgress?.(progress);
|
177
|
+
}
|
178
|
+
}
|
179
|
+
}
|
180
|
+
};
|
35
181
|
}
|
36
182
|
|
37
183
|
export const modelsService = new ModelsService();
|
@@ -71,7 +71,7 @@ export const createAiModelSlice: StateCreator<
|
|
71
71
|
fetchRemoteModelList: async (providerId) => {
|
72
72
|
const { modelsService } = await import('@/services/models');
|
73
73
|
|
74
|
-
const data = await modelsService.
|
74
|
+
const data = await modelsService.getModels(providerId);
|
75
75
|
if (data) {
|
76
76
|
await get().batchUpdateAiModels(
|
77
77
|
data.map((model) => ({
|
@@ -3,7 +3,7 @@ import { SWRResponse, mutate } from 'swr';
|
|
3
3
|
import { StateCreator } from 'zustand/vanilla';
|
4
4
|
|
5
5
|
import { DEFAULT_MODEL_PROVIDER_LIST } from '@/config/modelProviders';
|
6
|
-
import { isDeprecatedEdition } from '@/const/version';
|
6
|
+
import { isDeprecatedEdition, isDesktop, isUsePgliteDB } from '@/const/version';
|
7
7
|
import { useClientDataSWR } from '@/libs/swr';
|
8
8
|
import { aiProviderService } from '@/services/aiProvider';
|
9
9
|
import { AiInfraStore } from '@/store/aiInfra/store';
|
@@ -184,6 +184,7 @@ export const createAiProviderSlice: StateCreator<
|
|
184
184
|
};
|
185
185
|
},
|
186
186
|
{
|
187
|
+
focusThrottleInterval: isDesktop || isUsePgliteDB ? 100 : undefined,
|
187
188
|
onSuccess: async (data) => {
|
188
189
|
if (!data) return;
|
189
190
|
|
@@ -328,7 +328,7 @@ describe('LLMSettingsSliceAction', () => {
|
|
328
328
|
|
329
329
|
const spyOn = vi.spyOn(result.current, 'refreshDefaultModelProviderList');
|
330
330
|
|
331
|
-
vi.spyOn(modelsService, '
|
331
|
+
vi.spyOn(modelsService, 'getModels').mockResolvedValueOnce([]);
|
332
332
|
|
333
333
|
renderHook(() => result.current.useFetchProviderModelList(provider, enabledAutoFetch));
|
334
334
|
|
@@ -347,7 +347,7 @@ describe('LLMSettingsSliceAction', () => {
|
|
347
347
|
|
348
348
|
const spyOn = vi.spyOn(result.current, 'refreshDefaultModelProviderList');
|
349
349
|
|
350
|
-
vi.spyOn(modelsService, '
|
350
|
+
vi.spyOn(modelsService, 'getModels').mockResolvedValueOnce([]);
|
351
351
|
|
352
352
|
renderHook(() => result.current.useFetchProviderModelList(provider, enabledAutoFetch));
|
353
353
|
|
@@ -202,7 +202,7 @@ export const createModelListSlice: StateCreator<
|
|
202
202
|
async ([p]) => {
|
203
203
|
const { modelsService } = await import('@/services/models');
|
204
204
|
|
205
|
-
return modelsService.
|
205
|
+
return modelsService.getModels(p);
|
206
206
|
},
|
207
207
|
{
|
208
208
|
onSuccess: async (data) => {
|
@@ -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;
|