@lobehub/chat 1.84.18 → 1.84.20
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/package.json +1 -1
- package/src/features/ElectronTitlebar/index.tsx +7 -3
- package/src/libs/agent-runtime/qwen/index.ts +20 -8
- package/src/libs/agent-runtime/utils/streams/qwen.test.ts +76 -1
- package/src/libs/agent-runtime/utils/streams/qwen.ts +26 -2
- 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/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.20](https://github.com/lobehub/lobe-chat/compare/v1.84.19...v1.84.20)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-04**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Show Aliyun Bailian tokens usage tracking.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Show Aliyun Bailian tokens usage tracking, closes [#7660](https://github.com/lobehub/lobe-chat/issues/7660) ([3ef0542](https://github.com/lobehub/lobe-chat/commit/3ef0542))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.84.19](https://github.com/lobehub/lobe-chat/compare/v1.84.18...v1.84.19)
|
31
|
+
|
32
|
+
<sup>Released on **2025-05-04**</sup>
|
33
|
+
|
34
|
+
#### 💄 Styles
|
35
|
+
|
36
|
+
- **misc**: Fix init state of loading.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### Styles
|
44
|
+
|
45
|
+
- **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))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.84.18](https://github.com/lobehub/lobe-chat/compare/v1.84.17...v1.84.18)
|
6
56
|
|
7
57
|
<sup>Released on **2025-05-03**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Show Aliyun Bailian tokens usage tracking."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-04",
|
9
|
+
"version": "1.84.20"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"improvements": [
|
14
|
+
"Fix init state of loading."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-05-04",
|
18
|
+
"version": "1.84.19"
|
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.20",
|
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 />
|
@@ -24,16 +24,19 @@ export const LobeQwenAI = LobeOpenAICompatibleFactory({
|
|
24
24
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
25
25
|
chatCompletion: {
|
26
26
|
handlePayload: (payload) => {
|
27
|
-
const { model, presence_penalty, temperature, thinking, top_p, enabledSearch, ...rest } =
|
27
|
+
const { model, presence_penalty, temperature, thinking, top_p, enabledSearch, ...rest } =
|
28
|
+
payload;
|
28
29
|
|
29
30
|
return {
|
30
31
|
...rest,
|
31
|
-
...(
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
...(['qwen3', 'qwen-turbo', 'qwen-plus'].some((keyword) =>
|
33
|
+
model.toLowerCase().includes(keyword),
|
34
|
+
)
|
35
|
+
? {
|
36
|
+
enable_thinking: thinking !== undefined ? thinking.type === 'enabled' : false,
|
37
|
+
thinking_budget:
|
38
|
+
thinking?.budget_tokens === 0 ? 0 : thinking?.budget_tokens || undefined,
|
39
|
+
}
|
37
40
|
: {}),
|
38
41
|
frequency_penalty: undefined,
|
39
42
|
model,
|
@@ -77,7 +80,16 @@ export const LobeQwenAI = LobeOpenAICompatibleFactory({
|
|
77
80
|
models: async ({ client }) => {
|
78
81
|
const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
|
79
82
|
|
80
|
-
const functionCallKeywords = [
|
83
|
+
const functionCallKeywords = [
|
84
|
+
'qwen-max',
|
85
|
+
'qwen-plus',
|
86
|
+
'qwen-turbo',
|
87
|
+
'qwen-long',
|
88
|
+
'qwen1.5',
|
89
|
+
'qwen2',
|
90
|
+
'qwen2.5',
|
91
|
+
'qwen3',
|
92
|
+
];
|
81
93
|
|
82
94
|
const visionKeywords = ['qvq', 'vl'];
|
83
95
|
|
@@ -1,6 +1,8 @@
|
|
1
|
+
import OpenAI from 'openai';
|
1
2
|
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
2
3
|
|
3
|
-
import {
|
4
|
+
import { StreamContext } from './protocol';
|
5
|
+
import { QwenAIStream, transformQwenStream } from './qwen';
|
4
6
|
|
5
7
|
describe('QwenAIStream', () => {
|
6
8
|
beforeAll(() => {});
|
@@ -349,3 +351,76 @@ describe('QwenAIStream', () => {
|
|
349
351
|
]);
|
350
352
|
});
|
351
353
|
});
|
354
|
+
|
355
|
+
describe('transformQwenStream', () => {
|
356
|
+
it('should handle usage chunk', () => {
|
357
|
+
const mockChunk: OpenAI.ChatCompletionChunk = {
|
358
|
+
choices: [],
|
359
|
+
id: 'usage-test-id',
|
360
|
+
model: 'qwen-test-model',
|
361
|
+
object: 'chat.completion.chunk',
|
362
|
+
created: Date.now(),
|
363
|
+
usage: {
|
364
|
+
completion_tokens: 50,
|
365
|
+
prompt_tokens: 100,
|
366
|
+
total_tokens: 150,
|
367
|
+
completion_tokens_details: {}, // Ensure these exist even if empty
|
368
|
+
prompt_tokens_details: {}, // Ensure these exist even if empty
|
369
|
+
},
|
370
|
+
};
|
371
|
+
|
372
|
+
const streamContext: StreamContext = { id: '' };
|
373
|
+
|
374
|
+
const result = transformQwenStream(mockChunk, streamContext);
|
375
|
+
|
376
|
+
expect(result).toEqual({
|
377
|
+
id: 'usage-test-id',
|
378
|
+
type: 'usage',
|
379
|
+
data: {
|
380
|
+
inputTextTokens: 100,
|
381
|
+
outputTextTokens: 50,
|
382
|
+
totalInputTokens: 100,
|
383
|
+
totalOutputTokens: 50,
|
384
|
+
totalTokens: 150,
|
385
|
+
},
|
386
|
+
});
|
387
|
+
|
388
|
+
// Verify streamContext is updated
|
389
|
+
expect(streamContext.usage).toEqual({
|
390
|
+
inputTextTokens: 100,
|
391
|
+
outputTextTokens: 50,
|
392
|
+
totalInputTokens: 100,
|
393
|
+
totalOutputTokens: 50,
|
394
|
+
totalTokens: 150,
|
395
|
+
});
|
396
|
+
});
|
397
|
+
|
398
|
+
it('should handle usage chunk without streamContext', () => {
|
399
|
+
const mockChunk: OpenAI.ChatCompletionChunk = {
|
400
|
+
choices: [],
|
401
|
+
id: 'usage-test-id-no-ctx',
|
402
|
+
model: 'qwen-test-model',
|
403
|
+
object: 'chat.completion.chunk',
|
404
|
+
created: Date.now(),
|
405
|
+
usage: {
|
406
|
+
completion_tokens: 55,
|
407
|
+
prompt_tokens: 105,
|
408
|
+
total_tokens: 160,
|
409
|
+
},
|
410
|
+
};
|
411
|
+
|
412
|
+
const result = transformQwenStream(mockChunk); // No streamContext passed
|
413
|
+
|
414
|
+
expect(result).toEqual({
|
415
|
+
id: 'usage-test-id-no-ctx',
|
416
|
+
type: 'usage',
|
417
|
+
data: {
|
418
|
+
inputTextTokens: 105,
|
419
|
+
outputTextTokens: 55,
|
420
|
+
totalInputTokens: 105,
|
421
|
+
totalOutputTokens: 55,
|
422
|
+
totalTokens: 160,
|
423
|
+
},
|
424
|
+
});
|
425
|
+
});
|
426
|
+
});
|
@@ -4,17 +4,37 @@ import { ChatCompletionContentPart } from 'openai/resources/index.mjs';
|
|
4
4
|
import type { Stream } from 'openai/streaming';
|
5
5
|
|
6
6
|
import { ChatStreamCallbacks } from '../../types';
|
7
|
+
import { convertUsage } from '../usageConverter';
|
7
8
|
import {
|
9
|
+
StreamContext,
|
8
10
|
StreamProtocolChunk,
|
9
11
|
StreamProtocolToolCallChunk,
|
10
12
|
StreamToolCallChunkData,
|
11
13
|
convertIterableToStream,
|
12
14
|
createCallbacksTransformer,
|
13
15
|
createSSEProtocolTransformer,
|
16
|
+
createTokenSpeedCalculator,
|
14
17
|
generateToolCallId,
|
15
18
|
} from './protocol';
|
16
19
|
|
17
|
-
export const transformQwenStream = (
|
20
|
+
export const transformQwenStream = (
|
21
|
+
chunk: OpenAI.ChatCompletionChunk,
|
22
|
+
streamContext?: StreamContext,
|
23
|
+
): StreamProtocolChunk | StreamProtocolChunk[] => {
|
24
|
+
if (Array.isArray(chunk.choices) && chunk.choices.length === 0 && chunk.usage) {
|
25
|
+
const usage = convertUsage({
|
26
|
+
...chunk.usage,
|
27
|
+
completion_tokens_details: chunk.usage.completion_tokens_details || {},
|
28
|
+
prompt_tokens_details: chunk.usage.prompt_tokens_details || {},
|
29
|
+
});
|
30
|
+
|
31
|
+
if (streamContext) {
|
32
|
+
streamContext.usage = usage;
|
33
|
+
}
|
34
|
+
|
35
|
+
return { data: usage, id: chunk.id, type: 'usage' };
|
36
|
+
}
|
37
|
+
|
18
38
|
const item = chunk.choices[0];
|
19
39
|
|
20
40
|
if (!item) {
|
@@ -96,10 +116,14 @@ export const QwenAIStream = (
|
|
96
116
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
97
117
|
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
|
98
118
|
) => {
|
119
|
+
const streamContext: StreamContext = { id: '' };
|
99
120
|
const readableStream =
|
100
121
|
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
101
122
|
|
102
123
|
return readableStream
|
103
|
-
.pipeThrough(
|
124
|
+
.pipeThrough(
|
125
|
+
createTokenSpeedCalculator(transformQwenStream, { inputStartAt, streamStack: streamContext }),
|
126
|
+
)
|
127
|
+
.pipeThrough(createSSEProtocolTransformer((c) => c, streamContext))
|
104
128
|
.pipeThrough(createCallbacksTransformer(callbacks));
|
105
129
|
};
|
@@ -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;
|
@@ -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
|
-
};
|