@lobehub/lobehub 2.0.0-next.160 → 2.0.0-next.161
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/.env.example +10 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/e2e/src/steps/hooks.ts +1 -0
- package/locales/ar/setting.json +25 -0
- package/locales/bg-BG/setting.json +25 -0
- package/locales/de-DE/setting.json +25 -0
- package/locales/en-US/setting.json +25 -0
- package/locales/es-ES/setting.json +25 -0
- package/locales/fa-IR/setting.json +25 -0
- package/locales/fr-FR/setting.json +25 -0
- package/locales/it-IT/setting.json +25 -0
- package/locales/ja-JP/setting.json +25 -0
- package/locales/ko-KR/setting.json +25 -0
- package/locales/nl-NL/setting.json +25 -0
- package/locales/pl-PL/setting.json +25 -0
- package/locales/pt-BR/setting.json +25 -0
- package/locales/ru-RU/setting.json +25 -0
- package/locales/tr-TR/setting.json +25 -0
- package/locales/vi-VN/setting.json +25 -0
- package/locales/zh-CN/setting.json +25 -0
- package/locales/zh-TW/setting.json +25 -0
- package/next.config.ts +13 -1
- package/package.json +3 -1
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/klavis.ts +163 -0
- package/packages/database/migrations/meta/_journal.json +1 -1
- package/packages/database/src/core/migrations.json +1 -1
- package/packages/database/src/models/plugin.ts +1 -1
- package/packages/types/src/message/common/tools.ts +9 -0
- package/packages/types/src/serverConfig.ts +1 -0
- package/packages/types/src/tool/plugin.ts +10 -0
- package/src/config/klavis.ts +41 -0
- package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +351 -0
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +56 -4
- package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +174 -6
- package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +3 -1
- package/src/helpers/toolEngineering/index.test.ts +3 -0
- package/src/helpers/toolEngineering/index.ts +13 -2
- package/src/libs/klavis/index.ts +36 -0
- package/src/locales/default/setting.ts +25 -0
- package/src/server/globalConfig/index.ts +2 -0
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/klavis.ts +249 -0
- package/src/server/routers/tools/index.ts +2 -0
- package/src/server/routers/tools/klavis.ts +80 -0
- package/src/server/services/mcp/index.ts +61 -15
- package/src/services/import/index.test.ts +658 -0
- package/src/services/mcp.test.ts +1 -1
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +2 -3
- package/src/store/chat/slices/plugin/action.test.ts +0 -1
- package/src/store/chat/slices/plugin/actions/internals.ts +22 -2
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +108 -0
- package/src/store/serverConfig/index.ts +1 -1
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/tool/initialState.ts +4 -1
- package/src/store/tool/selectors/index.ts +1 -0
- package/src/store/tool/slices/builtin/selectors.ts +25 -3
- package/src/store/tool/slices/klavisStore/action.test.ts +512 -0
- package/src/store/tool/slices/klavisStore/action.ts +375 -0
- package/src/store/tool/slices/klavisStore/index.ts +4 -0
- package/src/store/tool/slices/klavisStore/initialState.ts +25 -0
- package/src/store/tool/slices/klavisStore/selectors.test.ts +371 -0
- package/src/store/tool/slices/klavisStore/selectors.ts +123 -0
- package/src/store/tool/slices/klavisStore/types.ts +100 -0
- package/src/store/tool/slices/plugin/selectors.ts +16 -13
- package/src/store/tool/store.ts +4 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
|
2
|
+
import { Checkbox } from 'antd';
|
|
3
|
+
import { Loader2, SquareArrowOutUpRight, Unplug } from 'lucide-react';
|
|
4
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
|
7
|
+
|
|
8
|
+
import { useAgentStore } from '@/store/agent';
|
|
9
|
+
import { agentSelectors } from '@/store/agent/selectors';
|
|
10
|
+
import { useToolStore } from '@/store/tool';
|
|
11
|
+
import { KlavisServer, KlavisServerStatus } from '@/store/tool/slices/klavisStore';
|
|
12
|
+
import { useUserStore } from '@/store/user';
|
|
13
|
+
import { userProfileSelectors } from '@/store/user/selectors';
|
|
14
|
+
|
|
15
|
+
// 轮询配置
|
|
16
|
+
const POLL_INTERVAL_MS = 1000; // 每秒轮询一次
|
|
17
|
+
const POLL_TIMEOUT_MS = 15_000; // 15 秒超时
|
|
18
|
+
|
|
19
|
+
interface KlavisServerItemProps {
|
|
20
|
+
/**
|
|
21
|
+
* Identifier used for storage (e.g., 'google-calendar')
|
|
22
|
+
*/
|
|
23
|
+
identifier: string;
|
|
24
|
+
label: string;
|
|
25
|
+
server?: KlavisServer;
|
|
26
|
+
/**
|
|
27
|
+
* Server name used to call Klavis API (e.g., 'Google Calendar')
|
|
28
|
+
*/
|
|
29
|
+
serverName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const KlavisServerItem = memo<KlavisServerItemProps>(
|
|
33
|
+
({ identifier, label, server, serverName }) => {
|
|
34
|
+
const { t } = useTranslation('setting');
|
|
35
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
36
|
+
const [isToggling, setIsToggling] = useState(false);
|
|
37
|
+
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
|
|
38
|
+
|
|
39
|
+
const oauthWindowRef = useRef<Window | null>(null);
|
|
40
|
+
const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
41
|
+
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
42
|
+
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
43
|
+
|
|
44
|
+
const userId = useUserStore(userProfileSelectors.userId);
|
|
45
|
+
const createKlavisServer = useToolStore((s) => s.createKlavisServer);
|
|
46
|
+
const refreshKlavisServerTools = useToolStore((s) => s.refreshKlavisServerTools);
|
|
47
|
+
const removeKlavisServer = useToolStore((s) => s.removeKlavisServer);
|
|
48
|
+
|
|
49
|
+
// 清理所有定时器
|
|
50
|
+
const cleanup = useCallback(() => {
|
|
51
|
+
if (windowCheckIntervalRef.current) {
|
|
52
|
+
clearInterval(windowCheckIntervalRef.current);
|
|
53
|
+
windowCheckIntervalRef.current = null;
|
|
54
|
+
}
|
|
55
|
+
if (pollIntervalRef.current) {
|
|
56
|
+
clearInterval(pollIntervalRef.current);
|
|
57
|
+
pollIntervalRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
if (pollTimeoutRef.current) {
|
|
60
|
+
clearTimeout(pollTimeoutRef.current);
|
|
61
|
+
pollTimeoutRef.current = null;
|
|
62
|
+
}
|
|
63
|
+
oauthWindowRef.current = null;
|
|
64
|
+
setIsWaitingAuth(false);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// 组件卸载时清理
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return () => {
|
|
70
|
+
cleanup();
|
|
71
|
+
};
|
|
72
|
+
}, [cleanup]);
|
|
73
|
+
|
|
74
|
+
// 当服务器状态变为 CONNECTED 时停止所有监听
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (server?.status === KlavisServerStatus.CONNECTED && isWaitingAuth) {
|
|
77
|
+
cleanup();
|
|
78
|
+
}
|
|
79
|
+
}, [server?.status, isWaitingAuth, cleanup, t]);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 启动降级轮询(当 window.closed 不可访问时)
|
|
83
|
+
*/
|
|
84
|
+
const startFallbackPolling = useCallback(
|
|
85
|
+
(serverName: string) => {
|
|
86
|
+
// 已经在轮询了,不重复启动
|
|
87
|
+
if (pollIntervalRef.current) return;
|
|
88
|
+
|
|
89
|
+
// 每秒轮询一次
|
|
90
|
+
pollIntervalRef.current = setInterval(async () => {
|
|
91
|
+
try {
|
|
92
|
+
await refreshKlavisServerTools(serverName);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('[Klavis] Failed to check auth status:', error);
|
|
95
|
+
}
|
|
96
|
+
}, POLL_INTERVAL_MS);
|
|
97
|
+
|
|
98
|
+
// 15 秒后超时停止
|
|
99
|
+
pollTimeoutRef.current = setTimeout(() => {
|
|
100
|
+
if (pollIntervalRef.current) {
|
|
101
|
+
clearInterval(pollIntervalRef.current);
|
|
102
|
+
pollIntervalRef.current = null;
|
|
103
|
+
}
|
|
104
|
+
setIsWaitingAuth(false);
|
|
105
|
+
}, POLL_TIMEOUT_MS);
|
|
106
|
+
},
|
|
107
|
+
[refreshKlavisServerTools, t],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 监听 OAuth 窗口关闭
|
|
112
|
+
*/
|
|
113
|
+
const startWindowMonitor = useCallback(
|
|
114
|
+
(oauthWindow: Window, serverName: string) => {
|
|
115
|
+
// 每 500ms 检查窗口状态
|
|
116
|
+
windowCheckIntervalRef.current = setInterval(() => {
|
|
117
|
+
try {
|
|
118
|
+
// 尝试访问 window.closed(可能被 COOP 阻止)
|
|
119
|
+
if (oauthWindow.closed) {
|
|
120
|
+
// 窗口已关闭,清理监听并检查认证状态
|
|
121
|
+
if (windowCheckIntervalRef.current) {
|
|
122
|
+
clearInterval(windowCheckIntervalRef.current);
|
|
123
|
+
windowCheckIntervalRef.current = null;
|
|
124
|
+
}
|
|
125
|
+
oauthWindowRef.current = null;
|
|
126
|
+
|
|
127
|
+
// 窗口关闭后立即检查一次认证状态
|
|
128
|
+
refreshKlavisServerTools(serverName);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// COOP 阻止了访问,降级到轮询方案
|
|
132
|
+
console.log('[Klavis] COOP blocked window.closed access, falling back to polling');
|
|
133
|
+
if (windowCheckIntervalRef.current) {
|
|
134
|
+
clearInterval(windowCheckIntervalRef.current);
|
|
135
|
+
windowCheckIntervalRef.current = null;
|
|
136
|
+
}
|
|
137
|
+
startFallbackPolling(serverName);
|
|
138
|
+
}
|
|
139
|
+
}, 500);
|
|
140
|
+
},
|
|
141
|
+
[refreshKlavisServerTools, startFallbackPolling],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 打开 OAuth 窗口
|
|
146
|
+
*/
|
|
147
|
+
const openOAuthWindow = useCallback(
|
|
148
|
+
(oauthUrl: string, serverName: string) => {
|
|
149
|
+
// 清理之前的状态
|
|
150
|
+
cleanup();
|
|
151
|
+
setIsWaitingAuth(true);
|
|
152
|
+
|
|
153
|
+
// 打开 OAuth 窗口
|
|
154
|
+
const oauthWindow = window.open(oauthUrl, '_blank', 'width=600,height=700');
|
|
155
|
+
if (oauthWindow) {
|
|
156
|
+
oauthWindowRef.current = oauthWindow;
|
|
157
|
+
startWindowMonitor(oauthWindow, serverName);
|
|
158
|
+
} else {
|
|
159
|
+
// 窗口被阻止,直接用轮询
|
|
160
|
+
startFallbackPolling(serverName);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[cleanup, startWindowMonitor, startFallbackPolling, t],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Get plugin ID for this server (使用 identifier 作为 pluginId)
|
|
167
|
+
const pluginId = server ? server.identifier : '';
|
|
168
|
+
const [checked, togglePlugin] = useAgentStore((s) => [
|
|
169
|
+
agentSelectors.currentAgentPlugins(s).includes(pluginId),
|
|
170
|
+
s.togglePlugin,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const handleConnect = async () => {
|
|
174
|
+
if (!userId) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (server) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setIsConnecting(true);
|
|
183
|
+
try {
|
|
184
|
+
const newServer = await createKlavisServer({
|
|
185
|
+
identifier,
|
|
186
|
+
serverName,
|
|
187
|
+
userId,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (newServer) {
|
|
191
|
+
// 安装完成后自动启用插件(使用 identifier)
|
|
192
|
+
const newPluginId = newServer.identifier;
|
|
193
|
+
await togglePlugin(newPluginId);
|
|
194
|
+
|
|
195
|
+
// 如果已认证,直接刷新工具列表,跳过 OAuth
|
|
196
|
+
if (newServer.isAuthenticated) {
|
|
197
|
+
await refreshKlavisServerTools(newServer.identifier);
|
|
198
|
+
} else if (newServer.oauthUrl) {
|
|
199
|
+
// 需要 OAuth,打开 OAuth 窗口并监听关闭
|
|
200
|
+
openOAuthWindow(newServer.oauthUrl, newServer.identifier);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('[Klavis] Failed to connect server:', error);
|
|
205
|
+
} finally {
|
|
206
|
+
setIsConnecting(false);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleToggle = async () => {
|
|
211
|
+
if (!server) return;
|
|
212
|
+
setIsToggling(true);
|
|
213
|
+
await togglePlugin(pluginId);
|
|
214
|
+
setIsToggling(false);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleDisconnect = async () => {
|
|
218
|
+
if (!server) return;
|
|
219
|
+
setIsToggling(true);
|
|
220
|
+
// 如果当前已启用,先禁用
|
|
221
|
+
if (checked) {
|
|
222
|
+
await togglePlugin(pluginId);
|
|
223
|
+
}
|
|
224
|
+
// 删除服务器(使用 identifier)
|
|
225
|
+
await removeKlavisServer(server.identifier);
|
|
226
|
+
setIsToggling(false);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// 渲染右侧控件
|
|
230
|
+
const renderRightControl = () => {
|
|
231
|
+
// 正在连接中
|
|
232
|
+
if (isConnecting) {
|
|
233
|
+
return (
|
|
234
|
+
<Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
|
|
235
|
+
<Icon icon={Loader2} spin />
|
|
236
|
+
</Flexbox>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 未连接,显示 Connect 按钮
|
|
241
|
+
if (!server) {
|
|
242
|
+
return (
|
|
243
|
+
<Flexbox
|
|
244
|
+
align="center"
|
|
245
|
+
gap={4}
|
|
246
|
+
horizontal
|
|
247
|
+
onClick={(e) => {
|
|
248
|
+
e.stopPropagation();
|
|
249
|
+
handleConnect();
|
|
250
|
+
}}
|
|
251
|
+
style={{ cursor: 'pointer', opacity: 0.65 }}
|
|
252
|
+
>
|
|
253
|
+
{t('tools.klavis.connect', { defaultValue: 'Connect' })}
|
|
254
|
+
<Icon icon={SquareArrowOutUpRight} size="small" />
|
|
255
|
+
</Flexbox>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 根据状态显示不同控件
|
|
260
|
+
switch (server.status) {
|
|
261
|
+
case KlavisServerStatus.CONNECTED: {
|
|
262
|
+
// 正在切换状态
|
|
263
|
+
if (isToggling) {
|
|
264
|
+
return <Icon icon={Loader2} spin />;
|
|
265
|
+
}
|
|
266
|
+
return (
|
|
267
|
+
<Flexbox align="center" gap={8} horizontal>
|
|
268
|
+
<Icon
|
|
269
|
+
icon={Unplug}
|
|
270
|
+
onClick={(e) => {
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
handleDisconnect();
|
|
273
|
+
}}
|
|
274
|
+
size="small"
|
|
275
|
+
style={{ cursor: 'pointer', opacity: 0.5 }}
|
|
276
|
+
/>
|
|
277
|
+
<Checkbox
|
|
278
|
+
checked={checked}
|
|
279
|
+
onClick={(e) => {
|
|
280
|
+
e.stopPropagation();
|
|
281
|
+
handleToggle();
|
|
282
|
+
}}
|
|
283
|
+
/>
|
|
284
|
+
</Flexbox>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
case KlavisServerStatus.PENDING_AUTH: {
|
|
288
|
+
// 正在等待认证
|
|
289
|
+
if (isWaitingAuth) {
|
|
290
|
+
return (
|
|
291
|
+
<Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
|
|
292
|
+
<Icon icon={Loader2} spin />
|
|
293
|
+
</Flexbox>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return (
|
|
297
|
+
<Flexbox
|
|
298
|
+
align="center"
|
|
299
|
+
gap={4}
|
|
300
|
+
horizontal
|
|
301
|
+
onClick={(e) => {
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
// 点击重新打开 OAuth 窗口
|
|
304
|
+
if (server.oauthUrl) {
|
|
305
|
+
openOAuthWindow(server.oauthUrl, server.identifier);
|
|
306
|
+
}
|
|
307
|
+
}}
|
|
308
|
+
style={{ cursor: 'pointer', opacity: 0.65 }}
|
|
309
|
+
>
|
|
310
|
+
{t('tools.klavis.pendingAuth', { defaultValue: 'Authorize' })}
|
|
311
|
+
<Icon icon={SquareArrowOutUpRight} size="small" />
|
|
312
|
+
</Flexbox>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
case KlavisServerStatus.ERROR: {
|
|
316
|
+
return (
|
|
317
|
+
<span style={{ color: 'red', fontSize: 12 }}>
|
|
318
|
+
{t('tools.klavis.error', { defaultValue: 'Error' })}
|
|
319
|
+
</span>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
default: {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<Flexbox
|
|
330
|
+
gap={24}
|
|
331
|
+
horizontal
|
|
332
|
+
justify={'space-between'}
|
|
333
|
+
onClick={(e) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
// 如果已连接,点击整行切换状态
|
|
336
|
+
if (server?.status === KlavisServerStatus.CONNECTED) {
|
|
337
|
+
handleToggle();
|
|
338
|
+
}
|
|
339
|
+
}}
|
|
340
|
+
style={{ paddingLeft: 8 }}
|
|
341
|
+
>
|
|
342
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
343
|
+
{label}
|
|
344
|
+
</Flexbox>
|
|
345
|
+
{renderRightControl()}
|
|
346
|
+
</Flexbox>
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
export default KlavisServerItem;
|
|
@@ -1,22 +1,43 @@
|
|
|
1
|
+
import { Segmented } from '@lobehub/ui';
|
|
1
2
|
import { Blocks } from 'lucide-react';
|
|
2
|
-
import { Suspense, memo, useState } from 'react';
|
|
3
|
+
import { Suspense, memo, useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
|
|
5
6
|
import PluginStore from '@/features/PluginStore';
|
|
6
7
|
import { useModelSupportToolUse } from '@/hooks/useModelSupportToolUse';
|
|
7
8
|
import { useAgentStore } from '@/store/agent';
|
|
8
9
|
import { agentSelectors } from '@/store/agent/selectors';
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
featureFlagsSelectors,
|
|
12
|
+
serverConfigSelectors,
|
|
13
|
+
useServerConfigStore,
|
|
14
|
+
} from '@/store/serverConfig';
|
|
10
15
|
|
|
11
16
|
import Action from '../components/Action';
|
|
12
17
|
import { useControls } from './useControls';
|
|
13
18
|
|
|
19
|
+
type TabType = 'all' | 'installed';
|
|
20
|
+
|
|
14
21
|
const Tools = memo(() => {
|
|
15
22
|
const { t } = useTranslation('setting');
|
|
16
23
|
const [modalOpen, setModalOpen] = useState(false);
|
|
17
24
|
const [updating, setUpdating] = useState(false);
|
|
18
|
-
const
|
|
25
|
+
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
|
26
|
+
const { marketItems, installedPluginItems } = useControls({
|
|
27
|
+
setModalOpen,
|
|
28
|
+
setUpdating,
|
|
29
|
+
});
|
|
19
30
|
const { enablePlugins } = useServerConfigStore(featureFlagsSelectors);
|
|
31
|
+
const enableKlavis = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
|
32
|
+
const isInitializedRef = useRef(false);
|
|
33
|
+
|
|
34
|
+
// Set default tab based on installed plugins (only on first load)
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!isInitializedRef.current && installedPluginItems.length >= 0) {
|
|
37
|
+
isInitializedRef.current = true;
|
|
38
|
+
setActiveTab(installedPluginItems.length > 0 ? 'installed' : 'all');
|
|
39
|
+
}
|
|
40
|
+
}, [installedPluginItems.length]);
|
|
20
41
|
|
|
21
42
|
const model = useAgentStore(agentSelectors.currentAgentModel);
|
|
22
43
|
const provider = useAgentStore(agentSelectors.currentAgentModelProvider);
|
|
@@ -27,13 +48,44 @@ const Tools = memo(() => {
|
|
|
27
48
|
if (!enableFC)
|
|
28
49
|
return <Action disabled icon={Blocks} showTooltip={true} title={t('tools.disabled')} />;
|
|
29
50
|
|
|
51
|
+
// Use effective tab for display (default to market while initializing)
|
|
52
|
+
const effectiveTab = activeTab ?? 'all';
|
|
53
|
+
const currentItems = effectiveTab === 'all' ? marketItems : installedPluginItems;
|
|
54
|
+
|
|
30
55
|
return (
|
|
31
56
|
<Suspense fallback={<Action disabled icon={Blocks} title={t('tools.title')} />}>
|
|
32
57
|
<Action
|
|
33
58
|
dropdown={{
|
|
34
59
|
maxHeight: 500,
|
|
35
60
|
maxWidth: 480,
|
|
36
|
-
menu: {
|
|
61
|
+
menu: {
|
|
62
|
+
items: [
|
|
63
|
+
{
|
|
64
|
+
key: 'tabs',
|
|
65
|
+
label: (
|
|
66
|
+
<Segmented
|
|
67
|
+
block
|
|
68
|
+
onChange={(v) => setActiveTab(v as TabType)}
|
|
69
|
+
options={[
|
|
70
|
+
{
|
|
71
|
+
label: t('tools.tabs.all', { defaultValue: 'all' }),
|
|
72
|
+
value: 'all',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
|
|
76
|
+
value: 'installed',
|
|
77
|
+
},
|
|
78
|
+
]}
|
|
79
|
+
size="small"
|
|
80
|
+
value={effectiveTab}
|
|
81
|
+
/>
|
|
82
|
+
),
|
|
83
|
+
type: 'group',
|
|
84
|
+
},
|
|
85
|
+
...currentItems,
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
minHeight: enableKlavis ? 500 : undefined,
|
|
37
89
|
minWidth: 320,
|
|
38
90
|
}}
|
|
39
91
|
icon={Blocks}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { KLAVIS_SERVER_TYPES, KlavisServerType } from '@lobechat/const';
|
|
1
2
|
import { Avatar, Icon, ItemType } from '@lobehub/ui';
|
|
3
|
+
import { useTheme } from 'antd-style';
|
|
2
4
|
import isEqual from 'fast-deep-equal';
|
|
3
5
|
import { ArrowRight, Store, ToyBrick } from 'lucide-react';
|
|
6
|
+
import Image from 'next/image';
|
|
7
|
+
import { memo, useMemo } from 'react';
|
|
4
8
|
import { useTranslation } from 'react-i18next';
|
|
5
9
|
import { Flexbox } from 'react-layout-kit';
|
|
6
10
|
|
|
@@ -9,11 +13,37 @@ import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
|
|
|
9
13
|
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
|
10
14
|
import { useAgentStore } from '@/store/agent';
|
|
11
15
|
import { agentSelectors } from '@/store/agent/selectors';
|
|
16
|
+
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
12
17
|
import { useToolStore } from '@/store/tool';
|
|
13
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
builtinToolSelectors,
|
|
20
|
+
klavisStoreSelectors,
|
|
21
|
+
pluginSelectors,
|
|
22
|
+
} from '@/store/tool/selectors';
|
|
14
23
|
|
|
24
|
+
import KlavisServerItem from './KlavisServerItem';
|
|
15
25
|
import ToolItem from './ToolItem';
|
|
16
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Klavis 服务器图标组件
|
|
29
|
+
* 对于 string 类型的 icon,使用 Image 组件渲染
|
|
30
|
+
* 对于 IconType 类型的 icon,使用 Icon 组件渲染,并根据主题设置填充色
|
|
31
|
+
*/
|
|
32
|
+
const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
|
|
35
|
+
if (typeof icon === 'string') {
|
|
36
|
+
return (
|
|
37
|
+
<Image alt={label} height={18} src={icon} style={{ flex: 'none' }} unoptimized width={18} />
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 使用主题色填充,在深色模式下自动适应
|
|
42
|
+
return <Icon fill={theme.colorText} icon={icon} size={18} />;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
KlavisIcon.displayName = 'KlavisIcon';
|
|
46
|
+
|
|
17
47
|
export const useControls = ({
|
|
18
48
|
setModalOpen,
|
|
19
49
|
setUpdating,
|
|
@@ -36,15 +66,67 @@ export const useControls = ({
|
|
|
36
66
|
);
|
|
37
67
|
const plugins = useAgentStore((s) => agentSelectors.currentAgentPlugins(s));
|
|
38
68
|
|
|
39
|
-
|
|
69
|
+
// Klavis 相关状态
|
|
70
|
+
const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
|
|
71
|
+
const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
|
72
|
+
|
|
73
|
+
const [useFetchPluginStore, useFetchUserKlavisServers] = useToolStore((s) => [
|
|
74
|
+
s.useFetchPluginStore,
|
|
75
|
+
s.useFetchUserKlavisServers,
|
|
76
|
+
]);
|
|
40
77
|
|
|
41
78
|
useFetchPluginStore();
|
|
42
79
|
useFetchInstalledPlugins();
|
|
43
80
|
useCheckPluginsIsInstalled(plugins);
|
|
44
81
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
// 使用 SWR 加载用户的 Klavis 集成(从数据库)
|
|
83
|
+
useFetchUserKlavisServers(isKlavisEnabledInEnv);
|
|
84
|
+
|
|
85
|
+
// 根据 identifier 获取已连接的服务器
|
|
86
|
+
const getServerByName = (identifier: string) => {
|
|
87
|
+
return allKlavisServers.find((server) => server.identifier === identifier);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// 获取所有 Klavis 服务器类型的 identifier 集合(用于过滤 builtinList)
|
|
91
|
+
// 这里使用 KLAVIS_SERVER_TYPES 而不是已连接的服务器,因为我们要过滤掉所有可能的 Klavis 类型
|
|
92
|
+
const allKlavisTypeIdentifiers = useMemo(
|
|
93
|
+
() => new Set(KLAVIS_SERVER_TYPES.map((type) => type.identifier)),
|
|
94
|
+
[],
|
|
95
|
+
);
|
|
96
|
+
// 过滤掉 builtinList 中的 klavis 工具(它们会单独显示在 Klavis 区域)
|
|
97
|
+
const filteredBuiltinList = useMemo(
|
|
98
|
+
() =>
|
|
99
|
+
isKlavisEnabledInEnv
|
|
100
|
+
? builtinList.filter((item) => !allKlavisTypeIdentifiers.has(item.identifier))
|
|
101
|
+
: builtinList,
|
|
102
|
+
[builtinList, allKlavisTypeIdentifiers, isKlavisEnabledInEnv],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Klavis 服务器列表项
|
|
106
|
+
const klavisServerItems = useMemo(
|
|
107
|
+
() =>
|
|
108
|
+
isKlavisEnabledInEnv
|
|
109
|
+
? KLAVIS_SERVER_TYPES.map((type) => ({
|
|
110
|
+
icon: <KlavisIcon icon={type.icon} label={type.label} />,
|
|
111
|
+
key: type.identifier,
|
|
112
|
+
label: (
|
|
113
|
+
<KlavisServerItem
|
|
114
|
+
identifier={type.identifier}
|
|
115
|
+
label={type.label}
|
|
116
|
+
server={getServerByName(type.identifier)}
|
|
117
|
+
serverName={type.serverName}
|
|
118
|
+
/>
|
|
119
|
+
),
|
|
120
|
+
}))
|
|
121
|
+
: [],
|
|
122
|
+
[isKlavisEnabledInEnv, allKlavisServers],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 合并 builtin 工具和 Klavis 服务器
|
|
126
|
+
const builtinItems = useMemo(
|
|
127
|
+
() => [
|
|
128
|
+
// 原有的 builtin 工具
|
|
129
|
+
...filteredBuiltinList.map((item) => ({
|
|
48
130
|
icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
|
|
49
131
|
key: item.identifier,
|
|
50
132
|
label: (
|
|
@@ -60,7 +142,16 @@ export const useControls = ({
|
|
|
60
142
|
/>
|
|
61
143
|
),
|
|
62
144
|
})),
|
|
145
|
+
// Klavis 服务器
|
|
146
|
+
...klavisServerItems,
|
|
147
|
+
],
|
|
148
|
+
[filteredBuiltinList, klavisServerItems, checked, togglePlugin, setUpdating],
|
|
149
|
+
);
|
|
63
150
|
|
|
151
|
+
// 市场 tab 的 items
|
|
152
|
+
const marketItems: ItemType[] = [
|
|
153
|
+
{
|
|
154
|
+
children: builtinItems,
|
|
64
155
|
key: 'builtins',
|
|
65
156
|
label: t('tools.builtins.groupName'),
|
|
66
157
|
type: 'group',
|
|
@@ -113,5 +204,82 @@ export const useControls = ({
|
|
|
113
204
|
},
|
|
114
205
|
];
|
|
115
206
|
|
|
116
|
-
|
|
207
|
+
// 已安装 tab 的 items - 只显示已安装的插件
|
|
208
|
+
const installedPluginItems: ItemType[] = useMemo(() => {
|
|
209
|
+
const installedItems: ItemType[] = [];
|
|
210
|
+
|
|
211
|
+
// 已安装的 builtin 工具
|
|
212
|
+
const enabledBuiltinItems = filteredBuiltinList
|
|
213
|
+
.filter((item) => checked.includes(item.identifier))
|
|
214
|
+
.map((item) => ({
|
|
215
|
+
icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
|
|
216
|
+
key: item.identifier,
|
|
217
|
+
label: (
|
|
218
|
+
<ToolItem
|
|
219
|
+
checked={true}
|
|
220
|
+
id={item.identifier}
|
|
221
|
+
label={item.meta?.title}
|
|
222
|
+
onUpdate={async () => {
|
|
223
|
+
setUpdating(true);
|
|
224
|
+
await togglePlugin(item.identifier);
|
|
225
|
+
setUpdating(false);
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
),
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
// 已连接的 Klavis 服务器(放在 builtin 里面)
|
|
232
|
+
const connectedKlavisItems = klavisServerItems.filter((item) =>
|
|
233
|
+
checked.includes(item.key as string),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// 合并 builtin 和 Klavis
|
|
237
|
+
const allBuiltinItems = [...enabledBuiltinItems, ...connectedKlavisItems];
|
|
238
|
+
|
|
239
|
+
if (allBuiltinItems.length > 0) {
|
|
240
|
+
installedItems.push({
|
|
241
|
+
children: allBuiltinItems,
|
|
242
|
+
key: 'installed-builtins',
|
|
243
|
+
label: t('tools.builtins.groupName'),
|
|
244
|
+
type: 'group',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 已安装的插件
|
|
249
|
+
const installedPlugins = list
|
|
250
|
+
.filter((item) => checked.includes(item.identifier))
|
|
251
|
+
.map((item) => ({
|
|
252
|
+
icon: item?.avatar ? (
|
|
253
|
+
<PluginAvatar avatar={item.avatar} size={20} />
|
|
254
|
+
) : (
|
|
255
|
+
<Icon icon={ToyBrick} size={20} />
|
|
256
|
+
),
|
|
257
|
+
key: item.identifier,
|
|
258
|
+
label: (
|
|
259
|
+
<ToolItem
|
|
260
|
+
checked={true}
|
|
261
|
+
id={item.identifier}
|
|
262
|
+
label={item.title}
|
|
263
|
+
onUpdate={async () => {
|
|
264
|
+
setUpdating(true);
|
|
265
|
+
await togglePlugin(item.identifier);
|
|
266
|
+
setUpdating(false);
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
),
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
if (installedPlugins.length > 0) {
|
|
273
|
+
installedItems.push({
|
|
274
|
+
children: installedPlugins,
|
|
275
|
+
key: 'installed-plugins',
|
|
276
|
+
label: t('tools.plugins.groupName'),
|
|
277
|
+
type: 'group',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return installedItems;
|
|
282
|
+
}, [filteredBuiltinList, list, klavisServerItems, checked, togglePlugin, setUpdating, t]);
|
|
283
|
+
|
|
284
|
+
return { installedPluginItems, marketItems };
|
|
117
285
|
};
|
|
@@ -22,6 +22,7 @@ const useStyles = createStyles(({ css, prefixCls }) => ({
|
|
|
22
22
|
export interface ActionDropdownProps extends DropdownProps {
|
|
23
23
|
maxHeight?: number | string;
|
|
24
24
|
maxWidth?: number | string;
|
|
25
|
+
minHeight?: number | string;
|
|
25
26
|
minWidth?: number | string;
|
|
26
27
|
/**
|
|
27
28
|
* 是否在挂载时预渲染弹层,避免首次触发展开时的渲染卡顿
|
|
@@ -30,7 +31,7 @@ export interface ActionDropdownProps extends DropdownProps {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const ActionDropdown = memo<ActionDropdownProps>(
|
|
33
|
-
({ menu, maxHeight, minWidth, maxWidth, children, placement = 'top', ...rest }) => {
|
|
34
|
+
({ menu, maxHeight, minWidth, maxWidth, children, placement = 'top', minHeight, ...rest }) => {
|
|
34
35
|
const { cx, styles } = useStyles();
|
|
35
36
|
const isMobile = useIsMobile();
|
|
36
37
|
|
|
@@ -48,6 +49,7 @@ const ActionDropdown = memo<ActionDropdownProps>(
|
|
|
48
49
|
style: {
|
|
49
50
|
maxHeight,
|
|
50
51
|
maxWidth: isMobile ? undefined : maxWidth,
|
|
52
|
+
minHeight,
|
|
51
53
|
minWidth: isMobile ? undefined : minWidth,
|
|
52
54
|
overflowX: 'hidden',
|
|
53
55
|
overflowY: 'scroll',
|