@lobehub/lobehub 2.0.0-next.224 → 2.0.0-next.226

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 (34) hide show
  1. package/.github/workflows/test.yml +18 -14
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v1.json +14 -0
  4. package/locales/en-US/common.json +3 -2
  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/database/src/models/user.ts +33 -0
  9. package/packages/database/src/repositories/knowledge/index.ts +1 -1
  10. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/ForkConfirmModal.tsx +67 -0
  11. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/PublishButton.tsx +92 -105
  12. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/index.tsx +13 -48
  13. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/useMarketPublish.ts +69 -93
  14. package/src/business/client/hooks/useRenderBusinessChatErrorMessageExtra.tsx +10 -0
  15. package/src/features/CommandMenu/ContextCommands.tsx +97 -37
  16. package/src/features/CommandMenu/SearchResults.tsx +100 -276
  17. package/src/features/CommandMenu/components/CommandItem.tsx +1 -1
  18. package/src/features/CommandMenu/utils/contextCommands.ts +56 -1
  19. package/src/features/Conversation/Error/index.tsx +7 -1
  20. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +11 -28
  21. package/src/layout/AuthProvider/MarketAuth/ProfileSetupModal.tsx +30 -25
  22. package/src/layout/AuthProvider/MarketAuth/useMarketUserProfile.ts +4 -9
  23. package/src/libs/redis/manager.ts +51 -15
  24. package/src/libs/trpc/lambda/middleware/index.ts +1 -0
  25. package/src/libs/trpc/lambda/middleware/marketSDK.ts +68 -0
  26. package/src/locales/default/common.ts +2 -2
  27. package/src/locales/default/setting.ts +5 -0
  28. package/src/server/routers/lambda/market/agent.ts +504 -0
  29. package/src/server/routers/lambda/market/index.ts +17 -0
  30. package/src/server/routers/lambda/market/oidc.ts +169 -0
  31. package/src/server/routers/lambda/market/social.ts +532 -0
  32. package/src/server/routers/lambda/market/user.ts +123 -0
  33. package/src/services/marketApi.ts +24 -84
  34. package/src/services/social.ts +70 -166
@@ -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
  /**
@@ -2,6 +2,33 @@ import { IoRedisRedisProvider } from './redis';
2
2
  import { type BaseRedisProvider, type RedisConfig } from './types';
3
3
  import { UpstashRedisProvider } from './upstash';
4
4
 
5
+ /**
6
+ * Create a Redis provider instance based on config
7
+ *
8
+ * @param config - Redis config
9
+ * @param prefix - Optional custom prefix to override config.prefix
10
+ * @returns Provider instance or null if disabled/unsupported
11
+ */
12
+ const createProvider = (config: RedisConfig, prefix?: string): BaseRedisProvider | null => {
13
+ if (!config.enabled) return null;
14
+
15
+ const actualPrefix = prefix ?? config.prefix;
16
+
17
+ if (config.provider === 'redis') {
18
+ return new IoRedisRedisProvider({ ...config, prefix: actualPrefix });
19
+ }
20
+
21
+ if (config.provider === 'upstash') {
22
+ return new UpstashRedisProvider({
23
+ prefix: actualPrefix,
24
+ token: config.token,
25
+ url: config.url,
26
+ });
27
+ }
28
+
29
+ return null;
30
+ };
31
+
5
32
  class RedisManager {
6
33
  private static instance: BaseRedisProvider | null = null;
7
34
  // NOTICE: initPromise keeps concurrent initialize() calls sharing the same in-flight setup,
@@ -13,25 +40,13 @@ class RedisManager {
13
40
  if (RedisManager.initPromise) return RedisManager.initPromise;
14
41
 
15
42
  RedisManager.initPromise = (async () => {
16
- if (!config.enabled) {
43
+ const provider = createProvider(config);
44
+
45
+ if (!provider) {
17
46
  RedisManager.instance = null;
18
47
  return null;
19
48
  }
20
49
 
21
- let provider: BaseRedisProvider;
22
-
23
- if (config.provider === 'redis') {
24
- provider = new IoRedisRedisProvider(config);
25
- } else if (config.provider === 'upstash') {
26
- provider = new UpstashRedisProvider({
27
- prefix: config.prefix,
28
- token: config.token,
29
- url: config.url,
30
- });
31
- } else {
32
- throw new Error(`Unsupported redis provider: ${String((config as any).provider)}`);
33
- }
34
-
35
50
  await provider.initialize();
36
51
  RedisManager.instance = provider;
37
52
 
@@ -58,3 +73,24 @@ export const initializeRedis = (config: RedisConfig) => RedisManager.initialize(
58
73
  export const resetRedisClient = () => RedisManager.reset();
59
74
  export const isRedisEnabled = (config: RedisConfig) => config.enabled;
60
75
  export { RedisManager };
76
+
77
+ /**
78
+ * Create a Redis client with custom prefix
79
+ *
80
+ * Unlike initializeRedis, this creates an independent client
81
+ * that doesn't share the singleton instance.
82
+ *
83
+ * @param config - Redis config
84
+ * @param prefix - Custom prefix for all keys (e.g., 'aiGeneration')
85
+ * @returns Redis client or null if Redis is disabled
86
+ */
87
+ export const createRedisWithPrefix = async (
88
+ config: RedisConfig,
89
+ prefix: string,
90
+ ): Promise<BaseRedisProvider | null> => {
91
+ const provider = createProvider(config, prefix);
92
+ if (!provider) return null;
93
+
94
+ await provider.initialize();
95
+ return provider;
96
+ };
@@ -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
+ });
@@ -133,8 +133,8 @@ export default {
133
133
  'cmdk.navigate': 'Navigate',
134
134
  'cmdk.newAgent': 'Create New Agent',
135
135
  'cmdk.newAgentTeam': 'Create New Group',
136
- 'cmdk.newLibrary': 'New Library',
137
- 'cmdk.newPage': 'New Page',
136
+ 'cmdk.newLibrary': 'Create New Library',
137
+ 'cmdk.newPage': 'Create New Page',
138
138
  'cmdk.newTopic': 'New topic in current Agent',
139
139
  'cmdk.noResults': 'No results found',
140
140
  'cmdk.openSettings': 'Open Settings',
@@ -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',