@lobehub/chat 1.84.17 → 1.84.19
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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/src/features/ElectronTitlebar/index.tsx +7 -3
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +9 -3
- package/src/libs/agent-runtime/utils/streams/qwen.test.ts +8 -4
- package/src/libs/agent-runtime/utils/streams/qwen.ts +3 -1
- package/src/libs/agent-runtime/utils/streams/spark.test.ts +6 -2
- package/src/libs/agent-runtime/utils/streams/spark.ts +3 -1
- package/src/server/modules/AssistantStore/index.ts +5 -2
- package/src/server/routers/edge/index.ts +2 -0
- package/src/server/routers/edge/market/index.ts +108 -0
- package/src/services/__tests__/assistant.test.ts +25 -18
- package/src/services/__tests__/tool.test.ts +14 -28
- package/src/services/_url.ts +0 -5
- package/src/services/assistant.ts +4 -9
- package/src/services/tool.ts +3 -6
- package/src/store/chat/slices/builtinTool/actions/__tests__/localFile.test.ts +211 -0
- package/src/store/electron/initialState.ts +2 -0
- package/src/app/(backend)/webapi/assistant/[id]/route.ts +0 -25
- package/src/app/(backend)/webapi/assistant/store/route.ts +0 -31
- package/src/app/(backend)/webapi/plugin/store/route.ts +0 -43
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.84.19](https://github.com/lobehub/lobe-chat/compare/v1.84.18...v1.84.19)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-04**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Fix init state of loading.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Fix init state of loading, closes [#7694](https://github.com/lobehub/lobe-chat/issues/7694) ([1d97a68](https://github.com/lobehub/lobe-chat/commit/1d97a68))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.84.18](https://github.com/lobehub/lobe-chat/compare/v1.84.17...v1.84.18)
|
31
|
+
|
32
|
+
<sup>Released on **2025-05-03**</sup>
|
33
|
+
|
34
|
+
#### ♻ Code Refactoring
|
35
|
+
|
36
|
+
- **misc**: Add perf stat support for openai factory.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### Code refactoring
|
44
|
+
|
45
|
+
- **misc**: Add perf stat support for openai factory, closes [#7677](https://github.com/lobehub/lobe-chat/issues/7677) ([40464d1](https://github.com/lobehub/lobe-chat/commit/40464d1))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.84.17](https://github.com/lobehub/lobe-chat/compare/v1.84.16...v1.84.17)
|
6
56
|
|
7
57
|
<sup>Released on **2025-05-03**</sup>
|
package/README.md
CHANGED
@@ -330,10 +330,10 @@ In addition, these plugins are not limited to news aggregation, but can also ext
|
|
330
330
|
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
331
331
|
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-03-23**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
|
332
332
|
| [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
|
333
|
-
| [MintbaseSearch](https://lobechat.com/discover/plugin/mintbasesearch)<br/><sup>By **mintbase** on **2024-12-31**</sup> | Find any NFT data on the NEAR Protocol.<br/>`crypto` `nft` |
|
334
333
|
| [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
|
334
|
+
| [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
|
335
335
|
|
336
|
-
> 📊 Total plugins: [<kbd>**
|
336
|
+
> 📊 Total plugins: [<kbd>**44**</kbd>](https://lobechat.com/discover/plugins)
|
337
337
|
|
338
338
|
<!-- PLUGIN LIST -->
|
339
339
|
|
package/README.zh-CN.md
CHANGED
@@ -323,10 +323,10 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
|
323
323
|
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
324
324
|
| [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-03-23**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
|
325
325
|
| [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
|
326
|
-
| [MintbaseSearch](https://lobechat.com/discover/plugin/mintbasesearch)<br/><sup>By **mintbase** on **2024-12-31**</sup> | 在 NEAR 协议上查找任何 NFT 数据。<br/>`加密货币` `nft` |
|
327
326
|
| [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
|
327
|
+
| [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
|
328
328
|
|
329
|
-
> 📊 Total plugins: [<kbd>**
|
329
|
+
> 📊 Total plugins: [<kbd>**44**</kbd>](https://lobechat.com/discover/plugins)
|
330
330
|
|
331
331
|
<!-- PLUGIN LIST -->
|
332
332
|
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Fix init state of loading."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-04",
|
9
|
+
"version": "1.84.19"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"improvements": [
|
14
|
+
"Add perf stat support for openai factory."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-05-03",
|
18
|
+
"version": "1.84.18"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"improvements": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.84.
|
3
|
+
"version": "1.84.19",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -15,10 +15,14 @@ import { TITLE_BAR_HEIGHT } from './const';
|
|
15
15
|
const isMac = isMacOS();
|
16
16
|
|
17
17
|
const TitleBar = memo(() => {
|
18
|
-
const initElectronAppState = useElectronStore((s) =>
|
18
|
+
const [isAppStateInit, initElectronAppState] = useElectronStore((s) => [
|
19
|
+
s.isAppStateInit,
|
20
|
+
s.useInitElectronAppState,
|
21
|
+
]);
|
19
22
|
|
20
23
|
initElectronAppState();
|
21
24
|
|
25
|
+
const showWinControl = isAppStateInit && !isMac;
|
22
26
|
return (
|
23
27
|
<Flexbox
|
24
28
|
align={'center'}
|
@@ -26,7 +30,7 @@ const TitleBar = memo(() => {
|
|
26
30
|
height={TITLE_BAR_HEIGHT}
|
27
31
|
horizontal
|
28
32
|
justify={'space-between'}
|
29
|
-
paddingInline={
|
33
|
+
paddingInline={showWinControl ? '12px 0' : 12}
|
30
34
|
style={{ minHeight: TITLE_BAR_HEIGHT }}
|
31
35
|
width={'100%'}
|
32
36
|
>
|
@@ -38,7 +42,7 @@ const TitleBar = memo(() => {
|
|
38
42
|
<UpdateNotification />
|
39
43
|
<Connection />
|
40
44
|
</Flexbox>
|
41
|
-
{
|
45
|
+
{showWinControl && (
|
42
46
|
<>
|
43
47
|
<Divider type={'vertical'} />
|
44
48
|
<WinControl />
|
@@ -68,7 +68,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
|
|
68
68
|
) => OpenAI.ChatCompletionCreateParamsStreaming;
|
69
69
|
handleStream?: (
|
70
70
|
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
71
|
-
callbacks?: ChatStreamCallbacks,
|
71
|
+
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number },
|
72
72
|
) => ReadableStream;
|
73
73
|
handleStreamBizErrorType?: (error: {
|
74
74
|
message: string;
|
@@ -256,7 +256,10 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
256
256
|
|
257
257
|
return StreamingResponse(
|
258
258
|
chatCompletion?.handleStream
|
259
|
-
? chatCompletion.handleStream(prod,
|
259
|
+
? chatCompletion.handleStream(prod, {
|
260
|
+
callbacks: streamOptions.callbacks,
|
261
|
+
inputStartAt,
|
262
|
+
})
|
260
263
|
: OpenAIStream(prod, { ...streamOptions, inputStartAt }),
|
261
264
|
{
|
262
265
|
headers: options?.headers,
|
@@ -276,7 +279,10 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
276
279
|
|
277
280
|
return StreamingResponse(
|
278
281
|
chatCompletion?.handleStream
|
279
|
-
? chatCompletion.handleStream(stream,
|
282
|
+
? chatCompletion.handleStream(stream, {
|
283
|
+
callbacks: streamOptions.callbacks,
|
284
|
+
inputStartAt,
|
285
|
+
})
|
280
286
|
: OpenAIStream(stream, { ...streamOptions, inputStartAt }),
|
281
287
|
{
|
282
288
|
headers: options?.headers,
|
@@ -46,9 +46,11 @@ describe('QwenAIStream', () => {
|
|
46
46
|
const onCompletionMock = vi.fn();
|
47
47
|
|
48
48
|
const protocolStream = QwenAIStream(mockOpenAIStream, {
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
callbacks: {
|
50
|
+
onStart: onStartMock,
|
51
|
+
onText: onTextMock,
|
52
|
+
onCompletion: onCompletionMock,
|
53
|
+
},
|
52
54
|
});
|
53
55
|
|
54
56
|
const decoder = new TextDecoder();
|
@@ -111,7 +113,9 @@ describe('QwenAIStream', () => {
|
|
111
113
|
const onToolCallMock = vi.fn();
|
112
114
|
|
113
115
|
const protocolStream = QwenAIStream(mockOpenAIStream, {
|
114
|
-
|
116
|
+
callbacks: {
|
117
|
+
onToolsCalling: onToolCallMock,
|
118
|
+
},
|
115
119
|
});
|
116
120
|
|
117
121
|
const decoder = new TextDecoder();
|
@@ -92,7 +92,9 @@ export const transformQwenStream = (chunk: OpenAI.ChatCompletionChunk): StreamPr
|
|
92
92
|
|
93
93
|
export const QwenAIStream = (
|
94
94
|
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
95
|
-
|
95
|
+
// TODO: preserve for RFC 097
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
97
|
+
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
|
96
98
|
) => {
|
97
99
|
const readableStream =
|
98
100
|
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
@@ -96,7 +96,9 @@ describe('SparkAIStream', () => {
|
|
96
96
|
const onToolCallMock = vi.fn();
|
97
97
|
|
98
98
|
const protocolStream = SparkAIStream(mockStream, {
|
99
|
-
|
99
|
+
callbacks: {
|
100
|
+
onToolsCalling: onToolCallMock,
|
101
|
+
},
|
100
102
|
});
|
101
103
|
|
102
104
|
const decoder = new TextDecoder();
|
@@ -156,7 +158,9 @@ describe('SparkAIStream', () => {
|
|
156
158
|
const onTextMock = vi.fn();
|
157
159
|
|
158
160
|
const protocolStream = SparkAIStream(mockStream, {
|
159
|
-
|
161
|
+
callbacks: {
|
162
|
+
onText: onTextMock,
|
163
|
+
},
|
160
164
|
});
|
161
165
|
|
162
166
|
const decoder = new TextDecoder();
|
@@ -123,7 +123,9 @@ export const transformSparkStream = (chunk: OpenAI.ChatCompletionChunk): StreamP
|
|
123
123
|
|
124
124
|
export const SparkAIStream = (
|
125
125
|
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
126
|
-
|
126
|
+
// TODO: preserve for RFC 097
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
128
|
+
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
|
127
129
|
) => {
|
128
130
|
const readableStream =
|
129
131
|
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
@@ -26,7 +26,10 @@ export class AssistantStore {
|
|
26
26
|
return urlJoin(this.baseUrl, `${identifier}.${normalizeLocale(lang)}.json`);
|
27
27
|
};
|
28
28
|
|
29
|
-
getAgentIndex = async (
|
29
|
+
getAgentIndex = async (
|
30
|
+
locale: Locales = DEFAULT_LANG,
|
31
|
+
revalidate?: number,
|
32
|
+
): Promise<AgentStoreIndex> => {
|
30
33
|
try {
|
31
34
|
let res: Response;
|
32
35
|
|
@@ -42,7 +45,7 @@ export class AssistantStore {
|
|
42
45
|
|
43
46
|
if (!res.ok) {
|
44
47
|
console.warn('fetch agent index error:', await res.text());
|
45
|
-
return [];
|
48
|
+
return { agents: [], schemaVersion: 1 };
|
46
49
|
}
|
47
50
|
|
48
51
|
const data: AgentStoreIndex = await res.json();
|
@@ -5,12 +5,14 @@ import { publicProcedure, router } from '@/libs/trpc/edge';
|
|
5
5
|
|
6
6
|
import { appStatusRouter } from './appStatus';
|
7
7
|
import { configRouter } from './config';
|
8
|
+
import { marketRouter } from './market';
|
8
9
|
import { uploadRouter } from './upload';
|
9
10
|
|
10
11
|
export const edgeRouter = router({
|
11
12
|
appStatus: appStatusRouter,
|
12
13
|
config: configRouter,
|
13
14
|
healthcheck: publicProcedure.query(() => "i'm live!"),
|
15
|
+
market: marketRouter,
|
14
16
|
upload: uploadRouter,
|
15
17
|
});
|
16
18
|
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
2
|
+
import { z } from 'zod';
|
3
|
+
|
4
|
+
import { DEFAULT_LANG } from '@/const/locale';
|
5
|
+
import { publicProcedure, router } from '@/libs/trpc/edge';
|
6
|
+
import { Locales } from '@/locales/resources';
|
7
|
+
import { AssistantStore } from '@/server/modules/AssistantStore';
|
8
|
+
import { PluginStore } from '@/server/modules/PluginStore';
|
9
|
+
import { AgentStoreIndex } from '@/types/discover';
|
10
|
+
|
11
|
+
export const marketRouter = router({
|
12
|
+
getAgent: publicProcedure
|
13
|
+
.input(
|
14
|
+
z.object({
|
15
|
+
id: z.string(),
|
16
|
+
locale: z.string().optional(),
|
17
|
+
}),
|
18
|
+
)
|
19
|
+
.query(async ({ input }) => {
|
20
|
+
const { id, locale } = input;
|
21
|
+
|
22
|
+
const market = new AssistantStore();
|
23
|
+
|
24
|
+
// 获取助手 URL
|
25
|
+
const url = market.getAgentUrl(id, locale as Locales);
|
26
|
+
|
27
|
+
// 获取助手数据
|
28
|
+
let res = await fetch(url);
|
29
|
+
|
30
|
+
// 如果找不到对应语言的助手,尝试获取默认语言的助手
|
31
|
+
if (res.status === 404) {
|
32
|
+
res = await fetch(market.getAgentUrl(id, DEFAULT_LANG));
|
33
|
+
}
|
34
|
+
|
35
|
+
if (!res.ok) {
|
36
|
+
throw new Error(`Failed to fetch agent with id ${id}`);
|
37
|
+
}
|
38
|
+
|
39
|
+
return res.json();
|
40
|
+
}),
|
41
|
+
|
42
|
+
getAgentIndex: publicProcedure
|
43
|
+
.input(
|
44
|
+
z
|
45
|
+
.object({
|
46
|
+
locale: z.string().optional(),
|
47
|
+
})
|
48
|
+
.optional(),
|
49
|
+
)
|
50
|
+
.query(async ({ input }): Promise<AgentStoreIndex> => {
|
51
|
+
const locale = input?.locale;
|
52
|
+
|
53
|
+
const market = new AssistantStore();
|
54
|
+
try {
|
55
|
+
return await market.getAgentIndex(locale as Locales);
|
56
|
+
} catch (e) {
|
57
|
+
// it means failed to fetch
|
58
|
+
if ((e as Error).message.includes('fetch failed')) {
|
59
|
+
return { agents: [], schemaVersion: 1 };
|
60
|
+
}
|
61
|
+
|
62
|
+
throw new TRPCError({
|
63
|
+
code: 'INTERNAL_SERVER_ERROR',
|
64
|
+
message: 'failed to fetch agent market index',
|
65
|
+
});
|
66
|
+
}
|
67
|
+
}),
|
68
|
+
|
69
|
+
getPluginIndex: publicProcedure
|
70
|
+
.input(
|
71
|
+
z
|
72
|
+
.object({
|
73
|
+
locale: z.string().optional(),
|
74
|
+
})
|
75
|
+
.optional(),
|
76
|
+
)
|
77
|
+
.query(async ({ input }) => {
|
78
|
+
const locale = input?.locale;
|
79
|
+
|
80
|
+
const pluginStore = new PluginStore();
|
81
|
+
|
82
|
+
try {
|
83
|
+
// 获取插件索引URL
|
84
|
+
let res = await fetch(pluginStore.getPluginIndexUrl(locale as Locales));
|
85
|
+
|
86
|
+
// 如果找不到对应语言的插件索引,尝试获取默认语言的插件索引
|
87
|
+
if (res.status === 404) {
|
88
|
+
res = await fetch(pluginStore.getPluginIndexUrl(DEFAULT_LANG));
|
89
|
+
}
|
90
|
+
|
91
|
+
if (res.ok) {
|
92
|
+
return res.json();
|
93
|
+
}
|
94
|
+
|
95
|
+
throw new Error('Failed to fetch plugin index');
|
96
|
+
} catch (e) {
|
97
|
+
// it means failed to fetch
|
98
|
+
if ((e as Error).message.includes('fetch failed')) {
|
99
|
+
return [];
|
100
|
+
}
|
101
|
+
|
102
|
+
throw new TRPCError({
|
103
|
+
code: 'INTERNAL_SERVER_ERROR',
|
104
|
+
message: 'failed to fetch plugin market index',
|
105
|
+
});
|
106
|
+
}
|
107
|
+
}),
|
108
|
+
});
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
2
|
|
3
|
+
import { edgeClient } from '@/libs/trpc/client';
|
3
4
|
import { globalHelpers } from '@/store/global/helpers';
|
4
5
|
|
5
6
|
import { assistantService } from '../assistant';
|
@@ -12,6 +13,19 @@ vi.mock('@/store/global/helpers', () => ({
|
|
12
13
|
},
|
13
14
|
}));
|
14
15
|
|
16
|
+
vi.mock('@/libs/trpc/client', () => ({
|
17
|
+
edgeClient: {
|
18
|
+
market: {
|
19
|
+
getAgentIndex: {
|
20
|
+
query: vi.fn(),
|
21
|
+
},
|
22
|
+
getAgent: {
|
23
|
+
query: vi.fn(),
|
24
|
+
},
|
25
|
+
},
|
26
|
+
},
|
27
|
+
}));
|
28
|
+
|
15
29
|
beforeEach(() => {
|
16
30
|
vi.resetAllMocks();
|
17
31
|
});
|
@@ -22,56 +36,49 @@ describe('AssistantService', () => {
|
|
22
36
|
// Arrange
|
23
37
|
const fakeResponse = { agents: [{ name: 'TestAssisstant' }] };
|
24
38
|
(globalHelpers.getCurrentLanguage as Mock).mockReturnValue('tt');
|
25
|
-
|
26
|
-
|
27
|
-
json: () => Promise.resolve(fakeResponse),
|
28
|
-
}),
|
29
|
-
) as any;
|
39
|
+
|
40
|
+
(edgeClient.market.getAgentIndex.query as Mock).mockResolvedValue(fakeResponse);
|
30
41
|
|
31
42
|
// Act
|
32
43
|
const assistantList = await assistantService.getAssistantList();
|
33
44
|
|
34
45
|
// Assert
|
35
46
|
expect(globalHelpers.getCurrentLanguage).toHaveBeenCalled();
|
36
|
-
expect(fetch).toHaveBeenCalledWith('/webapi/assistant/store?locale=tt');
|
37
47
|
expect(assistantList).toEqual(fakeResponse.agents);
|
38
48
|
});
|
39
49
|
|
40
50
|
it('should handle fetch error', async () => {
|
41
51
|
// Arrange
|
42
|
-
const fakeUrl = 'http://fake-url.com/plugins.json';
|
43
52
|
(globalHelpers.getCurrentLanguage as Mock).mockReturnValue('en');
|
44
|
-
|
53
|
+
(edgeClient.market.getAgentIndex.query as Mock).mockRejectedValue(
|
54
|
+
new Error('Network error'),
|
55
|
+
);
|
45
56
|
|
46
57
|
// Act & Assert
|
47
58
|
await expect(assistantService.getAssistantList()).rejects.toThrow('Network error');
|
48
59
|
});
|
49
60
|
});
|
50
|
-
|
51
|
-
|
61
|
+
|
62
|
+
describe('getAssistantById', () => {
|
63
|
+
it('should fetch and return the assistant by id', async () => {
|
52
64
|
// Arrange
|
53
65
|
const fakeResponse = { identifier: 'test-assisstant' };
|
54
66
|
(globalHelpers.getCurrentLanguage as Mock).mockReturnValue('tt');
|
55
|
-
|
56
|
-
|
57
|
-
json: () => Promise.resolve(fakeResponse),
|
58
|
-
}),
|
59
|
-
) as any;
|
67
|
+
|
68
|
+
(edgeClient.market.getAgent.query as Mock).mockResolvedValue(fakeResponse);
|
60
69
|
|
61
70
|
// Act
|
62
71
|
const assistant = await assistantService.getAssistantById('test-assisstant');
|
63
72
|
|
64
73
|
// Assert
|
65
74
|
expect(globalHelpers.getCurrentLanguage).toHaveBeenCalled();
|
66
|
-
expect(fetch).toHaveBeenCalledWith('/webapi/assistant/test-assisstant?locale=tt');
|
67
75
|
expect(assistant.identifier).toEqual(fakeResponse.identifier);
|
68
76
|
});
|
69
77
|
|
70
78
|
it('should handle fetch error', async () => {
|
71
79
|
// Arrange
|
72
|
-
const fakeUrl = 'http://fake-url.com/plugins.json';
|
73
80
|
(globalHelpers.getCurrentLanguage as Mock).mockReturnValue('en');
|
74
|
-
|
81
|
+
(edgeClient.market.getAgent.query as Mock).mockRejectedValue(new Error('Network error'));
|
75
82
|
|
76
83
|
// Act & Assert
|
77
84
|
await expect(assistantService.getAssistantById('test-assisstant')).rejects.toThrow(
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
2
|
|
3
|
+
import { edgeClient } from '@/libs/trpc/client';
|
3
4
|
import { globalHelpers } from '@/store/global/helpers';
|
4
5
|
|
5
6
|
import { toolService } from '../tool';
|
@@ -14,6 +15,16 @@ vi.mock('@/store/global/helpers', () => ({
|
|
14
15
|
},
|
15
16
|
}));
|
16
17
|
|
18
|
+
vi.mock('@/libs/trpc/client', () => ({
|
19
|
+
edgeClient: {
|
20
|
+
market: {
|
21
|
+
getPluginIndex: {
|
22
|
+
query: vi.fn(),
|
23
|
+
},
|
24
|
+
},
|
25
|
+
},
|
26
|
+
}));
|
27
|
+
|
17
28
|
beforeEach(() => {
|
18
29
|
vi.resetAllMocks();
|
19
30
|
});
|
@@ -24,26 +35,21 @@ describe('ToolService', () => {
|
|
24
35
|
// Arrange
|
25
36
|
const fakeResponse = { plugins: [{ name: 'TestPlugin' }] };
|
26
37
|
(globalHelpers.getCurrentLanguage as Mock).mockReturnValue('tt');
|
27
|
-
|
28
|
-
|
29
|
-
json: () => Promise.resolve(fakeResponse),
|
30
|
-
}),
|
31
|
-
) as any;
|
38
|
+
|
39
|
+
(edgeClient.market.getPluginIndex.query as Mock).mockResolvedValue(fakeResponse);
|
32
40
|
|
33
41
|
// Act
|
34
42
|
const pluginList = await toolService.getToolList();
|
35
43
|
|
36
44
|
// Assert
|
37
45
|
expect(globalHelpers.getCurrentLanguage).toHaveBeenCalled();
|
38
|
-
expect(fetch).toHaveBeenCalledWith('/webapi/plugin/store?locale=tt');
|
39
46
|
expect(pluginList).toEqual(fakeResponse.plugins);
|
40
47
|
});
|
41
48
|
|
42
49
|
it('should handle fetch error', async () => {
|
43
50
|
// Arrange
|
44
|
-
const fakeUrl = 'http://fake-url.com/plugins.json';
|
45
51
|
(globalHelpers.getCurrentLanguage as Mock).mockReturnValue('en');
|
46
|
-
|
52
|
+
(edgeClient.market.getPluginIndex.query as Mock).mockRejectedValue(new Error('Network error'));
|
47
53
|
|
48
54
|
// Act & Assert
|
49
55
|
await expect(toolService.getToolList()).rejects.toThrow('Network error');
|
@@ -142,26 +148,6 @@ describe('ToolService', () => {
|
|
142
148
|
expect(fetch).toHaveBeenCalledWith(manifestUrl);
|
143
149
|
});
|
144
150
|
|
145
|
-
it('should return error on manifestInvalid', async () => {
|
146
|
-
const fakeManifest = { name: 'TestPlugin', version: '1.0.0' };
|
147
|
-
const manifestUrl = 'http://fake-url.com/manifest.json';
|
148
|
-
global.fetch = vi.fn(() =>
|
149
|
-
Promise.resolve({
|
150
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
151
|
-
ok: true,
|
152
|
-
json: () => {
|
153
|
-
throw new Error('abc');
|
154
|
-
},
|
155
|
-
}),
|
156
|
-
) as any;
|
157
|
-
|
158
|
-
try {
|
159
|
-
await toolService.getToolManifest(manifestUrl);
|
160
|
-
} catch (e) {
|
161
|
-
expect(e).toEqual(new TypeError('urlError'));
|
162
|
-
}
|
163
|
-
});
|
164
|
-
|
165
151
|
it('should return error on manifestInvalid', async () => {
|
166
152
|
const fakeManifest = { name: 'TestPlugin', version: '1.0.0' };
|
167
153
|
const manifestUrl = 'http://fake-url.com/manifest.json';
|
package/src/services/_url.ts
CHANGED
@@ -19,13 +19,8 @@ export const API_ENDPOINTS = mapWithBasePath({
|
|
19
19
|
|
20
20
|
proxy: '/webapi/proxy',
|
21
21
|
|
22
|
-
// assistant
|
23
|
-
assistantStore: '/webapi/assistant/store',
|
24
|
-
assistant: (identifier: string) => withBasePath(`/webapi/assistant/${identifier}`),
|
25
|
-
|
26
22
|
// plugins
|
27
23
|
gateway: '/webapi/plugin/gateway',
|
28
|
-
pluginStore: '/webapi/plugin/store',
|
29
24
|
|
30
25
|
// trace
|
31
26
|
trace: '/webapi/trace',
|
@@ -1,28 +1,23 @@
|
|
1
1
|
import { cloneDeep, merge } from 'lodash-es';
|
2
2
|
|
3
3
|
import { DEFAULT_DISCOVER_ASSISTANT_ITEM } from '@/const/discover';
|
4
|
+
import { edgeClient } from '@/libs/trpc/client';
|
4
5
|
import { globalHelpers } from '@/store/global/helpers';
|
5
6
|
import { DiscoverAssistantItem } from '@/types/discover';
|
6
7
|
|
7
|
-
import { API_ENDPOINTS } from './_url';
|
8
|
-
|
9
8
|
class AssistantService {
|
10
9
|
getAssistantList = async (): Promise<DiscoverAssistantItem[]> => {
|
11
10
|
const locale = globalHelpers.getCurrentLanguage();
|
12
11
|
|
13
|
-
const
|
14
|
-
|
15
|
-
const json = await res.json();
|
12
|
+
const data = await edgeClient.market.getAgentIndex.query({ locale });
|
16
13
|
|
17
|
-
return
|
14
|
+
return data.agents as unknown as DiscoverAssistantItem[];
|
18
15
|
};
|
19
16
|
|
20
17
|
getAssistantById = async (identifier: string): Promise<DiscoverAssistantItem> => {
|
21
18
|
const locale = globalHelpers.getCurrentLanguage();
|
22
19
|
|
23
|
-
const
|
24
|
-
|
25
|
-
const assistant: DiscoverAssistantItem = await res.json();
|
20
|
+
const assistant = await edgeClient.market.getAgent.query({ id: identifier, locale });
|
26
21
|
|
27
22
|
return merge(cloneDeep(DEFAULT_DISCOVER_ASSISTANT_ITEM), assistant);
|
28
23
|
};
|
package/src/services/tool.ts
CHANGED
@@ -1,18 +1,15 @@
|
|
1
|
+
import { edgeClient } from '@/libs/trpc/client';
|
1
2
|
import { globalHelpers } from '@/store/global/helpers';
|
2
3
|
import { DiscoverPlugintem } from '@/types/discover';
|
3
4
|
import { convertOpenAIManifestToLobeManifest, getToolManifest } from '@/utils/toolManifest';
|
4
5
|
|
5
|
-
import { API_ENDPOINTS } from './_url';
|
6
|
-
|
7
6
|
class ToolService {
|
8
7
|
getToolList = async (): Promise<DiscoverPlugintem[]> => {
|
9
8
|
const locale = globalHelpers.getCurrentLanguage();
|
10
9
|
|
11
|
-
const
|
12
|
-
|
13
|
-
const json = await res.json();
|
10
|
+
const data = await edgeClient.market.getPluginIndex.query({ locale });
|
14
11
|
|
15
|
-
return
|
12
|
+
return data.plugins;
|
16
13
|
};
|
17
14
|
|
18
15
|
getToolManifest = getToolManifest;
|
@@ -0,0 +1,211 @@
|
|
1
|
+
import { LocalFileItem, LocalMoveFilesResultItem } from '@lobechat/electron-client-ipc';
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { localFileService } from '@/services/electron/localFileService';
|
5
|
+
import { ChatStore } from '@/store/chat/store';
|
6
|
+
|
7
|
+
import { localFileSlice } from '../localFile';
|
8
|
+
|
9
|
+
vi.mock('@/services/electron/localFileService', () => ({
|
10
|
+
localFileService: {
|
11
|
+
listLocalFiles: vi.fn(),
|
12
|
+
moveLocalFiles: vi.fn(),
|
13
|
+
readLocalFile: vi.fn(),
|
14
|
+
readLocalFiles: vi.fn(),
|
15
|
+
renameLocalFile: vi.fn(),
|
16
|
+
searchLocalFiles: vi.fn(),
|
17
|
+
writeFile: vi.fn(),
|
18
|
+
},
|
19
|
+
}));
|
20
|
+
|
21
|
+
const mockSet = vi.fn();
|
22
|
+
|
23
|
+
const mockStore = {
|
24
|
+
internal_triggerLocalFileToolCalling: vi.fn(),
|
25
|
+
internal_updateMessageContent: vi.fn(),
|
26
|
+
internal_updateMessagePluginError: vi.fn(),
|
27
|
+
set: mockSet,
|
28
|
+
toggleLocalFileLoading: vi.fn(),
|
29
|
+
updatePluginArguments: vi.fn(),
|
30
|
+
updatePluginState: vi.fn(),
|
31
|
+
} as unknown as ChatStore;
|
32
|
+
|
33
|
+
const createStore = () => {
|
34
|
+
return localFileSlice(
|
35
|
+
(set) => ({
|
36
|
+
...mockStore,
|
37
|
+
set,
|
38
|
+
}),
|
39
|
+
() => mockStore,
|
40
|
+
{} as any,
|
41
|
+
);
|
42
|
+
};
|
43
|
+
|
44
|
+
describe('localFileSlice', () => {
|
45
|
+
const store = createStore();
|
46
|
+
|
47
|
+
beforeEach(() => {
|
48
|
+
vi.clearAllMocks();
|
49
|
+
});
|
50
|
+
|
51
|
+
describe('internal_triggerLocalFileToolCalling', () => {
|
52
|
+
it('should handle successful calling', async () => {
|
53
|
+
const mockContent = { foo: 'bar' };
|
54
|
+
const mockState = { state: 'test' };
|
55
|
+
const mockService = vi.fn().mockResolvedValue({ content: mockContent, state: mockState });
|
56
|
+
|
57
|
+
await store.internal_triggerLocalFileToolCalling('test-id', mockService);
|
58
|
+
|
59
|
+
expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
|
60
|
+
expect(mockStore.updatePluginState).toBeCalledWith('test-id', mockState);
|
61
|
+
expect(mockStore.internal_updateMessageContent).toBeCalledWith(
|
62
|
+
'test-id',
|
63
|
+
JSON.stringify(mockContent),
|
64
|
+
);
|
65
|
+
expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', false);
|
66
|
+
});
|
67
|
+
|
68
|
+
it('should handle error', async () => {
|
69
|
+
const mockError = new Error('test error');
|
70
|
+
const mockService = vi.fn().mockRejectedValue(mockError);
|
71
|
+
|
72
|
+
await store.internal_triggerLocalFileToolCalling('test-id', mockService);
|
73
|
+
|
74
|
+
expect(mockStore.internal_updateMessagePluginError).toBeCalledWith('test-id', {
|
75
|
+
body: mockError,
|
76
|
+
message: 'test error',
|
77
|
+
type: 'PluginServerError',
|
78
|
+
});
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('listLocalFiles', () => {
|
83
|
+
it('should call listLocalFiles service and update state', async () => {
|
84
|
+
const mockResult: LocalFileItem[] = [
|
85
|
+
{
|
86
|
+
name: 'test.txt',
|
87
|
+
path: '/test.txt',
|
88
|
+
isDirectory: false,
|
89
|
+
createdTime: new Date(),
|
90
|
+
lastAccessTime: new Date(),
|
91
|
+
modifiedTime: new Date(),
|
92
|
+
size: 100,
|
93
|
+
type: 'file',
|
94
|
+
},
|
95
|
+
];
|
96
|
+
vi.mocked(localFileService.listLocalFiles).mockResolvedValue(mockResult);
|
97
|
+
|
98
|
+
await store.listLocalFiles('test-id', { path: '/test' });
|
99
|
+
|
100
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
101
|
+
});
|
102
|
+
});
|
103
|
+
|
104
|
+
describe('moveLocalFiles', () => {
|
105
|
+
it('should handle successful move', async () => {
|
106
|
+
const mockResults = [
|
107
|
+
{
|
108
|
+
sourcePath: '/test.txt',
|
109
|
+
destinationPath: '/target/test.txt',
|
110
|
+
success: true,
|
111
|
+
},
|
112
|
+
] as unknown as LocalMoveFilesResultItem[];
|
113
|
+
|
114
|
+
vi.mocked(localFileService.moveLocalFiles).mockResolvedValue(mockResults);
|
115
|
+
|
116
|
+
await store.moveLocalFiles('test-id', {
|
117
|
+
sourcePaths: ['/test.txt'],
|
118
|
+
destinationDir: '/target',
|
119
|
+
} as any);
|
120
|
+
|
121
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
describe('writeLocalFile', () => {
|
126
|
+
it('should handle successful write', async () => {
|
127
|
+
vi.mocked(localFileService.writeFile).mockResolvedValue({
|
128
|
+
success: true,
|
129
|
+
newPath: '/test.txt',
|
130
|
+
});
|
131
|
+
|
132
|
+
await store.writeLocalFile('test-id', { path: '/test.txt', content: 'test' });
|
133
|
+
|
134
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
135
|
+
});
|
136
|
+
|
137
|
+
it('should handle write error', async () => {
|
138
|
+
vi.mocked(localFileService.writeFile).mockResolvedValue({
|
139
|
+
success: false,
|
140
|
+
error: 'Write failed',
|
141
|
+
newPath: '/test.txt',
|
142
|
+
});
|
143
|
+
|
144
|
+
await store.writeLocalFile('test-id', { path: '/test.txt', content: 'test' });
|
145
|
+
|
146
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
147
|
+
});
|
148
|
+
});
|
149
|
+
|
150
|
+
describe('renameLocalFile', () => {
|
151
|
+
it('should handle successful rename', async () => {
|
152
|
+
vi.mocked(localFileService.renameLocalFile).mockResolvedValue({
|
153
|
+
success: true,
|
154
|
+
newPath: '/new.txt',
|
155
|
+
});
|
156
|
+
|
157
|
+
await store.renameLocalFile('test-id', { path: '/test.txt', newName: 'new.txt' });
|
158
|
+
|
159
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
160
|
+
});
|
161
|
+
|
162
|
+
it('should handle rename error', async () => {
|
163
|
+
vi.mocked(localFileService.renameLocalFile).mockResolvedValue({
|
164
|
+
success: false,
|
165
|
+
error: 'Rename failed',
|
166
|
+
newPath: '/test.txt',
|
167
|
+
});
|
168
|
+
|
169
|
+
await store.renameLocalFile('test-id', { path: '/test.txt', newName: 'new.txt' });
|
170
|
+
|
171
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
172
|
+
});
|
173
|
+
|
174
|
+
it('should validate new filename', async () => {
|
175
|
+
vi.mocked(localFileService.renameLocalFile).mockRejectedValue(
|
176
|
+
new Error('Invalid new name provided'),
|
177
|
+
);
|
178
|
+
|
179
|
+
await store.renameLocalFile('test-id', {
|
180
|
+
path: '/test.txt',
|
181
|
+
newName: '../invalid.txt',
|
182
|
+
});
|
183
|
+
|
184
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalledWith(
|
185
|
+
'test-id',
|
186
|
+
expect.any(Function),
|
187
|
+
);
|
188
|
+
});
|
189
|
+
});
|
190
|
+
|
191
|
+
describe('toggleLocalFileLoading', () => {
|
192
|
+
it('should toggle loading state', () => {
|
193
|
+
const mockSetFn = vi.fn();
|
194
|
+
const testStore = localFileSlice(mockSetFn, () => mockStore, {} as any);
|
195
|
+
|
196
|
+
testStore.toggleLocalFileLoading('test-id', true);
|
197
|
+
expect(mockSetFn).toHaveBeenCalledWith(
|
198
|
+
expect.any(Function),
|
199
|
+
false,
|
200
|
+
'toggleLocalFileLoading/start',
|
201
|
+
);
|
202
|
+
|
203
|
+
testStore.toggleLocalFileLoading('test-id', false);
|
204
|
+
expect(mockSetFn).toHaveBeenCalledWith(
|
205
|
+
expect.any(Function),
|
206
|
+
false,
|
207
|
+
'toggleLocalFileLoading/end',
|
208
|
+
);
|
209
|
+
});
|
210
|
+
});
|
211
|
+
});
|
@@ -5,6 +5,7 @@ export type RemoteServerError = 'CONFIG_ERROR' | 'AUTH_ERROR' | 'DISCONNECT_ERRO
|
|
5
5
|
export interface ElectronState {
|
6
6
|
appState: ElectronAppState;
|
7
7
|
dataSyncConfig: DataSyncConfig;
|
8
|
+
isAppStateInit: boolean;
|
8
9
|
isConnectingServer?: boolean;
|
9
10
|
isInitRemoteServerConfig: boolean;
|
10
11
|
isSyncActive?: boolean;
|
@@ -14,6 +15,7 @@ export interface ElectronState {
|
|
14
15
|
export const initialState: ElectronState = {
|
15
16
|
appState: {},
|
16
17
|
dataSyncConfig: { storageMode: 'local' },
|
18
|
+
isAppStateInit: false,
|
17
19
|
isConnectingServer: false,
|
18
20
|
isInitRemoteServerConfig: false,
|
19
21
|
isSyncActive: false,
|
@@ -1,25 +0,0 @@
|
|
1
|
-
import { DEFAULT_LANG } from '@/const/locale';
|
2
|
-
import { AssistantStore } from '@/server/modules/AssistantStore';
|
3
|
-
|
4
|
-
export const runtime = 'edge';
|
5
|
-
|
6
|
-
type Params = Promise<{ id: string }>;
|
7
|
-
|
8
|
-
export const GET = async (req: Request, segmentData: { params: Params }) => {
|
9
|
-
const params = await segmentData.params;
|
10
|
-
|
11
|
-
const { searchParams } = new URL(req.url);
|
12
|
-
|
13
|
-
const locale = searchParams.get('locale');
|
14
|
-
|
15
|
-
const market = new AssistantStore();
|
16
|
-
|
17
|
-
let res: Response;
|
18
|
-
|
19
|
-
res = await fetch(market.getAgentUrl(params.id, locale as any));
|
20
|
-
if (res.status === 404) {
|
21
|
-
res = await fetch(market.getAgentUrl(params.id, DEFAULT_LANG));
|
22
|
-
}
|
23
|
-
|
24
|
-
return res;
|
25
|
-
};
|
@@ -1,31 +0,0 @@
|
|
1
|
-
import { NextResponse } from 'next/server';
|
2
|
-
|
3
|
-
import { AssistantStore } from '@/server/modules/AssistantStore';
|
4
|
-
|
5
|
-
export const runtime = 'edge';
|
6
|
-
|
7
|
-
export const GET = async (req: Request) => {
|
8
|
-
try {
|
9
|
-
const locale = new URL(req.url).searchParams.get('locale');
|
10
|
-
|
11
|
-
const market = new AssistantStore();
|
12
|
-
|
13
|
-
const data = await market.getAgentIndex(locale as any);
|
14
|
-
|
15
|
-
return NextResponse.json(data);
|
16
|
-
} catch (e) {
|
17
|
-
// it means failed to fetch
|
18
|
-
if ((e as Error).message.includes('fetch failed')) {
|
19
|
-
return NextResponse.json([]);
|
20
|
-
}
|
21
|
-
|
22
|
-
console.error(e);
|
23
|
-
return new Response(`failed to fetch agent market index`, {
|
24
|
-
headers: {
|
25
|
-
'Access-Control-Allow-Origin': '*',
|
26
|
-
'Content-Type': 'application/json',
|
27
|
-
},
|
28
|
-
status: 500,
|
29
|
-
});
|
30
|
-
}
|
31
|
-
};
|
@@ -1,43 +0,0 @@
|
|
1
|
-
import { NextResponse } from 'next/server';
|
2
|
-
|
3
|
-
import { DEFAULT_LANG } from '@/const/locale';
|
4
|
-
import { PluginStore } from '@/server/modules/PluginStore';
|
5
|
-
|
6
|
-
export const runtime = 'edge';
|
7
|
-
|
8
|
-
export const GET = async (req: Request) => {
|
9
|
-
try {
|
10
|
-
const locale = new URL(req.url).searchParams.get('locale');
|
11
|
-
|
12
|
-
const pluginStore = new PluginStore();
|
13
|
-
|
14
|
-
let res: Response;
|
15
|
-
|
16
|
-
res = await fetch(pluginStore.getPluginIndexUrl(locale as any));
|
17
|
-
|
18
|
-
if (res.status === 404) {
|
19
|
-
res = await fetch(pluginStore.getPluginIndexUrl(DEFAULT_LANG));
|
20
|
-
}
|
21
|
-
|
22
|
-
if (res.ok) {
|
23
|
-
const data = await res.json();
|
24
|
-
return NextResponse.json(data);
|
25
|
-
}
|
26
|
-
|
27
|
-
return res;
|
28
|
-
} catch (e) {
|
29
|
-
// it means failed to fetch
|
30
|
-
if ((e as Error).message.includes('fetch failed')) {
|
31
|
-
return NextResponse.json([]);
|
32
|
-
}
|
33
|
-
|
34
|
-
console.error(e);
|
35
|
-
return new Response(`failed to fetch plugin market index`, {
|
36
|
-
headers: {
|
37
|
-
'Access-Control-Allow-Origin': '*',
|
38
|
-
'Content-Type': 'application/json',
|
39
|
-
},
|
40
|
-
status: 500,
|
41
|
-
});
|
42
|
-
}
|
43
|
-
};
|