@lobehub/lobehub 2.0.0-next.310 → 2.0.0-next.311

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/docs/development/basic/chat-api.mdx +0 -1
  5. package/docs/development/basic/chat-api.zh-CN.mdx +0 -1
  6. package/package.json +1 -1
  7. package/packages/model-runtime/src/core/BaseAI.ts +0 -2
  8. package/packages/model-runtime/src/core/ModelRuntime.test.ts +0 -37
  9. package/packages/model-runtime/src/core/ModelRuntime.ts +0 -5
  10. package/packages/model-runtime/src/core/RouterRuntime/baseRuntimeMap.ts +4 -0
  11. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.test.ts +325 -200
  12. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +205 -64
  13. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +0 -14
  14. package/packages/model-runtime/src/providers/aihubmix/index.test.ts +14 -20
  15. package/packages/model-runtime/src/types/index.ts +0 -1
  16. package/packages/model-runtime/src/utils/createError.test.ts +0 -20
  17. package/packages/model-runtime/src/utils/createError.ts +0 -1
  18. package/src/app/(backend)/market/agent/[[...segments]]/route.ts +3 -33
  19. package/src/app/(backend)/market/oidc/[[...segments]]/route.ts +5 -6
  20. package/src/app/(backend)/market/social/[[...segments]]/route.ts +5 -52
  21. package/src/app/(backend)/market/user/[username]/route.ts +3 -9
  22. package/src/app/(backend)/market/user/me/route.ts +3 -34
  23. package/src/features/ChatMiniMap/useMinimapData.ts +1 -1
  24. package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +20 -2
  25. package/src/features/Conversation/store/slices/virtuaList/action.ts +9 -0
  26. package/src/libs/trpc/lambda/middleware/marketSDK.ts +14 -23
  27. package/src/libs/trusted-client/index.ts +1 -1
  28. package/src/server/routers/lambda/market/index.ts +5 -0
  29. package/src/server/routers/lambda/market/oidc.ts +41 -61
  30. package/src/server/routers/tools/market.ts +12 -44
  31. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +7 -0
  32. package/src/server/services/agentRuntime/AgentRuntimeService.ts +1 -1
  33. package/src/server/services/aiAgent/__tests__/execAgent.threadId.test.ts +7 -0
  34. package/src/server/services/aiAgent/__tests__/execGroupSubAgentTask.test.ts +7 -0
  35. package/src/server/services/aiAgent/index.ts +9 -96
  36. package/src/server/services/discover/index.ts +11 -16
  37. package/src/server/services/market/index.ts +485 -0
  38. package/src/server/services/toolExecution/builtin.ts +11 -17
  39. package/src/server/services/toolExecution/index.ts +6 -2
  40. package/src/services/codeInterpreter.ts +0 -13
  41. package/packages/model-runtime/src/types/textToImage.ts +0 -36
  42. package/src/server/services/lobehubSkill/index.ts +0 -109
@@ -1,7 +1,6 @@
1
- import { MarketSDK } from '@lobehub/market-sdk';
2
1
  import { type NextRequest, NextResponse } from 'next/server';
3
2
 
4
- import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
3
+ import { MarketService } from '@/server/services/market';
5
4
 
