@lobehub/lobehub 2.0.0-next.270 → 2.0.0-next.272

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.
@@ -1,494 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
4
- import { Avatar, Button, Flexbox, Icon, type ItemType, Segmented } from '@lobehub/ui';
5
- import { createStaticStyles, cssVar } from 'antd-style';
6
- import isEqual from 'fast-deep-equal';
7
- import { ArrowRight, PlusIcon, Store, ToyBrick } from 'lucide-react';
8
- import Image from 'next/image';
9
- import React, { Suspense, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
- import { useTranslation } from 'react-i18next';
11
-
12
- import PluginAvatar from '@/components/Plugins/PluginAvatar';
13
- import KlavisServerItem from '@/features/ChatInput/ActionBar/Tools/KlavisServerItem';
14
- import ToolItem from '@/features/ChatInput/ActionBar/Tools/ToolItem';
15
- import ActionDropdown from '@/features/ChatInput/ActionBar/components/ActionDropdown';
16
- import PluginStore from '@/features/PluginStore';
17
- import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
18
- import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
19
- import { useAgentStore } from '@/store/agent';
20
- import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
21
- import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
22
- import { useToolStore } from '@/store/tool';
23
- import {
24
- builtinToolSelectors,
25
- klavisStoreSelectors,
26
- pluginSelectors,
27
- } from '@/store/tool/selectors';
28
-
29
- import PluginTag from './PluginTag';
30
-
31
- const WEB_BROWSING_IDENTIFIER = 'lobe-web-browsing';
32
-
33
- type TabType = 'all' | 'installed';
34
-
35
- const prefixCls = 'ant';
36
-
37
- const styles = createStaticStyles(({ css }) => ({
38
- dropdown: css`
39
- overflow: hidden;
40
-
41
- width: 100%;
42
- border: 1px solid ${cssVar.colorBorderSecondary};
43
- border-radius: ${cssVar.borderRadiusLG};
44
-
45
- background: ${cssVar.colorBgElevated};
46
- box-shadow: ${cssVar.boxShadowSecondary};
47
-
48
- .${prefixCls}-dropdown-menu {
49
- border-radius: 0 !important;
50
- background: transparent !important;
51
- box-shadow: none !important;
52
- }
53
- `,
54
- header: css`
55
- padding: ${cssVar.paddingXS};
56
- border-block-end: 1px solid ${cssVar.colorBorderSecondary};
57
- background: transparent;
58
- `,
59
- scroller: css`
60
- overflow: hidden auto;
61
- `,
62
- }));
3
+ import { AgentTool as SharedAgentTool } from '@/features/ProfileEditor';
63
4
 
64
5
  /**
65
- * Klavis 服务器图标组件
66
- * 对于 string 类型的 icon,使用 Image 组件渲染
67
- * 对于 IconType 类型的 icon,使用 Icon 组件渲染,并根据主题设置填充色
6
+ * AgentTool for agent profile editor
7
+ * - showWebBrowsing: Agent profile supports web browsing toggle
8
+ * - filterAvailableInWeb: Filter out desktop-only tools in web version
9
+ * - useAllMetaList: Use allMetaList to include hidden tools
68
10
  */
69
- const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
70
- if (typeof icon === 'string') {
71
- return (
72
- <Image alt={label} height={18} src={icon} style={{ flex: 'none' }} unoptimized width={18} />
73
- );
74
- }
75
-
76
- // 使用主题色填充,在深色模式下自动适应
77
- return <Icon fill={cssVar.colorText} icon={icon} size={18} />;
78
- });
79
-
80
- const AgentTool = memo(() => {
81
- const { t } = useTranslation('setting');
82
- const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
83
-
84
- // Plugin state management
85
- const plugins = config?.plugins || [];
86
-
87
- const toggleAgentPlugin = useAgentStore((s) => s.toggleAgentPlugin);
88
- const updateAgentChatConfig = useAgentStore((s) => s.updateAgentChatConfig);
89
- const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
90
- const builtinList = useToolStore(builtinToolSelectors.allMetaList, isEqual);
91
-
92
- // Web browsing uses searchMode instead of plugins array
93
- const isSearchEnabled = useAgentStore(agentChatConfigSelectors.isAgentEnableSearch);
94
-
95
- // Klavis 相关状态
96
- const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
97
- const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
98
-
99
- // Plugin store modal state
100
- const [modalOpen, setModalOpen] = useState(false);
101
- const [updating, setUpdating] = useState(false);
102
-
103
- // Tab state for dual-column layout
104
- const [activeTab, setActiveTab] = useState<TabType | null>(null);
105
- const isInitializedRef = useRef(false);
106
-
107
- // Fetch plugins
108
- const [useFetchPluginStore, useFetchUserKlavisServers] = useToolStore((s) => [
109
- s.useFetchPluginStore,
110
- s.useFetchUserKlavisServers,
111
- ]);
112
- useFetchPluginStore();
113
- useFetchInstalledPlugins();
114
- useCheckPluginsIsInstalled(plugins);
115
-
116
- // 使用 SWR 加载用户的 Klavis 集成(从数据库)
117
- useFetchUserKlavisServers(isKlavisEnabledInEnv);
118
-
119
- // Toggle web browsing via searchMode
120
- const toggleWebBrowsing = useCallback(async () => {
121
- const nextMode = isSearchEnabled ? 'off' : 'auto';
122
- await updateAgentChatConfig({ searchMode: nextMode });
123
- }, [isSearchEnabled, updateAgentChatConfig]);
124
-
125
- // Check if a tool is enabled (handles web browsing specially)
126
- const isToolEnabled = useCallback(
127
- (identifier: string) => {
128
- if (identifier === WEB_BROWSING_IDENTIFIER) {
129
- return isSearchEnabled;
130
- }
131
- return plugins.includes(identifier);
132
- },
133
- [plugins, isSearchEnabled],
134
- );
135
-
136
- // Toggle a tool (handles web browsing specially)
137
- const handleToggleTool = useCallback(
138
- async (identifier: string) => {
139
- if (identifier === WEB_BROWSING_IDENTIFIER) {
140
- await toggleWebBrowsing();
141
- } else {
142
- await toggleAgentPlugin(identifier);
143
- }
144
- },
145
- [toggleWebBrowsing, toggleAgentPlugin],
146
- );
147
-
148
- // Set default tab based on installed plugins (only on first load)
149
- useEffect(() => {
150
- if (!isInitializedRef.current && plugins.length >= 0) {
151
- isInitializedRef.current = true;
152
- setActiveTab(plugins.length > 0 ? 'installed' : 'all');
153
- }
154
- }, [plugins.length]);
155
-
156
- // 根据 identifier 获取已连接的服务器
157
- const getServerByName = (identifier: string) => {
158
- return allKlavisServers.find((server) => server.identifier === identifier);
159
- };
160
-
161
- // 获取所有 Klavis 服务器类型的 identifier 集合(用于过滤 builtinList)
162
- const allKlavisTypeIdentifiers = useMemo(
163
- () => new Set(KLAVIS_SERVER_TYPES.map((type) => type.identifier)),
164
- [],
165
- );
166
-
167
- // 过滤掉 builtinList 中的 klavis 工具(它们会单独显示在 Klavis 区域)
168
- // 同时过滤掉 availableInWeb: false 的工具(如 LocalSystem 仅桌面版可用)
169
- const filteredBuiltinList = useMemo(
170
- () =>
171
- builtinList
172
- .filter((item) => item.availableInWeb)
173
- .filter((item) =>
174
- isKlavisEnabledInEnv ? !allKlavisTypeIdentifiers.has(item.identifier) : true,
175
- ),
176
- [builtinList, allKlavisTypeIdentifiers, isKlavisEnabledInEnv],
177
- );
178
-
179
- // Klavis 服务器列表项
180
- const klavisServerItems = useMemo(
181
- () =>
182
- isKlavisEnabledInEnv
183
- ? KLAVIS_SERVER_TYPES.map((type) => ({
184
- icon: <KlavisIcon icon={type.icon} label={type.label} />,
185
- key: type.identifier,
186
- label: (
187
- <KlavisServerItem
188
- identifier={type.identifier}
189
- label={type.label}
190
- server={getServerByName(type.identifier)}
191
- serverName={type.serverName}
192
- />
193
- ),
194
- }))
195
- : [],
196
- [isKlavisEnabledInEnv, allKlavisServers],
197
- );
198
-
199
- // Handle plugin remove via Tag close
200
- const handleRemovePlugin =
201
- (pluginId: string | { enabled: boolean; identifier: string; settings: Record<string, any> }) =>
202
- async (e: React.MouseEvent) => {
203
- e.preventDefault();
204
- e.stopPropagation();
205
- const identifier = typeof pluginId === 'string' ? pluginId : pluginId?.identifier;
206
- if (identifier === WEB_BROWSING_IDENTIFIER) {
207
- await updateAgentChatConfig({ searchMode: 'off' });
208
- } else {
209
- toggleAgentPlugin(identifier, false);
210
- }
211
- };
212
-
213
- // Build dropdown menu items (adapted from useControls)
214
- const enablePluginCount = plugins.filter(
215
- (id) => !builtinList.some((b) => b.identifier === id),
216
- ).length;
217
-
218
- // 合并 builtin 工具和 Klavis 服务器
219
- const builtinItems = useMemo(
220
- () => [
221
- // 原有的 builtin 工具
222
- ...filteredBuiltinList.map((item) => ({
223
- icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
224
- key: item.identifier,
225
- label: (
226
- <ToolItem
227
- checked={isToolEnabled(item.identifier)}
228
- id={item.identifier}
229
- label={item.meta?.title}
230
- onUpdate={async () => {
231
- setUpdating(true);
232
- await handleToggleTool(item.identifier);
233
- setUpdating(false);
234
- }}
235
- />
236
- ),
237
- })),
238
- // Klavis 服务器
239
- ...klavisServerItems,
240
- ],
241
- [filteredBuiltinList, klavisServerItems, isToolEnabled, handleToggleTool],
242
- );
243
-
244
- // Plugin items for dropdown
245
- const pluginItems = useMemo(
246
- () =>
247
- installedPluginList.map((item) => ({
248
- icon: item?.avatar ? (
249
- <PluginAvatar avatar={item.avatar} size={20} />
250
- ) : (
251
- <Icon icon={ToyBrick} size={20} />
252
- ),
253
- key: item.identifier,
254
- label: (
255
- <ToolItem
256
- checked={plugins.includes(item.identifier)}
257
- id={item.identifier}
258
- label={item.title}
259
- onUpdate={async () => {
260
- setUpdating(true);
261
- await toggleAgentPlugin(item.identifier);
262
- setUpdating(false);
263
- }}
264
- />
265
- ),
266
- })),
267
- [installedPluginList, plugins, toggleAgentPlugin],
268
- );
269
-
270
- // All tab items (市场 tab)
271
- const allTabItems: ItemType[] = useMemo(
272
- () => [
273
- {
274
- children: builtinItems,
275
- key: 'builtins',
276
- label: t('tools.builtins.groupName'),
277
- type: 'group',
278
- },
279
- {
280
- children: pluginItems,
281
- key: 'plugins',
282
- label: (
283
- <Flexbox align={'center'} gap={40} horizontal justify={'space-between'}>
284
- {t('tools.plugins.groupName')}
285
- {enablePluginCount === 0 ? null : (
286
- <div style={{ fontSize: 12, marginInlineEnd: 4 }}>
287
- {t('tools.plugins.enabled', { num: enablePluginCount })}
288
- </div>
289
- )}
290
- </Flexbox>
291
- ),
292
- type: 'group',
293
- },
294
- {
295
- type: 'divider',
296
- },
297
- {
298
- extra: <Icon icon={ArrowRight} />,
299
- icon: Store,
300
- key: 'plugin-store',
301
- label: t('tools.plugins.store'),
302
- onClick: () => {
303
- setModalOpen(true);
304
- },
305
- },
306
- ],
307
- [builtinItems, pluginItems, enablePluginCount, t],
308
- );
309
-
310
- // Installed tab items - 只显示已启用的
311
- const installedTabItems: ItemType[] = useMemo(() => {
312
- const items: ItemType[] = [];
313
-
314
- // 已启用的 builtin 工具
315
- const enabledBuiltinItems = filteredBuiltinList
316
- .filter((item) => isToolEnabled(item.identifier))
317
- .map((item) => ({
318
- icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
319
- key: item.identifier,
320
- label: (
321
- <ToolItem
322
- checked={true}
323
- id={item.identifier}
324
- label={item.meta?.title}
325
- onUpdate={async () => {
326
- setUpdating(true);
327
- await handleToggleTool(item.identifier);
328
- setUpdating(false);
329
- }}
330
- />
331
- ),
332
- }));
333
-
334
- // 已连接且已启用的 Klavis 服务器
335
- const connectedKlavisItems = klavisServerItems.filter((item) =>
336
- plugins.includes(item.key as string),
337
- );
338
-
339
- // 合并 builtin 和 Klavis
340
- const allBuiltinItems = [...enabledBuiltinItems, ...connectedKlavisItems];
341
-
342
- if (allBuiltinItems.length > 0) {
343
- items.push({
344
- children: allBuiltinItems,
345
- key: 'installed-builtins',
346
- label: t('tools.builtins.groupName'),
347
- type: 'group',
348
- });
349
- }
350
-
351
- // 已启用的插件
352
- const installedPlugins = installedPluginList
353
- .filter((item) => plugins.includes(item.identifier))
354
- .map((item) => ({
355
- icon: item?.avatar ? (
356
- <PluginAvatar avatar={item.avatar} size={20} />
357
- ) : (
358
- <Icon icon={ToyBrick} size={20} />
359
- ),
360
- key: item.identifier,
361
- label: (
362
- <ToolItem
363
- checked={true}
364
- id={item.identifier}
365
- label={item.title}
366
- onUpdate={async () => {
367
- setUpdating(true);
368
- await toggleAgentPlugin(item.identifier);
369
- setUpdating(false);
370
- }}
371
- />
372
- ),
373
- }));
374
-
375
- if (installedPlugins.length > 0) {
376
- items.push({
377
- children: installedPlugins,
378
- key: 'installed-plugins',
379
- label: t('tools.plugins.groupName'),
380
- type: 'group',
381
- });
382
- }
383
-
384
- return items;
385
- }, [
386
- filteredBuiltinList,
387
- klavisServerItems,
388
- installedPluginList,
389
- plugins,
390
- isToolEnabled,
391
- handleToggleTool,
392
- toggleAgentPlugin,
393
- t,
394
- ]);
395
-
396
- // Use effective tab for display (default to all while initializing)
397
- const effectiveTab = activeTab ?? 'all';
398
- const currentItems = effectiveTab === 'all' ? allTabItems : installedTabItems;
399
-
400
- const button = (
401
- <Button
402
- icon={PlusIcon}
403
- loading={updating}
404
- size={'small'}
405
- style={{ color: cssVar.colorTextSecondary }}
406
- type={'text'}
407
- >
408
- {t('tools.add', { defaultValue: 'Add' })}
409
- </Button>
410
- );
411
-
412
- // Combine plugins and web browsing for display
413
- const allEnabledTools = useMemo(() => {
414
- const tools = [...plugins];
415
- // Add web browsing if enabled (it's not in plugins array)
416
- if (isSearchEnabled && !tools.includes(WEB_BROWSING_IDENTIFIER)) {
417
- tools.unshift(WEB_BROWSING_IDENTIFIER);
418
- }
419
- return tools;
420
- }, [plugins, isSearchEnabled]);
421
-
422
- return (
423
- <>
424
- {/* Plugin Selector and Tags */}
425
- <Flexbox align="center" gap={8} horizontal wrap={'wrap'}>
426
- {/* Second Row: Selected Plugins as Tags */}
427
- {allEnabledTools.map((pluginId) => {
428
- return (
429
- <PluginTag key={pluginId} onRemove={handleRemovePlugin(pluginId)} pluginId={pluginId} />
430
- );
431
- })}
432
- {/* Plugin Selector Dropdown - Using Action component pattern */}
433
-
434
- <Suspense fallback={button}>
435
- <ActionDropdown
436
- maxHeight={500}
437
- maxWidth={480}
438
- menu={{
439
- items: currentItems,
440
- style: {
441
- // let only the custom scroller scroll
442
- maxHeight: 'unset',
443
- overflowY: 'visible',
444
- },
445
- }}
446
- minHeight={isKlavisEnabledInEnv ? 500 : undefined}
447
- minWidth={320}
448
- placement={'bottomLeft'}
449
- popupRender={(menu) => (
450
- <div className={styles.dropdown}>
451
- {/* stopPropagation prevents dropdown's onClick from calling preventDefault on Segmented */}
452
- <div className={styles.header} onClick={(e) => e.stopPropagation()}>
453
- <Segmented
454
- block
455
- onChange={(v) => setActiveTab(v as TabType)}
456
- options={[
457
- {
458
- label: t('tools.tabs.all', { defaultValue: 'All' }),
459
- value: 'all',
460
- },
461
- {
462
- label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
463
- value: 'installed',
464
- },
465
- ]}
466
- size="small"
467
- value={effectiveTab}
468
- />
469
- </div>
470
- <div
471
- className={styles.scroller}
472
- style={{
473
- maxHeight: 500,
474
- minHeight: isKlavisEnabledInEnv ? 500 : undefined,
475
- }}
476
- >
477
- {menu}
478
- </div>
479
- </div>
480
- )}
481
- trigger={['click']}
482
- >
483
- {button}
484
- </ActionDropdown>
485
- </Suspense>
486
- </Flexbox>
487
-
488
- {/* PluginStore Modal - rendered outside Flexbox to avoid event interference */}
489
- {modalOpen && <PluginStore open={modalOpen} setOpen={setModalOpen} />}
490
- </>
491
- );
492
- });
11
+ const AgentTool = () => {
12
+ return <SharedAgentTool filterAvailableInWeb showWebBrowsing useAllMetaList />;
13
+ };
493
14
 
494
15
  export default AgentTool;