@lobehub/lobehub 2.0.0-next.225 → 2.0.0-next.227

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 (31) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/self-hosting/server-database/docker-compose.mdx +11 -0
  4. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +11 -0
  5. package/locales/en-US/setting.json +4 -0
  6. package/locales/zh-CN/setting.json +4 -0
  7. package/package.json +2 -2
  8. package/packages/model-bank/src/aiModels/lobehub.ts +28 -0
  9. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/ForkConfirmModal.tsx +67 -0
  10. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/PublishButton.tsx +92 -105
  11. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/index.tsx +13 -48
  12. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/useMarketPublish.ts +69 -93
  13. package/src/business/client/hooks/useBusinessErrorAlertConfig.ts +9 -0
  14. package/src/business/client/hooks/useBusinessErrorContent.ts +13 -0
  15. package/src/business/server/lambda-routers/file.ts +12 -0
  16. package/src/features/Conversation/Error/index.tsx +13 -3
  17. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +11 -28
  18. package/src/layout/AuthProvider/MarketAuth/ProfileSetupModal.tsx +30 -25
  19. package/src/layout/AuthProvider/MarketAuth/useMarketUserProfile.ts +4 -9
  20. package/src/libs/trpc/lambda/middleware/index.ts +1 -0
  21. package/src/libs/trpc/lambda/middleware/marketSDK.ts +68 -0
  22. package/src/locales/default/setting.ts +5 -0
  23. package/src/server/routers/lambda/__tests__/file.test.ts +7 -7
  24. package/src/server/routers/lambda/file.ts +12 -3
  25. package/src/server/routers/lambda/market/agent.ts +504 -0
  26. package/src/server/routers/lambda/market/index.ts +17 -0
  27. package/src/server/routers/lambda/market/oidc.ts +169 -0
  28. package/src/server/routers/lambda/market/social.ts +532 -0
  29. package/src/server/routers/lambda/market/user.ts +123 -0
  30. package/src/services/marketApi.ts +24 -84
  31. package/src/services/social.ts +70 -166
@@ -3,32 +3,45 @@ import { useCallback, useRef, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
 
5
5
  import { message } from '@/components/AntdStaticMethods';
6
- import { checkOwnership } from '@/hooks/useAgentOwnershipCheck';
7
6
  import { useTokenCount } from '@/hooks/useTokenCount';
8
7
  import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
9
- import { marketApiService } from '@/services/marketApi';
8
+ import { lambdaClient } from '@/libs/trpc/client';
10
9
  import { useAgentStore } from '@/store/agent';
11
10
  import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
12
11
  import { useGlobalStore } from '@/store/global';
13
12
  import { globalGeneralSelectors } from '@/store/global/selectors';
14
- import { useServerConfigStore } from '@/store/serverConfig';
15
- import { serverConfigSelectors } from '@/store/serverConfig/selectors';
16
13
 
17
14
  import type { MarketPublishAction } from './types';
18
- import { generateDefaultChangelog, generateMarketIdentifier } from './utils';
15
+ import { generateDefaultChangelog } from './utils';
16
+
17
+ export interface OriginalAgentInfo {
18
+ author?: {
19
+ avatar?: string;
20
+ name?: string;
21
+ userName?: string;
22
+ };
23
+ avatar?: string;
24
+ identifier: string;
25
+ name: string;
26
+ }
19
27
 
20
28
  interface UseMarketPublishOptions {
21
29
  action: MarketPublishAction;
22
30
  onSuccess?: (identifier: string) => void;
23
31
  }
24
32
 
33
+ export interface CheckOwnershipResult {
34
+ needsForkConfirm: boolean;
35
+ originalAgent: OriginalAgentInfo | null;
36
+ }
37
+
25
38
  export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions) => {
26
39
  const { t } = useTranslation('setting');
27
40
  const [isPublishing, setIsPublishing] = useState(false);
41
+ const [isCheckingOwnership, setIsCheckingOwnership] = useState(false);
28
42
  // 使用 ref 来同步跟踪发布状态,避免闭包导致的竞态问题
29
43
  const isPublishingRef = useRef(false);
30
- const { isAuthenticated, session, getCurrentUserInfo } = useMarketAuth();
31
- const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
44
+ const { isAuthenticated } = useMarketAuth();
32
45
 
33
46
  // Agent data from store
34
47
  const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual);
