@lobehub/lobehub 2.0.0-next.283 → 2.0.0-next.285

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 (32) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/locales/en-US/setting.json +1 -0
  4. package/locales/en-US/subscription.json +2 -0
  5. package/locales/zh-CN/setting.json +1 -0
  6. package/locales/zh-CN/subscription.json +2 -0
  7. package/package.json +1 -1
  8. package/packages/builtin-tool-agent-builder/src/ExecutionRuntime/index.ts +79 -2
  9. package/packages/builtin-tool-agent-builder/src/client/Intervention/InstallPlugin.tsx +66 -5
  10. package/packages/builtin-tool-agent-builder/src/client/Render/InstallPlugin.tsx +12 -4
  11. package/packages/builtin-tool-agent-builder/src/manifest.ts +3 -2
  12. package/packages/builtin-tool-agent-builder/src/systemRole.ts +8 -8
  13. package/packages/builtin-tool-agent-builder/src/types.ts +7 -3
  14. package/packages/context-engine/src/providers/AgentBuilderContextInjector.ts +20 -4
  15. package/packages/context-engine/src/providers/GroupAgentBuilderContextInjector.ts +18 -2
  16. package/packages/model-bank/src/aiModels/lobehub.ts +20 -1
  17. package/packages/model-runtime/src/providers/fal/index.test.ts +176 -1
  18. package/packages/model-runtime/src/providers/fal/index.ts +3 -1
  19. package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/MessageFromUrl.tsx +57 -9
  20. package/src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/PublishButton.tsx +5 -8
  21. package/src/app/[variants]/(main)/agent/profile/features/Header/index.tsx +0 -2
  22. package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +2 -0
  23. package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/MessageFromUrl.tsx +24 -10
  24. package/src/features/PluginStore/Content.tsx +1 -4
  25. package/src/features/ProfileEditor/AgentTool.tsx +68 -12
  26. package/src/features/ProfileEditor/PluginTag.tsx +56 -3
  27. package/src/layout/GlobalProvider/index.tsx +1 -1
  28. package/src/locales/default/setting.ts +1 -0
  29. package/src/locales/default/subscription.ts +2 -0
  30. package/src/services/chat/index.ts +24 -1
  31. package/src/services/chat/mecha/contextEngineering.ts +28 -2
  32. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +11 -1
@@ -479,7 +479,7 @@ describe('LobeFalAI', () => {
479
479
  });
480
480
  });
481
481
 
