@lobehub/lobehub 2.0.0-next.367 → 2.0.0-next.369

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 (107) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/auth.json +2 -1
  4. package/locales/ar/models.json +6 -14
  5. package/locales/ar/providers.json +1 -0
  6. package/locales/ar/setting.json +1 -0
  7. package/locales/bg-BG/auth.json +2 -1
  8. package/locales/bg-BG/models.json +33 -7
  9. package/locales/bg-BG/providers.json +1 -0
  10. package/locales/bg-BG/setting.json +1 -0
  11. package/locales/de-DE/auth.json +2 -1
  12. package/locales/de-DE/models.json +32 -14
  13. package/locales/de-DE/providers.json +1 -0
  14. package/locales/de-DE/setting.json +1 -0
  15. package/locales/en-US/auth.json +1 -1
  16. package/locales/en-US/models.json +16 -15
  17. package/locales/en-US/providers.json +1 -0
  18. package/locales/en-US/setting.json +1 -0
  19. package/locales/es-ES/auth.json +2 -1
  20. package/locales/es-ES/models.json +5 -13
  21. package/locales/es-ES/providers.json +1 -0
  22. package/locales/es-ES/setting.json +1 -0
  23. package/locales/fa-IR/auth.json +2 -1
  24. package/locales/fa-IR/models.json +38 -12
  25. package/locales/fa-IR/providers.json +1 -0
  26. package/locales/fa-IR/setting.json +1 -0
  27. package/locales/fr-FR/auth.json +2 -1
  28. package/locales/fr-FR/models.json +43 -15
  29. package/locales/fr-FR/providers.json +1 -0
  30. package/locales/fr-FR/setting.json +1 -0
  31. package/locales/it-IT/auth.json +2 -1
  32. package/locales/it-IT/models.json +51 -10
  33. package/locales/it-IT/providers.json +1 -0
  34. package/locales/it-IT/setting.json +1 -0
  35. package/locales/ja-JP/auth.json +2 -1
  36. package/locales/ja-JP/models.json +33 -15
  37. package/locales/ja-JP/providers.json +1 -0
  38. package/locales/ja-JP/setting.json +1 -0
  39. package/locales/ko-KR/auth.json +2 -1
  40. package/locales/ko-KR/models.json +47 -8
  41. package/locales/ko-KR/providers.json +1 -0
  42. package/locales/ko-KR/setting.json +1 -0
  43. package/locales/nl-NL/auth.json +2 -1
  44. package/locales/nl-NL/models.json +5 -13
  45. package/locales/nl-NL/providers.json +1 -0
  46. package/locales/nl-NL/setting.json +1 -0
  47. package/locales/pl-PL/auth.json +2 -1
  48. package/locales/pl-PL/models.json +35 -13
  49. package/locales/pl-PL/providers.json +1 -0
  50. package/locales/pl-PL/setting.json +1 -0
  51. package/locales/pt-BR/auth.json +2 -1
  52. package/locales/pt-BR/models.json +50 -13
  53. package/locales/pt-BR/providers.json +1 -0
  54. package/locales/pt-BR/setting.json +1 -0
  55. package/locales/ru-RU/auth.json +2 -1
  56. package/locales/ru-RU/models.json +6 -9
  57. package/locales/ru-RU/providers.json +1 -0
  58. package/locales/ru-RU/setting.json +1 -0
  59. package/locales/tr-TR/auth.json +2 -1
  60. package/locales/tr-TR/models.json +49 -15
  61. package/locales/tr-TR/providers.json +1 -0
  62. package/locales/tr-TR/setting.json +1 -0
  63. package/locales/vi-VN/auth.json +2 -1
  64. package/locales/vi-VN/models.json +34 -7
  65. package/locales/vi-VN/providers.json +1 -0
  66. package/locales/vi-VN/setting.json +1 -0
  67. package/locales/zh-CN/auth.json +1 -1
  68. package/locales/zh-CN/models.json +48 -12
  69. package/locales/zh-CN/providers.json +1 -0
  70. package/locales/zh-CN/setting.json +1 -0
  71. package/locales/zh-TW/auth.json +2 -1
  72. package/locales/zh-TW/models.json +31 -14
  73. package/locales/zh-TW/providers.json +1 -0
  74. package/locales/zh-TW/setting.json +1 -0
  75. package/package.json +1 -1
  76. package/packages/types/src/agentGroup/index.ts +4 -0
  77. package/src/app/[variants]/(main)/agent/profile/features/Header/AgentForkTag.tsx +86 -0
  78. package/src/app/[variants]/(main)/agent/profile/features/Header/AgentStatusTag.tsx +82 -0
  79. package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/AgentHeader.tsx +27 -15
  80. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/ForkGroupAndChat.tsx +1 -0
  81. package/src/app/[variants]/(main)/group/profile/features/GroupProfile/GroupForkTag.tsx +80 -0
  82. package/src/app/[variants]/(main)/group/profile/features/GroupProfile/GroupHeader.tsx +27 -15
  83. package/src/app/[variants]/(main)/group/profile/features/GroupProfile/GroupStatusTag.tsx +83 -0
  84. package/src/app/[variants]/(main)/settings/skill/features/Actions.tsx +0 -3
  85. package/src/app/[variants]/(main)/settings/skill/features/McpSkillItem.tsx +9 -3
  86. package/src/app/[variants]/(main)/settings/skill/features/SkillList.tsx +35 -1
  87. package/src/components/Plugins/PluginTag.tsx +36 -23
  88. package/src/features/ChatInput/ActionBar/Tools/PopoverContent.tsx +3 -3
  89. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +96 -78
  90. package/src/features/PluginDetailModal/index.tsx +1 -0
  91. package/src/features/ProfileEditor/AgentTool.tsx +141 -126
  92. package/src/features/ProfileEditor/Empty.tsx +22 -0
  93. package/src/features/ProfileEditor/PluginTag.tsx +33 -7
  94. package/src/features/ProfileEditor/PopoverContent.tsx +142 -0
  95. package/src/features/SkillStore/CommunityList/Item.tsx +5 -2
  96. package/src/features/SkillStore/CustomList/Item.tsx +149 -0
  97. package/src/features/SkillStore/CustomList/index.tsx +71 -0
  98. package/src/features/SkillStore/Empty.tsx +3 -0
  99. package/src/features/SkillStore/Search/index.tsx +5 -0
  100. package/src/features/SkillStore/SkillStoreContent.tsx +10 -2
  101. package/src/features/SkillStore/index.tsx +8 -1
  102. package/src/libs/trpc/lambda/middleware/marketUserInfo.ts +13 -1
  103. package/src/locales/default/setting.ts +1 -0
  104. package/src/services/marketApi.ts +1 -1
  105. package/src/store/agentGroup/selectors/byId.ts +1 -0
  106. package/src/store/tool/slices/customPlugin/initialState.ts +2 -0
  107. package/src/store/tool/slices/mcpStore/action.ts +1 -0
