@lobehub/lobehub 2.0.0-next.250 → 2.0.0-next.251
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 +25 -0
- package/changelog/v1.json +5 -0
- package/package.json +1 -1
- package/packages/types/src/discover/mcp.ts +1 -1
- package/src/app/[variants]/(main)/community/(detail)/assistant/features/Sidebar/ActionButton/AddAgent.tsx +6 -0
- package/src/app/[variants]/(main)/community/(list)/assistant/features/List/Item.tsx +12 -3
- package/src/app/[variants]/(main)/community/(list)/mcp/features/List/Item.tsx +14 -4
- package/src/features/ResourceManager/components/Explorer/useCheckTaskStatus.ts +1 -1
- package/src/server/routers/lambda/market/index.ts +45 -4
- package/src/server/services/discover/index.ts +29 -3
- package/src/services/discover.ts +38 -1
- package/src/store/tool/slices/mcpStore/action.test.ts +38 -0
- package/src/store/tool/slices/mcpStore/action.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.251](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.250...v2.0.0-next.251)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-09**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **community**: Support to report for agent & mcp plugin interaction for recommendation.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **community**: Support to report for agent & mcp plugin interaction for recommendation, closes [#11289](https://github.com/lobehub/lobe-chat/issues/11289) ([6f98792](https://github.com/lobehub/lobe-chat/commit/6f98792))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.250](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.249...v2.0.0-next.250)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2026-01-09**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.251",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -83,11 +83,17 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|
|
83
83
|
// Report agent installation to marketplace if it has a market identifier
|
|
84
84
|
if (identifier) {
|
|
85
85
|
discoverService.reportAgentInstall(identifier);
|
|
86
|
+
discoverService.reportAgentEvent({
|
|
87
|
+
event: 'add',
|
|
88
|
+
identifier,
|
|
89
|
+
source: location.pathname,
|
|
90
|
+
})
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
if (shouldNavigate) {
|
|
89
94
|
console.log(shouldNavigate);
|
|
90
95
|
}
|
|
96
|
+
|
|
91
97
|
return result;
|
|
92
98
|
};
|
|
93
99
|
|
|
@@ -10,6 +10,7 @@ import urlJoin from 'url-join';
|
|
|
10
10
|
import PublishedTime from '@/components/PublishedTime';
|
|
11
11
|
import { useQuery } from '@/hooks/useQuery';
|
|
12
12
|
import { type AssistantMarketSource, type DiscoverAssistantItem } from '@/types/discover';
|
|
13
|
+
import { discoverService } from '@/services/discover';
|
|
13
14
|
|
|
14
15
|
import TokenTag from './TokenTag';
|
|
15
16
|
|
|
@@ -91,14 +92,22 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
|
|
91
92
|
[userName, navigate],
|
|
92
93
|
);
|
|
93
94
|
|
|
95
|
+
const handleClick = useCallback(() => {
|
|
96
|
+
discoverService.reportAgentEvent({
|
|
97
|
+
event: 'click',
|
|
98
|
+
identifier,
|
|
99
|
+
source: location.pathname,
|
|
100
|
+
}).catch(() => {});
|
|
101
|
+
|
|
102
|
+
navigate(link);
|
|
103
|
+
}, [identifier, link, navigate]);
|
|
104
|
+
|
|
94
105
|
return (
|
|
95
106
|
<Block
|
|
96
107
|
clickable
|
|
97
108
|
data-testid="assistant-item"
|
|
98
109
|
height={'100%'}
|
|
99
|
-
onClick={
|
|
100
|
-
navigate(link);
|
|
101
|
-
}}
|
|
110
|
+
onClick={handleClick}
|
|
102
111
|
style={{
|
|
103
112
|
overflow: 'hidden',
|
|
104
113
|
position: 'relative',
|
|
@@ -5,7 +5,7 @@ import { ActionIcon, Avatar, Block, Flexbox, Icon, Tag, Text, Tooltip } from '@l
|
|
|
5
5
|
import { Spotlight } from '@lobehub/ui/awesome';
|
|
6
6
|
import { createStaticStyles, cssVar } from 'antd-style';
|
|
7
7
|
import { ClockIcon } from 'lucide-react';
|
|
8
|
-
import { memo } from 'react';
|
|
8
|
+
import { memo, useCallback } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { Link, useNavigate } from 'react-router-dom';
|
|
11
11
|
import urlJoin from 'url-join';
|
|
@@ -14,6 +14,7 @@ import InstallationIcon from '@/components/MCPDepsIcon';
|
|
|
14
14
|
import OfficialIcon from '@/components/OfficialIcon';
|
|
15
15
|
import PublishedTime from '@/components/PublishedTime';
|
|
16
16
|
import Scores from '@/features/MCP/Scores';
|
|
17
|
+
import { discoverService } from '@/services/discover';
|
|
17
18
|
import { type DiscoverMcpItem } from '@/types/discover';
|
|
18
19
|
|
|
19
20
|
import ConnectionTypeTag from './ConnectionTypeTag';
|
|
@@ -77,14 +78,23 @@ const McpItem = memo<DiscoverMcpItem>(
|
|
|
77
78
|
const { t } = useTranslation('discover');
|
|
78
79
|
const navigate = useNavigate();
|
|
79
80
|
const link = urlJoin('/community/mcp', identifier);
|
|
81
|
+
|
|
82
|
+
const handleClick = useCallback(() => {
|
|
83
|
+
discoverService.reportMcpEvent({
|
|
84
|
+
event: 'click',
|
|
85
|
+
identifier,
|
|
86
|
+
source: location.pathname,
|
|
87
|
+
}).catch(() => {});
|
|
88
|
+
|
|
89
|
+
navigate(link);
|
|
90
|
+
}, [identifier, link, navigate]);
|
|
91
|
+
|
|
80
92
|
return (
|
|
81
93
|
<Block
|
|
82
94
|
clickable
|
|
83
95
|
data-testid="mcp-item"
|
|
84
96
|
height={'100%'}
|
|
85
|
-
onClick={
|
|
86
|
-
navigate(link);
|
|
87
|
-
}}
|
|
97
|
+
onClick={handleClick}
|
|
88
98
|
style={{
|
|
89
99
|
overflow: 'hidden',
|
|
90
100
|
position: 'relative',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
import { AsyncTaskStatus } from '@/types/asyncTask';
|
|
4
3
|
import { revalidateResources } from '@/store/file/slices/resource/hooks';
|
|
4
|
+
import { AsyncTaskStatus } from '@/types/asyncTask';
|
|
5
5
|
import { type FileListItem } from '@/types/files';
|
|
6
6
|
|
|
7
7
|
export const useCheckTaskStatus = (data: FileListItem[] | undefined) => {
|
|
@@ -167,7 +167,7 @@ export const marketRouter = router({
|
|
|
167
167
|
}),
|
|
168
168
|
|
|
169
169
|
// ============================== MCP Market ==============================
|
|
170
|
-
|
|
170
|
+
getMcpCategories: marketProcedure
|
|
171
171
|
.input(
|
|
172
172
|
z
|
|
173
173
|
.object({
|
|
@@ -351,7 +351,7 @@ export const marketRouter = router({
|
|
|
351
351
|
}),
|
|
352
352
|
|
|
353
353
|
// ============================== Plugin Market ==============================
|
|
354
|
-
|
|
354
|
+
getPluginCategories: marketProcedure
|
|
355
355
|
.input(
|
|
356
356
|
z
|
|
357
357
|
.object({
|
|
@@ -439,7 +439,7 @@ export const marketRouter = router({
|
|
|
439
439
|
}),
|
|
440
440
|
|
|
441
441
|
// ============================== Providers ==============================
|
|
442
|
-
|
|
442
|
+
getProviderDetail: marketProcedure
|
|
443
443
|
.input(
|
|
444
444
|
z.object({
|
|
445
445
|
identifier: z.string(),
|
|
@@ -503,7 +503,7 @@ export const marketRouter = router({
|
|
|
503
503
|
}),
|
|
504
504
|
|
|
505
505
|
// ============================== User Profile ==============================
|
|
506
|
-
|
|
506
|
+
getUserInfo: marketProcedure
|
|
507
507
|
.input(
|
|
508
508
|
z.object({
|
|
509
509
|
locale: z.string().optional(),
|
|
@@ -598,6 +598,26 @@ export const marketRouter = router({
|
|
|
598
598
|
}
|
|
599
599
|
}),
|
|
600
600
|
|
|
601
|
+
reportAgentEvent: marketProcedure
|
|
602
|
+
.input(
|
|
603
|
+
z.object({
|
|
604
|
+
event: z.enum(['add', 'chat', 'click']),
|
|
605
|
+
identifier: z.string(),
|
|
606
|
+
source: z.string().optional(),
|
|
607
|
+
}),
|
|
608
|
+
)
|
|
609
|
+
.mutation(async ({ input, ctx }) => {
|
|
610
|
+
log('createAgentEvent input: %O', input);
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
await ctx.discoverService.createAgentEvent(input);
|
|
614
|
+
return { success: true };
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error('Error reporting Agent event: %O', error);
|
|
617
|
+
return { success: false };
|
|
618
|
+
}
|
|
619
|
+
}),
|
|
620
|
+
|
|
601
621
|
reportAgentInstall: marketProcedure
|
|
602
622
|
.input(
|
|
603
623
|
z.object({
|
|
@@ -655,6 +675,27 @@ export const marketRouter = router({
|
|
|
655
675
|
}
|
|
656
676
|
}),
|
|
657
677
|
|
|
678
|
+
|
|
679
|
+
reportMcpEvent: marketProcedure
|
|
680
|
+
.input(
|
|
681
|
+
z.object({
|
|
682
|
+
event: z.enum(['click', 'install', 'activate', 'uninstall']),
|
|
683
|
+
identifier: z.string(),
|
|
684
|
+
source: z.string().optional(),
|
|
685
|
+
}),
|
|
686
|
+
)
|
|
687
|
+
.mutation(async ({ input, ctx }) => {
|
|
688
|
+
log('createMcpEvent input: %O', input);
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
await ctx.discoverService.createPluginEvent(input);
|
|
692
|
+
return { success: true };
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error('Error reporting MCP event: %O', error);
|
|
695
|
+
return { success: false };
|
|
696
|
+
}
|
|
697
|
+
}),
|
|
698
|
+
|
|
658
699
|
reportMcpInstallResult: marketProcedure
|
|
659
700
|
.input(
|
|
660
701
|
z.object({
|
|
@@ -25,13 +25,15 @@ import {
|
|
|
25
25
|
type DiscoverProviderItem,
|
|
26
26
|
type DiscoverUserProfile,
|
|
27
27
|
type IdentifiersResponse,
|
|
28
|
+
McpCategory,
|
|
28
29
|
type McpListResponse,
|
|
29
30
|
type McpQueryParams,
|
|
31
|
+
McpSorts,
|
|
30
32
|
type ModelListResponse,
|
|
31
33
|
type ModelQueryParams,
|
|
32
34
|
ModelSorts,
|
|
33
35
|
type PluginListResponse,
|
|
34
|
-
type PluginQueryParams,
|
|
36
|
+
type PluginQueryParams as PluginQueryParams,
|
|
35
37
|
PluginSorts,
|
|
36
38
|
type ProviderListResponse,
|
|
37
39
|
type ProviderQueryParams,
|
|
@@ -48,7 +50,12 @@ import {
|
|
|
48
50
|
MarketSDK,
|
|
49
51
|
type UserInfoResponse,
|
|
50
52
|
} from '@lobehub/market-sdk';
|
|
51
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
AgentEventRequest,
|
|
55
|
+
type CallReportRequest,
|
|
56
|
+
type InstallReportRequest,
|
|
57
|
+
type PluginEventRequest,
|
|
58
|
+
} from '@lobehub/market-types';
|
|
52
59
|
import dayjs from 'dayjs';
|
|
53
60
|
import debug from 'debug';
|
|
54
61
|
import { cloneDeep, countBy, isString, merge, uniq, uniqBy } from 'es-toolkit/compat';
|
|
@@ -850,12 +857,16 @@ export class DiscoverService {
|
|
|
850
857
|
|
|
851
858
|
getMcpList = async (params: McpQueryParams = {}): Promise<McpListResponse> => {
|
|
852
859
|
log('getMcpList: params=%O', params);
|
|
853
|
-
const { locale } = params;
|
|
860
|
+
const { category, locale, sort } = params;
|
|
854
861
|
const normalizedLocale = normalizeLocale(locale);
|
|
862
|
+
const isDiscoverCategory = category === McpCategory.Discover;
|
|
863
|
+
|
|
855
864
|
const result = await this.market.plugins.getPluginList(
|
|
856
865
|
{
|
|
857
866
|
...params,
|
|
867
|
+
category: isDiscoverCategory ? undefined : category,
|
|
858
868
|
locale: normalizedLocale,
|
|
869
|
+
sort: isDiscoverCategory ? McpSorts.Recommended : sort,
|
|
859
870
|
},
|
|
860
871
|
{
|
|
861
872
|
next: {
|
|
@@ -897,6 +908,21 @@ export class DiscoverService {
|
|
|
897
908
|
await this.market.plugins.reportInstallation(params);
|
|
898
909
|
};
|
|
899
910
|
|
|
911
|
+
/**
|
|
912
|
+
* record Agent plugin event
|
|
913
|
+
*/
|
|
914
|
+
createAgentEvent = async (params: AgentEventRequest) => {
|
|
915
|
+
await this.market.agents.createEvent(params);
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* record MCP plugin event
|
|
920
|
+
*/
|
|
921
|
+
createPluginEvent = async (params: PluginEventRequest) => {
|
|
922
|
+
await this.market.plugins.createEvent(params);
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
|
|
900
926
|
/**
|
|
901
927
|
* report plugin call result to marketplace
|
|
902
928
|
*/
|
package/src/services/discover.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type CategoryItem, type CategoryListQuery, type PluginManifest } from '@lobehub/market-sdk';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AgentEventRequest,
|
|
4
|
+
type CallReportRequest,
|
|
5
|
+
type InstallReportRequest,
|
|
6
|
+
type PluginEventRequest,
|
|
7
|
+
} from '@lobehub/market-types';
|
|
3
8
|
|
|
4
9
|
import { lambdaClient } from '@/libs/trpc/client';
|
|
5
10
|
import { globalHelpers } from '@/store/global/helpers';
|
|
@@ -195,6 +200,22 @@ class DiscoverService {
|
|
|
195
200
|
});
|
|
196
201
|
};
|
|
197
202
|
|
|
203
|
+
reportMcpEvent = async (eventData: PluginEventRequest) => {
|
|
204
|
+
const allow = userGeneralSettingsSelectors.telemetry(useUserStore.getState());
|
|
205
|
+
if (!allow) return;
|
|
206
|
+
|
|
207
|
+
await this.injectMPToken();
|
|
208
|
+
|
|
209
|
+
const payload = cleanObject({
|
|
210
|
+
...eventData,
|
|
211
|
+
source: eventData.source ?? 'community/mcp',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
lambdaClient.market.reportMcpEvent.mutate(payload).catch((error) => {
|
|
215
|
+
console.warn('Failed to report MCP event:', error);
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
198
219
|
/**
|
|
199
220
|
* Report agent installation to increase install count
|
|
200
221
|
*/
|
|
@@ -211,6 +232,22 @@ class DiscoverService {
|
|
|
211
232
|
});
|
|
212
233
|
};
|
|
213
234
|
|
|
235
|
+
reportAgentEvent = async (eventData: AgentEventRequest) => {
|
|
236
|
+
const allow = userGeneralSettingsSelectors.telemetry(useUserStore.getState());
|
|
237
|
+
if (!allow) return;
|
|
238
|
+
|
|
239
|
+
await this.injectMPToken();
|
|
240
|
+
|
|
241
|
+
const payload = cleanObject({
|
|
242
|
+
...eventData,
|
|
243
|
+
source: eventData.source ?? 'community/agent',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
lambdaClient.market.reportAgentEvent.mutate(payload).catch((error) => {
|
|
247
|
+
console.warn('Failed to report Agent event:', error);
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
214
251
|
// ============================== Models ==============================
|
|
215
252
|
|
|
216
253
|
getModelCategories = async (params: CategoryListQuery = {}): Promise<CategoryItem[]> => {
|
|
@@ -12,6 +12,37 @@ import { CheckMcpInstallResult, MCPInstallStep } from '@/types/plugins';
|
|
|
12
12
|
|
|
13
13
|
import { useToolStore } from '../../store';
|
|
14
14
|
|
|
15
|
+
vi.mock('@/libs/trpc/client', () => ({
|
|
16
|
+
asyncClient: {},
|
|
17
|
+
lambdaClient: {
|
|
18
|
+
market: {
|
|
19
|
+
getMcpCategories: { query: vi.fn() },
|
|
20
|
+
getMcpDetail: { query: vi.fn() },
|
|
21
|
+
getMcpList: { query: vi.fn() },
|
|
22
|
+
getMcpManifest: { query: vi.fn() },
|
|
23
|
+
registerClientInMarketplace: {
|
|
24
|
+
mutate: vi.fn().mockResolvedValue({
|
|
25
|
+
clientId: 'test-client-id',
|
|
26
|
+
clientSecret: 'test-client-secret',
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
registerM2MToken: { query: vi.fn().mockResolvedValue({ success: true }) },
|
|
30
|
+
reportCall: { mutate: vi.fn().mockResolvedValue(undefined) },
|
|
31
|
+
reportMcpEvent: { mutate: vi.fn().mockResolvedValue(undefined) },
|
|
32
|
+
reportMcpInstallResult: { mutate: vi.fn().mockResolvedValue(undefined) },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
toolsClient: {
|
|
36
|
+
market: {
|
|
37
|
+
callCloudMcpEndpoint: { mutate: vi.fn() },
|
|
38
|
+
},
|
|
39
|
+
mcp: {
|
|
40
|
+
callTool: { mutate: vi.fn() },
|
|
41
|
+
getStreamableMcpServerManifest: { query: vi.fn() },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
|
|
15
46
|
// Keep zustand mock as it's needed globally
|
|
16
47
|
vi.mock('zustand/traditional');
|
|
17
48
|
|
|
@@ -61,6 +92,13 @@ const bootstrapToolStoreWithDesktop = async (isDesktopEnv: boolean) => {
|
|
|
61
92
|
beforeEach(() => {
|
|
62
93
|
vi.clearAllMocks();
|
|
63
94
|
|
|
95
|
+
vi.spyOn(discoverService, 'injectMPToken').mockResolvedValue(undefined);
|
|
96
|
+
vi.spyOn(discoverService, 'registerClient').mockResolvedValue({
|
|
97
|
+
clientId: 'test-client-id',
|
|
98
|
+
clientSecret: 'test-client-secret',
|
|
99
|
+
});
|
|
100
|
+
vi.spyOn(discoverService, 'reportMcpEvent').mockResolvedValue(undefined as any);
|
|
101
|
+
|
|
64
102
|
// Reset store state
|
|
65
103
|
act(() => {
|
|
66
104
|
useToolStore.setState(
|
|
@@ -588,6 +588,12 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
588
588
|
// Calculate installation duration
|
|
589
589
|
const installDurationMs = Date.now() - installStartTime;
|
|
590
590
|
|
|
591
|
+
discoverService.reportMcpEvent({
|
|
592
|
+
event: 'install',
|
|
593
|
+
identifier: plugin.identifier,
|
|
594
|
+
source: 'self',
|
|
595
|
+
})
|
|
596
|
+
|
|
591
597
|
discoverService.reportMcpInstallResult({
|
|
592
598
|
identifier: plugin.identifier,
|
|
593
599
|
installDurationMs,
|
|
@@ -790,6 +796,12 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
790
796
|
n('testMcpConnection/success'),
|
|
791
797
|
);
|
|
792
798
|
|
|
799
|
+
discoverService.reportMcpEvent({
|
|
800
|
+
event: 'activate',
|
|
801
|
+
identifier: identifier,
|
|
802
|
+
source: 'self',
|
|
803
|
+
})
|
|
804
|
+
|
|
793
805
|
return { manifest, success: true };
|
|
794
806
|
} catch (error) {
|
|
795
807
|
// Silently handle errors caused by cancellation
|
|
@@ -817,6 +829,12 @@ export const createMCPPluginStoreSlice: StateCreator<
|
|
|
817
829
|
uninstallMCPPlugin: async (identifier) => {
|
|
818
830
|
await pluginService.uninstallPlugin(identifier);
|
|
819
831
|
await get().refreshPlugins();
|
|
832
|
+
|
|
833
|
+
discoverService.reportMcpEvent({
|
|
834
|
+
event: 'uninstall',
|
|
835
|
+
identifier: identifier,
|
|
836
|
+
source: 'self',
|
|
837
|
+
})
|
|
820
838
|
},
|
|
821
839
|
|
|
822
840
|
updateMCPInstallProgress: (identifier, progress) => {
|