feat(community): support to report for agent & mcp plugin interaction for recommendation (#11289)

This commit is contained in:
Neko
2026-01-09 18:22:19 +08:00
committed by GitHub
parent fde900b6e1
commit 6f987929c6
9 changed files with 201 additions and 16 deletions

View File

@@ -18,7 +18,7 @@ export enum McpCategory {
Tools = 'tools',
TravelTransport = 'travel-transport',
Weather = 'weather',
WebSearch = 'web-search',
WebSearch = 'web-search'
}
export enum McpSorts {

View File

@@ -83,11 +83,17 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
// Report agent installation to marketplace if it has a market identifier
if (identifier) {
discoverService.reportAgentInstall(identifier);
discoverService.reportAgentEvent({
event: 'add',
identifier,
source: location.pathname,
})
}
if (shouldNavigate) {
console.log(shouldNavigate);
}
return result;
};

View File

@@ -10,6 +10,7 @@ import urlJoin from 'url-join';
import PublishedTime from '@/components/PublishedTime';
import { useQuery } from '@/hooks/useQuery';
import { type AssistantMarketSource, type DiscoverAssistantItem } from '@/types/discover';
import { discoverService } from '@/services/discover';
import TokenTag from './TokenTag';
@@ -91,14 +92,22 @@ const AssistantItem = memo<DiscoverAssistantItem>(
[userName, navigate],
);
const handleClick = useCallback(() => {
discoverService.reportAgentEvent({
event: 'click',
identifier,
source: location.pathname,
}).catch(() => {});
navigate(link);
}, [identifier, link, navigate]);
return (
<Block
clickable
data-testid="assistant-item"
height={'100%'}
onClick={() => {
navigate(link);
}}
onClick={handleClick}
style={{
overflow: 'hidden',
position: 'relative',

View File

@@ -5,7 +5,7 @@ import { ActionIcon, Avatar, Block, Flexbox, Icon, Tag, Text, Tooltip } from '@l
import { Spotlight } from '@lobehub/ui/awesome';
import { createStaticStyles, cssVar } from 'antd-style';
import { ClockIcon } from 'lucide-react';
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import urlJoin from 'url-join';
@@ -14,6 +14,7 @@ import InstallationIcon from '@/components/MCPDepsIcon';
import OfficialIcon from '@/components/OfficialIcon';
import PublishedTime from '@/components/PublishedTime';
import Scores from '@/features/MCP/Scores';
import { discoverService } from '@/services/discover';
import { type DiscoverMcpItem } from '@/types/discover';
import ConnectionTypeTag from './ConnectionTypeTag';
@@ -77,14 +78,23 @@ const McpItem = memo<DiscoverMcpItem>(
const { t } = useTranslation('discover');
const navigate = useNavigate();
const link = urlJoin('/community/mcp', identifier);
const handleClick = useCallback(() => {
discoverService.reportMcpEvent({
event: 'click',
identifier,
source: location.pathname,
}).catch(() => {});
navigate(link);
}, [identifier, link, navigate]);
return (
<Block
clickable
data-testid="mcp-item"
height={'100%'}
onClick={() => {
navigate(link);
}}
onClick={handleClick}
style={{
overflow: 'hidden',
position: 'relative',

View File

@@ -167,7 +167,7 @@ export const marketRouter = router({
}),
// ============================== MCP Market ==============================
getMcpCategories: marketProcedure
getMcpCategories: marketProcedure
.input(
z
.object({
@@ -351,7 +351,7 @@ export const marketRouter = router({
}),
// ============================== Plugin Market ==============================
getPluginCategories: marketProcedure
getPluginCategories: marketProcedure
.input(
z
.object({
@@ -439,7 +439,7 @@ export const marketRouter = router({
}),
// ============================== Providers ==============================
getProviderDetail: marketProcedure
getProviderDetail: marketProcedure
.input(
z.object({
identifier: z.string(),
@@ -503,7 +503,7 @@ export const marketRouter = router({
}),
// ============================== User Profile ==============================
getUserInfo: marketProcedure
getUserInfo: marketProcedure
.input(
z.object({
locale: z.string().optional(),
@@ -598,6 +598,26 @@ export const marketRouter = router({
}
}),
reportAgentEvent: marketProcedure
.input(
z.object({
event: z.enum(['add', 'chat', 'click']),
identifier: z.string(),
source: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
log('createAgentEvent input: %O', input);
try {
await ctx.discoverService.createAgentEvent(input);
return { success: true };
} catch (error) {
console.error('Error reporting Agent event: %O', error);
return { success: false };
}
}),
reportAgentInstall: marketProcedure
.input(
z.object({
@@ -655,6 +675,27 @@ export const marketRouter = router({
}
}),
reportMcpEvent: marketProcedure
.input(
z.object({
event: z.enum(['click', 'install', 'activate', 'uninstall']),
identifier: z.string(),
source: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
log('createMcpEvent input: %O', input);
try {
await ctx.discoverService.createPluginEvent(input);
return { success: true };
} catch (error) {
console.error('Error reporting MCP event: %O', error);
return { success: false };
}
}),
reportMcpInstallResult: marketProcedure
.input(
z.object({

View File

@@ -25,13 +25,15 @@ import {
type DiscoverProviderItem,
type DiscoverUserProfile,
type IdentifiersResponse,
McpCategory,
type McpListResponse,
type McpQueryParams,
McpSorts,
type ModelListResponse,
type ModelQueryParams,
ModelSorts,
type PluginListResponse,
type PluginQueryParams,
type PluginQueryParams as PluginQueryParams,
PluginSorts,
type ProviderListResponse,
type ProviderQueryParams,
@@ -48,7 +50,12 @@ import {
MarketSDK,
type UserInfoResponse,
} from '@lobehub/market-sdk';
import { type CallReportRequest, type InstallReportRequest } from '@lobehub/market-types';
import {
AgentEventRequest,
type CallReportRequest,
type InstallReportRequest,
type PluginEventRequest,
} from '@lobehub/market-types';
import dayjs from 'dayjs';
import debug from 'debug';
import { cloneDeep, countBy, isString, merge, uniq, uniqBy } from 'es-toolkit/compat';
@@ -850,12 +857,16 @@ export class DiscoverService {
getMcpList = async (params: McpQueryParams = {}): Promise<McpListResponse> => {
log('getMcpList: params=%O', params);
const { locale } = params;
const { category, locale, sort } = params;
const normalizedLocale = normalizeLocale(locale);
const isDiscoverCategory = category === McpCategory.Discover;
const result = await this.market.plugins.getPluginList(
{
...params,
category: isDiscoverCategory ? undefined : category,
locale: normalizedLocale,
sort: isDiscoverCategory ? McpSorts.Recommended : sort,
},
{
next: {
@@ -897,6 +908,21 @@ export class DiscoverService {
await this.market.plugins.reportInstallation(params);
};
/**
* record Agent plugin event
*/
createAgentEvent = async (params: AgentEventRequest) => {
await this.market.agents.createEvent(params);
};
/**
* record MCP plugin event
*/
createPluginEvent = async (params: PluginEventRequest) => {
await this.market.plugins.createEvent(params);
};
/**
* report plugin call result to marketplace
*/

View File

@@ -1,5 +1,10 @@
import { type CategoryItem, type CategoryListQuery, type PluginManifest } from '@lobehub/market-sdk';
import { type CallReportRequest, type InstallReportRequest } from '@lobehub/market-types';
import {
AgentEventRequest,
type CallReportRequest,
type InstallReportRequest,
type PluginEventRequest,
} from '@lobehub/market-types';
import { lambdaClient } from '@/libs/trpc/client';
import { globalHelpers } from '@/store/global/helpers';
@@ -195,6 +200,22 @@ class DiscoverService {
});
};
reportMcpEvent = async (eventData: PluginEventRequest) => {
const allow = userGeneralSettingsSelectors.telemetry(useUserStore.getState());
if (!allow) return;
await this.injectMPToken();
const payload = cleanObject({
...eventData,
source: eventData.source ?? 'community/mcp',
});
lambdaClient.market.reportMcpEvent.mutate(payload).catch((error) => {
console.warn('Failed to report MCP event:', error);
});
};
/**
* Report agent installation to increase install count
*/
@@ -211,6 +232,22 @@ class DiscoverService {
});
};
reportAgentEvent = async (eventData: AgentEventRequest) => {
const allow = userGeneralSettingsSelectors.telemetry(useUserStore.getState());
if (!allow) return;
await this.injectMPToken();
const payload = cleanObject({
...eventData,
source: eventData.source ?? 'community/agent',
});
lambdaClient.market.reportAgentEvent.mutate(payload).catch((error) => {
console.warn('Failed to report Agent event:', error);
});
};
// ============================== Models ==============================
getModelCategories = async (params: CategoryListQuery = {}): Promise<CategoryItem[]> => {

View File

@@ -12,6 +12,37 @@ import { CheckMcpInstallResult, MCPInstallStep } from '@/types/plugins';
import { useToolStore } from '../../store';
vi.mock('@/libs/trpc/client', () => ({
asyncClient: {},
lambdaClient: {
market: {
getMcpCategories: { query: vi.fn() },
getMcpDetail: { query: vi.fn() },
getMcpList: { query: vi.fn() },
getMcpManifest: { query: vi.fn() },
registerClientInMarketplace: {
mutate: vi.fn().mockResolvedValue({
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
}),
},
registerM2MToken: { query: vi.fn().mockResolvedValue({ success: true }) },
reportCall: { mutate: vi.fn().mockResolvedValue(undefined) },
reportMcpEvent: { mutate: vi.fn().mockResolvedValue(undefined) },
reportMcpInstallResult: { mutate: vi.fn().mockResolvedValue(undefined) },
},
},
toolsClient: {
market: {
callCloudMcpEndpoint: { mutate: vi.fn() },
},
mcp: {
callTool: { mutate: vi.fn() },
getStreamableMcpServerManifest: { query: vi.fn() },
},
},
}));
// Keep zustand mock as it's needed globally
vi.mock('zustand/traditional');
@@ -61,6 +92,13 @@ const bootstrapToolStoreWithDesktop = async (isDesktopEnv: boolean) => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(discoverService, 'injectMPToken').mockResolvedValue(undefined);
vi.spyOn(discoverService, 'registerClient').mockResolvedValue({
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
});
vi.spyOn(discoverService, 'reportMcpEvent').mockResolvedValue(undefined as any);
// Reset store state
act(() => {
useToolStore.setState(

View File

@@ -588,6 +588,12 @@ export const createMCPPluginStoreSlice: StateCreator<
// Calculate installation duration
const installDurationMs = Date.now() - installStartTime;
discoverService.reportMcpEvent({
event: 'install',
identifier: plugin.identifier,
source: 'self',
})
discoverService.reportMcpInstallResult({
identifier: plugin.identifier,
installDurationMs,
@@ -790,6 +796,12 @@ export const createMCPPluginStoreSlice: StateCreator<
n('testMcpConnection/success'),
);
discoverService.reportMcpEvent({
event: 'activate',
identifier: identifier,
source: 'self',
})
return { manifest, success: true };
} catch (error) {
// Silently handle errors caused by cancellation
@@ -817,6 +829,12 @@ export const createMCPPluginStoreSlice: StateCreator<
uninstallMCPPlugin: async (identifier) => {
await pluginService.uninstallPlugin(identifier);
await get().refreshPlugins();
discoverService.reportMcpEvent({
event: 'uninstall',
identifier: identifier,
source: 'self',
})
},
updateMCPInstallProgress: (identifier, progress) => {