UNPKG

@lobehub/chat

Version:

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.

622 lines (561 loc) 18.4 kB
import { TRPCError } from '@trpc/server'; import { serialize } from 'cookie'; import debug from 'debug'; import { z } from 'zod'; import { isDesktop } from '@/const/version'; import { publicProcedure, router } from '@/libs/trpc/lambda'; import { DiscoverService } from '@/server/services/discover'; import { AssistantSorts, McpSorts, ModelSorts, PluginSorts, ProviderSorts } from '@/types/discover'; const log = debug('lambda-router:market'); const marketProcedure = publicProcedure.use(async ({ ctx, next }) => { return next({ ctx: { discoverService: new DiscoverService({ accessToken: ctx.marketAccessToken }), }, }); }); export const marketRouter = router({ // ============================== Assistant Market ============================== getAssistantCategories: marketProcedure .input( z .object({ locale: z.string().optional(), q: z.string().optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log(' getAssistantCategories: marketProcedure\n input: %O', input); try { return await ctx.discoverService.getAssistantCategories(input); } catch (error) { log('Error fetching assistant categories: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch assistant categories', }); } }), getAssistantDetail: marketProcedure .input( z.object({ identifier: z.string(), locale: z.string().optional(), }), ) .query(async ({ input, ctx }) => { log('getAssistantDetail input: %O', input); try { return await ctx.discoverService.getAssistantDetail(input); } catch (error) { log('Error fetching assistants detail: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch assistants detail', }); } }), getAssistantIdentifiers: marketProcedure.query(async ({ ctx }) => { log('getAssistantIdentifiers called'); try { return await ctx.discoverService.getAssistantIdentifiers(); } catch (error) { log('Error fetching assistant identifiers: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch assistant identifiers', }); } }), getAssistantList: marketProcedure .input( z .object({ category: z.string().optional(), locale: z.string().optional(), order: z.enum(['asc', 'desc']).optional(), page: z.number().optional(), pageSize: z.number().optional(), q: z.string().optional(), sort: z.nativeEnum(AssistantSorts).optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getAssistantList input: %O', input); try { return await ctx.discoverService.getAssistantList(input); } catch (error) { log('Error fetching assistant list: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch assistant list', }); } }), getLegacyPluginList: marketProcedure .input( z .object({ locale: z.string().optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getLegacyPluginList input: %O', input); try { return await ctx.discoverService.getLegacyPluginList(input); } catch (error) { log('Error fetching legacy plugin list: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch plugin list', }); } }), // ============================== MCP Market ============================== getMcpCategories: marketProcedure .input( z .object({ locale: z.string().optional(), q: z.string().optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getMcpCategories input: %O', input); try { return await ctx.discoverService.getMcpCategories(input); } catch (error) { log('Error fetching mcp categories: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch mcp categories', }); } }), getMcpDetail: marketProcedure .input( z.object({ identifier: z.string(), locale: z.string().optional(), version: z.string().optional(), }), ) .query(async ({ input, ctx }) => { log('getMcpDetail input: %O', input); try { return await ctx.discoverService.getMcpDetail(input); } catch (error) { console.error('Error fetching mcp detail: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch mcp detail', }); } }), getMcpIdentifiers: marketProcedure.query(async ({ ctx }) => { log('getMcpIdentifiers called'); try { return await ctx.discoverService.getMcpIdentifiers(); } catch (error) { log('Error fetching mcp identifiers: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch mcp identifiers', }); } }), getMcpList: marketProcedure .input( z .object({ category: z.string().optional(), locale: z.string().optional(), order: z.enum(['asc', 'desc']).optional(), page: z.number().optional(), pageSize: z.number().optional(), q: z.string().optional(), sort: z.nativeEnum(McpSorts).optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getMcpList input: %O', input); try { return await ctx.discoverService.getMcpList(input); } catch (error) { log('Error fetching mcp list: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch mcp list', }); } }), getMcpManifest: marketProcedure .input( z.object({ identifier: z.string(), install: z.boolean().optional(), locale: z.string().optional(), version: z.string().optional(), }), ) .query(async ({ input, ctx }) => { log('getMcpManifest input: %O', input); try { return await ctx.discoverService.getMcpManifest(input); } catch (error) { log('Error fetching mcp manifest: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch mcp manifest', }); } }), // ============================== Models ============================== getModelCategories: marketProcedure .input( z .object({ q: z.string().optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getModelCategories input: %O', input); try { return await ctx.discoverService.getModelCategories(input); } catch (error) { log('Error fetching model categories: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch model categories', }); } }), getModelDetail: marketProcedure .input( z.object({ identifier: z.string(), locale: z.string().optional(), }), ) .query(async ({ input, ctx }) => { log('getModelDetail input: %O', input); try { return await ctx.discoverService.getModelDetail(input); } catch (error) { log('Error fetching model details: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch model details', }); } }), getModelIdentifiers: marketProcedure.query(async ({ ctx }) => { log('getModelIdentifiers called'); try { return await ctx.discoverService.getModelIdentifiers(); } catch (error) { log('Error fetching model identifiers: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch model identifiers', }); } }), getModelList: marketProcedure .input( z .object({ category: z.string().optional(), locale: z.string().optional(), order: z.enum(['asc', 'desc']).optional(), page: z.number().optional(), pageSize: z.number().optional(), q: z.string().optional(), sort: z.nativeEnum(ModelSorts).optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getModelList input: %O', input); try { return await ctx.discoverService.getModelList(input); } catch (error) { log('Error fetching model list: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch model list', }); } }), // ============================== Plugin Market ============================== getPluginCategories: marketProcedure .input( z .object({ locale: z.string().optional(), q: z.string().optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getPluginCategories input: %O', input); try { return await ctx.discoverService.getPluginCategories(input); } catch (error) { log('Error fetching plugin categories: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch plugin categories', }); } }), getPluginDetail: marketProcedure .input( z.object({ identifier: z.string(), locale: z.string().optional(), withManifest: z.boolean().optional(), }), ) .query(async ({ input, ctx }) => { log('getPluginDetail input: %O', input); try { return await ctx.discoverService.getPluginDetail(input); } catch (error) { log('Error fetching plugin details: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch plugin details', }); } }), getPluginIdentifiers: marketProcedure.query(async ({ ctx }) => { log('getPluginIdentifiers called'); try { return await ctx.discoverService.getPluginIdentifiers(); } catch (error) { log('Error fetching plugin identifiers: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch plugin identifiers', }); } }), getPluginList: marketProcedure .input( z .object({ category: z.string().optional(), locale: z.string().optional(), order: z.enum(['asc', 'desc']).optional(), page: z.number().optional(), pageSize: z.number().optional(), q: z.string().optional(), sort: z.nativeEnum(PluginSorts).optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getPluginList input: %O', input); try { return await ctx.discoverService.getPluginList(input); } catch (error) { log('Error fetching plugin list: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch plugin list', }); } }), // ============================== Providers ============================== getProviderDetail: marketProcedure .input( z.object({ identifier: z.string(), locale: z.string().optional(), withReadme: z.boolean().optional(), }), ) .query(async ({ input, ctx }) => { log('getProviderDetail input: %O', input); try { return await ctx.discoverService.getProviderDetail(input); } catch (error) { log('Error fetching provider details: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch provider details', }); } }), getProviderIdentifiers: marketProcedure.query(async ({ ctx }) => { log('getProviderIdentifiers called'); try { return await ctx.discoverService.getProviderIdentifiers(); } catch (error) { log('Error fetching provider identifiers: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch provider identifiers', }); } }), getProviderList: marketProcedure .input( z .object({ locale: z.string().optional(), order: z.enum(['asc', 'desc']).optional(), page: z.number().optional(), pageSize: z.number().optional(), q: z.string().optional(), sort: z.nativeEnum(ProviderSorts).optional(), }) .optional(), ) .query(async ({ input, ctx }) => { log('getProviderList input: %O', input); try { return await ctx.discoverService.getProviderList(input); } catch (error) { log('Error fetching provider list: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch provider list', }); } }), registerClientInMarketplace: marketProcedure.input(z.object({})).mutation(async ({ ctx }) => { return ctx.discoverService.registerClient({ userAgent: ctx.userAgent, }); }), registerM2MToken: marketProcedure .input( z.object({ clientId: z.string(), clientSecret: z.string(), }), ) .query(async ({ input, ctx }) => { log('registerM2MToken input: %O', { clientId: input.clientId, clientSecret: '[HIDDEN]' }); try { const { accessToken, expiresIn } = await ctx.discoverService.fetchM2MToken(input); // check accessToken if (!accessToken) { // clean Cookies ctx.resHeaders?.append('Set-Cookie', serialize('mp_token_status', '', { maxAge: -1 })); ctx.resHeaders?.append('Set-Cookie', serialize('mp_token', '', { maxAge: -1 })); return { success: false }; } log('get access token, expiresIn value:', expiresIn); log('expiresIn type:', typeof expiresIn); const expirationTime = new Date(Date.now() + (expiresIn - 60) * 1000); // 提前 60 秒过期 log('expirationTime:', expirationTime.toISOString()); // 设置 HTTP-Only Cookie 存储实际的 access token const tokenCookie = serialize('mp_token', accessToken, { expires: expirationTime, httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', }); // 设置客户端可读的状态标记 cookie(不包含实际 token) const statusCookie = serialize('mp_token_status', 'active', { expires: expirationTime, httpOnly: false, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', }); // 通过 context 的 resHeaders 设置 Set-Cookie 头 ctx.resHeaders?.append('Set-Cookie', tokenCookie); ctx.resHeaders?.append('Set-Cookie', statusCookie); return { expiresIn: expiresIn - 60, success: true, }; } catch (error) { console.error('Error fetching M2M token: %O', error); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to fetch M2M token', }); } }), // ============================== Analytics ============================== reportCall: marketProcedure .input( z.object({ callDurationMs: z.number(), clientId: z.string().optional(), clientIp: z.string().optional(), customPluginInfo: z.any().optional(), errorCode: z.string().optional(), errorMessage: z.string().optional(), identifier: z.string(), inputParams: z.any().optional(), isCustomPlugin: z.boolean().optional(), metadata: z.record(z.any()).optional(), methodName: z.string(), methodType: z.enum(['tool', 'prompt', 'resource']), outputResult: z.any().optional(), platform: z.string().optional(), requestSizeBytes: z.number().optional(), responseSizeBytes: z.number().optional(), sessionId: z.string().optional(), success: z.boolean(), traceId: z.string().optional(), userAgent: z.string().optional(), version: z.string(), }), ) .mutation(async ({ input, ctx }) => { log('reportCall input: %O', input); try { await ctx.discoverService.reportCall({ ...input, platform: isDesktop ? process.platform : 'web', userAgent: ctx.userAgent, }); return { success: true }; } catch (error) { console.error('Error reporting call: %O', error); // 不抛出错误,因为上报失败不应影响主流程 return { success: false }; } }), reportMcpInstallResult: marketProcedure .input( z.object({ errorCode: z.any().optional(), errorMessage: z.any().optional(), identifier: z.string(), installDurationMs: z.number().optional(), installParams: z.any().optional(), manifest: z.any().optional(), metadata: z.any().optional(), platform: z.string().optional(), success: z.boolean(), userAgent: z.string().optional(), version: z.string(), }), ) .mutation(async ({ input, ctx }) => { log('reportMcpInstallResult input: %O', input); try { await ctx.discoverService.reportPluginInstallation(input); return { success: true }; } catch (error) { log('Error reporting MCP installation result: %O', error); // 不抛出错误,因为上报失败不应影响主流程 return { success: false }; } }), });