482
- describe('Seedream v4 special endpoints', () => {
482
+ describe('Seedream and Hunyuan special endpoints', () => {
483
483
  it('should use text-to-image endpoint when no imageUrls provided', async () => {
484
484
  // Arrange
485
485
  const mockImageResponse = {
@@ -698,6 +698,181 @@ describe('LobeFalAI', () => {
698
698
  },
699
699
  });
700
700
  });
701
+
702
+ it('should use text-to-image endpoint for seedream v4.5 when no imageUrls provided', async () => {
703
+ // Arrange
704
+ const mockImageResponse = {
705
+ requestId: 'test-request-id',
706
+ data: {
707
+ images: [
708
+ {
709
+ url: 'https://example.com/generated.jpg',
710
+ width: 2048,
711
+ height: 2048,
712
+ },
713
+ ],
714
+ },
715
+ };
716
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
717
+
718
+ const payload: CreateImagePayload = {
719
+ model: 'bytedance/seedream/v4.5',
720
+ params: {
721
+ prompt: 'A beautiful landscape',
722
+ width: 2048,
723
+ height: 2048,
724
+ },
725
+ };
726
+
727
+ // Act
728
+ await instance.createImage(payload);
729
+
730
+ // Assert
731
+ expect(mockFal.subscribe).toHaveBeenCalledWith(
732
+ 'fal-ai/bytedance/seedream/v4.5/text-to-image',
733
+ {
734
+ input: {
735
+ enable_safety_checker: false,
736
+ num_images: 1,
737
+ prompt: 'A beautiful landscape',
738
+ image_size: {
739
+ width: 2048,
740
+ height: 2048,
741
+ },
742
+ },
743
+ },
744
+ );
745
+ });
746
+
747
+ it('should use edit endpoint for seedream v4.5 when imageUrls is provided', async () => {
748
+ // Arrange
749
+ const mockImageResponse = {
750
+ requestId: 'test-request-id',
751
+ data: {
752
+ images: [
753
+ {
754
+ url: 'https://example.com/edited.jpg',
755
+ width: 2048,
756
+ height: 2048,
757
+ },
758
+ ],
759
+ },
760
+ };
761
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
762
+
763
+ const payload: CreateImagePayload = {
764
+ model: 'bytedance/seedream/v4.5',
765
+ params: {
766
+ prompt: 'Edit this image',
767
+ imageUrls: ['https://example.com/input.jpg'],
768
+ width: 2048,
769
+ height: 2048,
770
+ },
771
+ };
772
+
773
+ // Act
774
+ await instance.createImage(payload);
775
+
776
+ // Assert
777
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/bytedance/seedream/v4.5/edit', {
778
+ input: {
779
+ enable_safety_checker: false,
780
+ num_images: 1,
781
+ prompt: 'Edit this image',
782
+ image_urls: ['https://example.com/input.jpg'],
783
+ image_size: {
784
+ width: 2048,
785
+ height: 2048,
786
+ },
787
+ },
788
+ });
789
+ });
790
+
791
+ it('should use text-to-image endpoint for hunyuan-image v3 when no imageUrls provided', async () => {
792
+ // Arrange
793
+ const mockImageResponse = {
794
+ requestId: 'test-request-id',
795
+ data: {
796
+ images: [
797
+ {
798
+ url: 'https://example.com/generated.jpg',
799
+ width: 1024,
800
+ height: 1024,
801
+ },
802
+ ],
803
+ },
804
+ };
805
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
806
+
807
+ const payload: CreateImagePayload = {
808
+ model: 'hunyuan-image/v3',
809
+ params: {
810
+ prompt: 'A scenic mountain view',
811
+ width: 1024,
812
+ height: 1024,
813
+ },
814
+ };
815
+
816
+ // Act
817
+ await instance.createImage(payload);
818
+
819
+ // Assert
820
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/hunyuan-image/v3/text-to-image', {
821
+ input: {
822
+ enable_safety_checker: false,
823
+ num_images: 1,
824
+ prompt: 'A scenic mountain view',
825
+ image_size: {
826
+ width: 1024,
827
+ height: 1024,
828
+ },
829
+ },
830
+ });
831
+ });
832
+
833
+ it('should use edit endpoint for hunyuan-image v3 when imageUrls is provided', async () => {
834
+ // Arrange
835
+ const mockImageResponse = {
836
+ requestId: 'test-request-id',
837
+ data: {
838
+ images: [
839
+ {
840
+ url: 'https://example.com/edited.jpg',
841
+ width: 1024,
842
+ height: 1024,
843
+ },
844
+ ],
845
+ },
846
+ };
847
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
848
+
849
+ const payload: CreateImagePayload = {
850
+ model: 'hunyuan-image/v3',
851
+ params: {
852
+ prompt: 'Edit this image',
853
+ imageUrls: ['https://example.com/input.jpg'],
854
+ width: 1024,
855
+ height: 1024,
856
+ },
857
+ };
858
+
859
+ // Act
860
+ await instance.createImage(payload);
861
+
862
+ // Assert
863
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/hunyuan-image/v3/edit', {
864
+ input: {
865
+ enable_safety_checker: false,
866
+ num_images: 1,
867
+ prompt: 'Edit this image',
868
+ image_urls: ['https://example.com/input.jpg'],
869
+ image_size: {
870
+ width: 1024,
871
+ height: 1024,
872
+ },
873
+ },
874
+ });
875
+ });
701
876
  });
702
877
 
