@lobehub/lobehub 2.0.0-next.321 → 2.0.0-next.323

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 (111) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +9 -76
  3. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +0 -1
  4. package/apps/desktop/src/main/modules/updater/configs.ts +0 -4
  5. package/changelog/v1.json +20 -0
  6. package/e2e/src/mocks/llm/index.ts +3 -3
  7. package/locales/ar/common.json +5 -0
  8. package/locales/ar/error.json +10 -1
  9. package/locales/bg-BG/common.json +5 -0
  10. package/locales/bg-BG/error.json +10 -1
  11. package/locales/de-DE/common.json +5 -0
  12. package/locales/de-DE/error.json +10 -1
  13. package/locales/en-US/common.json +5 -0
  14. package/locales/es-ES/common.json +5 -0
  15. package/locales/es-ES/error.json +10 -1
  16. package/locales/fa-IR/common.json +5 -0
  17. package/locales/fa-IR/error.json +10 -1
  18. package/locales/fr-FR/common.json +5 -0
  19. package/locales/fr-FR/error.json +10 -1
  20. package/locales/it-IT/common.json +5 -0
  21. package/locales/it-IT/error.json +10 -1
  22. package/locales/ja-JP/common.json +5 -0
  23. package/locales/ja-JP/error.json +10 -1
  24. package/locales/ko-KR/common.json +5 -0
  25. package/locales/ko-KR/error.json +10 -1
  26. package/locales/nl-NL/common.json +5 -0
  27. package/locales/nl-NL/error.json +10 -1
  28. package/locales/pl-PL/common.json +5 -0
  29. package/locales/pl-PL/error.json +10 -1
  30. package/locales/pt-BR/common.json +5 -0
  31. package/locales/pt-BR/error.json +10 -1
  32. package/locales/ru-RU/common.json +5 -0
  33. package/locales/ru-RU/error.json +10 -1
  34. package/locales/tr-TR/common.json +5 -0
  35. package/locales/tr-TR/error.json +10 -1
  36. package/locales/vi-VN/common.json +5 -0
  37. package/locales/vi-VN/error.json +10 -1
  38. package/locales/zh-CN/common.json +5 -0
  39. package/locales/zh-TW/common.json +5 -0
  40. package/locales/zh-TW/error.json +10 -1
  41. package/package.json +2 -2
  42. package/packages/business/const/src/branding.ts +1 -0
  43. package/packages/business/const/src/llm.ts +2 -1
  44. package/packages/const/src/settings/llm.ts +2 -1
  45. package/packages/const/src/settings/systemAgent.ts +12 -7
  46. package/packages/database/src/models/agent.ts +18 -1
  47. package/packages/database/src/models/chatGroup.ts +18 -1
  48. package/packages/database/src/types/chatGroup.ts +1 -0
  49. package/packages/model-bank/package.json +1 -1
  50. package/packages/model-bank/src/aiModels/index.ts +2 -2
  51. package/packages/model-bank/src/aiModels/lobehub/chat/anthropic.ts +256 -0
  52. package/packages/model-bank/src/aiModels/lobehub/chat/deepseek.ts +45 -0
  53. package/packages/model-bank/src/aiModels/lobehub/chat/google.ts +267 -0
  54. package/packages/model-bank/src/aiModels/lobehub/chat/index.ts +26 -0
  55. package/packages/model-bank/src/aiModels/lobehub/chat/minimax.ts +75 -0
  56. package/packages/model-bank/src/aiModels/lobehub/chat/moonshot.ts +28 -0
  57. package/packages/model-bank/src/aiModels/lobehub/chat/openai.ts +345 -0
  58. package/packages/model-bank/src/aiModels/lobehub/chat/xai.ts +32 -0
  59. package/packages/model-bank/src/aiModels/lobehub/image.ts +240 -0
  60. package/packages/model-bank/src/aiModels/lobehub/index.ts +10 -0
  61. package/packages/model-bank/src/aiModels/lobehub/utils.ts +58 -0
  62. package/packages/model-bank/src/modelProviders/index.ts +10 -10
  63. package/packages/model-runtime/src/core/streams/qwen.test.ts +320 -0
  64. package/packages/model-runtime/src/core/streams/qwen.ts +19 -10
  65. package/packages/types/package.json +1 -1
  66. package/packages/types/src/agentGroup/index.ts +2 -0
  67. package/packages/types/src/discover/assistants.ts +9 -0
  68. package/packages/types/src/discover/fork.ts +163 -0
  69. package/packages/types/src/discover/groupAgents.ts +13 -4
  70. package/packages/types/src/discover/index.ts +9 -0
  71. package/src/app/[variants]/(auth)/_layout/index.tsx +2 -1
  72. package/src/app/[variants]/(auth)/auth-error/page.tsx +5 -5
  73. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/index.tsx +1 -2
  74. package/src/app/[variants]/(main)/agent/profile/index.tsx +15 -2
  75. package/src/app/[variants]/(main)/community/(detail)/agent/features/Header.tsx +37 -0
  76. package/src/app/[variants]/(main)/community/(detail)/agent/features/Sidebar/ActionButton/ForkAndChat.tsx +133 -0
  77. package/src/app/[variants]/(main)/community/(detail)/agent/features/Sidebar/ActionButton/index.tsx +2 -2
  78. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Details/index.tsx +7 -10
  79. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/ForkGroupAndChat.tsx +208 -0
  80. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/index.tsx +2 -2
  81. package/src/app/[variants]/(main)/community/(detail)/user/features/DetailProvider.tsx +2 -0
  82. package/src/app/[variants]/(main)/community/(detail)/user/features/UserContent.tsx +7 -0
  83. package/src/app/[variants]/(main)/community/(detail)/user/features/UserForkedAgentGroups.tsx +63 -0
  84. package/src/app/[variants]/(main)/community/(detail)/user/features/UserForkedAgents.tsx +61 -0
  85. package/src/app/[variants]/(main)/community/(detail)/user/index.tsx +3 -1
  86. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +1 -2
  87. package/src/app/[variants]/(main)/settings/profile/index.tsx +92 -68
  88. package/src/app/[variants]/(mobile)/chat/features/Topic/index.tsx +2 -1
  89. package/src/features/CommandMenu/AskAgentCommands.tsx +105 -0
  90. package/src/features/CommandMenu/CommandMenuContext.tsx +57 -38
  91. package/src/features/CommandMenu/components/CommandInput.tsx +43 -9
  92. package/src/features/CommandMenu/index.tsx +89 -27
  93. package/src/features/CommandMenu/types.ts +6 -0
  94. package/src/features/CommandMenu/useCommandMenu.ts +62 -39
  95. package/src/features/PageEditor/PageEditor.tsx +20 -8
  96. package/src/locales/default/common.ts +5 -0
  97. package/src/locales/default/discover.ts +371 -0
  98. package/src/server/globalConfig/parseMemoryExtractionConfig.ts +7 -8
  99. package/src/server/routers/lambda/agent.ts +14 -0
  100. package/src/server/routers/lambda/agentGroup.ts +19 -3
  101. package/src/server/routers/lambda/market/agent.ts +234 -26
  102. package/src/server/routers/lambda/market/agentGroup.ts +204 -1
  103. package/src/server/services/discover/index.ts +52 -2
  104. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +12 -2
  105. package/src/server/services/memory/userMemory/extract.ts +11 -2
  106. package/src/services/agent.ts +8 -0
  107. package/src/services/chatGroup/index.ts +8 -0
  108. package/src/services/marketApi.ts +78 -0
  109. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +12 -12
  110. package/src/utils/styles.ts +10 -0
  111. package/packages/model-bank/src/aiModels/lobehub.ts +0 -1315