6
5
  type RouteContext = {
7
6
  params: Promise<{
@@ -9,19 +8,6 @@ type RouteContext = {
9
8
  }>;
10
9
  };
11
10
 
12
- const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
13
-
14
- /**
15
- * Helper to get authorization header
16
- */
17
- const getAccessToken = (req: NextRequest): string | undefined => {
18
- const authHeader = req.headers.get('authorization');
19
- if (authHeader?.startsWith('Bearer ')) {
20
- return authHeader.slice(7);
21
- }
22
- return undefined;
23
- };
24
-
25
11
  /**
26
12
  * POST /market/social/follow
27
13
  * POST /market/social/unfollow
@@ -34,22 +20,9 @@ const getAccessToken = (req: NextRequest): string | undefined => {
34
20
  export const POST = async (req: NextRequest, context: RouteContext) => {
35
21
  const { segments = [] } = await context.params;
36
22
  const action = segments[0];
37
- const accessToken = getAccessToken(req);
38
- const trustedClientToken = await getTrustedClientTokenForSession();
39
-
40
- const market = new MarketSDK({
41
- accessToken,
42
- baseURL: MARKET_BASE_URL,
43
- trustedClientToken,
44
- });
45
23
 
46
- // Only require accessToken if trusted client token is not available
47
- if (!accessToken && !trustedClientToken) {
48
- return NextResponse.json(
49
- { error: 'unauthorized', message: 'Access token required' },
50
- { status: 401 },
51
- );
52
- }
24
+ const marketService = await MarketService.createFromRequest(req);
25
+ const market = marketService.market;
53
26
 
54
27
  try {
55
28
  const body = await req.json();
@@ -135,14 +108,9 @@ export const POST = async (req: NextRequest, context: RouteContext) => {
135
108
  export const GET = async (req: NextRequest, context: RouteContext) => {
136
109
  const { segments = [] } = await context.params;
137
110
  const action = segments[0];
138
- const accessToken = getAccessToken(req);
139
- const trustedClientToken = await getTrustedClientTokenForSession();
140
111
 
141
- const market = new MarketSDK({
142
- accessToken,
143
- baseURL: MARKET_BASE_URL,
144
- trustedClientToken,
145
- });
112
+ const marketService = await MarketService.createFromRequest(req);
113
+ const market = marketService.market;
146
114
 
147
115
  const url = new URL(req.url);
148
116
  const limit = url.searchParams.get('pageSize') || url.searchParams.get('limit');
@@ -158,9 +126,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
158
126
  // Follow queries
159
127
  case 'follow-status': {
160
128
  const targetUserId = Number(segments[1]);
161
- if (!accessToken && !trustedClientToken) {
162
- return NextResponse.json({ isFollowing: false, isMutual: false });
163
- }
164
129
  const result = await market.follows.checkFollowStatus(targetUserId);
165
130
  return NextResponse.json(result);
166
131
  }
@@ -193,9 +158,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
193
158
  case 'favorite-status': {
194
159
  const targetType = segments[1] as 'agent' | 'plugin';
195
160
  const targetIdOrIdentifier = segments[2];
196
- if (!accessToken && !trustedClientToken) {
197
- return NextResponse.json({ isFavorited: false });
198
- }
199
161
  // SDK accepts both number (targetId) and string (identifier)
200
162
  const isNumeric = /^\d+$/.test(targetIdOrIdentifier);
201
163
  const targetValue = isNumeric ? Number(targetIdOrIdentifier) : targetIdOrIdentifier;
@@ -204,12 +166,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
204
166
  }
205
167
 
206
168
  case 'favorites': {
207
- if (!accessToken) {
208
- return NextResponse.json(
209
- { error: 'unauthorized', message: 'Access token required' },
210
- { status: 401 },
211
- );
212
- }
213
169
  const result = await market.favorites.getMyFavorites(paginationParams);
214
170
  return NextResponse.json(result);
215
171
  }
@@ -236,9 +192,6 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
236
192
  case 'like-status': {
237
193
  const targetType = segments[1] as 'agent' | 'plugin';
238
194
  const targetIdOrIdentifier = segments[2];
239
- if (!accessToken && !trustedClientToken) {
240
- return NextResponse.json({ isLiked: false });
241
- }
242
195
  const isNumeric = /^\d+$/.test(targetIdOrIdentifier);
243
196
  const targetValue = isNumeric ? Number(targetIdOrIdentifier) : targetIdOrIdentifier;
244
197
  const result = await market.likes.checkLike(targetType, targetValue as number);
@@ -1,7 +1,6 @@
1
- import { MarketSDK } from '@lobehub/market-sdk';
2
1
  import { type NextRequest, NextResponse } from 'next/server';
3
2
 
4
- import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
3
+ import { MarketService } from '@/server/services/market';
5
4
 
6
5
  type RouteContext = {
7
6
  params: Promise<{
@@ -9,8 +8,6 @@ type RouteContext = {
9
8
  }>;
10
9
  };
11
10
 
12
- const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
13
-
14
11
  /**
15
12
  * GET /market/user/[username]
16
13
  *
@@ -20,12 +17,9 @@ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://mark
20
17
  export const GET = async (req: NextRequest, context: RouteContext) => {
21
18
  const { username } = await context.params;
22
19
  const decodedUsername = decodeURIComponent(username);
23
- const trustedClientToken = await getTrustedClientTokenForSession();
24
20
 
25
- const market = new MarketSDK({
26
- baseURL: MARKET_BASE_URL,
27
- trustedClientToken,
28
- });
21
+ const marketService = await MarketService.createFromRequest(req);
22
+ const market = marketService.market;
29
23
 
30
24
  try {
31
25
  const response = await market.user.getUserInfo(decodedUsername);
@@ -1,19 +1,6 @@
1
- import { MarketSDK } from '@lobehub/market-sdk';
2
1
  import { type NextRequest, NextResponse } from 'next/server';
3
2
 
4
- import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
5
-
6
- const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
7
-
8
- const extractAccessToken = (req: NextRequest) => {
9
- const authorization = req.headers.get('authorization');
10
- if (!authorization) return undefined;
11
-
12
- const [scheme, token] = authorization.split(' ');
13
- if (scheme?.toLowerCase() !== 'bearer' || !token) return undefined;
14
-
15
- return token;
16
- };
3
+ import { MarketService } from '@/server/services/market';
17
4
 
18
5
  /**
19
6
  * PUT /market/user/me
@@ -28,26 +15,8 @@ const extractAccessToken = (req: NextRequest) => {
28
15
  * - meta?: { description?: string; socialLinks?: { github?: string; twitter?: string; website?: string } }
29
16
  */
30
17
  export const PUT = async (req: NextRequest) => {
31
- const accessToken = extractAccessToken(req);
32
- const trustedClientToken = await getTrustedClientTokenForSession();
33
-
34
- const market = new MarketSDK({
35
- accessToken,
36
- baseURL: MARKET_BASE_URL,
37
- trustedClientToken,
38
- });
39
-
40
- // Only require accessToken if trusted client token is not available
41
- if (!accessToken && !trustedClientToken) {
42
- return NextResponse.json(
43
- {
44
- error: 'unauthorized',
45
- message: 'Authentication required to update user profile',
46
- status: 'error',
47
- },
48
- { status: 401 },
49
- );
50
- }
18
+ const marketService = await MarketService.createFromRequest(req);
19
+ const market = marketService.market;
51
20
 
52
21
  try {
53
22
  const payload = await req.json();
@@ -91,7 +91,7 @@ export const useMinimapData = () => {
91
91
  }
92
92
  } else {
93
93
  // No active index, go to first/last
94
- targetPosition = direction === 'prev' ? indicators.length - 1 : 0;
94
+ targetPosition = direction === 'prev' ? 0 : indicators.length - 1;
95
95
  }
96
96
 
97
97
  const targetIndicator = indicators[targetPosition];
@@ -30,7 +30,9 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
30
30
  const setScrollState = useConversationStore((s) => s.setScrollState);
31
31
  const resetVisibleItems = useConversationStore((s) => s.resetVisibleItems);
32
32
  const scrollToBottom = useConversationStore((s) => s.scrollToBottom);
33
+ const setActiveIndex = useConversationStore((s) => s.setActiveIndex);
33
34
  const atBottom = useConversationStore(virtuaListSelectors.atBottom);
35
+ const activeIndex = useConversationStore(virtuaListSelectors.activeIndex);
34
36
 
35
37
  // Check if at bottom based on scroll position
36
38
  const checkAtBottom = useCallback(() => {
@@ -46,6 +48,16 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
46
48
 
47
49
  // Handle scroll events
48
50
  const handleScroll = useCallback(() => {
51
+ const refForActive = virtuaRef.current;
52
+ const activeFromFindRaw =
53
+ refForActive && typeof refForActive.findItemIndex === 'function'
54
+ ? refForActive.findItemIndex(refForActive.scrollOffset + refForActive.viewportSize * 0.25)
55
+ : null;
56
+ const activeFromFind =
57
+ typeof activeFromFindRaw === 'number' && activeFromFindRaw >= 0 ? activeFromFindRaw : null;
58
+
59
+ if (activeFromFind !== activeIndex) setActiveIndex(activeFromFind);
60
+
49
61
  setScrollState({ isScrolling: true });
50
62
 
51
63
  // Check if at bottom
@@ -61,7 +73,7 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
61
73
  scrollEndTimerRef.current = setTimeout(() => {
62
74
  setScrollState({ isScrolling: false });
63
75
  }, 150);
64
- }, [checkAtBottom, setScrollState]);
76
+ }, [activeIndex, checkAtBottom, setActiveIndex, setScrollState]);
65
77
 
66
78
  const handleScrollEnd = useCallback(() => {
67
79
  setScrollState({ isScrolling: false });
@@ -77,12 +89,18 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
77
89
  getViewportSize: () => ref.viewportSize,
78
90
  scrollToIndex: (index, options) => ref.scrollToIndex(index, options),
79
91
  });
92
+
93
+ // Seed active index once on mount (avoid requiring user scroll)
94
+ const initialActiveRaw = ref.findItemIndex(ref.scrollOffset + ref.viewportSize * 0.25);
95
+ const initialActive =
96
+ typeof initialActiveRaw === 'number' && initialActiveRaw >= 0 ? initialActiveRaw : null;
97
+ setActiveIndex(initialActive);
80
98
  }
81
99
 
82
100
  return () => {
83
101
  registerVirtuaScrollMethods(null);
84
102
  };
85
- }, [registerVirtuaScrollMethods]);
103
+ }, [registerVirtuaScrollMethods, setActiveIndex]);
86
104
 
