@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
@@ -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,10 @@
1
+ import { type ChatMessageError } from '@lobechat/types';
2
+
3
+ export default function useRenderBusinessChatErrorMessageExtra(
4
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
5
+ error: ChatMessageError | null | undefined,
6
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
7
+ messageId: string,
8
+ ) {
9
+ return null;
10
+ }
@@ -6,11 +6,12 @@ import { useTranslation } from 'react-i18next';
6
6
  import { useCommandMenuContext } from './CommandMenuContext';
7
7
  import { CommandItem } from './components';
8
8
  import { useCommandMenu } from './useCommandMenu';
9
- import { getContextCommands } from './utils/contextCommands';
9
+ import { CONTEXT_COMMANDS, getContextCommands } from './utils/contextCommands';
10
10
 
11
11
  const ContextCommands = memo(() => {
12
12
  const { t } = useTranslation('setting');
13
13
  const { t: tAuth } = useTranslation('auth');
14
+ const { t: tSubscription } = useTranslation('subscription');
14
15
  const { t: tCommon } = useTranslation('common');
15
16
  const { handleNavigate } = useCommandMenu();
16
17
  const { menuContext, pathname } = useCommandMenuContext();
@@ -23,48 +24,107 @@ const ContextCommands = memo(() => {
23
24
 
24
25
  const commands = getContextCommands(menuContext, subPath);
25
26
 
26
- if (commands.length === 0) return null;
27
+ // Get settings commands to show globally (when not in settings context)
28
+ const globalSettingsCommands = useMemo(() => {
29
+ if (menuContext === 'settings') return [];
30
+ return CONTEXT_COMMANDS.settings;
31
+ }, [menuContext]);
32
+
33
+ const hasCommands = commands.length > 0 || globalSettingsCommands.length > 0;
34
+
35
+ if (!hasCommands) return null;
27
36
 
28
37
  // Get localized context name
29
38
  const contextName = tCommon(`cmdk.context.${menuContext}`, { defaultValue: menuContext });
39
+ const settingsContextName = tCommon('cmdk.context.settings', { defaultValue: 'settings' });
30
40
 
31
41
  return (
32
- <Command.Group>
33
- {commands.map((cmd) => {
34
- const Icon = cmd.icon;
35
- // Get localized label using the correct namespace
36
- let label = cmd.label;
37
- if (cmd.labelKey) {
38
- if (cmd.labelNamespace === 'auth') {
39
- label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
40
- } else {
41
- label = t(cmd.labelKey, { defaultValue: cmd.label });
42
- }
43
- }
44
- const searchValue = `${contextName} ${label} ${cmd.keywords.join(' ')}`;
42
+ <>
43
+ {/* Current context commands */}
44
+ {commands.length > 0 && (
45
+ <Command.Group>
46
+ {commands.map((cmd) => {
47
+ const Icon = cmd.icon;
48
+ // Get localized label using the correct namespace
49
+ let label = cmd.label;
50
+ if (cmd.labelKey) {
51
+ if (cmd.labelNamespace === 'auth') {
52
+ label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
53
+ } else if (cmd.labelNamespace === 'subscription') {
54
+ label = tSubscription(cmd.labelKey, { defaultValue: cmd.label });
55
+ } else {
56
+ label = t(cmd.labelKey, { defaultValue: cmd.label });
57
+ }
58
+ }
59
+ const searchValue = `${contextName} ${label} ${cmd.keywords.join(' ')}`;
60
+
61
+ return (
62
+ <CommandItem
63
+ icon={<Icon />}
64
+ key={cmd.path}
65
+ onSelect={() => handleNavigate(cmd.path)}
66
+ value={searchValue}
67
+ >
68
+ <span style={{ opacity: 0.5 }}>{contextName}</span>
69
+ <ChevronRight
70
+ size={14}
71
+ style={{
72
+ display: 'inline',
73
+ marginInline: '6px',
74
+ opacity: 0.5,
75
+ verticalAlign: 'middle',
76
+ }}
77
+ />
78
+ {label}
79
+ </CommandItem>
80
+ );
81
+ })}
82
+ </Command.Group>
83
+ )}
84
+
85
+ {/* Global settings commands (searchable from any page) */}
86
+ {globalSettingsCommands.length > 0 && (
87
+ <Command.Group>
88
+ {globalSettingsCommands.map((cmd) => {
89
+ const Icon = cmd.icon;
90
+ // Get localized label using the correct namespace
91
+ let label = cmd.label;
92
+ if (cmd.labelKey) {
93
+ if (cmd.labelNamespace === 'auth') {
94
+ label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
95
+ } else if (cmd.labelNamespace === 'subscription') {
96
+ label = tSubscription(cmd.labelKey, { defaultValue: cmd.label });
97
+ } else {
98
+ label = t(cmd.labelKey, { defaultValue: cmd.label });
99
+ }
100
+ }
101
+ const searchValue = `${settingsContextName} ${label} ${cmd.keywords.join(' ')}`;
45
102
 
46
- return (
47
- <CommandItem
48
- icon={<Icon />}
49
- key={cmd.path}
50
- onSelect={() => handleNavigate(cmd.path)}
51
- value={searchValue}
52
- >
53
- <span style={{ opacity: 0.5 }}>{contextName}</span>
54
- <ChevronRight
55
- size={14}
56
- style={{
57
- display: 'inline',
58
- marginInline: '6px',
59
- opacity: 0.5,
60
- verticalAlign: 'middle',
61
- }}
62
- />
63
- {label}
64
- </CommandItem>
65
- );
66
- })}
67
- </Command.Group>
103
+ return (
104
+ <CommandItem
105
+ icon={<Icon />}
106
+ key={cmd.path}
107
+ onSelect={() => handleNavigate(cmd.path)}
108
+ unpinned={true}
109
+ value={searchValue}
110
+ >
111
+ <span style={{ opacity: 0.5 }}>{settingsContextName}</span>
112
+ <ChevronRight
113
+ size={14}
114
+ style={{
115
+ display: 'inline',
116
+ marginInline: '6px',
117
+ opacity: 0.5,
118
+ verticalAlign: 'middle',
119
+ }}
120
+ />
121
+ {label}
122
+ </CommandItem>
123
+ );
124
+ })}
125
+ </Command.Group>
126
+ )}
127
+ </>
68
128
  );
69
129
  });
70
130