@@ -1,15 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import { SiDiscord } from '@icons-pack/react-simple-icons';
4
+ import { SOCIAL_URL } from '@lobechat/business-const';
4
5
  import { Alert, Button, Flexbox, Icon } from '@lobehub/ui';
5
- import Link from '@/libs/next/Link';
6
+ import { cssVar } from 'antd-style';
6
7
  import { parseAsString, useQueryState } from 'nuqs';
7
8
  import { memo } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
 
10
11
  import AuthCard from '@/features/AuthCard';
11
-
12
- const DISCORD_URL = 'https://discord.gg/AYFPHvv2jT';
12
+ import Link from '@/libs/next/Link';
13
13
 
14
14
  const normalizeErrorCode = (code?: string | null) =>
15
15
  (code || 'UNKNOWN').trim().toUpperCase().replaceAll('-', '_');
@@ -35,8 +35,8 @@ const AuthErrorPage = memo(() => {
35
35
  {t('actions.home')}
36
36
  </Button>
37
37
  </Link>
38
- <Link href={DISCORD_URL} rel="noopener noreferrer" target="_blank">
39
- <Button block icon={<Icon icon={SiDiscord} />} type="text">
38
+ <Link href={SOCIAL_URL.discord} rel="noopener noreferrer" target="_blank">
39
+ <Button block icon={<Icon fill={cssVar.colorText} icon={SiDiscord} />} type="text">
40
40
  {t('actions.discord')}
41
41
  </Button>
42
42
  </Link>
@@ -3,7 +3,6 @@ import { cssVar } from 'antd-style';
3
3
  import { MessageSquareDashed, Star } from 'lucide-react';
4
4
  import { Suspense, memo, useCallback, useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import urlJoin from 'url-join';
7
6
 
8
7
  import { isDesktop } from '@/const/version';
9
8
  import NavItem from '@/features/NavPanel/components/NavItem';
@@ -33,7 +32,7 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
33
32
  // Construct href for cmd+click support
34
33
  const href = useMemo(() => {
35
34
  if (!activeAgentId || !id) return undefined;
36
- return urlJoin('/chat', `?agent=${activeAgentId}&topic=${id}`);
35
+ return `/agent/${activeAgentId}?topic=${id}`;
37
36
  }, [activeAgentId, id]);
38
37
 
39
38
  const [editing, isLoading] = useChatStore((s) => [
@@ -8,6 +8,7 @@ import AgentBuilder from '@/features/AgentBuilder';
8
8
  import WideScreenContainer from '@/features/WideScreenContainer';
9
9
  import { useAgentStore } from '@/store/agent';
10
10
  import { agentSelectors } from '@/store/agent/selectors';
11
+ import { StyleSheet } from '@/utils/styles';
11
12
 
12
13
  import Header from './features/Header';
13
14
  import ProfileEditor from './features/ProfileEditor';
@@ -15,13 +16,25 @@ import ProfileHydration from './features/ProfileHydration';
15
16
  import ProfileProvider from './features/ProfileProvider';
16
17
  import { useProfileStore } from './features/store';
17
18
 
19
+ const styles = StyleSheet.create({
20
+ contentWrapper: {
21
+ cursor: 'text',
22
+ display: 'flex',
23
+ overflowY: 'auto',
24
+ position: 'relative',
25
+ },
26
+ profileArea: {
27
+ minWidth: 0,
28
+ },
29
+ });
30
+
18
31
  const ProfileArea = memo(() => {
19
32
  const editor = useProfileStore((s) => s.editor);
20
33
  const isAgentConfigLoading = useAgentStore(agentSelectors.isAgentConfigLoading);
21
34
 
22
35
  return (
23
36
  <>
24
- <Flexbox flex={1} height={'100%'}>
37
+ <Flexbox flex={1} height={'100%'} style={styles.profileArea}>
25
38
  {isAgentConfigLoading ? (
26
39
  <Loading debugId="ProfileArea" />
27
40
  ) : (
@@ -33,7 +46,7 @@ const ProfileArea = memo(() => {
33
46
  onClick={() => {
34
47
  editor?.focus();
35
48
  }}
36
- style={{ cursor: 'text', display: 'flex', overflowY: 'auto', position: 'relative' }}
49
+ style={styles.contentWrapper}
37
50
  width={'100%'}
38
51
  >
39
52
  <WideScreenContainer>
@@ -19,6 +19,8 @@ import {
19
19
  BookmarkIcon,
20
20
  CoinsIcon,
21
21
  DotIcon,
22
+ GitBranchIcon,
23
+ GitForkIcon,
22
24
  HeartIcon,
23
25
  } from 'lucide-react';
24
26
  import qs from 'query-string';
@@ -29,6 +31,7 @@ import useSWR from 'swr';
29
31
  import urlJoin from 'url-join';
30
32
 
31
33
  import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
34
+ import { marketApiService } from '@/services/marketApi';
32
35
  import { socialService } from '@/services/social';
33
36
  import { formatIntergerNumber } from '@/utils/format';
34
37
 
@@ -57,6 +60,8 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
57
60
  pluginCount,
58
61
  knowledgeCount,
59
62
  userName,
63
+ forkCount,
64
+ forkedFromAgentId,
60
65
  } = useDetailContext();
61
66
  const { mobile = isMobile } = useResponsive();
62
67
  const { isAuthenticated, signIn, session } = useMarketAuth();
@@ -85,6 +90,13 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
85
90
  );
86
91
  const isLiked = likeStatus?.isLiked ?? false;
87
92
 
93
+ // Fetch fork source info
94
+ const { data: forkSource } = useSWR(
95
+ identifier && forkedFromAgentId ? ['fork-source', identifier] : null,
96
+ () => marketApiService.getAgentForkSource(identifier!),
97
+ { revalidateOnFocus: false },
98
+ );
99
+
88
100
  const handleFavoriteClick = async () => {
89
101
  if (!isAuthenticated) {
90
102
  await signIn();
@@ -223,6 +235,31 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
223
235
  template={'MMM DD, YYYY'}
224
236
  />
225
237
  </Flexbox>
238
+
239
+ {/* Fork information */}
240
+ {(forkSource?.source || (forkCount && forkCount > 0)) && (
241
+ <Flexbox align={'center'} gap={12} horizontal>
242
+ {forkSource?.source && (
243
+ <Flexbox align={'center'} gap={6} horizontal>
244
+ <Icon icon={GitForkIcon} size={'small'} />
245
+ <Text className={styles.time} type={'secondary'}>
246
+ {t('fork.forkedFrom')}:{' '}
247
+ <Link to={urlJoin('/community/agent', forkSource.source.identifier)}>
248
+ {forkSource.source.name}
249
+ </Link>
250
+ </Text>
251
+ </Flexbox>
252
+ )}
253
+ {forkCount && forkCount > 0 && (
254
+ <Flexbox align={'center'} gap={6} horizontal>
255
+ <Icon icon={GitBranchIcon} size={'small'} />
256
+ <Text className={styles.time} type={'secondary'}>
257
+ {forkCount} {t('fork.forks')}
258
+ </Text>
259
+ </Flexbox>
260
+ )}
261
+ </Flexbox>
262
+ )}
226
263
  </Flexbox>
227
264
  </Flexbox>
228
265
  <TooltipGroup>
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@lobehub/ui';
4
+ import { App } from 'antd';
5
+ import { createStaticStyles } from 'antd-style';
6
+ import { customAlphabet } from 'nanoid/non-secure';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useNavigate } from 'react-router-dom';
10
+
11
+ import { SESSION_CHAT_URL } from '@/const/url';
12
+ import { agentService } from '@/services/agent';
13
+ import { discoverService } from '@/services/discover';
14
+ import { marketApiService } from '@/services/marketApi';
15
+ import { useAgentStore } from '@/store/agent';
16
+ import { useHomeStore } from '@/store/home';
17
+
18
+ import { useDetailContext } from '../../DetailProvider';
19
+
20
+ const styles = createStaticStyles(({ css }) => ({
21
+ buttonGroup: css`
22
+ width: 100%;
23
+ `,
24
+ }));
25
+
26
+ /**
27
+ * Generate a market identifier (8-character lowercase alphanumeric string)
28
+ */
29
+ const generateMarketIdentifier = () => {
30
+ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
31
+ const generate = customAlphabet(alphabet, 8);
32
+ return generate();
33
+ };
34
+
35
+ const ForkAndChat = memo<{ mobile?: boolean }>(({ mobile }) => {
36
+ const { identifier, title, config, avatar, backgroundColor, description, tags, editorData } =
37
+ useDetailContext();
38
+ const [isLoading, setIsLoading] = useState(false);
39
+ const createAgent = useAgentStore((s) => s.createAgent);
40
+ const refreshAgentList = useHomeStore((s) => s.refreshAgentList);
41
+ const { message } = App.useApp();
42
+ const navigate = useNavigate();
43
+ const { t } = useTranslation('discover');
44
+
45
+ const meta = {
46
+ avatar,
47
+ backgroundColor,
48
+ description,
49
+ marketIdentifier: identifier,
50
+ tags,
51
+ title,
52
+ };
53
+
54
+ const handleForkAndChat = async () => {
55
+ try {
56
+ setIsLoading(true);
57
+
58
+ // Step 1: Check if user has already forked this agent
59
+ const existingAgentId = await agentService.getAgentByForkedFromIdentifier(identifier!);
60
+
61
+ if (existingAgentId) {
62
+ // User has already forked this agent, navigate to existing fork
63
+ message.info(t('fork.alreadyForked'));
64
+ navigate(SESSION_CHAT_URL(existingAgentId, mobile));
65
+ return;
66
+ }
67
+
68
+ // Generate a unique identifier for the forked agent
69
+ const newIdentifier = generateMarketIdentifier();
70
+
71
+ // Step 2: Fork the agent via Market API
72
+ const forkResult = await marketApiService.forkAgent(identifier!, {
73
+ identifier: newIdentifier,
74
+ name: title,
75
+ status: 'published',
76
+ visibility: 'public',
77
+ });
78
+
79
+ // Step 3: Create agent config with forked data
80
+ if (!config) throw new Error('Agent config is missing');
81
+
82
+ const agentData = {
83
+ config: {
84
+ ...config,
85
+ editorData,
86
+ ...meta,
87
+ marketIdentifier: forkResult.agent.identifier,
88
+ params: {
89
+ ...config.params,
90
+ forkedFromIdentifier: identifier, // Store the source agent identifier
91
+ },
92
+ title: forkResult.agent.name,
93
+ },
94
+ };
95
+
96
+ // Step 4: Add to local agent list
97
+ const result = await createAgent(agentData);
98
+ await refreshAgentList();
99
+
100
+ // Step 5: Report fork event (using 'add' event type)
101
+ discoverService.reportAgentEvent({
102
+ event: 'add',
103
+ identifier: forkResult.agent.identifier,
104
+ source: location.pathname,
105
+ });
106
+
107
+ message.success(t('fork.success'));
108
+
109
+ // Step 6: Navigate to chat
110
+ navigate(SESSION_CHAT_URL(result!.agentId || result!.sessionId, mobile));
111
+ } catch (error: any) {
112
+ console.error('Fork failed:', error);
113
+ message.error(t('fork.failed'));
114
+ } finally {
115
+ setIsLoading(false);
116
+ }
117
+ };
118
+
119
+ return (
120
+ <Button
121
+ block
122
+ className={styles.buttonGroup}
123
+ loading={isLoading}
124
+ onClick={handleForkAndChat}
125
+ size={'large'}
126
+ type={'primary'}
127
+ >
128
+ {t('fork.forkAndChat')}
129
+ </Button>
130
+ );
131
+ });
132
+
133
+ export default ForkAndChat;
@@ -8,13 +8,13 @@ import { OFFICIAL_URL } from '@/const/url';
8
8
 
9
9
  import ShareButton from '../../../../features/ShareButton';
10
10
  import { useDetailContext } from '../../DetailProvider';
11
- import AddAgent from './AddAgent';
11
+ import ForkAndChat from './ForkAndChat';
12
12
 
13
13
  const ActionButton = memo<{ mobile?: boolean }>(({ mobile }) => {
14
14
  const { avatar, description, tags, title, identifier } = useDetailContext();
15
15
  return (
16
16
  <Flexbox align={'center'} gap={8} horizontal>
17
- <AddAgent mobile={mobile} />
17
+ <ForkAndChat mobile={mobile} />
18
18
  <ShareButton
19
19
  meta={{
20
20
  avatar: avatar,
@@ -1,8 +1,9 @@
1
1
  import { Flexbox } from '@lobehub/ui';
2
2
  import { useResponsive } from 'antd-style';
3
- import { useQueryState } from 'nuqs';
4
3
  import { memo } from 'react';
5
4
 
5
+ import { useQueryState } from '@/hooks/useQueryParam';
6
+
6
7
  import Sidebar from '../Sidebar';
7
8
  import Nav, { GroupAgentNavKey } from './Nav';
8
9
  import Overview from './Overview';
@@ -11,37 +12,33 @@ import Versions from './Versions';
11
12
 
12
13
  const Details = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
13
14
  const { mobile = isMobile } = useResponsive();
14
- const [activeTabParam, setActiveTab] = useQueryState('activeTab');
15
- const activeTab = activeTabParam || GroupAgentNavKey.Overview;
15
+ const [activeTab, setActiveTab] = useQueryState('activeTab', {
16
+ clearOnDefault: true,
17
+ defaultValue: GroupAgentNavKey.Overview,
18
+ });
16
19
 
17
20
  return (
18
21
  <Flexbox gap={24}>
19
- {/* Navigation */}
20
22
  <Nav
21
23
  activeTab={activeTab as GroupAgentNavKey}
22
24
  mobile={mobile}
23
- setActiveTab={(tab) => setActiveTab(tab)}
25
+ setActiveTab={setActiveTab}
24
26
  />
25
-
26
27
  <Flexbox
27
28
  gap={48}
28
29
  horizontal={!mobile}
29
30
  style={mobile ? { flexDirection: 'column-reverse' } : undefined}
30
31
  >
31
- {/* Main Content */}
32
32
  <Flexbox
33
33
  style={{
34
34
  overflow: 'hidden',
35
35
  }}
36
36
  width={'100%'}
37
37
  >
38
- {/* Tab Content */}
39
38
  {activeTab === GroupAgentNavKey.Overview && <Overview />}
40
39
  {activeTab === GroupAgentNavKey.SystemRole && <SystemRole />}
41
40
  {activeTab === GroupAgentNavKey.Versions && <Versions />}
42
41
  </Flexbox>
43
-
44
- {/* Sidebar */}
45
42
  <Sidebar mobile={mobile} />
46
43
  </Flexbox>
47
44
  </Flexbox>
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@lobehub/ui';
4
+ import { App } from 'antd';
5
+ import { createStaticStyles } from 'antd-style';
6
+ import { customAlphabet } from 'nanoid/non-secure';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useNavigate } from 'react-router-dom';
10
+ import urlJoin from 'url-join';
11
+
12
+ import { chatGroupService } from '@/services/chatGroup';
13
+ import { discoverService } from '@/services/discover';
14
+ import { marketApiService } from '@/services/marketApi';
15
+ import { useAgentGroupStore } from '@/store/agentGroup';
16
+
17
+ import { useDetailContext } from '../../DetailProvider';
18
+
19
+ const styles = createStaticStyles(({ css }) => ({
20
+ buttonGroup: css`
21
+ width: 100%;
22
+ `,
23
+ }));
24
+
25
+ /**
26
+ * Generate a market identifier (8-character lowercase alphanumeric string)
27
+ */
28
+ const generateMarketIdentifier = () => {
29
+ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
30
+ const generate = customAlphabet(alphabet, 8);
31
+ return generate();
32
+ };
33
+
34
+ const ForkGroupAndChat = memo<{ mobile?: boolean }>(() => {
35
+ const {
36
+ avatar,
37
+ backgroundColor,
38
+ description,
39
+ tags,
40
+ title,
41
+ config,
42
+ identifier,
43
+ memberAgents = [],
44
+ } = useDetailContext();
45
+ const [isLoading, setIsLoading] = useState(false);
46
+ const { message } = App.useApp();
47
+ const { t } = useTranslation('discover');
48
+ const navigate = useNavigate();
49
+ const loadGroups = useAgentGroupStore((s) => s.loadGroups);
50
+
51
+ const meta = {
52
+ avatar,
53
+ backgroundColor,
54
+ description,
55
+ tags,
56
+ title,
57
+ };
58
+
59
+ const handleForkAndChat = async () => {
60
+ try {
61
+ setIsLoading(true);
62
+
63
+ // Step 1: Check if user has already forked this group
64
+ const existingGroupId = await chatGroupService.getGroupByForkedFromIdentifier(identifier!);
65
+
66
+ if (existingGroupId) {
67
+ // User has already forked this group, navigate to existing fork
68
+ message.info(t('fork.alreadyForked'));
69
+ navigate(urlJoin('/group', existingGroupId));
70
+ return;
71
+ }
72
+
73
+ if (!config) {
74
+ message.error(
75
+ t('groupAgents.noConfig', { defaultValue: 'Group configuration not available' }),
76
+ );
77
+ return;
78
+ }
79
+
80
+ // Generate a unique identifier for the forked group
81
+ const newIdentifier = generateMarketIdentifier();
82
+
83
+ // Step 2: Fork the group via Market API
84
+ const forkResult = await marketApiService.forkAgentGroup(identifier!, {
85
+ identifier: newIdentifier,
86
+ name: title,
87
+ status: 'published',
88
+ visibility: 'public',
89
+ });
90
+
91
+ // Step 3: Find supervisor from memberAgents
92
+ const supervisorMember = memberAgents.find((member: any) => {
93
+ const agent = member.agent || member;
94
+ const role = member.role || agent.role;
95
+ return role === 'supervisor';
96
+ });
97
+
98
+ // Prepare supervisor config
99
+ let supervisorConfig;
100
+ if (supervisorMember) {
101
+ const member = supervisorMember as any;
102
+ const agent = member.agent || member;
103
+ const currentVersion = member.currentVersion || member;
104
+ const rawConfig = {
105
+ avatar: currentVersion.avatar,
106
+ backgroundColor: currentVersion.backgroundColor,
107
+ description: currentVersion.description,
108
+ model: currentVersion.config?.model || currentVersion.model,
109
+ params: currentVersion.config?.params || currentVersion.params,
110
+ provider: currentVersion.config?.provider || currentVersion.provider,
111
+ systemRole:
112
+ currentVersion.config?.systemRole ||
113
+ currentVersion.config?.systemPrompt ||
114
+ currentVersion.systemRole ||
115
+ currentVersion.content,
116
+ tags: currentVersion.tags,
117
+ title: currentVersion.name || agent.name || 'Supervisor',
118
+ };
119
+ // Filter out null/undefined values
120
+ supervisorConfig = Object.fromEntries(
121
+ // eslint-disable-next-line eqeqeq, @typescript-eslint/no-unused-vars
122
+ Object.entries(rawConfig).filter(([_, v]) => v != null),
123
+ );
124
+ }
125
+
126
+ // Step 4: Prepare group config
127
+ const groupConfig = {
128
+ config: {
129
+ ...config,
130
+ forkedFromIdentifier: identifier, // Store the source group identifier
131
+ },
132
+ // Group content is the supervisor's systemRole (for backward compatibility)
133
+ content: supervisorConfig?.systemRole || config.systemRole,
134
+ ...meta,
135
+ };
136
+
137
+ // Step 5: Prepare member agents from market data
138
+ // Filter out supervisor role as it will be created separately using supervisorConfig
139
+ const members = memberAgents
140
+ .filter((member: any) => {
141
+ const agent = member.agent || member;
142
+ const role = member.role || agent.role;
143
+ return role !== 'supervisor';
144
+ })
145
+ .map((member: any) => {
146
+ const agent = member.agent || member;
147
+ const currentVersion = member.currentVersion || member;
148
+ return {
149
+ avatar: currentVersion.avatar,
150
+ backgroundColor: currentVersion.backgroundColor,
151
+ description: currentVersion.description,
152
+ model: currentVersion.config?.model || currentVersion.model,
153
+ plugins: currentVersion.plugins,
154
+ provider: currentVersion.config?.provider || currentVersion.provider,
155
+ systemRole:
156
+ currentVersion.config?.systemRole ||
157
+ currentVersion.config?.systemPrompt ||
158
+ currentVersion.systemRole ||
159
+ currentVersion.content,
160
+ tags: currentVersion.tags,
161
+ title: currentVersion.name || agent.name,
162
+ };
163
+ });
164
+
165
+ // Step 6: Create group with all members in one request
166
+ const result = await chatGroupService.createGroupWithMembers(
167
+ groupConfig,
168
+ members,
169
+ supervisorConfig,
170
+ );
171
+
172
+ // Refresh group list
173
+ await loadGroups();
174
+
175
+ // Step 7: Report fork event (using 'add' event type)
176
+ discoverService.reportAgentEvent({
177
+ event: 'add',
178
+ identifier: forkResult.group.identifier,
179
+ source: location.pathname,
180
+ });
181
+
182
+ message.success(t('fork.success'));
183
+
184
+ // Step 8: Navigate to chat
185
+ navigate(urlJoin('/group', result.groupId));
186
+ } catch (error: any) {
187
+ console.error('Fork group failed:', error);
188
+ message.error(t('fork.failed'));
189
+ } finally {
190
+ setIsLoading(false);
191
+ }
192
+ };
193
+
194
+ return (
195
+ <Button
196
+ block
197
+ className={styles.buttonGroup}
198
+ loading={isLoading}
199
+ onClick={handleForkAndChat}
200
+ size={'large'}
201
+ type={'primary'}
202
+ >
203
+ {t('fork.forkAndChat')}
204
+ </Button>
205
+ );
206
+ });
207
+
208
+ export default ForkGroupAndChat;
@@ -8,14 +8,14 @@ import { OFFICIAL_URL } from '@/const/url';
8
8
 
9
9
  import ShareButton from '../../../../features/ShareButton';
10
10
  import { useDetailContext } from '../../DetailProvider';
11
- import AddGroupAgent from './AddGroupAgent';
11
+ import ForkGroupAndChat from './ForkGroupAndChat';
12
12
 
13
13
  const ActionButton = memo<{ mobile?: boolean }>(({ mobile }) => {
14
14
  const { avatar, title, description, tags, identifier } = useDetailContext();
15
15
 
16
16
  return (
17
17
  <Flexbox align={'center'} gap={8} horizontal>
18
- <AddGroupAgent mobile={mobile} />
18
+ <ForkGroupAndChat mobile={mobile} />
19
19
  {identifier && (
20
20
  <ShareButton
21
21
  meta={{
@@ -13,6 +13,8 @@ export interface UserDetailContextConfig {
13
13
  agentCount: number;
14
14
  agentGroups?: DiscoverGroupAgentItem[];
15
15
  agents: DiscoverAssistantItem[];
16
+ forkedAgentGroups?: DiscoverGroupAgentItem[];
17
+ forkedAgents?: DiscoverAssistantItem[];
16
18
  groupCount: number;
17
19
  isOwner: boolean;
18
20
  mobile?: boolean;
@@ -3,16 +3,23 @@
3
3
  import { Flexbox } from '@lobehub/ui';
4
4
  import { memo } from 'react';
5
5
 
6
+ import { useUserDetailContext } from './DetailProvider';
6
7
  import UserAgentList from './UserAgentList';
7
8
  import UserFavoriteAgents from './UserFavoriteAgents';
8
9
  import UserFavoritePlugins from './UserFavoritePlugins';
10
+ import UserForkedAgentGroups from './UserForkedAgentGroups';
11
+ import UserForkedAgents from './UserForkedAgents';
9
12
  import UserGroupList from './UserGroupList';
10
13
 
11
14
  const UserContent = memo(() => {
15
+ const { forkedAgents, forkedAgentGroups } = useUserDetailContext();
16
+
12
17
  return (
13
18
  <Flexbox gap={32}>
14
19
  <UserAgentList />
15
20
  <UserGroupList />
21
+ <UserForkedAgents agents={forkedAgents} />
22
+ <UserForkedAgentGroups agentGroups={forkedAgentGroups} />
16
23
  <UserFavoriteAgents />
17
24
  <UserFavoritePlugins />
18
25
  </Flexbox>