mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
♻️ refactor: refactor market sdk into market servers (#11604)
* refactor: refactor market sdk into market servers * fix: fixed the test failed problem
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{
|
||||
@@ -9,18 +8,6 @@ type RouteContext = {
|
||||
}>;
|
||||
};
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
const extractAccessToken = (req: NextRequest) => {
|
||||
const authorization = req.headers.get('authorization');
|
||||
if (!authorization) return undefined;
|
||||
|
||||
const [scheme, token] = authorization.split(' ');
|
||||
if (scheme?.toLowerCase() !== 'bearer' || !token) return undefined;
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
const methodNotAllowed = (methods: string[]) =>
|
||||
NextResponse.json(
|
||||
{
|
||||
@@ -55,14 +42,8 @@ const notFound = (reason: string) =>
|
||||
);
|
||||
|
||||
const handleAgent = async (req: NextRequest, segments: string[]) => {
|
||||
const accessToken = extractAccessToken(req);
|
||||
const trustedClientToken = await getTrustedClientTokenForSession();
|
||||
|
||||
const market = new MarketSDK({
|
||||
accessToken,
|
||||
baseURL: MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
const marketService = await MarketService.createFromRequest(req);
|
||||
const market = marketService.market;
|
||||
|
||||
if (segments.length === 0) {
|
||||
return notFound('Missing agent action.');
|
||||
@@ -94,17 +75,6 @@ const handleAgent = async (req: NextRequest, segments: string[]) => {
|
||||
if (action === 'own') {
|
||||
if (req.method !== 'GET') return methodNotAllowed(['GET']);
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'unauthorized',
|
||||
message: 'Authentication required to get own agents',
|
||||
status: 'error',
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse query parameters from the request URL
|
||||
const url = new URL(req.url);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{
|
||||
segments?: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
const ALLOWED_ENDPOINTS = new Set(['handoff', 'token', 'userinfo']);
|
||||
|
||||
const ensureEndpoint = (segments?: string[]) => {
|
||||
@@ -44,9 +44,8 @@ const methodNotAllowed = (allowed: string[]) =>
|
||||
);
|
||||
|
||||
const handleProxy = async (req: NextRequest, context: RouteContext) => {
|
||||
const market = new MarketSDK({
|
||||
baseURL: MARKET_BASE_URL,
|
||||
});
|
||||
const marketService = new MarketService();
|
||||
const market = marketService.market;
|
||||
|
||||
const { segments } = await context.params;
|
||||
const endpointResult = ensureEndpoint(segments);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{
|
||||
@@ -9,19 +8,6 @@ type RouteContext = {
|
||||
}>;
|
||||
};
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
/**
|
||||
* Helper to get authorization header
|
||||
*/
|
||||
const getAccessToken = (req: NextRequest): string | undefined => {
|
||||
const authHeader = req.headers.get('authorization');
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /market/social/follow
|
||||
* POST /market/social/unfollow
|
||||
@@ -34,22 +20,9 @@ const getAccessToken = (req: NextRequest): string | undefined => {
|
||||
export const POST = async (req: NextRequest, context: RouteContext) => {
|
||||
const { segments = [] } = await context.params;
|
||||
const action = segments[0];
|
||||
const accessToken = getAccessToken(req);
|
||||
const trustedClientToken = await getTrustedClientTokenForSession();
|
||||
|
||||
const market = new MarketSDK({
|
||||
accessToken,
|
||||
baseURL: MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
|
||||
// Only require accessToken if trusted client token is not available
|
||||
if (!accessToken && !trustedClientToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'unauthorized', message: 'Access token required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const marketService = await MarketService.createFromRequest(req);
|
||||
const market = marketService.market;
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
@@ -135,14 +108,9 @@ export const POST = async (req: NextRequest, context: RouteContext) => {
|
||||
export const GET = async (req: NextRequest, context: RouteContext) => {
|
||||
const { segments = [] } = await context.params;
|
||||
const action = segments[0];
|
||||
const accessToken = getAccessToken(req);
|
||||
const trustedClientToken = await getTrustedClientTokenForSession();
|
||||
|
||||
const market = new MarketSDK({
|
||||
accessToken,
|
||||
baseURL: MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
const marketService = await MarketService.createFromRequest(req);
|
||||
const market = marketService.market;
|
||||
|
||||
const url = new URL(req.url);
|
||||
const limit = url.searchParams.get('pageSize') || url.searchParams.get('limit');
|
||||
@@ -158,9 +126,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
||||
// Follow queries
|
||||
case 'follow-status': {
|
||||
const targetUserId = Number(segments[1]);
|
||||
if (!accessToken && !trustedClientToken) {
|
||||
return NextResponse.json({ isFollowing: false, isMutual: false });
|
||||
}
|
||||
const result = await market.follows.checkFollowStatus(targetUserId);
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
@@ -193,9 +158,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
||||
case 'favorite-status': {
|
||||
const targetType = segments[1] as 'agent' | 'plugin';
|
||||
const targetIdOrIdentifier = segments[2];
|
||||
if (!accessToken && !trustedClientToken) {
|
||||
return NextResponse.json({ isFavorited: false });
|
||||
}
|
||||
// SDK accepts both number (targetId) and string (identifier)
|
||||
const isNumeric = /^\d+$/.test(targetIdOrIdentifier);
|
||||
const targetValue = isNumeric ? Number(targetIdOrIdentifier) : targetIdOrIdentifier;
|
||||
@@ -204,12 +166,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
||||
}
|
||||
|
||||
case 'favorites': {
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'unauthorized', message: 'Access token required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const result = await market.favorites.getMyFavorites(paginationParams);
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
@@ -236,9 +192,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
|
||||
case 'like-status': {
|
||||
const targetType = segments[1] as 'agent' | 'plugin';
|
||||
const targetIdOrIdentifier = segments[2];
|
||||
if (!accessToken && !trustedClientToken) {
|
||||
return NextResponse.json({ isLiked: false });
|
||||
}
|
||||
const isNumeric = /^\d+$/.test(targetIdOrIdentifier);
|
||||
const targetValue = isNumeric ? Number(targetIdOrIdentifier) : targetIdOrIdentifier;
|
||||
const result = await market.likes.checkLike(targetType, targetValue as number);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{
|
||||
@@ -9,8 +8,6 @@ type RouteContext = {
|
||||
}>;
|
||||
};
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
/**
|
||||
* GET /market/user/[username]
|
||||
*
|
||||
@@ -20,12 +17,9 @@ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://mark
|
||||
export const GET = async (req: NextRequest, context: RouteContext) => {
|
||||
const { username } = await context.params;
|
||||
const decodedUsername = decodeURIComponent(username);
|
||||
const trustedClientToken = await getTrustedClientTokenForSession();
|
||||
|
||||
const market = new MarketSDK({
|
||||
baseURL: MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
const marketService = await MarketService.createFromRequest(req);
|
||||
const market = marketService.market;
|
||||
|
||||
try {
|
||||
const response = await market.user.getUserInfo(decodedUsername);
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
const extractAccessToken = (req: NextRequest) => {
|
||||
const authorization = req.headers.get('authorization');
|
||||
if (!authorization) return undefined;
|
||||
|
||||
const [scheme, token] = authorization.split(' ');
|
||||
if (scheme?.toLowerCase() !== 'bearer' || !token) return undefined;
|
||||
|
||||
return token;
|
||||
};
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
/**
|
||||
* PUT /market/user/me
|
||||
@@ -28,26 +15,8 @@ const extractAccessToken = (req: NextRequest) => {
|
||||
* - meta?: { description?: string; socialLinks?: { github?: string; twitter?: string; website?: string } }
|
||||
*/
|
||||
export const PUT = async (req: NextRequest) => {
|
||||
const accessToken = extractAccessToken(req);
|
||||
const trustedClientToken = await getTrustedClientTokenForSession();
|
||||
|
||||
const market = new MarketSDK({
|
||||
accessToken,
|
||||
baseURL: MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
|
||||
// Only require accessToken if trusted client token is not available
|
||||
if (!accessToken && !trustedClientToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'unauthorized',
|
||||
message: 'Authentication required to update user profile',
|
||||
status: 'error',
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
const marketService = await MarketService.createFromRequest(req);
|
||||
const market = marketService.market;
|
||||
|
||||
try {
|
||||
const payload = await req.json();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
|
||||
import { generateTrustedClientToken, type TrustedClientUserInfo } from '@/libs/trusted-client';
|
||||
import { type TrustedClientUserInfo } from '@/libs/trusted-client';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
import { trpc } from '../init';
|
||||
|
||||
@@ -10,53 +9,45 @@ interface ContextWithMarketUserInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware that initializes MarketSDK with proper authentication.
|
||||
* Middleware that initializes MarketService with proper authentication.
|
||||
* This requires marketUserInfo middleware to be applied first.
|
||||
*
|
||||
* Provides:
|
||||
* - ctx.marketSDK: Initialized MarketSDK instance with trustedClientToken and optional accessToken
|
||||
* - ctx.trustedClientToken: The generated trusted client token (if available)
|
||||
* - ctx.marketSDK: MarketSDK instance for backward compatibility
|
||||
* - ctx.marketService: MarketService instance (recommended)
|
||||
*/
|
||||
export const marketSDK = trpc.middleware(async (opts) => {
|
||||
const ctx = opts.ctx as ContextWithMarketUserInfo;
|
||||
|
||||
// Generate trusted client token if user info is available
|
||||
const trustedClientToken = ctx.marketUserInfo
|
||||
? generateTrustedClientToken(ctx.marketUserInfo)
|
||||
: undefined;
|
||||
|
||||
// Initialize MarketSDK with both authentication methods
|
||||
const market = new MarketSDK({
|
||||
// Initialize MarketService with authentication
|
||||
const marketService = new MarketService({
|
||||
accessToken: ctx.marketAccessToken,
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
userInfo: ctx.marketUserInfo,
|
||||
});
|
||||
|
||||
return opts.next({
|
||||
ctx: {
|
||||
marketSDK: market,
|
||||
trustedClientToken,
|
||||
marketSDK: marketService.market, // Backward compatibility
|
||||
marketService, // New recommended way
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Middleware that requires authentication for Market API access.
|
||||
* This middleware ensures that either accessToken or trustedClientToken is available.
|
||||
* This middleware ensures that either accessToken or marketUserInfo is available.
|
||||
* It should be used after marketUserInfo and marketSDK middlewares.
|
||||
*
|
||||
* Throws UNAUTHORIZED error if neither authentication method is available.
|
||||
*/
|
||||
export const requireMarketAuth = trpc.middleware(async (opts) => {
|
||||
const ctx = opts.ctx as ContextWithMarketUserInfo & {
|
||||
trustedClientToken?: string;
|
||||
};
|
||||
const ctx = opts.ctx as ContextWithMarketUserInfo;
|
||||
|
||||
// Check if any authentication is available
|
||||
const hasAccessToken = !!ctx.marketAccessToken;
|
||||
const hasTrustedToken = !!ctx.trustedClientToken;
|
||||
const hasUserInfo = !!ctx.marketUserInfo;
|
||||
|
||||
if (!hasAccessToken && !hasTrustedToken) {
|
||||
if (!hasAccessToken && !hasUserInfo) {
|
||||
const { TRPCError } = await import('@trpc/server');
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { buildTrustedClientPayload, createTrustedClientToken } from '@lobehub/ma
|
||||
import { appEnv } from '@/envs/app';
|
||||
|
||||
export interface TrustedClientUserInfo {
|
||||
email: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
||||
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { DiscoverService } from '@/server/services/discover';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
import {
|
||||
AssistantSorts,
|
||||
McpConnectionType,
|
||||
@@ -37,6 +38,10 @@ const marketProcedure = publicProcedure
|
||||
accessToken: ctx.marketAccessToken,
|
||||
userInfo: ctx.marketUserInfo,
|
||||
}),
|
||||
marketService: new MarketService({
|
||||
accessToken: ctx.marketAccessToken,
|
||||
userInfo: ctx.marketUserInfo,
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import debug from 'debug';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
const log = debug('lambda-router:market:oidc');
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
// OIDC procedures are public (used during authentication flow)
|
||||
const oidcProcedure = publicProcedure.use(serverDatabase).use(marketUserInfo);
|
||||
const oidcProcedure = publicProcedure
|
||||
.use(serverDatabase)
|
||||
.use(marketUserInfo)
|
||||
.use(async ({ ctx, next }) => {
|
||||
// Initialize MarketService (may be without auth for public endpoints)
|
||||
const marketService = new MarketService({
|
||||
userInfo: ctx.marketUserInfo,
|
||||
});
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
marketService,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const oidcRouter = router({
|
||||
/**
|
||||
@@ -28,19 +39,11 @@ export const oidcRouter = router({
|
||||
redirectUri: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('exchangeAuthorizationCode input: %O', { ...input, code: '[REDACTED]' });
|
||||
|
||||
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
|
||||
|
||||
try {
|
||||
const response = await market.auth.exchangeOAuthToken({
|
||||
clientId: input.clientId,
|
||||
code: input.code,
|
||||
codeVerifier: input.codeVerifier,
|
||||
grantType: 'authorization_code',
|
||||
redirectUri: input.redirectUri,
|
||||
});
|
||||
const response = await ctx.marketService.exchangeAuthorizationCode(input);
|
||||
return response;
|
||||
} catch (error) {
|
||||
log('Error exchanging authorization code: %O', error);
|
||||
@@ -56,23 +59,23 @@ export const oidcRouter = router({
|
||||
* Get OAuth handoff information
|
||||
* GET /market/oidc/handoff?id=xxx
|
||||
*/
|
||||
getOAuthHandoff: oidcProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
|
||||
log('getOAuthHandoff input: %O', input);
|
||||
getOAuthHandoff: oidcProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('getOAuthHandoff input: %O', input);
|
||||
|
||||
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
|
||||
|
||||
try {
|
||||
const handoff = await market.auth.getOAuthHandoff(input.id);
|
||||
return handoff;
|
||||
} catch (error) {
|
||||
log('Error getting OAuth handoff: %O', error);
|
||||
throw new TRPCError({
|
||||
cause: error,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Failed to get OAuth handoff',
|
||||
});
|
||||
}
|
||||
}),
|
||||
try {
|
||||
const handoff = await ctx.marketService.getOAuthHandoff(input.id);
|
||||
return handoff;
|
||||
} catch (error) {
|
||||
log('Error getting OAuth handoff: %O', error);
|
||||
throw new TRPCError({
|
||||
cause: error,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Failed to get OAuth handoff',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get user info from token or trusted client
|
||||
@@ -83,37 +86,17 @@ export const oidcRouter = router({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('getUserInfo input: token=%s', input.token ? '[REDACTED]' : 'undefined');
|
||||
|
||||
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
|
||||
|
||||
try {
|
||||
// If token is provided, use it
|
||||
if (input.token) {
|
||||
const response = await market.auth.getUserInfo(input.token);
|
||||
const response = await ctx.marketService.getUserInfo(input.token);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Otherwise, try to use trustedClientToken
|
||||
if (ctx.marketUserInfo) {
|
||||
const trustedClientToken = generateTrustedClientToken(ctx.marketUserInfo);
|
||||
|
||||
if (trustedClientToken) {
|
||||
const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
|
||||
const response = await fetch(userInfoUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-lobe-trust-token': trustedClientToken,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch user info: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
const response = await ctx.marketService.getUserInfoWithTrustedClient();
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
@@ -143,15 +126,12 @@ export const oidcRouter = router({
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('refreshToken input: %O', { ...input, refreshToken: '[REDACTED]' });
|
||||
|
||||
const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
|
||||
|
||||
try {
|
||||
const response = await market.auth.exchangeOAuthToken({
|
||||
clientId: input.clientId,
|
||||
grantType: 'refresh_token',
|
||||
const response = await ctx.marketService.refreshToken({
|
||||
clientId: input.clientId || '',
|
||||
refreshToken: input.refreshToken,
|
||||
});
|
||||
return response;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type CodeInterpreterToolName, MarketSDK } from '@lobehub/market-sdk';
|
||||
import { type CodeInterpreterToolName } from '@lobehub/market-sdk';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import debug from 'debug';
|
||||
import { sha256 } from 'js-sha256';
|
||||
@@ -8,10 +8,11 @@ import { type ToolCallContent } from '@/libs/mcp';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
|
||||
import { marketSDK, requireMarketAuth } from '@/libs/trpc/lambda/middleware/marketSDK';
|
||||
import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
|
||||
import { isTrustedClientEnabled } from '@/libs/trusted-client';
|
||||
import { FileS3 } from '@/server/modules/S3';
|
||||
import { DiscoverService } from '@/server/services/discover';
|
||||
import { FileService } from '@/server/services/file';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
import {
|
||||
contentBlocksToString,
|
||||
processContentBlocks,
|
||||
@@ -37,6 +38,10 @@ const marketToolProcedure = authedProcedure
|
||||
userInfo: ctx.marketUserInfo,
|
||||
}),
|
||||
fileService: new FileService(ctx.serverDB, ctx.userId),
|
||||
marketService: new MarketService({
|
||||
accessToken: ctx.marketAccessToken,
|
||||
userInfo: ctx.marketUserInfo,
|
||||
}),
|
||||
userModel,
|
||||
},
|
||||
});
|
||||
@@ -79,7 +84,6 @@ const metaSchema = z
|
||||
|
||||
// Schema for code interpreter tool call request
|
||||
const callCodeInterpreterToolSchema = z.object({
|
||||
marketAccessToken: z.string().optional(),
|
||||
params: z.record(z.any()),
|
||||
toolName: z.string(),
|
||||
topicId: z.string(),
|
||||
@@ -224,27 +228,17 @@ export const marketRouter = router({
|
||||
callCodeInterpreterTool: marketToolProcedure
|
||||
.input(callCodeInterpreterToolSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { toolName, params, userId, topicId, marketAccessToken } = input;
|
||||
const { toolName, params, userId, topicId } = input;
|
||||
|
||||
log('Calling cloud code interpreter tool: %s with params: %O', toolName, {
|
||||
params,
|
||||
topicId,
|
||||
userId,
|
||||
});
|
||||
log('Market access token available: %s', marketAccessToken ? 'yes' : 'no');
|
||||
|
||||
// Generate trusted client token if user info is available
|
||||
const trustedClientToken = ctx.marketUserInfo
|
||||
? generateTrustedClientToken(ctx.marketUserInfo)
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
// Initialize MarketSDK with market access token and trusted client token
|
||||
const market = new MarketSDK({
|
||||
accessToken: marketAccessToken,
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
// Use marketService from ctx
|
||||
const market = ctx.marketService.market;
|
||||
|
||||
// Call market-sdk's runBuildInTool
|
||||
const response = await market.plugins.runBuildInTool(
|
||||
@@ -555,34 +549,8 @@ export const marketRouter = router({
|
||||
const uploadUrl = await s3.createPreSignedUrl(key);
|
||||
log('Generated upload URL for key: %s', key);
|
||||
|
||||
// Step 2: Generate trusted client token if user info is available
|
||||
const trustedClientToken = ctx.marketUserInfo
|
||||
? generateTrustedClientToken(ctx.marketUserInfo)
|
||||
: undefined;
|
||||
|
||||
// Only require user accessToken if trusted client is not available
|
||||
let userAccessToken: string | undefined;
|
||||
if (!trustedClientToken) {
|
||||
const userState = await ctx.userModel.getUserState(async () => ({}));
|
||||
userAccessToken = userState.settings?.market?.accessToken;
|
||||
|
||||
if (!userAccessToken) {
|
||||
return {
|
||||
error: { message: 'User access token not found. Please sign in to Market first.' },
|
||||
filename,
|
||||
success: false,
|
||||
} as ExportAndUploadFileResult;
|
||||
}
|
||||
} else {
|
||||
log('Using trusted client authentication for exportAndUploadFile');
|
||||
}
|
||||
|
||||
// Initialize MarketSDK
|
||||
const market = new MarketSDK({
|
||||
accessToken: userAccessToken,
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
// Step 2: Use MarketService from ctx
|
||||
const market = ctx.marketService.market;
|
||||
|
||||
// Step 3: Call sandbox's exportFile tool with the upload URL
|
||||
const response = await market.plugins.runBuildInTool(
|
||||
|
||||
@@ -3,6 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AgentRuntimeService } from './AgentRuntimeService';
|
||||
import type { AgentExecutionParams, OperationCreationParams, StartExecutionParams } from './types';
|
||||
|
||||
// Mock trusted client to avoid server-side env access
|
||||
vi.mock('@/libs/trusted-client', () => ({
|
||||
generateTrustedClientToken: vi.fn().mockReturnValue(undefined),
|
||||
getTrustedClientTokenForSession: vi.fn().mockResolvedValue(undefined),
|
||||
isTrustedClientEnabled: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
// Mock database and models
|
||||
vi.mock('@/database/models/message', () => ({
|
||||
MessageModel: vi.fn().mockImplementation(() => ({
|
||||
|
||||
@@ -152,7 +152,7 @@ export class AgentRuntimeService {
|
||||
|
||||
// Initialize ToolExecutionService with dependencies
|
||||
const pluginGatewayService = new PluginGatewayService();
|
||||
const builtinToolsExecutor = new BuiltinToolsExecutor();
|
||||
const builtinToolsExecutor = new BuiltinToolsExecutor(db, userId);
|
||||
|
||||
this.toolExecutionService = new ToolExecutionService({
|
||||
builtinToolsExecutor,
|
||||
|
||||
@@ -2,6 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AiAgentService } from '../index';
|
||||
|
||||
// Mock trusted client to avoid server-side env access
|
||||
vi.mock('@/libs/trusted-client', () => ({
|
||||
generateTrustedClientToken: vi.fn().mockReturnValue(undefined),
|
||||
getTrustedClientTokenForSession: vi.fn().mockResolvedValue(undefined),
|
||||
isTrustedClientEnabled: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
// Mock MessageModel to capture create calls
|
||||
const mockMessageCreate = vi.fn();
|
||||
|
||||
|
||||
@@ -3,6 +3,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AiAgentService } from '../index';
|
||||
|
||||
// Mock trusted client to avoid server-side env access
|
||||
vi.mock('@/libs/trusted-client', () => ({
|
||||
generateTrustedClientToken: vi.fn().mockReturnValue(undefined),
|
||||
getTrustedClientTokenForSession: vi.fn().mockResolvedValue(undefined),
|
||||
isTrustedClientEnabled: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
// Mock ThreadModel
|
||||
const mockThreadModel = {
|
||||
create: vi.fn(),
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
} from '@lobechat/types';
|
||||
import { ThreadStatus, ThreadType } from '@lobechat/types';
|
||||
import { nanoid } from '@lobechat/utils';
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import debug from 'debug';
|
||||
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
@@ -20,8 +19,6 @@ import { MessageModel } from '@/database/models/message';
|
||||
import { PluginModel } from '@/database/models/plugin';
|
||||
import { ThreadModel } from '@/database/models/thread';
|
||||
import { TopicModel } from '@/database/models/topic';
|
||||
import { UserModel } from '@/database/models/user';
|
||||
import { generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
import {
|
||||
type ServerAgentToolsContext,
|
||||
createServerAgentToolsEngine,
|
||||
@@ -30,6 +27,7 @@ import {
|
||||
import { AgentService } from '@/server/services/agent';
|
||||
import { AgentRuntimeService } from '@/server/services/agentRuntime';
|
||||
import type { StepLifecycleCallbacks } from '@/server/services/agentRuntime/types';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
const log = debug('lobe-server:ai-agent-service');
|
||||
|
||||
@@ -88,6 +86,7 @@ export class AiAgentService {
|
||||
private readonly threadModel: ThreadModel;
|
||||
private readonly topicModel: TopicModel;
|
||||
private readonly agentRuntimeService: AgentRuntimeService;
|
||||
private readonly marketService: MarketService;
|
||||
|
||||
constructor(db: LobeChatDatabase, userId: string) {
|
||||
this.userId = userId;
|
||||
@@ -99,6 +98,7 @@ export class AiAgentService {
|
||||
this.threadModel = new ThreadModel(db, userId);
|
||||
this.topicModel = new TopicModel(db, userId);
|
||||
this.agentRuntimeService = new AgentRuntimeService(db, userId);
|
||||
this.marketService = new MarketService({ userInfo: { userId } });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +190,12 @@ export class AiAgentService {
|
||||
};
|
||||
|
||||
// 5. Fetch LobeHub Skills manifests (temporary solution until LOBE-3517 is implemented)
|
||||
const lobehubSkillManifests = await this.fetchLobehubSkillManifests();
|
||||
let lobehubSkillManifests: LobeToolManifest[] = [];
|
||||
try {
|
||||
lobehubSkillManifests = await this.marketService.getLobehubSkillManifests();
|
||||
} catch (error) {
|
||||
log('execAgent: failed to fetch lobehub skill manifests: %O', error);
|
||||
}
|
||||
log('execAgent: got %d lobehub skill manifests', lobehubSkillManifests.length);
|
||||
|
||||
// 6. Create tools using Server AgentToolsEngine
|
||||
@@ -736,98 +741,6 @@ export class AiAgentService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch LobeHub Skills manifests from Market API
|
||||
* This is a temporary solution until LOBE-3517 is implemented (store skills in DB)
|
||||
*/
|
||||
private async fetchLobehubSkillManifests(): Promise<LobeToolManifest[]> {
|
||||
try {
|
||||
// 1. Get user info for trusted client token
|
||||
const user = await UserModel.findById(this.db, this.userId);
|
||||
if (!user?.email) {
|
||||
log('fetchLobehubSkillManifests: user email not found, skipping');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Generate trusted client token
|
||||
const trustedClientToken = generateTrustedClientToken({
|
||||
email: user.email,
|
||||
name: user.fullName || user.firstName || undefined,
|
||||
userId: this.userId,
|
||||
});
|
||||
|
||||
if (!trustedClientToken) {
|
||||
log('fetchLobehubSkillManifests: trusted client not configured, skipping');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Create MarketSDK instance
|
||||
const marketSDK = new MarketSDK({
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
|
||||
// 4. Get user's connected skills
|
||||
const { connections } = await marketSDK.connect.listConnections();
|
||||
if (!connections || connections.length === 0) {
|
||||
log('fetchLobehubSkillManifests: no connected skills found');
|
||||
return [];
|
||||
}
|
||||
|
||||
log('fetchLobehubSkillManifests: found %d connected skills', connections.length);
|
||||
|
||||
// 5. Fetch tools for each connection and build manifests
|
||||
const manifests: LobeToolManifest[] = [];
|
||||
|
||||
for (const connection of connections) {
|
||||
try {
|
||||
// Connection returns providerId (e.g., 'twitter', 'linear'), not numeric id
|
||||
const providerId = (connection as any).providerId;
|
||||
if (!providerId) {
|
||||
log('fetchLobehubSkillManifests: connection missing providerId: %O', connection);
|
||||
continue;
|
||||
}
|
||||
const providerName =
|
||||
(connection as any).providerName || (connection as any).name || providerId;
|
||||
const icon = (connection as any).icon;
|
||||
|
||||
const { tools } = await marketSDK.skills.listTools(providerId);
|
||||
if (!tools || tools.length === 0) continue;
|
||||
|
||||
const manifest: LobeToolManifest = {
|
||||
api: tools.map((tool: any) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || { properties: {}, type: 'object' },
|
||||
})),
|
||||
identifier: providerId,
|
||||
meta: {
|
||||
avatar: icon || '🔗',
|
||||
description: `LobeHub Skill: ${providerName}`,
|
||||
tags: ['lobehub-skill', providerId],
|
||||
title: providerName,
|
||||
},
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
manifests.push(manifest);
|
||||
log(
|
||||
'fetchLobehubSkillManifests: built manifest for %s with %d tools',
|
||||
providerId,
|
||||
tools.length,
|
||||
);
|
||||
} catch (error) {
|
||||
log('fetchLobehubSkillManifests: failed to fetch tools for connection: %O', error);
|
||||
}
|
||||
}
|
||||
|
||||
return manifests;
|
||||
} catch (error) {
|
||||
log('fetchLobehubSkillManifests: error fetching skills: %O', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total tokens from AgentState usage object
|
||||
* AgentState.usage is of type Usage from @lobechat/agent-runtime
|
||||
|
||||
@@ -62,10 +62,11 @@ import { cloneDeep, countBy, isString, merge, uniq, uniqBy } from 'es-toolkit/co
|
||||
import matter from 'gray-matter';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { type TrustedClientUserInfo, generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
import { type TrustedClientUserInfo } from '@/libs/trusted-client';
|
||||
import { normalizeLocale } from '@/locales/resources';
|
||||
import { AssistantStore } from '@/server/modules/AssistantStore';
|
||||
import { PluginStore } from '@/server/modules/PluginStore';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
|
||||
const log = debug('lobe-server:discover');
|
||||
|
||||
@@ -84,18 +85,14 @@ export class DiscoverService {
|
||||
constructor(options: DiscoverServiceOptions = {}) {
|
||||
const { accessToken, userInfo } = options;
|
||||
|
||||
// Generate trusted client token if user info is available
|
||||
const trustedClientToken = userInfo ? generateTrustedClientToken(userInfo) : undefined;
|
||||
// Use MarketService to initialize MarketSDK
|
||||
const marketService = new MarketService({ accessToken, userInfo });
|
||||
this.market = marketService.market;
|
||||
|
||||
this.market = new MarketSDK({
|
||||
accessToken,
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
log(
|
||||
'DiscoverService initialized with market baseURL: %s, hasTrustedToken: %s, userId: %s',
|
||||
'DiscoverService initialized with market baseURL: %s, hasAuth: %s, userId: %s',
|
||||
process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
!!trustedClientToken,
|
||||
!!(accessToken || userInfo),
|
||||
userInfo?.userId,
|
||||
);
|
||||
}
|
||||
@@ -135,14 +132,12 @@ export class DiscoverService {
|
||||
}
|
||||
|
||||
async fetchM2MToken(params: { clientId: string; clientSecret: string }) {
|
||||
// 使用传入的客户端凭证创建新的 MarketSDK 实例
|
||||
const tokenMarket = new MarketSDK({
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
clientId: params.clientId,
|
||||
clientSecret: params.clientSecret,
|
||||
// Use MarketService with M2M credentials
|
||||
const marketService = new MarketService({
|
||||
clientCredentials: params,
|
||||
});
|
||||
|
||||
const tokenInfo = await tokenMarket.fetchM2MToken();
|
||||
const tokenInfo = await marketService.fetchM2MToken();
|
||||
|
||||
return {
|
||||
accessToken: tokenInfo.accessToken,
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { type LobeChatDatabase } from '@lobechat/database';
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import debug from 'debug';
|
||||
|
||||
import { UserModel } from '@/database/models/user';
|
||||
import { generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
|
||||
const log = debug('lobe-server:lobehub-skill-service');
|
||||
|
||||
export interface LobehubSkillExecuteParams {
|
||||
args: Record<string, any>;
|
||||
provider: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export interface LobehubSkillExecuteResult {
|
||||
content: string;
|
||||
error?: { code: string; message?: string };
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class LobehubSkillService {
|
||||
private db: LobeChatDatabase;
|
||||
private userId: string;
|
||||
private marketSDK?: MarketSDK;
|
||||
|
||||
constructor(db: LobeChatDatabase, userId: string) {
|
||||
this.db = db;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize MarketSDK with trusted client token
|
||||
*/
|
||||
private async getMarketSDK(): Promise<MarketSDK | null> {
|
||||
if (this.marketSDK) return this.marketSDK;
|
||||
|
||||
try {
|
||||
const user = await UserModel.findById(this.db, this.userId);
|
||||
if (!user?.email) {
|
||||
log('getMarketSDK: user email not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const trustedClientToken = generateTrustedClientToken({
|
||||
email: user.email,
|
||||
name: user.fullName || user.firstName || undefined,
|
||||
userId: this.userId,
|
||||
});
|
||||
|
||||
if (!trustedClientToken) {
|
||||
log('getMarketSDK: trusted client not configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.marketSDK = new MarketSDK({
|
||||
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
|
||||
trustedClientToken,
|
||||
});
|
||||
|
||||
return this.marketSDK;
|
||||
} catch (error) {
|
||||
log('getMarketSDK: error creating SDK: %O', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a LobeHub Skill tool
|
||||
*/
|
||||
async execute(params: LobehubSkillExecuteParams): Promise<LobehubSkillExecuteResult> {
|
||||
const { provider, toolName, args } = params;
|
||||
|
||||
log('execute: %s/%s with args: %O', provider, toolName, args);
|
||||
|
||||
const sdk = await this.getMarketSDK();
|
||||
if (!sdk) {
|
||||
return {
|
||||
content:
|
||||
'MarketSDK not available. Please ensure you are authenticated with LobeHub Market.',
|
||||
error: { code: 'MARKET_SDK_NOT_AVAILABLE' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sdk.skills.callTool(provider, {
|
||||
args,
|
||||
tool: toolName,
|
||||
});
|
||||
|
||||
log('execute: response: %O', response);
|
||||
|
||||
return {
|
||||
content: typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
|
||||
success: response.success,
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('LobehubSkillService.execute error %s/%s: %O', provider, toolName, err);
|
||||
|
||||
return {
|
||||
content: err.message,
|
||||
error: { code: 'LOBEHUB_SKILL_ERROR', message: err.message },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
485
src/server/services/market/index.ts
Normal file
485
src/server/services/market/index.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import type { LobeToolManifest } from '@lobechat/context-engine';
|
||||
import { MarketSDK } from '@lobehub/market-sdk';
|
||||
import debug from 'debug';
|
||||
import { type NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
type TrustedClientUserInfo,
|
||||
generateTrustedClientToken,
|
||||
getTrustedClientTokenForSession,
|
||||
} from '@/libs/trusted-client';
|
||||
|
||||
const log = debug('lobe-server:market-service');
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
// ============================== Helper Functions ==============================
|
||||
|
||||
/**
|
||||
* Extract access token from Authorization header
|
||||
*/
|
||||
export function extractAccessToken(req: NextRequest): string | undefined {
|
||||
const authHeader = req.headers.get('authorization');
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
return authHeader.slice(7);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface LobehubSkillExecuteParams {
|
||||
args: Record<string, any>;
|
||||
provider: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export interface LobehubSkillExecuteResult {
|
||||
content: string;
|
||||
error?: { code: string; message?: string };
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface MarketServiceOptions {
|
||||
/** Access token from OIDC flow (user token) */
|
||||
accessToken?: string;
|
||||
/** Client credentials for M2M authentication */
|
||||
clientCredentials?: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
/** Pre-generated trusted client token (alternative to userInfo) */
|
||||
trustedClientToken?: string;
|
||||
/** User info for generating trusted client token */
|
||||
userInfo?: TrustedClientUserInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Market Service
|
||||
*
|
||||
* Provides a unified interface to MarketSDK with business logic encapsulation.
|
||||
* This service wraps MarketSDK methods to avoid repetition across the codebase.
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* // From Next.js request (API Routes) - recommended
|
||||
* const marketService = await MarketService.createFromRequest(req);
|
||||
* await marketService.submitFeedback({ ... });
|
||||
*
|
||||
* // With user authentication
|
||||
* const service = new MarketService({ accessToken, userInfo });
|
||||
*
|
||||
* // With trusted client only
|
||||
* const service = new MarketService({ userInfo });
|
||||
*
|
||||
* // M2M authentication
|
||||
* const service = new MarketService({ clientCredentials: { clientId, clientSecret } });
|
||||
*
|
||||
* // Public endpoints (no auth)
|
||||
* const service = new MarketService();
|
||||
* ```
|
||||
*/
|
||||
export class MarketService {
|
||||
market: MarketSDK;
|
||||
|
||||
constructor(options: MarketServiceOptions = {}) {
|
||||
const { accessToken, userInfo, clientCredentials, trustedClientToken } = options;
|
||||
|
||||
// Use provided trustedClientToken or generate from userInfo
|
||||
const resolvedTrustedClientToken =
|
||||
trustedClientToken || (userInfo ? generateTrustedClientToken(userInfo) : undefined);
|
||||
|
||||
this.market = new MarketSDK({
|
||||
accessToken,
|
||||
baseURL: MARKET_BASE_URL,
|
||||
clientId: clientCredentials?.clientId,
|
||||
clientSecret: clientCredentials?.clientSecret,
|
||||
trustedClientToken: resolvedTrustedClientToken,
|
||||
});
|
||||
|
||||
log(
|
||||
'MarketService initialized: baseURL=%s, hasAccessToken=%s, hasTrustedToken=%s, hasClientCredentials=%s',
|
||||
MARKET_BASE_URL,
|
||||
!!accessToken,
|
||||
!!resolvedTrustedClientToken,
|
||||
!!clientCredentials,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================== Factory Methods ==============================
|
||||
|
||||
/**
|
||||
* Create MarketService from Next.js request (server-side only)
|
||||
* Extracts accessToken from Authorization header and trustedClientToken from session
|
||||
*/
|
||||
static async createFromRequest(req: NextRequest): Promise<MarketService> {
|
||||
const accessToken = extractAccessToken(req);
|
||||
const trustedClientToken = await getTrustedClientTokenForSession();
|
||||
|
||||
return new MarketService({
|
||||
accessToken,
|
||||
trustedClientToken,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================== Feedback Methods ==============================
|
||||
|
||||
/**
|
||||
* Submit feedback to LobeHub
|
||||
*/
|
||||
async submitFeedback(params: {
|
||||
clientInfo?: {
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
url?: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
email?: string;
|
||||
message: string;
|
||||
screenshotUrl?: string;
|
||||
title: string;
|
||||
}) {
|
||||
const { title, message, email, screenshotUrl, clientInfo } = params;
|
||||
|
||||
// Build message with screenshot if available
|
||||
let feedbackMessage = message;
|
||||
if (screenshotUrl) {
|
||||
feedbackMessage += `\n\n**Screenshot**: ${screenshotUrl}`;
|
||||
}
|
||||
|
||||
return this.market.feedback.submitFeedback({
|
||||
clientInfo,
|
||||
email: email || '',
|
||||
message: feedbackMessage,
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================== Auth Methods ==============================
|
||||
|
||||
/**
|
||||
* Exchange OAuth authorization code for tokens
|
||||
*/
|
||||
async exchangeAuthorizationCode(params: {
|
||||
clientId: string;
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
}) {
|
||||
return this.market.auth.exchangeOAuthToken({
|
||||
clientId: params.clientId,
|
||||
code: params.code,
|
||||
codeVerifier: params.codeVerifier,
|
||||
grantType: 'authorization_code',
|
||||
redirectUri: params.redirectUri,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth handoff information
|
||||
*/
|
||||
async getOAuthHandoff(id: string) {
|
||||
return this.market.auth.getOAuthHandoff(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info from token
|
||||
*/
|
||||
async getUserInfo(token: string) {
|
||||
return this.market.auth.getUserInfo(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user info with trusted client token (server-side)
|
||||
*/
|
||||
async getUserInfoWithTrustedClient() {
|
||||
const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
|
||||
const response = await fetch(userInfoUrl, {
|
||||
// @ts-ignore
|
||||
headers: this.market.headers,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh OAuth token
|
||||
*/
|
||||
async refreshToken(params: { clientId: string; refreshToken: string }) {
|
||||
return this.market.auth.exchangeOAuthToken({
|
||||
clientId: params.clientId,
|
||||
grantType: 'refresh_token',
|
||||
refreshToken: params.refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================== Client Methods ==============================
|
||||
|
||||
/**
|
||||
* Register client for M2M authentication
|
||||
*/
|
||||
async registerClient(params: {
|
||||
clientName: string;
|
||||
clientType: string;
|
||||
deviceId: string;
|
||||
platform?: string;
|
||||
version?: string;
|
||||
}) {
|
||||
// @ts-ignore
|
||||
return this.market.registerClient(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch M2M token with client credentials
|
||||
*/
|
||||
async fetchM2MToken() {
|
||||
return this.market.fetchM2MToken();
|
||||
}
|
||||
|
||||
// ============================== Skills Methods ==============================
|
||||
|
||||
/**
|
||||
* List available tools for a provider
|
||||
*/
|
||||
async listSkillTools(providerId: string) {
|
||||
return this.market.skills.listTools(providerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a skill tool
|
||||
*/
|
||||
async callSkillTool(provider: string, params: { args: Record<string, any>; tool: string }) {
|
||||
return this.market.skills.callTool(provider, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* List user's connected skills
|
||||
*/
|
||||
async listSkillConnections() {
|
||||
return this.market.connect.listConnections();
|
||||
}
|
||||
|
||||
// ============================== Plugin Methods ==============================
|
||||
|
||||
/**
|
||||
* Call cloud MCP endpoint
|
||||
*/
|
||||
async callCloudMcpEndpoint(
|
||||
params: {
|
||||
apiParams: Record<string, any>;
|
||||
identifier: string;
|
||||
toolName: string;
|
||||
},
|
||||
options?: {
|
||||
headers?: Record<string, string>;
|
||||
},
|
||||
) {
|
||||
return this.market.plugins.callCloudGateway(params, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin manifest
|
||||
*/
|
||||
async getPluginManifest(params: {
|
||||
identifier: string;
|
||||
install?: boolean;
|
||||
locale?: string;
|
||||
version?: string;
|
||||
}) {
|
||||
return this.market.plugins.getPluginManifest(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report plugin installation
|
||||
*/
|
||||
async reportPluginInstallation(params: any) {
|
||||
return this.market.plugins.reportInstallation(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report plugin call
|
||||
*/
|
||||
async reportPluginCall(params: any) {
|
||||
return this.market.plugins.reportCall(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create plugin event
|
||||
*/
|
||||
async createPluginEvent(params: any) {
|
||||
return this.market.plugins.createEvent(params);
|
||||
}
|
||||
|
||||
// ============================== Agent Methods ==============================
|
||||
|
||||
/**
|
||||
* Get agent detail
|
||||
*/
|
||||
async getAgentDetail(identifier: string, options?: { locale?: string; version?: string }) {
|
||||
return this.market.agents.getAgentDetail(identifier, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent list
|
||||
*/
|
||||
async getAgentList(params?: any) {
|
||||
return this.market.agents.getAgentList(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase agent install count
|
||||
*/
|
||||
async increaseAgentInstallCount(identifier: string) {
|
||||
return this.market.agents.increaseInstallCount(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent event
|
||||
*/
|
||||
async createAgentEvent(params: any) {
|
||||
return this.market.agents.createEvent(params);
|
||||
}
|
||||
|
||||
// ============================== Agent Group Methods ==============================
|
||||
|
||||
/**
|
||||
* Get agent group detail
|
||||
*/
|
||||
async getAgentGroupDetail(identifier: string, options?: { locale?: string; version?: number }) {
|
||||
return this.market.agentGroups.getAgentGroupDetail(identifier, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent group list
|
||||
*/
|
||||
async getAgentGroupList(params?: any) {
|
||||
return this.market.agentGroups.getAgentGroupList(params);
|
||||
}
|
||||
|
||||
// ============================== User Methods ==============================
|
||||
|
||||
/**
|
||||
* Get user profile by username
|
||||
*/
|
||||
async getUserProfile(username: string, options?: { locale?: string }) {
|
||||
return this.market.user.getUserInfo(username, options);
|
||||
}
|
||||
|
||||
// ============================== Skills Methods ==============================
|
||||
|
||||
/**
|
||||
* Execute a LobeHub Skill tool
|
||||
* @param params - The skill execution parameters (provider, toolName, args)
|
||||
* @returns Execution result with content and success status
|
||||
*/
|
||||
async executeLobehubSkill(params: LobehubSkillExecuteParams): Promise<LobehubSkillExecuteResult> {
|
||||
const { provider, toolName, args } = params;
|
||||
|
||||
log('executeLobehubSkill: %s/%s with args: %O', provider, toolName, args);
|
||||
|
||||
try {
|
||||
const response = await this.market.skills.callTool(provider, {
|
||||
args,
|
||||
tool: toolName,
|
||||
});
|
||||
|
||||
log('executeLobehubSkill: response: %O', response);
|
||||
|
||||
return {
|
||||
content: typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
|
||||
success: response.success,
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('MarketService.executeLobehubSkill error %s/%s: %O', provider, toolName, err);
|
||||
|
||||
return {
|
||||
content: err.message,
|
||||
error: { code: 'LOBEHUB_SKILL_ERROR', message: err.message },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch LobeHub Skills manifests from Market API
|
||||
* Gets user's connected skills and builds tool manifests for agent execution
|
||||
*
|
||||
* @returns Array of tool manifests for connected skills
|
||||
*/
|
||||
async getLobehubSkillManifests(): Promise<LobeToolManifest[]> {
|
||||
try {
|
||||
// 1. Get user's connected skills
|
||||
const { connections } = await this.market.connect.listConnections();
|
||||
if (!connections || connections.length === 0) {
|
||||
log('getLobehubSkillManifests: no connected skills found');
|
||||
return [];
|
||||
}
|
||||
|
||||
log('getLobehubSkillManifests: found %d connected skills', connections.length);
|
||||
|
||||
// 2. Fetch tools for each connection and build manifests
|
||||
const manifests: LobeToolManifest[] = [];
|
||||
|
||||
for (const connection of connections) {
|
||||
try {
|
||||
// Connection returns providerId (e.g., 'twitter', 'linear'), not numeric id
|
||||
const providerId = (connection as any).providerId;
|
||||
if (!providerId) {
|
||||
log('getLobehubSkillManifests: connection missing providerId: %O', connection);
|
||||
continue;
|
||||
}
|
||||
const providerName =
|
||||
(connection as any).providerName || (connection as any).name || providerId;
|
||||
const icon = (connection as any).icon;
|
||||
|
||||
const { tools } = await this.market.skills.listTools(providerId);
|
||||
if (!tools || tools.length === 0) continue;
|
||||
|
||||
const manifest: LobeToolManifest = {
|
||||
api: tools.map((tool: any) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || { properties: {}, type: 'object' },
|
||||
})),
|
||||
identifier: providerId,
|
||||
meta: {
|
||||
avatar: icon || '🔗',
|
||||
description: `LobeHub Skill: ${providerName}`,
|
||||
tags: ['lobehub-skill', providerId],
|
||||
title: providerName,
|
||||
},
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
manifests.push(manifest);
|
||||
log(
|
||||
'getLobehubSkillManifests: built manifest for %s with %d tools',
|
||||
providerId,
|
||||
tools.length,
|
||||
);
|
||||
} catch (error) {
|
||||
log('getLobehubSkillManifests: failed to fetch tools for connection: %O', error);
|
||||
}
|
||||
}
|
||||
|
||||
return manifests;
|
||||
} catch (error) {
|
||||
log('getLobehubSkillManifests: error fetching skills: %O', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Direct SDK Access ==============================
|
||||
|
||||
/**
|
||||
* Get MarketSDK instance for advanced usage
|
||||
* Use this when you need direct access to SDK methods not wrapped by this service
|
||||
*/
|
||||
getSDK(): MarketSDK {
|
||||
return this.market;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
|
||||
import { WebBrowsingExecutionRuntime } from '@lobechat/builtin-tool-web-browsing/executionRuntime';
|
||||
import { type LobeChatDatabase } from '@lobechat/database';
|
||||
import { type ChatToolPayload } from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import debug from 'debug';
|
||||
|
||||
import { LobehubSkillService } from '@/server/services/lobehubSkill';
|
||||
import { MarketService } from '@/server/services/market';
|
||||
import { SearchService } from '@/server/services/search';
|
||||
|
||||
import { type IToolExecutor, type ToolExecutionContext, type ToolExecutionResult } from './types';
|
||||
import { type IToolExecutor, type ToolExecutionResult } from './types';
|
||||
|
||||
const log = debug('lobe-server:builtin-tools-executor');
|
||||
|
||||
@@ -16,10 +17,12 @@ const BuiltinToolServerRuntimes: Record<string, any> = {
|
||||
};
|
||||
|
||||
export class BuiltinToolsExecutor implements IToolExecutor {
|
||||
async execute(
|
||||
payload: ChatToolPayload,
|
||||
context: ToolExecutionContext,
|
||||
): Promise<ToolExecutionResult> {
|
||||
private marketService: MarketService;
|
||||
|
||||
constructor(db: LobeChatDatabase, userId: string) {
|
||||
this.marketService = new MarketService({ userInfo: { userId } });
|
||||
}
|
||||
async execute(payload: ChatToolPayload): Promise<ToolExecutionResult> {
|
||||
const { identifier, apiName, arguments: argsStr, source } = payload;
|
||||
const args = safeParseJSON(argsStr) || {};
|
||||
|
||||
@@ -31,18 +34,9 @@ export class BuiltinToolsExecutor implements IToolExecutor {
|
||||
args,
|
||||
);
|
||||
|
||||
// Route LobeHub Skills to dedicated service
|
||||
// Route LobeHub Skills to MarketService
|
||||
if (source === 'lobehubSkill') {
|
||||
if (!context.serverDB || !context.userId) {
|
||||
return {
|
||||
content: 'Server context not available for LobeHub Skills execution.',
|
||||
error: { code: 'CONTEXT_NOT_AVAILABLE' },
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const skillService = new LobehubSkillService(context.serverDB, context.userId);
|
||||
return skillService.execute({
|
||||
return this.marketService.executeLobehubSkill({
|
||||
args,
|
||||
provider: identifier,
|
||||
toolName: apiName,
|
||||
|
||||
@@ -4,7 +4,11 @@ import debug from 'debug';
|
||||
import { type MCPService } from '../mcp';
|
||||
import { type PluginGatewayService } from '../pluginGateway';
|
||||
import { type BuiltinToolsExecutor } from './builtin';
|
||||
import { type ToolExecutionContext, type ToolExecutionResult, type ToolExecutionResultResponse } from './types';
|
||||
import {
|
||||
type ToolExecutionContext,
|
||||
type ToolExecutionResult,
|
||||
type ToolExecutionResultResponse,
|
||||
} from './types';
|
||||
|
||||
const log = debug('lobe-server:tool-execution-service');
|
||||
|
||||
@@ -43,7 +47,7 @@ export class ToolExecutionService {
|
||||
let data: ToolExecutionResult;
|
||||
switch (typeStr) {
|
||||
case 'builtin': {
|
||||
data = await this.builtinToolsExecutor.execute(payload, context);
|
||||
data = await this.builtinToolsExecutor.execute(payload);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,6 @@ import type {
|
||||
ExportAndUploadFileInput,
|
||||
ExportAndUploadFileResult,
|
||||
} from '@/server/routers/tools/market';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/slices/settings/selectors/settings';
|
||||
|
||||
/**
|
||||
* Get Market access token from user settings (stored by MarketAuthProvider)
|
||||
*/
|
||||
const getMarketAccessToken = (): string | undefined => {
|
||||
const settings = settingsSelectors.currentSettings(useUserStore.getState());
|
||||
return settings.market?.accessToken;
|
||||
};
|
||||
|
||||
class CodeInterpreterService {
|
||||
/**
|
||||
@@ -28,10 +18,7 @@ class CodeInterpreterService {
|
||||
params: Record<string, any>,
|
||||
context: { topicId: string; userId: string },
|
||||
): Promise<CallToolResult> {
|
||||
const marketAccessToken = getMarketAccessToken();
|
||||
|
||||
const input: CallCodeInterpreterToolInput = {
|
||||
marketAccessToken,
|
||||
params,
|
||||
toolName,
|
||||
topicId: context.topicId,
|
||||
|
||||
Reference in New Issue
Block a user