87
105
  // Cleanup on unmount
88
106
  useEffect(() => {
@@ -32,6 +32,11 @@ export interface VirtuaListAction {
32
32
  options?: { align?: 'start' | 'center' | 'end'; smooth?: boolean },
33
33
  ) => void;
34
34
 
35
+ /**
36
+ * Set active index directly (derived from scroll position)
37
+ */
38
+ setActiveIndex: (index: number | null) => void;
39
+
35
40
  /**
36
41
  * Update scroll state (atBottom, isScrolling)
37
42
  */
@@ -107,6 +112,10 @@ export const virtuaListSlice: StateCreator<State & VirtuaListAction, [], [], Vir
107
112
  virtuaScrollMethods?.scrollToIndex(index, options);
108
113
  },
109
114
 
115
+ setActiveIndex: (index) => {
116
+ set({ activeIndex: index });
117
+ },
118
+
110
119
  setScrollState: (state) => {
111
120
  set(state);
112
121
  },
@@ -1,6 +1,5 @@
1
- import { MarketSDK } from '@lobehub/market-sdk';
2
-
3
- import { generateTrustedClientToken, type TrustedClientUserInfo } from '@/libs/trusted-client';
1
+ import { type TrustedClientUserInfo } from '@/libs/trusted-client';
2
+ import { MarketService } from '@/server/services/market';
4
3
 
5
4
  import { trpc } from '../init';
6
5
 
@@ -10,53 +9,45 @@ interface ContextWithMarketUserInfo {
10
9
  }
11
10
 
12
11
  /**
13
- * Middleware that initializes MarketSDK with proper authentication.
12
+ * Middleware that initializes MarketService with proper authentication.
14
13
  * This requires marketUserInfo middleware to be applied first.
15
14
  *
16
15
  * Provides:
17
- * - ctx.marketSDK: Initialized MarketSDK instance with trustedClientToken and optional accessToken
18
- * - ctx.trustedClientToken: The generated trusted client token (if available)
16
+ * - ctx.marketSDK: MarketSDK instance for backward compatibility
17
+ * - ctx.marketService: MarketService instance (recommended)
19
18
  */
20
19
  export const marketSDK = trpc.middleware(async (opts) => {
21
20
  const ctx = opts.ctx as ContextWithMarketUserInfo;
22
21
 
23
- // Generate trusted client token if user info is available
24
- const trustedClientToken = ctx.marketUserInfo
25
- ? generateTrustedClientToken(ctx.marketUserInfo)
26
- : undefined;
27
-
28
- // Initialize MarketSDK with both authentication methods
29
- const market = new MarketSDK({
22
+ // Initialize MarketService with authentication
23
+ const marketService = new MarketService({
30
24
  accessToken: ctx.marketAccessToken,
31
- baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
32
- trustedClientToken,
25
+ userInfo: ctx.marketUserInfo,
33
26
  });
34
27
 
35
28
  return opts.next({
36
29
  ctx: {
37
- marketSDK: market,
38
- trustedClientToken,
30
+ marketSDK: marketService.market, // Backward compatibility
31
+ marketService, // New recommended way
39
32
  },
40
33
  });
41
34
  });
42
35
 
43
36
  /**
44
37
  * Middleware that requires authentication for Market API access.
45
- * This middleware ensures that either accessToken or trustedClientToken is available.
38
+ * This middleware ensures that either accessToken or marketUserInfo is available.
46
39
  * It should be used after marketUserInfo and marketSDK middlewares.
47
40
  *
48
41
  * Throws UNAUTHORIZED error if neither authentication method is available.
49
42
  */
50
43
  export const requireMarketAuth = trpc.middleware(async (opts) => {
51
- const ctx = opts.ctx as ContextWithMarketUserInfo & {
52
- trustedClientToken?: string;
53
- };
44
+ const ctx = opts.ctx as ContextWithMarketUserInfo;
54
45
 
55
46
  // Check if any authentication is available
56
47
  const hasAccessToken = !!ctx.marketAccessToken;
57
- const hasTrustedToken = !!ctx.trustedClientToken;
48
+ const hasUserInfo = !!ctx.marketUserInfo;
58
49
 
59
- if (!hasAccessToken && !hasTrustedToken) {
50
+ if (!hasAccessToken && !hasUserInfo) {
60
51
  const { TRPCError } = await import('@trpc/server');
61
52
  throw new TRPCError({
62
53
  code: 'UNAUTHORIZED',
@@ -3,7 +3,7 @@ import { buildTrustedClientPayload, createTrustedClientToken } from '@lobehub/ma
3
3
  import { appEnv } from '@/envs/app';
4
4
 
5
5
  export interface TrustedClientUserInfo {
6
- email: string;
6
+ email?: string;
7
7
  name?: string;
8
8
  userId: string;
9
9
  }
@@ -7,6 +7,7 @@ import { z } from 'zod';
7
7
  import { publicProcedure, router } from '@/libs/trpc/lambda';
8
8
  import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
9
9
  import { DiscoverService } from '@/server/services/discover';
10
+ import { MarketService } from '@/server/services/market';
10
11
  import {
11
12
  AssistantSorts,
12
13
  McpConnectionType,
@@ -37,6 +38,10 @@ const marketProcedure = publicProcedure
37
38
  accessToken: ctx.marketAccessToken,
38
39
  userInfo: ctx.marketUserInfo,
39
40
  }),
41
+ marketService: new MarketService({
42
+ accessToken: ctx.marketAccessToken,
43
+ userInfo: ctx.marketUserInfo,
44
+ }),
40
45
  },
41
46
  });
42
47
  });
@@ -1,18 +1,29 @@
1
- import { MarketSDK } from '@lobehub/market-sdk';
2
1
  import { TRPCError } from '@trpc/server';
3
2
  import debug from 'debug';
4
3
  import { z } from 'zod';
5
4
 
6
5
  import { publicProcedure, router } from '@/libs/trpc/lambda';
7
6
  import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
8
- import { generateTrustedClientToken } from '@/libs/trusted-client';
7
+ import { MarketService } from '@/server/services/market';
9
8
 
10
9
  const log = debug('lambda-router:market:oidc');
11
10
 
12
- const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
13
-
14
11
  // OIDC procedures are public (used during authentication flow)
15
- const oidcProcedure = publicProcedure.use(serverDatabase).use(marketUserInfo);
12
+ const oidcProcedure = publicProcedure
13
+ .use(serverDatabase)
14
+ .use(marketUserInfo)
15
+ .use(async ({ ctx, next }) => {
16
+ // Initialize MarketService (may be without auth for public endpoints)
17
+ const marketService = new MarketService({
18
+ userInfo: ctx.marketUserInfo,
19
+ });
20
+
21
+ return next({
22
+ ctx: {
23
+ marketService,
24
+ },
25
+ });
26
+ });
16
27
 
17
28
  export const oidcRouter = router({
18
29
  /**
@@ -28,19 +39,11 @@ export const oidcRouter = router({
28
39
  redirectUri: z.string(),
29
40
  }),
30
41
  )
31
- .mutation(async ({ input }) => {
42
+ .mutation(async ({ input, ctx }) => {
32
43
  log('exchangeAuthorizationCode input: %O', { ...input, code: '[REDACTED]' });
33
44
 
34
- const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
35
-
36
45
  try {
37
- const response = await market.auth.exchangeOAuthToken({
38
- clientId: input.clientId,
39
- code: input.code,
40
- codeVerifier: input.codeVerifier,
41
- grantType: 'authorization_code',
42
- redirectUri: input.redirectUri,
43
- });
46
+ const response = await ctx.marketService.exchangeAuthorizationCode(input);
44
47
  return response;
45
48
  } catch (error) {
46
49
  log('Error exchanging authorization code: %O', error);
@@ -56,23 +59,23 @@ export const oidcRouter = router({
56
59
  * Get OAuth handoff information
57
60
  * GET /market/oidc/handoff?id=xxx
58
61
  */
59
- getOAuthHandoff: oidcProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
60
- log('getOAuthHandoff input: %O', input);
61
-
62
- const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
63
-
64
- try {
65
- const handoff = await market.auth.getOAuthHandoff(input.id);
66
- return handoff;
67
- } catch (error) {
68
- log('Error getting OAuth handoff: %O', error);
69
- throw new TRPCError({
70
- cause: error,
71
- code: 'INTERNAL_SERVER_ERROR',
72
- message: error instanceof Error ? error.message : 'Failed to get OAuth handoff',
73
- });
74
- }
75
- }),
62
+ getOAuthHandoff: oidcProcedure
63
+ .input(z.object({ id: z.string() }))
64
+ .query(async ({ input, ctx }) => {
65
+ log('getOAuthHandoff input: %O', input);
66
+
67
+ try {
68
+ const handoff = await ctx.marketService.getOAuthHandoff(input.id);
69
+ return handoff;
70
+ } catch (error) {
71
+ log('Error getting OAuth handoff: %O', error);
72
+ throw new TRPCError({
73
+ cause: error,
74
+ code: 'INTERNAL_SERVER_ERROR',
75
+ message: error instanceof Error ? error.message : 'Failed to get OAuth handoff',
76
+ });
77
+ }
78
+ }),
76
79
 
77
80
  /**
78
81
  * Get user info from token or trusted client
@@ -83,37 +86,17 @@ export const oidcRouter = router({
83
86
  .mutation(async ({ input, ctx }) => {
84
87
  log('getUserInfo input: token=%s', input.token ? '[REDACTED]' : 'undefined');
85
88
 
86
- const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
87
-
88
89
  try {
89
90
  // If token is provided, use it
90
91
  if (input.token) {
91
- const response = await market.auth.getUserInfo(input.token);
92
+ const response = await ctx.marketService.getUserInfo(input.token);
92
93
  return response;
93
94
  }
94
95
 
95
96
  // Otherwise, try to use trustedClientToken
96
97
  if (ctx.marketUserInfo) {
97
- const trustedClientToken = generateTrustedClientToken(ctx.marketUserInfo);
98
-
99
- if (trustedClientToken) {
100
- const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
101
- const response = await fetch(userInfoUrl, {
102
- headers: {
103
- 'Content-Type': 'application/json',
104
- 'x-lobe-trust-token': trustedClientToken,
105
- },
106
- method: 'GET',
107
- });
108
-
109
- if (!response.ok) {
110
- throw new Error(
111
- `Failed to fetch user info: ${response.status} ${response.statusText}`,
112
- );
113
- }
114
-
115
- return await response.json();
116
- }
98
+ const response = await ctx.marketService.getUserInfoWithTrustedClient();
99
+ return response;
117
100
  }
118
101
 
119
102
  throw new TRPCError({
@@ -143,15 +126,12 @@ export const oidcRouter = router({
143
126
  refreshToken: z.string(),
144
127
  }),
145
128
  )
146
- .mutation(async ({ input }) => {
129
+ .mutation(async ({ input, ctx }) => {
147
130
  log('refreshToken input: %O', { ...input, refreshToken: '[REDACTED]' });
148
131
 
149
- const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
150
-
151
132
  try {
152
- const response = await market.auth.exchangeOAuthToken({
153
- clientId: input.clientId,
154
- grantType: 'refresh_token',
133
+ const response = await ctx.marketService.refreshToken({
134
+ clientId: input.clientId || '',
155
135
  refreshToken: input.refreshToken,
156
136
  });
157
137
  return response;
@@ -1,4 +1,4 @@
1
- import { type CodeInterpreterToolName, MarketSDK } from '@lobehub/market-sdk';
1
+ import { type CodeInterpreterToolName } from '@lobehub/market-sdk';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import debug from 'debug';
4
4
  import { sha256 } from 'js-sha256';
@@ -8,10 +8,11 @@ import { type ToolCallContent } from '@/libs/mcp';
8
8
  import { authedProcedure, router } from '@/libs/trpc/lambda';
9
9
  import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
10
10
  import { marketSDK, requireMarketAuth } from '@/libs/trpc/lambda/middleware/marketSDK';
11
- import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
11
+ import { isTrustedClientEnabled } from '@/libs/trusted-client';
12
12
  import { FileS3 } from '@/server/modules/S3';
13
13
  import { DiscoverService } from '@/server/services/discover';
14
14
  import { FileService } from '@/server/services/file';
15
+ import { MarketService } from '@/server/services/market';
15
16
  import {
16
17
  contentBlocksToString,
17
18
  processContentBlocks,
@@ -37,6 +38,10 @@ const marketToolProcedure = authedProcedure
37
38
  userInfo: ctx.marketUserInfo,
38
39
  }),
39
40
  fileService: new FileService(ctx.serverDB, ctx.userId),
41
+ marketService: new MarketService({
42
+ accessToken: ctx.marketAccessToken,
43
+ userInfo: ctx.marketUserInfo,
44
+ }),
40
45
  userModel,
41
46
  },
42
47
  });
@@ -79,7 +84,6 @@ const metaSchema = z
79
84
 
80
85
  // Schema for code interpreter tool call request
81
86
  const callCodeInterpreterToolSchema = z.object({
82
- marketAccessToken: z.string().optional(),
83
87
  params: z.record(z.any()),
84
88
  toolName: z.string(),
85
89
  topicId: z.string(),
@@ -224,27 +228,17 @@ export const marketRouter = router({
224
228
  callCodeInterpreterTool: marketToolProcedure
225
229
  .input(callCodeInterpreterToolSchema)
226
230
  .mutation(async ({ input, ctx }) => {
227
- const { toolName, params, userId, topicId, marketAccessToken } = input;
231
+ const { toolName, params, userId, topicId } = input;
228
232
 
229
233
  log('Calling cloud code interpreter tool: %s with params: %O', toolName, {
230
234
  params,
231
235
  topicId,
232
236
  userId,
233
237
  });
234
- log('Market access token available: %s', marketAccessToken ? 'yes' : 'no');
235
-
236
- // Generate trusted client token if user info is available
237
- const trustedClientToken = ctx.marketUserInfo
238
- ? generateTrustedClientToken(ctx.marketUserInfo)
239
- : undefined;
240
238
 
241
239
  try {
242
- // Initialize MarketSDK with market access token and trusted client token
243
- const market = new MarketSDK({
244
- accessToken: marketAccessToken,
245
- baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
246
- trustedClientToken,
247
- });
240
+ // Use marketService from ctx
241
+ const market = ctx.marketService.market;
248
242
 
249
243
  // Call market-sdk's runBuildInTool
250
244
  const response = await market.plugins.runBuildInTool(
@@ -555,34 +549,8 @@ export const marketRouter = router({
555
549
  const uploadUrl = await s3.createPreSignedUrl(key);
556
550
  log('Generated upload URL for key: %s', key);
557
551
 
558
- // Step 2: Generate trusted client token if user info is available
559
- const trustedClientToken = ctx.marketUserInfo
560
- ? generateTrustedClientToken(ctx.marketUserInfo)
561
- : undefined;
562
-
563
- // Only require user accessToken if trusted client is not available
564
- let userAccessToken: string | undefined;
565
- if (!trustedClientToken) {
566
- const userState = await ctx.userModel.getUserState(async () => ({}));
567
- userAccessToken = userState.settings?.market?.accessToken;
568
-
569
- if (!userAccessToken) {
570
- return {
571
- error: { message: 'User access token not found. Please sign in to Market first.' },
572
- filename,
573
- success: false,
574
- } as ExportAndUploadFileResult;
575
- }
576
- } else {
577
- log('Using trusted client authentication for exportAndUploadFile');
578
- }
579
-
580
- // Initialize MarketSDK
581
- const market = new MarketSDK({
582
- accessToken: userAccessToken,
583
- baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
584
- trustedClientToken,
585
- });
552
+ // Step 2: Use MarketService from ctx
553
+ const market = ctx.marketService.market;
586
554
 
587
555
  // Step 3: Call sandbox's exportFile tool with the upload URL
588
556
  const response = await market.plugins.runBuildInTool(
@@ -3,6 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import { AgentRuntimeService } from './AgentRuntimeService';
4
4
  import type { AgentExecutionParams, OperationCreationParams, StartExecutionParams } from './types';
5
5
 
6
+ // Mock trusted client to avoid server-side env access
7
+ vi.mock('@/libs/trusted-client', () => ({
8
+ generateTrustedClientToken: vi.fn().mockReturnValue(undefined),
9
+ getTrustedClientTokenForSession: vi.fn().mockResolvedValue(undefined),
10
+ isTrustedClientEnabled: vi.fn().mockReturnValue(false),
11
+ }));
12
+
6
13
  // Mock database and models
7
14
  vi.mock('@/database/models/message', () => ({
8
15
  MessageModel: vi.fn().mockImplementation(() => ({