@@ -46,15 +59,49 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
46
59
 
47
60
  const isSubmit = action === 'submit';
48
61
 
62
+ /**
63
+ * Check ownership before publishing
64
+ * Returns whether fork confirmation is needed and original agent info
65
+ */
66
+ const checkOwnership = useCallback(async (): Promise<CheckOwnershipResult> => {
67
+ const identifier = meta?.marketIdentifier;
68
+
69
+ // No identifier means new agent, no need to check
70
+ if (!identifier) {
71
+ return { needsForkConfirm: false, originalAgent: null };
72
+ }
73
+
74
+ try {
75
+ setIsCheckingOwnership(true);
76
+ const result = await lambdaClient.market.agent.checkOwnership.query({ identifier });
77
+
78
+ // If agent doesn't exist or user is owner, no confirmation needed
79
+ if (!result.exists || result.isOwner) {
80
+ return { needsForkConfirm: false, originalAgent: null };
81
+ }
82
+
83
+ // User is not owner, need fork confirmation
84
+ return {
85
+ needsForkConfirm: true,
86
+ originalAgent: result.originalAgent as OriginalAgentInfo,
87
+ };
88
+ } catch (error) {
89
+ console.error('[useMarketPublish] Failed to check ownership:', error);
90
+ // On error, proceed without confirmation
91
+ return { needsForkConfirm: false, originalAgent: null };
92
+ } finally {
93
+ setIsCheckingOwnership(false);
94
+ }
95
+ }, [meta?.marketIdentifier]);
96
+
49
97
  const publish = useCallback(async () => {
50
98
  // 防止重复发布:使用 ref 同步检查,避免闭包导致的竞态问题
51
99
  if (isPublishingRef.current) {
52
100
  return { success: false };
53
101
  }
54
102
 
55
- // 如果启用了 trustedClient,只需要检查 isAuthenticated
56
- // 因为后端会自动注入 trustedClientToken
57
- if (!isAuthenticated || (!enableMarketTrustedClient && !session?.accessToken)) {
103
+ // 检查认证状态 - tRPC 会自动处理 trustedClient
104
+ if (!isAuthenticated) {
58
105
  return { success: false };
59
106
  }
60
107
 
@@ -63,8 +110,6 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
63
110
  ? t('marketPublish.modal.loading.submit')
64
111
  : t('marketPublish.modal.loading.upload');
65
112
 
66
- let identifier = meta?.marketIdentifier;
67
-
68
113
  const changelog = generateDefaultChangelog();
69
114
 
70
115
  try {
@@ -72,61 +117,9 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
72
117
  isPublishingRef.current = true;
73
118
  setIsPublishing(true);
74
119
  message.loading({ content: loadingMessage, key: messageKey });
75
- // 只有在非 trustedClient 模式下才需要设置 accessToken
76
- if (session?.accessToken) {
77
- marketApiService.setAccessToken(session.accessToken);
78
- }
79
-
80
- // 判断是否需要创建新 agent
81
- let needsCreateAgent = false;
82
-
83
- if (!identifier) {
84
- // 没有 marketIdentifier,需要创建新 agent
85
- needsCreateAgent = true;
86
- } else if (isSubmit) {
87
- // 有 marketIdentifier 且是 submit 操作,需要检查是否是自己的 agent
88
- const userInfo = getCurrentUserInfo?.() ?? session?.userInfo;
89
- const accountId = userInfo?.accountId;
90
-
91
- if (accountId) {
92
- const isOwner = await checkOwnership({
93
- accessToken: session?.accessToken,
94
- accountId,
95
- enableMarketTrustedClient,
96
- marketIdentifier: identifier,
97
- });
98
-
99
- if (!isOwner) {
100
- // 不是自己的 agent,需要创建新的
101
- needsCreateAgent = true;
102
- }
103
- } else {
104
- // 无法获取用户 ID,为安全起见创建新 agent
105
- needsCreateAgent = true;
106
- }
107
- }
108
120
 
109
- if (needsCreateAgent) {
110
- identifier = generateMarketIdentifier();
111
-
112
- try {
113
- await marketApiService.getAgentDetail(identifier);
114
- } catch {
115
- const createPayload: Record<string, unknown> = {
116
- identifier,
117
- name: meta?.title || '',
118
- };
119
- await marketApiService.createAgent(createPayload as any);
120
- }
121
- } else if (!identifier) {
122
- message.error({
123
- content: t('marketPublish.modal.messages.missingIdentifier'),
124
- key: messageKey,
125
- });
126
- return { success: false };
127
- }
128
-
129
- const versionPayload = {
121
+ // 使用 tRPC publishOrCreate - 后端会自动处理 ownership 检查
122
+ const result = await lambdaClient.market.agent.publishOrCreate.mutate({
130
123
  avatar: meta?.avatar,
131
124
  changelog,
132
125
  config: {
@@ -157,31 +150,16 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
157
150
  },
158
151
  description: meta?.description || '',
159
152
  editorData: editorData,
160
- identifier: identifier,
153
+ // 传递现有的 identifier,后端会检查 ownership
154
+ identifier: meta?.marketIdentifier,
161
155
  name: meta?.title || '',
162
156
  tags: meta?.tags,
163
157
  tokenUsage: tokenUsage,
164
- };
165
-
166
- try {
167
- await marketApiService.createAgentVersion(versionPayload);
168
- } catch (versionError) {
169
- const errorMessage =
170
- versionError instanceof Error
171
- ? versionError.message
172
- : t('unknownError', { ns: 'common' });
173
- message.error({
174
- content: t('marketPublish.modal.messages.createVersionFailed', {
175
- message: errorMessage,
176
- }),
177
- key: messageKey,
178
- });
179
- return { success: false };
180
- }
158
+ });
181
159
 
182
- // 只有在首次创建 agent 时才需要更新 meta
183
- if (needsCreateAgent) {
184
- updateAgentMeta({ marketIdentifier: identifier });
160
+ // 如果是新创建的 agent,需要更新 meta
161
+ if (result.isNewAgent) {
162
+ updateAgentMeta({ marketIdentifier: result.identifier });
185
163
  }
186
164
 
187
165
  message.success({
@@ -189,8 +167,8 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
189
167
  key: messageKey,
190
168
  });
191
169
 
192
- onSuccess?.(identifier!);
193
- return { identifier, success: true };
170
+ onSuccess?.(result.identifier);
171
+ return { identifier: result.identifier, success: true };
194
172
  } catch (error) {
195
173
  const errorMessage =
196
174
  error instanceof Error ? error.message : t('unknownError', { ns: 'common' });
@@ -211,8 +189,6 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
211
189
  chatConfig?.historyCount,
212
190
  chatConfig?.searchMode,
213
191
  editorData,
214
- enableMarketTrustedClient,
215
- getCurrentUserInfo,
216
192
  isAuthenticated,
217
193
  isSubmit,
218
194
  language,
@@ -225,8 +201,6 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
225
201
  onSuccess,
226
202
  plugins,
227
203
  provider,
228
- session?.accessToken,
229
- session?.userInfo,
230
204
  systemRole,
231
205
  tokenUsage,
232
206
  t,
@@ -234,6 +208,8 @@ export const useMarketPublish = ({ action, onSuccess }: UseMarketPublishOptions)
234
208
  ]);
235
209
 
236
210
  return {
211
+ checkOwnership,
212
+ isCheckingOwnership,
237
213
  isPublishing,
238
214
  publish,
239
215
  };
@@ -0,0 +1,9 @@
1
+ import type { ErrorType } from '@lobechat/types';
2
+ import type { AlertProps } from '@lobehub/ui';
3
+
4
+ export default function useBusinessErrorAlertConfig(
5
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
6
+ errorType?: ErrorType,
7
+ ): AlertProps | undefined {
8
+ return undefined;
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { ErrorType } from '@lobechat/types';
2
+
3
+ export interface BusinessErrorContentResult {
4
+ errorType?: string;
5
+ hideMessage?: boolean;
6
+ }
7
+
8
+ export default function useBusinessErrorContent(
9
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
10
+ errorType?: ErrorType | string,
11
+ ): BusinessErrorContentResult {
12
+ return {};
13
+ }
@@ -0,0 +1,12 @@
1
+ export interface BusinessFileUploadCheckParams {
2
+ actualSize: number;
3
+ clientIp?: string;
4
+ inputSize: number;
5
+ url: string;
6
+ userId: string;
7
+ }
8
+
9
+ export async function businessFileUploadCheck(
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ params: BusinessFileUploadCheckParams,
12
+ ): Promise<void> {}
@@ -7,6 +7,8 @@ import dynamic from 'next/dynamic';
7
7
  import { memo, useMemo } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
+ import useBusinessErrorAlertConfig from '@/business/client/hooks/useBusinessErrorAlertConfig';
11
+ import useBusinessErrorContent from '@/business/client/hooks/useBusinessErrorContent';
10
12
  import useRenderBusinessChatErrorMessageExtra from '@/business/client/hooks/useRenderBusinessChatErrorMessageExtra';
11
13
  import ErrorContent from '@/features/Conversation/ChatItem/components/ErrorContent';
12
14
  import { useProviderName } from '@/hooks/useProviderName';
@@ -85,18 +87,26 @@ const getErrorAlertConfig = (
85
87
  export const useErrorContent = (error: any) => {
86
88
  const { t } = useTranslation('error');
87
89
  const providerName = useProviderName(error?.body?.provider || '');
90
+ const businessAlertConfig = useBusinessErrorAlertConfig(error?.type);
91
+ const { errorType: businessErrorType, hideMessage } = useBusinessErrorContent(error?.type);
88
92
 
89
93
  return useMemo<AlertProps | undefined>(() => {
90
94
  if (!error) return;
91
95
  const messageError = error;
92
96
 
93
- const alertConfig = getErrorAlertConfig(messageError.type);
97
+ // Use business alert config if provided, otherwise fall back to default
98
+ const alertConfig = businessAlertConfig ?? getErrorAlertConfig(messageError.type);
99
+
100
+ // Use business error type if provided, otherwise use original
101
+ const finalErrorType = businessErrorType ?? messageError.type;
94
102
 
95
103
  return {
96
- message: t(`response.${messageError.type}` as any, { provider: providerName }),
104
+ message: hideMessage
105
+ ? undefined
106
+ : t(`response.${finalErrorType}` as any, { provider: providerName }),
97
107
  ...alertConfig,
98
108
  };
99
- }, [error]);
109
+ }, [businessAlertConfig, businessErrorType, error, hideMessage, providerName, t]);
100
110
  };
101
111
 
102
112
  interface ErrorExtraProps {
@@ -5,7 +5,8 @@ import { type ReactNode, createContext, useCallback, useContext, useEffect, useS
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { mutate as globalMutate } from 'swr';
7
7
 
8
- import { MARKET_ENDPOINTS, MARKET_OIDC_ENDPOINTS } from '@/services/_url';
8
+ import { lambdaClient } from '@/libs/trpc/client';
9
+ import { MARKET_OIDC_ENDPOINTS } from '@/services/_url';
9
10
  import { useServerConfigStore } from '@/store/serverConfig';
10
11
  import { serverConfigSelectors } from '@/store/serverConfig/selectors';
11
12
  import { useUserStore } from '@/store/user';
@@ -32,31 +33,16 @@ interface MarketAuthProviderProps {
32
33
  }
33
34
 
34
35
  /**
35
- * 获取用户信息(从 OIDC userinfo endpoint)
36
+ * 获取用户信息(通过 tRPC OIDC endpoint)
36
37
  * @param accessToken - 可选的 access token,如果不传则后端会尝试使用 trustedClientToken
37
38
  */
38
39
  const fetchUserInfo = async (accessToken?: string): Promise<MarketUserInfo | null> => {
39
40
  try {
40
- const response = await fetch(MARKET_OIDC_ENDPOINTS.userinfo, {
41
- body: JSON.stringify({ token: accessToken }),
42
- headers: {
43
- 'Content-Type': 'application/json',
44
- },
45
- method: 'POST',
41
+ const userInfo = await lambdaClient.market.oidc.getUserInfo.mutate({
42
+ token: accessToken,
46
43
  });
47
44
 
48
- if (!response.ok) {
49
- console.error(
50
- '[MarketAuth] Failed to fetch user info:',
51
- response.status,
52
- response.statusText,
53
- );
54
- return null;
55
- }
56
-
57
- const userInfo = (await response.json()) as MarketUserInfo;
58
-
59
- return userInfo;
45
+ return userInfo as MarketUserInfo;
60
46
  } catch (error) {
61
47
  console.error('[MarketAuth] Error fetching user info:', error);
62
48
  return null;
@@ -136,16 +122,11 @@ const refreshToken = async (): Promise<boolean> => {
136
122
  */
137
123
  const checkNeedsProfileSetup = async (username: string): Promise<boolean> => {
138
124
  try {
139
- const response = await fetch(MARKET_ENDPOINTS.getUserProfile(username));
140
- if (!response.ok) {
141
- // User profile not found, needs setup
142
- return true;
143
- }
144
- const profile = (await response.json()) as MarketUserProfile;
125
+ const profile = await lambdaClient.market.user.getUserByUsername.query({ username });
145
126
  // If userName is not set, user needs to complete profile setup
146
127
  return !profile.userName;
147
128
  } catch {
148
- // Error fetching profile, assume needs setup
129
+ // Error fetching profile (e.g., NOT_FOUND), assume needs setup
149
130
  return true;
150
131
  }
151
132
  };
@@ -177,7 +158,9 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
177
158
  const isUserStateInit = useUserStore((s) => s.isUserStateInit);
178
159
 
179
160
  // 检查是否启用了 Market Trusted Client 认证
180
- const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
161
+ const enableMarketTrustedClient = useServerConfigStore(
162
+ serverConfigSelectors.enableMarketTrustedClient,
163
+ );
181
164
 
182
165
  // 初始化 OIDC 客户端(仅在客户端)
183
166
  useEffect(() => {
@@ -9,7 +9,7 @@ import { memo, useCallback, useEffect, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
11
11
  import EmojiPicker from '@/components/EmojiPicker';
12
- import { MARKET_ENDPOINTS } from '@/services/_url';
12
+ import { lambdaClient } from '@/libs/trpc/client';
13
13
  import { useFileStore } from '@/store/file';
14
14
  import { useGlobalStore } from '@/store/global';
15
15
  import { globalGeneralSelectors } from '@/store/global/selectors';
@@ -211,37 +211,42 @@ const ProfileSetupModal = memo<ProfileSetupModalProps>(
211
211
  if (bannerUrl) meta.bannerUrl = bannerUrl;
212
212
  if (Object.keys(socialLinks).length > 0) meta.socialLinks = socialLinks;
213
213
 
214
- const response = await fetch(MARKET_ENDPOINTS.updateUserProfile, {
215
- body: JSON.stringify({
216
- avatarUrl: avatarUrl || undefined,
217
- displayName: values.displayName,
218
- meta: Object.keys(meta).length > 0 ? meta : undefined,
219
- userName: values.userName,
220
- }),
221
- headers: {
222
- 'Authorization': `Bearer ${accessToken}`,
223
- 'Content-Type': 'application/json',
224
- },
225
- method: 'PUT',
214
+ const result = await lambdaClient.market.user.updateUserProfile.mutate({
215
+ avatarUrl: avatarUrl || undefined,
216
+ displayName: values.displayName,
217
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
218
+ userName: values.userName,
226
219
  });
227
220
 
228
- if (!response.ok) {
229
- const errorData = await response.json();
230
- if (errorData.error === 'username_taken') {
231
- message.error(t('profileSetup.errors.usernameTaken'));
232
- return;
233
- }
234
- throw new Error(errorData.message || 'Update failed');
235
- }
236
-
237
- const data = await response.json();
238
221
  message.success(t('profileSetup.success'));
239
- onSuccess?.(data.user);
222
+ // Cast result.user to MarketUserProfile with required fields
223
+ const userProfile: MarketUserProfile = {
224
+ avatarUrl: result.user?.avatarUrl || avatarUrl || null,
225
+ bannerUrl: bannerUrl || null,
226
+ createdAt: result.user?.createdAt || new Date().toISOString(),
227
+ description: values.description || null,
228
+ displayName: values.displayName || null,
229
+ id: result.user?.id || 0,
230
+ namespace: result.user?.namespace || '',
231
+ socialLinks: Object.keys(socialLinks).length > 0 ? socialLinks : null,
232
+ type: result.user?.type || null,
233
+ userName: values.userName || null,
234
+ };
235
+ onSuccess?.(userProfile);
240
236
  onClose();
241
237
  } catch (error) {
242
238
  console.error('[ProfileSetupModal] Update failed:', error);
243
239
  if (error instanceof Error && error.message !== 'Validation failed') {
244
- message.error(t('profileSetup.errors.updateFailed'));
240
+ // Check for username taken error (tRPC CONFLICT code)
241
+ const errorMessage = error.message || '';
242
+ if (
243
+ errorMessage.toLowerCase().includes('already taken') ||
244
+ errorMessage.includes('CONFLICT')
245
+ ) {
246
+ message.error(t('profileSetup.errors.usernameTaken'));
247
+ } else {
248
+ message.error(t('profileSetup.errors.updateFailed'));
249
+ }
245
250
  }
246
251
  } finally {
247
252
  setLoading(false);
@@ -1,20 +1,15 @@
1
1
  import useSWR from 'swr';
2
2
 
3
- import { MARKET_ENDPOINTS } from '@/services/_url';
3
+ import { lambdaClient } from '@/libs/trpc/client';
4
4
 
5
5
  import { type MarketUserProfile } from './types';
6
6
 
7
7
  /**
8
- * Fetcher function for user profile
8
+ * Fetcher function for user profile using tRPC
9
9
  */
10
10
  const fetchUserProfile = async (username: string): Promise<MarketUserProfile | null> => {
11
- const response = await fetch(MARKET_ENDPOINTS.getUserProfile(username));
12
-
13
- if (!response.ok) {
14
- throw new Error(`Failed to fetch user profile: ${response.status}`);
15
- }
16
-
17
- return response.json();
11
+ const result = await lambdaClient.market.user.getUserByUsername.query({ username });
12
+ return result as MarketUserProfile;
18
13
  };
19
14
 
20
15
  /**
@@ -1,4 +1,5 @@
1
1
  export * from './keyVaults';
2
+ export * from './marketSDK';
2
3
  export * from './marketUserInfo';
3
4
  export * from './serverDatabase';
4
5
  export * from './telemetry';
@@ -0,0 +1,68 @@
1
+ import { MarketSDK } from '@lobehub/market-sdk';
2
+
3
+ import { generateTrustedClientToken, type TrustedClientUserInfo } from '@/libs/trusted-client';
4
+
5
+ import { trpc } from '../init';
6
+
7
+ interface ContextWithMarketUserInfo {
8
+ marketAccessToken?: string;
9
+ marketUserInfo?: TrustedClientUserInfo;
10
+ }
11
+
12
+ /**
13
+ * Middleware that initializes MarketSDK with proper authentication.
14
+ * This requires marketUserInfo middleware to be applied first.
15
+ *
16
+ * Provides:
17
+ * - ctx.marketSDK: Initialized MarketSDK instance with trustedClientToken and optional accessToken
18
+ * - ctx.trustedClientToken: The generated trusted client token (if available)
19
+ */
20
+ export const marketSDK = trpc.middleware(async (opts) => {
21
+ const ctx = opts.ctx as ContextWithMarketUserInfo;
22
+
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({
30
+ accessToken: ctx.marketAccessToken,
31
+ baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
32
+ trustedClientToken,
33
+ });
34
+
35
+ return opts.next({
36
+ ctx: {
37
+ marketSDK: market,
38
+ trustedClientToken,
39
+ },
40
+ });
41
+ });
42
+
43
+ /**
44
+ * Middleware that requires authentication for Market API access.
45
+ * This middleware ensures that either accessToken or trustedClientToken is available.
46
+ * It should be used after marketUserInfo and marketSDK middlewares.
47
+ *
48
+ * Throws UNAUTHORIZED error if neither authentication method is available.
49
+ */
50
+ export const requireMarketAuth = trpc.middleware(async (opts) => {
51
+ const ctx = opts.ctx as ContextWithMarketUserInfo & {
52
+ trustedClientToken?: string;
53
+ };
54
+
55
+ // Check if any authentication is available
56
+ const hasAccessToken = !!ctx.marketAccessToken;
57
+ const hasTrustedToken = !!ctx.trustedClientToken;
58
+
59
+ if (!hasAccessToken && !hasTrustedToken) {
60
+ const { TRPCError } = await import('@trpc/server');
61
+ throw new TRPCError({
62
+ code: 'UNAUTHORIZED',
63
+ message: 'Authentication required. Please sign in.',
64
+ });
65
+ }
66
+
67
+ return opts.next();
68
+ });
@@ -140,6 +140,11 @@ export default {
140
140
  'llm.proxyUrl.title': 'API proxy URL',
141
141
  'llm.waitingForMore': 'More models are <1>planned to be added</1>, stay tuned',
142
142
  'llm.waitingForMoreLinkAriaLabel': 'Open the Provider request form',
143
+ 'marketPublish.forkConfirm.by': 'by {{author}}',
144
+ 'marketPublish.forkConfirm.confirm': 'Confirm Publish',
145
+ 'marketPublish.forkConfirm.description':
146
+ 'You are about to publish a derivative version based on an existing agent from the community. Your new agent will be created as a separate entry in the marketplace.',
147
+ 'marketPublish.forkConfirm.title': 'Publish Derivative Agent',
143
148
  'marketPublish.modal.changelog.extra':
144
149
  'Describe the key changes and improvements in this version',
145
150
  'marketPublish.modal.changelog.label': 'Changelog',
@@ -273,7 +273,7 @@ describe('fileRouter', () => {
273
273
  );
274
274
  });
275
275
 
276
- it('should throw error when getFileMetadata fails and input size is less than 1', async () => {
276
+ it('should throw error when getFileMetadata fails and input size is negative', async () => {
277
277
  mockFileModelCheckHash.mockResolvedValue({ isExist: false });
278
278
  mockFileServiceGetFileMetadata.mockRejectedValue(new Error('File not found in S3'));
279
279
 
@@ -282,11 +282,11 @@ describe('fileRouter', () => {
282
282
  hash: 'test-hash',
283
283
  fileType: 'text',
284
284
  name: 'test.txt',
285
- size: 0,
285
+ size: -1,
286
286
  url: 'files/non-existent.txt',
287
287
  metadata: {},
288
288
  }),
289
- ).rejects.toThrow('File size must be at least 1 byte');
289
+ ).rejects.toThrow('File size cannot be negative');
290
290
  });
291
291
 
292
292
  it('should use input size when getFileMetadata returns contentLength less than 1', async () => {
@@ -315,10 +315,10 @@ describe('fileRouter', () => {
315
315
  );
316
316
  });
317
317
 
318
- it('should throw error when both getFileMetadata contentLength and input size are less than 1', async () => {
318
+ it('should throw error when both getFileMetadata contentLength and input size are negative', async () => {
319
319
  mockFileModelCheckHash.mockResolvedValue({ isExist: false });
320
320
  mockFileServiceGetFileMetadata.mockResolvedValue({
321
- contentLength: 0,
321
+ contentLength: -1,
322
322
  contentType: 'text/plain',
323
323
  });
324
324
 
@@ -327,11 +327,11 @@ describe('fileRouter', () => {
327
327
  hash: 'test-hash',
328
328
  fileType: 'text',
329
329
  name: 'test.txt',
330
- size: 0,
330
+ size: -1,
331
331
  url: 'files/test.txt',
332
332
  metadata: {},
333
333
  }),
334
- ).rejects.toThrow('File size must be at least 1 byte');
334
+ ).rejects.toThrow('File size cannot be negative');
335
335
  });
336
336
  });
337
337