@lobehub/lobehub 2.0.0-next.284 → 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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/setting.json +1 -0
- package/locales/en-US/subscription.json +2 -0
- package/locales/zh-CN/setting.json +1 -0
- package/locales/zh-CN/subscription.json +2 -0
- package/package.json +1 -1
- package/packages/builtin-tool-agent-builder/src/ExecutionRuntime/index.ts +79 -2
- package/packages/builtin-tool-agent-builder/src/client/Intervention/InstallPlugin.tsx +66 -5
- package/packages/builtin-tool-agent-builder/src/client/Render/InstallPlugin.tsx +12 -4
- package/packages/builtin-tool-agent-builder/src/manifest.ts +3 -2
- package/packages/builtin-tool-agent-builder/src/systemRole.ts +8 -8
- package/packages/builtin-tool-agent-builder/src/types.ts +7 -3
- package/packages/context-engine/src/providers/AgentBuilderContextInjector.ts +20 -4
- package/packages/context-engine/src/providers/GroupAgentBuilderContextInjector.ts +18 -2
- package/packages/model-bank/src/aiModels/lobehub.ts +20 -1
- package/packages/model-runtime/src/providers/fal/index.test.ts +176 -1
- package/packages/model-runtime/src/providers/fal/index.ts +3 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/MessageFromUrl.tsx +57 -9
- package/src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/PublishButton.tsx +5 -8
- package/src/app/[variants]/(main)/agent/profile/features/Header/index.tsx +0 -2
- package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +2 -0
- package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/MessageFromUrl.tsx +24 -10
- package/src/features/ProfileEditor/AgentTool.tsx +68 -12
- package/src/features/ProfileEditor/PluginTag.tsx +56 -3
- package/src/locales/default/setting.ts +1 -0
- package/src/locales/default/subscription.ts +2 -0
- package/src/services/chat/index.ts +24 -1
- package/src/services/chat/mecha/contextEngineering.ts +28 -2
- 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
|
|
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 (
|
|
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';
|
package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/MessageFromUrl.tsx
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
};
|
package/src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/PublishButton.tsx
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
-
<
|
|
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')}
|
package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/MessageFromUrl.tsx
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
43
|
+
// Send the message
|
|
30
44
|
sendMessage({ message });
|
|
31
|
-
}, [searchParams, setSearchParams,
|
|
45
|
+
}, [searchParams, setSearchParams, sendMessage, agentId]);
|
|
32
46
|
|
|
33
47
|
return null;
|
|
34
48
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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(
|
|
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] =
|
|
144
|
-
s
|
|
145
|
-
|
|
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
|
|
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
|
-
//
|
|
417
|
-
const
|
|
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}
|