703
878
  describe('Edge cases', () => {
@@ -71,7 +71,9 @@ export class LobeFalAI implements LobeRuntimeAI {
71
71
  // Ensure model has fal-ai/ prefix
72
72
  let endpoint = model.startsWith('fal-ai/') ? model : `fal-ai/${model}`;
73
73
  const hasImageUrls = (params.imageUrls?.length ?? 0) > 0;
74
- if (['fal-ai/bytedance/seedream/v4', 'fal-ai/hunyuan-image/v3'].includes(endpoint)) {
74
+ if (
75
+ ['fal-ai/bytedance/seedream/v', 'fal-ai/hunyuan-image/v'].some((m) => endpoint.startsWith(m))
76
+ ) {
75
77
  endpoint += hasImageUrls ? '/edit' : '/text-to-image';
76
78
  } else if (endpoint === 'fal-ai/nano-banana' && hasImageUrls) {
77
79
  endpoint += '/edit';
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useEffect } from 'react';
4
- import { useSearchParams } from 'react-router-dom';
3
+ import { useEffect, useMemo, useRef } from 'react';
4
+ import { useLocation, useSearchParams } from 'react-router-dom';
5
5
 
6
6
  import { useConversationStore } from '@/features/Conversation';
7
+ import { useAgentStore } from '@/store/agent';
8
+ import { agentSelectors } from '@/store/agent/selectors';
7
9
 
8
10
  /**
9
11
  * MessageFromUrl
@@ -12,23 +14,69 @@ import { useConversationStore } from '@/features/Conversation';
12
14
  * Uses ConversationStore for input and send operations.
13
15
  */
14
16
  const MessageFromUrl = () => {
15
- const [updateInputMessage, sendMessage] = useConversationStore((s) => [
16
- s.updateInputMessage,
17
+ const [sendMessage, context, messagesInit] = useConversationStore((s) => [
17
18
  s.sendMessage,
19
+ s.context,
20
+ s.messagesInit,
18
21
  ]);
22
+ const agentId = context.agentId;
19
23
  const [searchParams, setSearchParams] = useSearchParams();
24
+ const location = useLocation();
25
+ const isAgentConfigLoading = useAgentStore(agentSelectors.isAgentConfigLoading);
26
+
27
+ const routeAgentId = useMemo(() => {
28
+ const match = location.pathname?.match(/^\/agent\/([^#/?]+)/);
29
+ return match?.[1];
30
+ }, [location.pathname]);
31
+
32
+ // Track last processed (agentId, message) to prevent duplicate sends on re-render,
33
+ // while still allowing sending when navigating to a different agent (or message).
34
+ const lastProcessedSignatureRef = useRef<string | null>(null);
20
35
 
21
36
  useEffect(() => {
22
37
  const message = searchParams.get('message');
23
38
  if (!message) return;
24
39
 
25
- const params = new URLSearchParams(searchParams.toString());
26
- params.delete('message');
27
- setSearchParams(params, { replace: true });
40
+ // Wait for agentId to be available before sending
41
+ if (!agentId) return;
42
+
43
+ // During agent switching, URL/searchParams may update before ConversationStore context updates.
44
+ // Only consume the param when the route agentId matches the ConversationStore agentId.
45
+ if (routeAgentId && routeAgentId !== agentId) return;
46
+
47
+ // Ensure required agent info is loaded before consuming the param.
48
+ // For existing conversations (topicId exists), also wait until messages are initialized
49
+ // to avoid sending during skeleton fetch states.
50
+ const isNewConversation = !context.topicId;
51
+ const isReady = !isAgentConfigLoading && (isNewConversation || messagesInit);
52
+ if (!isReady) return;
28
53
 
29
- updateInputMessage(message);
54
+ const signature = `${agentId}::${message}`;
55
+ if (lastProcessedSignatureRef.current === signature) return;
56
+ lastProcessedSignatureRef.current = signature;
57
+
58
+ // Use functional update to safely remove message param without affecting other params
59
+ setSearchParams(
60
+ (prev) => {
61
+ const newParams = new URLSearchParams(prev);
62
+ newParams.delete('message');
63
+ return newParams;
64
+ },
65
+ { replace: true },
66
+ );
67
+
68
+ // Send the message
30
69
  sendMessage({ message });
31
- }, [searchParams, setSearchParams, updateInputMessage, sendMessage]);
70
+ }, [
71
+ searchParams,
72
+ setSearchParams,
73
+ sendMessage,
74
+ agentId,
75
+ context.topicId,
76
+ isAgentConfigLoading,
77
+ messagesInit,
78
+ routeAgentId,
79
+ ]);
32
80
 
33
81
  return null;
34
82
  };
@@ -1,13 +1,11 @@
1
- import { ActionIcon } from '@lobehub/ui';
1
+ import { Button } from '@lobehub/ui';
2
2
  import { ShapesUploadIcon } from '@lobehub/ui/icons';
3
3
  import { memo, useCallback, useMemo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { message } from '@/components/AntdStaticMethods';
7
- import { HEADER_ICON_SIZE } from '@/const/layoutTokens';
8
7
  import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
9
8
  import { resolveMarketAuthError } from '@/layout/AuthProvider/MarketAuth/errors';
10
- import { useServerConfigStore } from '@/store/serverConfig';
11
9
 
12
10
  import ForkConfirmModal from './ForkConfirmModal';
13
11
  import type { MarketPublishAction } from './types';
@@ -21,8 +19,6 @@ interface MarketPublishButtonProps {
21
19
  const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess }) => {
22
20
  const { t } = useTranslation(['setting', 'marketAuth']);
23
21
 
24
- const mobile = useServerConfigStore((s) => s.isMobile);
25
-
26
22
  const { isAuthenticated, isLoading, signIn } = useMarketAuth();
27
23
  const { checkOwnership, isCheckingOwnership, isPublishing, publish } = useMarketPublish({
28
24
  action,
@@ -102,13 +98,14 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
102
98
 
103
99
  return (
104
100
  <>
105
- <ActionIcon
101
+ <Button
106
102
  icon={ShapesUploadIcon}
107
103
  loading={loading}
108
104
  onClick={handleButtonClick}
109
- size={HEADER_ICON_SIZE(mobile)}
110
105
  title={buttonTitle}
111
- />
106
+ >
107
+ {t('publishToCommunity')}
108
+ </Button>
112
109
  <ForkConfirmModal
113
110
  loading={isPublishing}
114
111
  onCancel={handleForkCancel}
@@ -5,7 +5,6 @@ import NavHeader from '@/features/NavHeader';
5
5
  import ToggleRightPanelButton from '@/features/RightPanel/ToggleRightPanelButton';
6
6
  import WideScreenButton from '@/features/WideScreenContainer/WideScreenButton';
7
7
 
8
- import AgentPublishButton from './AgentPublishButton';
9
8
  import AutoSaveHint from './AutoSaveHint';
10
9
 
11
10
  const Header = memo(() => {
@@ -16,7 +15,6 @@ const Header = memo(() => {
16
15
  <>
17
16
  <WideScreenButton />
18
17
  <ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} />
19
- <AgentPublishButton />
20
18
  </>
21
19
  }
22
20
  />
@@ -17,6 +17,7 @@ import { useChatStore } from '@/store/chat';
17
17
 
18
18
  import AgentCronJobs from '../AgentCronJobs';
19
19
  import EditorCanvas from '../EditorCanvas';
20
+ import AgentPublishButton from '../Header/AgentPublishButton';
20
21
  import AgentHeader from './AgentHeader';
21
22
  import AgentTool from './AgentTool';
22
23
 
@@ -79,6 +80,7 @@ const ProfileEditor = memo(() => {
79
80
  >
80
81
  {t('startConversation')}
81
82
  </Button>
83
+ <AgentPublishButton />
82
84
  {ENABLE_BUSINESS_FEATURES && (
83
85
  <Button icon={Clock} onClick={handleCreateCronJob}>
84
86
  {t('agentCronJobs.addJob')}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect } from 'react';
3
+ import { useEffect, useRef } from 'react';
4
4
  import { useSearchParams } from 'react-router-dom';
5
5
 
6
6
  import { useConversationStore } from '@/features/Conversation';
@@ -12,23 +12,37 @@ import { useConversationStore } from '@/features/Conversation';
12
12
  * Uses ConversationStore for input and send operations.
13
13
  */
14
14
  const MessageFromUrl = () => {
15
- const [updateInputMessage, sendMessage] = useConversationStore((s) => [
16
- s.updateInputMessage,
17
- s.sendMessage,
18
- ]);
15
+ const [sendMessage, agentId] = useConversationStore((s) => [s.sendMessage, s.context.agentId]);
19
16
  const [searchParams, setSearchParams] = useSearchParams();
20
17
 
18
+ // Track if we've processed the initial message to prevent duplicate sends
19
+ const hasProcessedRef = useRef(false);
20
+
21
21
  useEffect(() => {
22
+ // Only process once
23
+ if (hasProcessedRef.current) return;
24
+
22
25
  const message = searchParams.get('message');
23
26
  if (!message) return;
24
27
 
25
- const params = new URLSearchParams(searchParams.toString());
26
- params.delete('message');
27
- setSearchParams(params, { replace: true });
28
+ // Wait for agentId to be available before sending
29
+ if (!agentId) return;
30
+
31
+ hasProcessedRef.current = true;
32
+
33
+ // Use functional update to safely remove message param without affecting other params
34
+ setSearchParams(
35
+ (prev) => {
36
+ const newParams = new URLSearchParams(prev);
37
+ newParams.delete('message');
38
+ return newParams;
39
+ },
40
+ { replace: true },
41
+ );
28
42
 
29
- updateInputMessage(message);
43
+ // Send the message
30
44
  sendMessage({ message });
31
- }, [searchParams, setSearchParams, updateInputMessage, sendMessage]);
45
+ }, [searchParams, setSearchParams, sendMessage, agentId]);
32
46
 
33
47
  return null;
34
48
  };
@@ -10,7 +10,6 @@ import { PluginStoreTabs } from '@/store/tool/slices/oldStore';
10
10
  import AddPluginButton from './AddPluginButton';
11
11
  import InstalledList from './InstalledList';
12
12
  import McpList from './McpList';
13
- import PluginList from './PluginList';
14
13
  import Search from './Search';
15
14
 
16
15
  export const Content = memo(() => {
@@ -21,9 +20,8 @@ export const Content = memo(() => {
21
20
 
22
21
  const options = [
23
22
  { label: t('store.tabs.mcp'), value: PluginStoreTabs.MCP },
24
- { label: t('store.tabs.old'), value: PluginStoreTabs.Plugin },
25
23
  { label: t('store.tabs.installed'), value: PluginStoreTabs.Installed },
26
- ].filter(Boolean) as SegmentedOptions;
24
+ ] as SegmentedOptions;
27
25
 
28
26
  return (
29
27
  <Flexbox
@@ -48,7 +46,6 @@ export const Content = memo(() => {
48
46
  <Search />
49
47
  </Flexbox>
50
48
  {listType === PluginStoreTabs.MCP && <McpList />}
51
- {listType === PluginStoreTabs.Plugin && <PluginList />}
52
49
  {listType === PluginStoreTabs.Installed && <InstalledList keywords={keywords} />}
53
50
  </Flexbox>
54
51
  );
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
3
+ import {
4
+ KLAVIS_SERVER_TYPES,
5
+ type KlavisServerType,
6
+ LOBEHUB_SKILL_PROVIDERS,
7
+ type LobehubSkillProviderType,
8
+ } from '@lobechat/const';
4
9
  import { Avatar, Button, Flexbox, Icon, type ItemType, Segmented } from '@lobehub/ui';
5
10
  import { createStaticStyles, cssVar } from 'antd-style';
6
11
  import isEqual from 'fast-deep-equal';
@@ -10,6 +15,7 @@ import { useTranslation } from 'react-i18next';
10
15
 
11
16
  import PluginAvatar from '@/components/Plugins/PluginAvatar';
12
17
  import KlavisServerItem from '@/features/ChatInput/ActionBar/Tools/KlavisServerItem';
18
+ import LobehubSkillServerItem from '@/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem';
13
19
  import ToolItem from '@/features/ChatInput/ActionBar/Tools/ToolItem';
14
20
  import ActionDropdown from '@/features/ChatInput/ActionBar/components/ActionDropdown';
15
21
  import PluginStore from '@/features/PluginStore';
@@ -22,6 +28,7 @@ import { useToolStore } from '@/store/tool';
22
28
  import {
23
29
  builtinToolSelectors,
24
30
  klavisStoreSelectors,
31
+ lobehubSkillStoreSelectors,
25
32
  pluginSelectors,
26
33
  } from '@/store/tool/selectors';
27
34
  import { type LobeToolMetaWithAvailability } from '@/store/tool/slices/builtin/selectors';
@@ -81,6 +88,19 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
81
88
  return <Icon className={styles.icon} fill={cssVar.colorText} icon={icon} size={18} />;
82
89
  });
83
90
 
91
+ /**
92
+ * LobeHub Skill Provider 图标组件
93
+ */
94
+ const LobehubSkillIcon = memo<Pick<LobehubSkillProviderType, 'icon' | 'label'>>(
95
+ ({ icon, label }) => {
96
+ if (typeof icon === 'string') {
97
+ return <img alt={label} className={styles.icon} height={18} src={icon} width={18} />;
98
+ }
99
+
100
+ return <Icon className={styles.icon} fill={cssVar.colorText} icon={icon} size={18} />;
101
+ },
102
+ );
103
+
84
104
  export interface AgentToolProps {
85
105
  /**
86
106
  * Optional agent ID to use instead of currentAgentConfig
@@ -125,12 +145,18 @@ const AgentTool = memo<AgentToolProps>(
125
145
  );
126
146
 
127
147
  // Web browsing uses searchMode instead of plugins array - use byId selector
128
- const isSearchEnabled = useAgentStore(chatConfigByIdSelectors.isEnableSearchById(effectiveAgentId));
148
+ const isSearchEnabled = useAgentStore(
149
+ chatConfigByIdSelectors.isEnableSearchById(effectiveAgentId),
150
+ );
129
151
 
130
152
  // Klavis 相关状态
131
153
  const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
132
154
  const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
133
155
 
156
+ // LobeHub Skill 相关状态
157
+ const allLobehubSkillServers = useToolStore(lobehubSkillStoreSelectors.getServers, isEqual);
158
+ const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
159
+
134
160
  // Plugin store modal state
135
161
  const [modalOpen, setModalOpen] = useState(false);
136
162
  const [updating, setUpdating] = useState(false);
@@ -140,10 +166,12 @@ const AgentTool = memo<AgentToolProps>(
140
166
  const isInitializedRef = useRef(false);
141
167
 
142
168
  // Fetch plugins
143
- const [useFetchPluginStore, useFetchUserKlavisServers] = useToolStore((s) => [
144
- s.useFetchPluginStore,
145
- s.useFetchUserKlavisServers,
146
- ]);
169
+ const [useFetchPluginStore, useFetchUserKlavisServers, useFetchLobehubSkillConnections] =
170
+ useToolStore((s) => [
171
+ s.useFetchPluginStore,
172
+ s.useFetchUserKlavisServers,
173
+ s.useFetchLobehubSkillConnections,
174
+ ]);
147
175
  useFetchPluginStore();
148
176
  useFetchInstalledPlugins();
149
177
  useCheckPluginsIsInstalled(plugins);
@@ -151,6 +179,9 @@ const AgentTool = memo<AgentToolProps>(
151
179
  // 使用 SWR 加载用户的 Klavis 集成(从数据库)
152
180
  useFetchUserKlavisServers(isKlavisEnabledInEnv);
153
181
 
182
+ // 使用 SWR 加载用户的 LobeHub Skill 连接
183
+ useFetchLobehubSkillConnections(isLobehubSkillEnabled);
184
+
154
185
  // Toggle web browsing via searchMode - use byId action
155
186
  const toggleWebBrowsing = useCallback(async () => {
156
187
  if (!effectiveAgentId) return;
@@ -270,6 +301,19 @@ const AgentTool = memo<AgentToolProps>(
270
301
  [isKlavisEnabledInEnv, allKlavisServers],
271
302
  );
272
303
 
304
+ // LobeHub Skill Provider 列表项
305
+ const lobehubSkillItems = useMemo(
306
+ () =>
307
+ isLobehubSkillEnabled
308
+ ? LOBEHUB_SKILL_PROVIDERS.map((provider) => ({
309
+ icon: <LobehubSkillIcon icon={provider.icon} label={provider.label} />,
310
+ key: provider.id, // 使用 provider.id 作为 key,与 pluginId 保持一致
311
+ label: <LobehubSkillServerItem label={provider.label} provider={provider.id} />,
312
+ }))
313
+ : [],
314
+ [isLobehubSkillEnabled, allLobehubSkillServers],
315
+ );
316
+
273
317
  // Handle plugin remove via Tag close - use byId actions
274
318
  const handleRemovePlugin =
275
319
  (
@@ -292,7 +336,7 @@ const AgentTool = memo<AgentToolProps>(
292
336
  (id) => !builtinList.some((b) => b.identifier === id),
293
337
  ).length;
294
338
 
295
- // 合并 builtin 工具和 Klavis 服务器
339
+ // 合并 builtin 工具、LobeHub Skill Providers 和 Klavis 服务器
296
340
  const builtinItems = useMemo(
297
341
  () => [
298
342
  // 原有的 builtin 工具
@@ -312,10 +356,12 @@ const AgentTool = memo<AgentToolProps>(
312
356
  />
313
357
  ),
314
358
  })),
359
+ // LobeHub Skill Providers
360
+ ...lobehubSkillItems,
315
361
  // Klavis 服务器
316
362
  ...klavisServerItems,
317
363
  ],
318
- [filteredBuiltinList, klavisServerItems, isToolEnabled, handleToggleTool],
364
+ [filteredBuiltinList, klavisServerItems, lobehubSkillItems, isToolEnabled, handleToggleTool],
319
365
  );
320
366
 
321
367
  // Plugin items for dropdown
@@ -413,8 +459,17 @@ const AgentTool = memo<AgentToolProps>(
413
459
  plugins.includes(item.key as string),
414
460
  );
415
461
 
416
- // 合并 builtin Klavis
417
- const allBuiltinItems = [...enabledBuiltinItems, ...connectedKlavisItems];
462
+ // 已连接的 LobeHub Skill Providers
463
+ const connectedLobehubSkillItems = lobehubSkillItems.filter((item) =>
464
+ plugins.includes(item.key as string),
465
+ );
466
+
467
+ // 合并 builtin、LobeHub Skill 和 Klavis
468
+ const allBuiltinItems = [
469
+ ...enabledBuiltinItems,
470
+ ...connectedKlavisItems,
471
+ ...connectedLobehubSkillItems,
472
+ ];
418
473
 
419
474
  if (allBuiltinItems.length > 0) {
420
475
  items.push({
@@ -462,6 +517,7 @@ const AgentTool = memo<AgentToolProps>(
462
517
  }, [
463
518
  filteredBuiltinList,
464
519
  klavisServerItems,
520
+ lobehubSkillItems,
465
521
  installedPluginList,
466
522
  plugins,
467
523
  isToolEnabled,
@@ -526,7 +582,7 @@ const AgentTool = memo<AgentToolProps>(
526
582
  overflowY: 'visible',
527
583
  },
528
584
  }}
529
- minHeight={isKlavisEnabledInEnv ? 500 : undefined}
585
+ minHeight={isKlavisEnabledInEnv || isLobehubSkillEnabled ? 500 : undefined}
530
586
  minWidth={400}
531
587
  placement={'bottomLeft'}
532
588
  popupRender={(menu) => (
@@ -554,7 +610,7 @@ const AgentTool = memo<AgentToolProps>(
554
610
  className={styles.scroller}
555
611
  style={{
556
612
  maxHeight: 500,
557
- minHeight: isKlavisEnabledInEnv ? 500 : undefined,
613
+ minHeight: isKlavisEnabledInEnv || isLobehubSkillEnabled ? 500 : undefined,
558
614
  }}
559
615
  >
560
616
  {menu}