@@ -9,7 +9,7 @@ import {
9
9
  import { Avatar, Icon, Tag } from '@lobehub/ui';
10
10
  import { createStaticStyles, cssVar } from 'antd-style';
11
11
  import isEqual from 'fast-deep-equal';
12
- import { AlertCircle, X } from 'lucide-react';
12
+ import { AlertCircle, Loader2, X } from 'lucide-react';
13
13
  import React, { memo, useMemo } from 'react';
14
14
  import { useTranslation } from 'react-i18next';
15
15
 
@@ -51,6 +51,21 @@ const LobehubSkillIcon = memo<Pick<LobehubSkillProviderType, 'icon' | 'label'>>(
51
51
  );
52
52
 
53
53
  const styles = createStaticStyles(({ css, cssVar }) => ({
54
+ loadingIcon: css`
55
+ flex-shrink: 0;
56
+ color: ${cssVar.colorTextSecondary};
57
+ animation: spin 1s linear infinite;
58
+
59
+ @keyframes spin {
60
+ from {
61
+ transform: rotate(0deg);
62
+ }
63
+
64
+ to {
65
+ transform: rotate(360deg);
66
+ }
67
+ }
68
+ `,
54
69
  notInstalledTag: css`
55
70
  border-color: ${cssVar.colorWarningBorder};
56
71
  background: ${cssVar.colorWarningBg};
@@ -196,11 +211,18 @@ const PluginTag = memo<PluginTagProps>(
196
211
  type: 'plugin' as const,
197
212
  };
198
213
 
199
- const displayTitle = isLoading ? 'Loading...' : meta.title;
214
+ // Use identifier as title when loading, otherwise use meta.title
215
+ const displayTitle = meta.title;
200
216
  const isDesktopOnly = showDesktopOnlyLabel && !meta.availableInWeb;
201
217
 
202
218
  // Render icon based on type
203
219
  const renderIcon = () => {
220
+ // Show loading spinner when loading
221
+ if (isLoading) {
222
+ return <Loader2 className={styles.loadingIcon} size={14} />;
223
+ }
224
+
225
+ // Show warning icon when not installed
204
226
  if (!meta.isInstalled) {
205
227
  return <AlertCircle className={styles.warningIcon} size={14} />;
206
228
  }
@@ -234,24 +256,28 @@ const PluginTag = memo<PluginTagProps>(
234
256
  if (isDesktopOnly) {
235
257
  text += ` (${t('tools.desktopOnly', { defaultValue: 'Desktop Only' })})`;
236
258
  }
237
- if (!meta.isInstalled) {
259
+ // Don't show "Not Installed" when loading
260
+ if (!meta.isInstalled && !isLoading) {
238
261
  text += ` (${t('tools.notInstalled', { defaultValue: 'Not Installed' })})`;
239
262
  }
240
263
  return text;
241
264
  };
242
265
 
266
+ // Only show error state when not installed and not loading
267
+ const showErrorState = !meta.isInstalled && !isLoading;
268
+
243
269
  return (
244
270
  <Tag
245
271
  className={styles.tag}
246
272
  closable
247
273
  closeIcon={<X size={12} />}
248
- color={meta.isInstalled ? undefined : 'error'}
274
+ color={showErrorState ? 'error' : undefined}
249
275
  icon={renderIcon()}
250
276
  onClose={onRemove}
251
277
  title={
252
- meta.isInstalled
253
- ? undefined
254
- : t('tools.notInstalledWarning', { defaultValue: 'This tool is not installed' })
278
+ showErrorState
279
+ ? t('tools.notInstalledWarning', { defaultValue: 'This tool is not installed' })
280
+ : undefined
255
281
  }
256
282
  variant={isDarkMode ? 'filled' : 'outlined'}
257
283
  >
@@ -0,0 +1,142 @@
1
+ import { Flexbox, Icon, type ItemType, Segmented } from '@lobehub/ui';
2
+ import { createStaticStyles, cssVar } from 'antd-style';
3
+ import { ArrowRight, ExternalLink, Settings, Store } from 'lucide-react';
4
+ import { type ReactNode, memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useNavigate } from 'react-router-dom';
7
+
8
+ import Empty from './Empty';
9
+
10
+ type TabType = 'all' | 'installed';
11
+
12
+ const prefixCls = 'ant';
13
+
14
+ const styles = createStaticStyles(({ css }) => ({
15
+ dropdown: css`
16
+ overflow: hidden;
17
+ width: 100%;
18
+
19
+ .${prefixCls}-dropdown-menu {
20
+ border-radius: 0 !important;
21
+ background: transparent !important;
22
+ box-shadow: none !important;
23
+ }
24
+ `,
25
+ footerItem: css`
26
+ cursor: pointer;
27
+
28
+ display: flex;
29
+ gap: 12px;
30
+ align-items: center;
31
+
32
+ padding-block: 8px;
33
+ padding-inline: 12px;
34
+ border-radius: 6px;
35
+
36
+ transition: background-color 0.2s;
37
+
38
+ &:hover {
39
+ background: ${cssVar.colorFillTertiary};
40
+ }
41
+ `,
42
+ footerItemContent: css`
43
+ flex: 1;
44
+ min-width: 0;
45
+ `,
46
+ footerItemIcon: css`
47
+ display: flex;
48
+ flex-shrink: 0;
49
+ align-items: center;
50
+ justify-content: center;
51
+
52
+ width: 24px;
53
+ height: 24px;
54
+ `,
55
+ header: css`
56
+ padding: ${cssVar.paddingXS};
57
+ border-block-end: 1px solid ${cssVar.colorBorderSecondary};
58
+ background: transparent;
59
+ `,
60
+ scroller: css`
61
+ overflow: hidden auto;
62
+ `,
63
+ trailingIcon: css`
64
+ opacity: 0.5;
65
+ `,
66
+ }));
67
+
68
+ interface PopoverContentProps {
69
+ activeTab: TabType;
70
+ installedTabItems: ItemType[];
71
+ menu: ReactNode;
72
+ onClose?: () => void;
73
+ onOpenStore: () => void;
74
+ onTabChange: (tab: TabType) => void;
75
+ }
76
+
77
+ const PopoverContent = memo<PopoverContentProps>(
78
+ ({ menu, activeTab, onTabChange, installedTabItems, onOpenStore, onClose }) => {
79
+ const { t } = useTranslation('setting');
80
+ const navigate = useNavigate();
81
+
82
+ return (
83
+ <Flexbox className={styles.dropdown} style={{ maxHeight: 500 }}>
84
+ {/* stopPropagation prevents dropdown's onClick from calling preventDefault on Segmented */}
85
+ <div className={styles.header} onClick={(e) => e.stopPropagation()}>
86
+ <Segmented
87
+ block
88
+ onChange={(v) => onTabChange(v as TabType)}
89
+ options={[
90
+ {
91
+ label: t('tools.tabs.all', { defaultValue: 'All' }),
92
+ value: 'all',
93
+ },
94
+ {
95
+ label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
96
+ value: 'installed',
97
+ },
98
+ ]}
99
+ size="small"
100
+ value={activeTab}
101
+ />
102
+ </div>
103
+ <div className={styles.scroller} style={{ flex: 1 }}>
104
+ {activeTab === 'installed' && installedTabItems.length === 0 ? <Empty /> : menu}
105
+ </div>
106
+ <div
107
+ style={{
108
+ borderBlockStart: `1px solid ${cssVar.colorBorderSecondary}`,
109
+ padding: 4,
110
+ }}
111
+ >
112
+ <div className={styles.footerItem} onClick={onOpenStore} role="button" tabIndex={0}>
113
+ <div className={styles.footerItemIcon}>
114
+ <Icon icon={Store} size={20} />
115
+ </div>
116
+ <div className={styles.footerItemContent}>{t('skillStore.title')}</div>
117
+ <Icon className={styles.trailingIcon} icon={ArrowRight} size={16} />
118
+ </div>
119
+ <div
120
+ className={styles.footerItem}
121
+ onClick={() => {
122
+ onClose?.();
123
+ navigate('/settings/skill');
124
+ }}
125
+ role="button"
126
+ tabIndex={0}
127
+ >
128
+ <div className={styles.footerItemIcon}>
129
+ <Icon icon={Settings} size={20} />
130
+ </div>
131
+ <div className={styles.footerItemContent}>{t('tools.plugins.management')}</div>
132
+ <Icon className={styles.trailingIcon} icon={ExternalLink} size={16} />
133
+ </div>
134
+ </div>
135
+ </Flexbox>
136
+ );
137
+ },
138
+ );
139
+
140
+ PopoverContent.displayName = 'PopoverContent';
141
+
142
+ export default PopoverContent;
@@ -4,12 +4,13 @@ import { ActionIcon, Block, DropdownMenu, Flexbox, Icon, Modal } from '@lobehub/
4
4
  import { App, Button } from 'antd';
5
5
  import isEqual from 'fast-deep-equal';
6
6
  import { MoreVerticalIcon, Plus, Trash2 } from 'lucide-react';
7
- import React, { memo, useState } from 'react';
7
+ import React, { Suspense, memo, useState } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
10
  import PluginAvatar from '@/components/Plugins/PluginAvatar';
11
11
  import McpDetail from '@/features/MCP/MCPDetail';
12
12
  import MCPInstallProgress from '@/features/MCP/MCPInstallProgress';
13
+ import McpDetailLoading from '@/features/MCP/MCPDetail/Loading';
13
14
  import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
14
15
  import { useAgentStore } from '@/store/agent';
15
16
  import { agentSelectors } from '@/store/agent/selectors';
@@ -148,7 +149,9 @@ const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) =>
148
149
  title={t('dev.title.skillDetails')}
149
150
  width={800}
150
151
  >
151
- <McpDetail identifier={identifier} noSettings />
152
+ <Suspense fallback={<McpDetailLoading />}>
153
+ <McpDetail identifier={identifier} noSettings />
154
+ </Suspense>
152
155
  </Modal>
153
156
  </>
154
157
  );
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+
3
+ import { ActionIcon, Block, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
4
+ import { App } from 'antd';
5
+ import { createStaticStyles, cssVar } from 'antd-style';
6
+ import { MoreVerticalIcon, PackageSearch, Trash2 } from 'lucide-react';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import PluginAvatar from '@/components/Plugins/PluginAvatar';
11
+ import PluginDetailModal from '@/features/PluginDetailModal';
12
+ import DevModal from '@/features/PluginDevModal';
13
+ import { useAgentStore } from '@/store/agent';
14
+ import { agentSelectors } from '@/store/agent/selectors';
15
+ import { useToolStore } from '@/store/tool';
16
+ import { pluginSelectors } from '@/store/tool/selectors';
17
+
18
+ import { itemStyles } from '../style';
19
+
20
+ const styles = createStaticStyles(({ css }) => ({
21
+ title: css`
22
+ cursor: pointer;
23
+
24
+ overflow: hidden;
25
+
26
+ font-size: 14px;
27
+ font-weight: 500;
28
+ color: ${cssVar.colorText};
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+
32
+ &:hover {
33
+ color: ${cssVar.colorPrimary};
34
+ }
35
+ `,
36
+ }));
37
+
38
+ interface ItemProps {
39
+ avatar?: string;
40
+ description?: string;
41
+ identifier: string;
42
+ title?: string;
43
+ }
44
+
45
+ const Item = memo<ItemProps>(({ identifier, title, description, avatar }) => {
46
+ const { t } = useTranslation('plugin');
47
+ const { modal } = App.useApp();
48
+ const [configOpen, setConfigOpen] = useState(false);
49
+ const [detailOpen, setDetailOpen] = useState(false);
50
+
51
+ const [customPlugin, uninstallPlugin, updateCustomPlugin, pluginManifest] = useToolStore((s) => [
52
+ pluginSelectors.getCustomPluginById(identifier)(s),
53
+ s.uninstallPlugin,
54
+ s.updateCustomPlugin,
55
+ pluginSelectors.getToolManifestById(identifier)(s),
56
+ ]);
57
+
58
+ const [togglePlugin, isPluginEnabledInAgent] = useAgentStore((s) => [
59
+ s.togglePlugin,
60
+ agentSelectors.currentAgentPlugins(s).includes(identifier),
61
+ ]);
62
+
63
+ const handleDelete = () => {
64
+ modal.confirm({
65
+ centered: true,
66
+ okButtonProps: { danger: true },
67
+ onOk: async () => {
68
+ if (isPluginEnabledInAgent) {
69
+ await togglePlugin(identifier, false);
70
+ }
71
+ await uninstallPlugin(identifier);
72
+ },
73
+ title: t('store.actions.confirmUninstall'),
74
+ type: 'error',
75
+ });
76
+ };
77
+
78
+ return (
79
+ <>
80
+ <Flexbox className={itemStyles.container} gap={0}>
81
+ <Block
82
+ align={'center'}
83
+ gap={12}
84
+ horizontal
85
+ paddingBlock={12}
86
+ paddingInline={12}
87
+ variant={'filled'}
88
+ >
89
+ <PluginAvatar avatar={avatar} size={40} />
90
+ <Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
91
+ <span className={styles.title} onClick={() => setDetailOpen(true)}>
92
+ {title || identifier}
93
+ </span>
94
+ {description && <span className={itemStyles.description}>{description}</span>}
95
+ </Flexbox>
96
+ <Flexbox horizontal>
97
+ <ActionIcon
98
+ icon={PackageSearch}
99
+ onClick={() => setConfigOpen(true)}
100
+ title={t('store.actions.manifest')}
101
+ />
102
+ <DropdownMenu
103
+ items={[
104
+ {
105
+ danger: true,
106
+ icon: <Icon icon={Trash2} />,
107
+ key: 'uninstall',
108
+ label: t('store.actions.uninstall'),
109
+ onClick: handleDelete,
110
+ },
111
+ ]}
112
+ placement="bottomRight"
113
+ >
114
+ <ActionIcon icon={MoreVerticalIcon} />
115
+ </DropdownMenu>
116
+ </Flexbox>
117
+ </Block>
118
+ </Flexbox>
119
+ {customPlugin && (
120
+ <DevModal
121
+ mode="edit"
122
+ onDelete={async () => {
123
+ if (isPluginEnabledInAgent) {
124
+ await togglePlugin(identifier, false);
125
+ }
126
+ await uninstallPlugin(identifier);
127
+ }}
128
+ onOpenChange={setConfigOpen}
129
+ onSave={async (value) => {
130
+ await updateCustomPlugin(identifier, value);
131
+ }}
132
+ open={configOpen}
133
+ value={customPlugin}
134
+ />
135
+ )}
136
+ <PluginDetailModal
137
+ id={identifier}
138
+ onClose={() => setDetailOpen(false)}
139
+ open={detailOpen}
140
+ schema={pluginManifest?.settings}
141
+ tab="info"
142
+ />
143
+ </>
144
+ );
145
+ });
146
+
147
+ Item.displayName = 'CustomListItem';
148
+
149
+ export default Item;
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import { createStaticStyles, responsive } from 'antd-style';
4
+ import isEqual from 'fast-deep-equal';
5
+ import { memo, useMemo } from 'react';
6
+
7
+ import { useToolStore } from '@/store/tool';
8
+ import { pluginSelectors } from '@/store/tool/selectors';
9
+
10
+ import Empty from '../Empty';
11
+ import Item from './Item';
12
+
13
+ const styles = createStaticStyles(({ css }) => ({
14
+ container: css`
15
+ min-height: 50vh;
16
+ `,
17
+ grid: css`
18
+ display: grid;
19
+ grid-template-columns: repeat(2, 1fr);
20
+ gap: 12px;
21
+
22
+ padding-block-end: 16px;
23
+ padding-inline: 16px;
24
+
25
+ ${responsive.sm} {
26
+ grid-template-columns: 1fr;
27
+ }
28
+ `,
29
+ }));
30
+
31
+ export const CustomList = memo(() => {
32
+ const customPlugins = useToolStore(pluginSelectors.installedCustomPluginMetaList, isEqual);
33
+ const searchKeywords = useToolStore((s) => s.customPluginSearchKeywords || '');
34
+
35
+ const filteredItems = useMemo(() => {
36
+ const lowerKeywords = searchKeywords.toLowerCase().trim();
37
+ if (!lowerKeywords) return customPlugins;
38
+
39
+ return customPlugins.filter((plugin) => {
40
+ const title = plugin.title?.toLowerCase() || '';
41
+ const identifier = plugin.identifier?.toLowerCase() || '';
42
+ return title.includes(lowerKeywords) || identifier.includes(lowerKeywords);
43
+ });
44
+ }, [customPlugins, searchKeywords]);
45
+
46
+ const hasSearchKeywords = Boolean(searchKeywords && searchKeywords.trim());
47
+
48
+ if (filteredItems.length === 0) {
49
+ return <Empty search={hasSearchKeywords} />;
50
+ }
51
+
52
+ return (
53
+ <div className={styles.container}>
54
+ <div className={styles.grid}>
55
+ {filteredItems.map((plugin) => (
56
+ <Item
57
+ avatar={plugin.avatar}
58
+ description={plugin.description}
59
+ identifier={plugin.identifier}
60
+ key={plugin.identifier}
61
+ title={plugin.title}
62
+ />
63
+ ))}
64
+ </div>
65
+ </div>
66
+ );
67
+ });
68
+
69
+ CustomList.displayName = 'CustomList';
70
+
71
+ export default CustomList;
@@ -3,6 +3,8 @@ import { Plug2 } from 'lucide-react';
3
3
  import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
+ import AddSkillButton from './AddSkillButton';
7
+
6
8
  interface SkillEmptyProps extends Omit<EmptyProps, 'icon'> {
7
9
  search?: boolean;
8
10
  }
@@ -13,6 +15,7 @@ const Empty = memo<SkillEmptyProps>(({ search, ...rest }) => {
13
15
  return (
14
16
  <Center height="100%" style={{ minHeight: '50vh' }} width="100%">
15
17
  <EmptyComponent
18
+ action={!search && <AddSkillButton />}
16
19
  description={search ? t('skillStore.emptySearch') : t('skillStore.empty')}
17
20
  descriptionProps={{
18
21
  fontSize: 14,
@@ -17,6 +17,8 @@ export const Search = memo<SearchProps>(({ activeTab, onLobeHubSearch }) => {
17
17
  const { t } = useTranslation('setting');
18
18
  const mcpKeywords = useToolStore((s) => s.mcpSearchKeywords);
19
19
 
20
+ const isCustomTab = activeTab === SkillStoreTab.Custom;
21
+
20
22
  const keywords = activeTab === SkillStoreTab.Community ? mcpKeywords : '';
21
23
 
22
24
  return (
@@ -28,11 +30,14 @@ export const Search = memo<SearchProps>(({ activeTab, onLobeHubSearch }) => {
28
30
  onSearch={(keywords: string) => {
29
31
  if (activeTab === SkillStoreTab.Community) {
30
32
  useToolStore.setState({ mcpSearchKeywords: keywords, searchLoading: true });
33
+ } else if (isCustomTab) {
34
+ useToolStore.setState({ customPluginSearchKeywords: keywords });
31
35
  } else {
32
36
  onLobeHubSearch(keywords);
33
37
  }
34
38
  }}
35
39
  placeholder={t('skillStore.search')}
40
+ variant="filled"
36
41
  />
37
42
  </Flexbox>
38
43
  </Flexbox>
@@ -7,11 +7,13 @@ import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import AddSkillButton from './AddSkillButton';
9
9
  import CommunityList from './CommunityList';
10
+ import CustomList from './CustomList';
10
11
  import LobeHubList from './LobeHubList';
11
12
  import Search from './Search';
12
13
 
13
14
  export enum SkillStoreTab {
14
15
  Community = 'community',
16
+ Custom = 'custom',
15
17
  LobeHub = 'lobehub',
16
18
  }
17
19
 
@@ -23,14 +25,17 @@ export const SkillStoreContent = () => {
23
25
  const options: SegmentedOptions = [
24
26
  { label: t('skillStore.tabs.lobehub'), value: SkillStoreTab.LobeHub },
25
27
  { label: t('skillStore.tabs.community'), value: SkillStoreTab.Community },
28
+ { label: t('skillStore.tabs.custom'), value: SkillStoreTab.Custom },
26
29
  ];
27
30
 
28
31
  const isLobeHub = activeTab === SkillStoreTab.LobeHub;
32
+ const isCommunity = activeTab === SkillStoreTab.Community;
33
+ const isCustom = activeTab === SkillStoreTab.Custom;
29
34
 
30
35
  return (
31
36
  <Flexbox gap={8} style={{ maxHeight: '75vh' }} width={'100%'}>
32
37
  <Flexbox gap={8} paddingInline={16}>
33
- <Flexbox align="center" gap={8} horizontal>
38
+ <Flexbox align={'center'} gap={8} horizontal>
34
39
  <Segmented
35
40
  block
36
41
  onChange={(v) => setActiveTab(v as SkillStoreTab)}
@@ -46,9 +51,12 @@ export const SkillStoreContent = () => {
46
51
  <Flexbox flex={1} style={{ display: isLobeHub ? 'flex' : 'none', overflow: 'auto' }}>
47
52
  <LobeHubList keywords={lobehubKeywords} />
48
53
  </Flexbox>
49
- <Flexbox flex={1} style={{ display: !isLobeHub ? 'flex' : 'none', overflow: 'auto' }}>
54
+ <Flexbox flex={1} style={{ display: isCommunity ? 'flex' : 'none', overflow: 'auto' }}>
50
55
  <CommunityList />
51
56
  </Flexbox>
57
+ <Flexbox flex={1} style={{ display: isCustom ? 'flex' : 'none', overflow: 'auto' }}>
58
+ <CustomList />
59
+ </Flexbox>
52
60
  </Flexbox>
53
61
  );
54
62
  };
@@ -3,12 +3,19 @@
3
3
  import { createModal } from '@lobehub/ui';
4
4
  import { t } from 'i18next';
5
5
 
6
+ import { isDesktop } from '@/const/version';
7
+ import { MarketAuthProvider } from '@/layout/AuthProvider/MarketAuth';
8
+
6
9
  import { SkillStoreContent } from './SkillStoreContent';
7
10
 
8
11
  export const createSkillStoreModal = () =>
9
12
  createModal({
10
13
  allowFullscreen: true,
11
- children: <SkillStoreContent />,
14
+ children: (
15
+ <MarketAuthProvider isDesktop={isDesktop}>
16
+ <SkillStoreContent />
17
+ </MarketAuthProvider>
18
+ ),
12
19
  destroyOnHidden: false,
13
20
  footer: null,
14
21
  styles: {
@@ -6,6 +6,7 @@ import type { TrustedClientUserInfo } from '@/libs/trusted-client';
6
6
  import { trpc } from '../init';
7
7
 
8
8
  interface ContextWithServerDB {
9
+ marketAccessToken?: string;
9
10
  serverDB?: LobeChatDatabase;
10
11
  userId?: string | null;
11
12
  }
@@ -39,8 +40,19 @@ export const marketUserInfo = trpc.middleware(async (opts) => {
39
40
  userId: ctx.userId,
40
41
  };
41
42
 
43
+ // Fetch market access token from user_settings.market
44
+ const userModel = new UserModel(ctx.serverDB, ctx.userId);
45
+ const userSettings = await userModel.getUserSettings();
46
+ const marketTokenFromDB = (userSettings?.market as any)?.accessToken;
47
+
48
+ // Prioritize database token over cookie token
49
+ const marketAccessToken = marketTokenFromDB || ctx.marketAccessToken;
50
+
42
51
  return opts.next({
43
- ctx: { marketUserInfo },
52
+ ctx: {
53
+ marketAccessToken,
54
+ marketUserInfo,
55
+ },
44
56
  });
45
57
  } catch {
46
58
  // If fetching user info fails, continue without it
@@ -563,6 +563,7 @@ export default {
563
563
  'skillStore.networkError': 'Network error, please try again',
564
564
  'skillStore.search': 'Search skills by name or keyword, press Enter to search…',
565
565
  'skillStore.tabs.community': 'Community',
566
+ 'skillStore.tabs.custom': 'Custom',
566
567
  'skillStore.tabs.lobehub': 'LobeHub',
567
568
  'skillStore.title': 'Skill Store',
568
569
  'startConversation': 'Start Conversation',
@@ -45,7 +45,7 @@ export class MarketApiService {
45
45
  }
46
46
 
47
47
  // Get agent detail by identifier
48
- async getAgentDetail(identifier: string): Promise<AgentItemDetail> {
48
+ async getAgentDetail(identifier: string): Promise<AgentItemDetail & { forkedFromAgentId?: string }> {
49
49
  return lambdaClient.market.agent.getAgentDetail.query({
50
50
  identifier,
51
51
  }) as Promise<AgentItemDetail>;
@@ -22,6 +22,7 @@ const groupMeta = (groupId: string) => (s: ChatGroupStore) => {
22
22
  avatar: group?.avatar || undefined,
23
23
  backgroundColor: group?.backgroundColor || undefined,
24
24
  description: group?.description || '',
25
+ marketIdentifier: group?.marketIdentifier || undefined,
25
26
  title: group?.title || '',
26
27
  });
27
28
  };
@@ -1,6 +1,7 @@
1
1
  import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
2
2
 
3
3
  export interface CustomPluginState {
4
+ customPluginSearchKeywords?: string;
4
5
  newCustomPlugin: Partial<LobeToolCustomPlugin>;
5
6
  }
6
7
  export const defaultCustomPlugin: Partial<LobeToolCustomPlugin> = {
@@ -13,5 +14,6 @@ export const defaultCustomPlugin: Partial<LobeToolCustomPlugin> = {
13
14
  };
14
15
 
15
16
  export const initialCustomPluginState: CustomPluginState = {
17
+ customPluginSearchKeywords: '',
16
18
  newCustomPlugin: defaultCustomPlugin,
17
19
  };
@@ -731,6 +731,7 @@ export const createMCPPluginStoreSlice: StateCreator<
731
731
  draft.mcpPluginItems = [];
732
732
  draft.currentPage = 1;
733
733
  draft.mcpSearchKeywords = keywords;
734
+ draft.isMcpListInit = false;
734
735
  }),
735
736
  false,
736
737
  n('resetMCPPluginList'),