mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat(community): support to report for agent & mcp plugin interaction for recommendation (#11289)
This commit is contained in:
@@ -18,7 +18,7 @@ export enum McpCategory {
|
||||
Tools = 'tools',
|
||||
TravelTransport = 'travel-transport',
|
||||
Weather = 'weather',
|
||||
WebSearch = 'web-search',
|
||||
WebSearch = 'web-search'
|
||||
}
|
||||
|
||||
export enum McpSorts {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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[]> => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user