@lobehub/lobehub 2.0.0-next.233 → 2.0.0-next.234

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 (86) hide show
  1. package/.github/workflows/e2e.yml +6 -12
  2. package/.github/workflows/test.yml +3 -3
  3. package/CHANGELOG.md +34 -0
  4. package/CLAUDE.md +1 -1
  5. package/changelog/v1.json +9 -0
  6. package/docs/development/basic/feature-development.mdx +4 -5
  7. package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
  8. package/e2e/README.md +6 -6
  9. package/e2e/src/features/community/detail-pages.feature +9 -9
  10. package/e2e/src/features/community/interactions.feature +13 -13
  11. package/e2e/src/features/community/smoke.feature +6 -6
  12. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
  13. package/e2e/src/steps/agent/conversation.steps.ts +58 -0
  14. package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
  15. package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
  16. package/e2e/src/steps/community/interactions.steps.ts +145 -32
  17. package/e2e/src/steps/hooks.ts +12 -2
  18. package/locales/en-US/setting.json +3 -0
  19. package/locales/zh-CN/file.json +4 -0
  20. package/locales/zh-CN/setting.json +3 -0
  21. package/package.json +5 -5
  22. package/packages/const/src/index.ts +1 -0
  23. package/packages/const/src/lobehubSkill.ts +55 -0
  24. package/packages/types/package.json +1 -1
  25. package/packages/types/src/files/upload.ts +11 -1
  26. package/packages/types/src/message/common/tools.ts +1 -1
  27. package/packages/types/src/serverConfig.ts +1 -0
  28. package/public/not-compatible.html +1296 -0
  29. package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
  30. package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
  31. package/src/app/[variants]/layout.tsx +50 -1
  32. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
  33. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
  34. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
  35. package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
  36. package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
  37. package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
  38. package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
  39. package/src/features/FileViewer/index.tsx +135 -24
  40. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
  41. package/src/features/PageEditor/store/initialState.ts +2 -1
  42. package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
  43. package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
  44. package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
  45. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
  46. package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
  47. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
  48. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
  49. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
  50. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
  51. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
  52. package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
  53. package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
  54. package/src/features/ResourceManager/index.tsx +1 -0
  55. package/src/helpers/toolEngineering/index.test.ts +3 -0
  56. package/src/helpers/toolEngineering/index.ts +12 -1
  57. package/src/locales/default/file.ts +4 -0
  58. package/src/locales/default/setting.ts +3 -0
  59. package/src/server/globalConfig/index.ts +1 -0
  60. package/src/server/modules/ModelRuntime/index.test.ts +214 -1
  61. package/src/server/modules/ModelRuntime/index.ts +43 -7
  62. package/src/server/routers/lambda/document.ts +44 -0
  63. package/src/server/routers/tools/market.ts +261 -0
  64. package/src/server/services/document/index.ts +22 -0
  65. package/src/services/document/index.ts +4 -0
  66. package/src/services/upload.ts +22 -2
  67. package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
  68. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
  69. package/src/store/file/slices/fileManager/action.test.ts +9 -3
  70. package/src/store/file/slices/fileManager/action.ts +165 -70
  71. package/src/store/file/slices/upload/action.ts +3 -0
  72. package/src/store/global/actions/general.ts +15 -0
  73. package/src/store/global/initialState.ts +13 -0
  74. package/src/store/serverConfig/selectors.ts +1 -0
  75. package/src/store/tool/initialState.ts +11 -2
  76. package/src/store/tool/selectors/index.ts +1 -0
  77. package/src/store/tool/selectors/tool.ts +3 -1
  78. package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
  79. package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
  80. package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
  81. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
  82. package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
  83. package/src/store/tool/store.ts +8 -2
  84. package/vitest.config.mts +1 -0
  85. package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
  86. package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { ActionIcon, Flexbox, Icon, Tag } from '@lobehub/ui';
4
4
  import { Descriptions, Divider } from 'antd';
5
- import { cssVar } from 'antd-style';
6
5
  import dayjs from 'dayjs';
7
6
  import { BoltIcon, DownloadIcon } from 'lucide-react';
8
7
  import { memo } from 'react';
@@ -12,10 +11,23 @@ import { type FileListItem } from '@/types/files';
12
11
  import { downloadFile } from '@/utils/client/downloadFile';
13
12
  import { formatSize } from '@/utils/format';
14
13
 
15
- export const DETAIL_PANEL_WIDTH = 300;
14
+ interface FileDetailProps extends FileListItem {
15
+ showDownloadButton?: boolean;
16
+ showTitle?: boolean;
17
+ }
16
18
 
17
- const FileDetail = memo<FileListItem>((props) => {
18
- const { name, embeddingStatus, size, createdAt, updatedAt, chunkCount, url } = props || {};
19
+ const FileDetail = memo<FileDetailProps>((props) => {
20
+ const {
21
+ name,
22
+ embeddingStatus,
23
+ size,
24
+ createdAt,
25
+ updatedAt,
26
+ chunkCount,
27
+ url,
28
+ showDownloadButton = true,
29
+ showTitle = true,
30
+ } = props || {};
19
31
  const { t } = useTranslation('file');
20
32
 
21
33
  if (!props) return null;
@@ -64,16 +76,12 @@ const FileDetail = memo<FileListItem>((props) => {
64
76
  ];
65
77
 
66
78
  return (
67
- <Flexbox
68
- padding={16}
69
- style={{ borderInlineStart: `1px solid ${cssVar.colorSplit}` }}
70
- width={DETAIL_PANEL_WIDTH}
71
- >
79
+ <Flexbox>
72
80
  <Descriptions
73
81
  colon={false}
74
82
  column={1}
75
83
  extra={
76
- url && (
84
+ showDownloadButton && url ? (
77
85
  <ActionIcon
78
86
  icon={DownloadIcon}
79
87
  onClick={() => {
@@ -81,12 +89,12 @@ const FileDetail = memo<FileListItem>((props) => {
81
89
  }}
82
90
  title={t('download', { ns: 'common' })}
83
91
  />
84
- )
92
+ ) : undefined
85
93
  }
86
94
  items={items}
87
95
  labelStyle={{ width: 120 }}
88
96
  size={'small'}
89
- title={t('detail.basic.title')}
97
+ title={showTitle ? t('detail.basic.title') : undefined}
90
98
  />
91
99
  <Divider />
92
100
  <Descriptions
@@ -5,8 +5,6 @@ import { ConfigProvider } from 'antd';
5
5
  import { createStaticStyles, cx } from 'antd-style';
6
6
  import { type ReactNode, useCallback, useState } from 'react';
7
7
 
8
- import { DETAIL_PANEL_WIDTH } from '../FileDetail';
9
-
10
8
  const styles = createStaticStyles(({ css, cssVar }) => ({
11
9
  body: css`
12
10
  height: 100%;
@@ -23,7 +21,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
23
21
  inset-block: 0 0;
24
22
  inset-inline-end: 0;
25
23
 
26
- width: ${DETAIL_PANEL_WIDTH}px;
24
+ width: 0;
27
25
  border-inline-start: 1px solid ${cssVar.colorSplit};
28
26
 
29
27
  background: ${cssVar.colorBgLayout};
@@ -46,7 +44,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
46
44
  }
47
45
  `,
48
46
  modal_withDetail: css`
49
- width: calc(100vw - ${DETAIL_PANEL_WIDTH}px) !important;
47
+ width: calc(100vw) !important;
50
48
  `,
51
49
  }));
52
50
 
@@ -50,6 +50,9 @@ const RootLayout = async ({ children, params }: RootLayoutProps) => {
50
50
  return (
51
51
  <html dir={direction} lang={locale} suppressHydrationWarning>
52
52
  <head>
53
+ {/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
54
+ <script dangerouslySetInnerHTML={{ __html: `(${outdateBrowserScript.toString()})();` }} />
55
+
53
56
  {/* <script dangerouslySetInnerHTML={{ __html: 'setTimeout(() => {debugger}, 16)' }} /> */}
54
57
  {process.env.DEBUG_REACT_SCAN === '1' && (
55
58
  <Script
@@ -74,6 +77,52 @@ const RootLayout = async ({ children, params }: RootLayoutProps) => {
74
77
  );
75
78
  };
76
79
 
80
+ function outdateBrowserScript() {
81
+ // eslint-disable-next-line unicorn/consistent-function-scoping
82
+ function supportsImportMaps(): boolean {
83
+ return (
84
+ typeof HTMLScriptElement !== 'undefined' &&
85
+ typeof (HTMLScriptElement as any).supports === 'function' &&
86
+ (HTMLScriptElement as any).supports('importmap')
87
+ );
88
+ }
89
+
90
+ // eslint-disable-next-line unicorn/consistent-function-scoping
91
+ function supportsCascadeLayers(): boolean {
92
+ if (typeof document === 'undefined') return false;
93
+
94
+ const el = document.createElement('div');
95
+ el.className = '__layer_test__';
96
+ el.style.position = 'absolute';
97
+ el.style.left = '-99999px';
98
+ el.style.top = '-99999px';
99
+
100
+ const style = document.createElement('style');
101
+ style.textContent = `
102
+ @layer a, b;
103
+ @layer a { .__layer_test__ { color: rgb(1, 2, 3); } }
104
+ @layer b { .__layer_test__ { color: rgb(4, 5, 6); } }
105
+ `;
106
+
107
+ document.documentElement.append(style);
108
+ document.documentElement.append(el);
109
+
110
+ const color = getComputedStyle(el).color;
111
+
112
+ el.remove();
113
+ style.remove();
114
+
115
+ return color === 'rgb(4, 5, 6)';
116
+ }
117
+
118
+ const isOutdateBrowser = !(supportsImportMaps() && supportsCascadeLayers());
119
+ if (isOutdateBrowser) {
120
+ window.location.href = '/not-compatible.html';
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+
77
126
  export default RootLayout;
78
127
 
79
128
  export { generateMetadata } from './metadata';
@@ -99,7 +148,7 @@ export const generateViewport = async (props: DynamicLayoutProps): ResolvingView
99
148
 
100
149
  export const generateStaticParams = () => {
101
150
  const mobileOptions = isDesktop ? [false] : [true, false];
102
- // only static for serveral page, other go to dynamtic
151
+ // only static for several page, other go to dynamic
103
152
  const staticLocales: Locales[] = [DEFAULT_LANG, 'zh-CN'];
104
153
 
105
154
  const variants: { variants: string }[] = [];
@@ -0,0 +1,304 @@
1
+ import { Checkbox, Flexbox, Icon } from '@lobehub/ui';
2
+ import { Loader2, SquareArrowOutUpRight, Unplug } from 'lucide-react';
3
+ import { memo, useCallback, useEffect, useRef, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { useAgentStore } from '@/store/agent';
7
+ import { agentSelectors } from '@/store/agent/selectors';
8
+ import { useToolStore } from '@/store/tool';
9
+ import { lobehubSkillStoreSelectors } from '@/store/tool/selectors';
10
+ import { LobehubSkillStatus } from '@/store/tool/slices/lobehubSkillStore/types';
11
+
12
+ const POLL_INTERVAL_MS = 1000;
13
+ const POLL_TIMEOUT_MS = 15_000;
14
+
15
+ interface LobehubSkillServerItemProps {
16
+ /**
17
+ * Display label for the provider
18
+ */
19
+ label: string;
20
+ /**
21
+ * Provider ID (e.g., 'linear', 'github')
22
+ */
23
+ provider: string;
24
+ }
25
+
26
+ const LobehubSkillServerItem = memo<LobehubSkillServerItemProps>(({ provider, label }) => {
27
+ const { t } = useTranslation('setting');
28
+ const [isConnecting, setIsConnecting] = useState(false);
29
+ const [isToggling, setIsToggling] = useState(false);
30
+ const [isWaitingAuth, setIsWaitingAuth] = useState(false);
31
+
32
+ const oauthWindowRef = useRef<Window | null>(null);
33
+ const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
34
+ const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
35
+ const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
36
+
37
+ const server = useToolStore(lobehubSkillStoreSelectors.getServerByIdentifier(provider));
38
+ const checkStatus = useToolStore((s) => s.checkLobehubSkillStatus);
39
+ const revokeConnect = useToolStore((s) => s.revokeLobehubSkill);
40
+ const getAuthorizeUrl = useToolStore((s) => s.getLobehubSkillAuthorizeUrl);
41
+
42
+ const cleanup = useCallback(() => {
43
+ if (windowCheckIntervalRef.current) {
44
+ clearInterval(windowCheckIntervalRef.current);
45
+ windowCheckIntervalRef.current = null;
46
+ }
47
+ if (pollIntervalRef.current) {
48
+ clearInterval(pollIntervalRef.current);
49
+ pollIntervalRef.current = null;
50
+ }
51
+ if (pollTimeoutRef.current) {
52
+ clearTimeout(pollTimeoutRef.current);
53
+ pollTimeoutRef.current = null;
54
+ }
55
+ oauthWindowRef.current = null;
56
+ setIsWaitingAuth(false);
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ return () => {
61
+ cleanup();
62
+ };
63
+ }, [cleanup]);
64
+
65
+ useEffect(() => {
66
+ if (server?.status === LobehubSkillStatus.CONNECTED && isWaitingAuth) {
67
+ cleanup();
68
+ }
69
+ }, [server?.status, isWaitingAuth, cleanup]);
70
+
71
+ const startFallbackPolling = useCallback(() => {
72
+ if (pollIntervalRef.current) return;
73
+
74
+ pollIntervalRef.current = setInterval(async () => {
75
+ try {
76
+ await checkStatus(provider);
77
+ } catch (error) {
78
+ console.error('[LobehubSkill] Failed to check status:', error);
79
+ }
80
+ }, POLL_INTERVAL_MS);
81
+
82
+ pollTimeoutRef.current = setTimeout(() => {
83
+ if (pollIntervalRef.current) {
84
+ clearInterval(pollIntervalRef.current);
85
+ pollIntervalRef.current = null;
86
+ }
87
+ setIsWaitingAuth(false);
88
+ }, POLL_TIMEOUT_MS);
89
+ }, [checkStatus, provider]);
90
+
91
+ const startWindowMonitor = useCallback(
92
+ (oauthWindow: Window) => {
93
+ windowCheckIntervalRef.current = setInterval(() => {
94
+ try {
95
+ if (oauthWindow.closed) {
96
+ if (windowCheckIntervalRef.current) {
97
+ clearInterval(windowCheckIntervalRef.current);
98
+ windowCheckIntervalRef.current = null;
99
+ }
100
+ oauthWindowRef.current = null;
101
+ checkStatus(provider);
102
+ }
103
+ } catch {
104
+ console.log('[LobehubSkill] COOP blocked window.closed access, falling back to polling');
105
+ if (windowCheckIntervalRef.current) {
106
+ clearInterval(windowCheckIntervalRef.current);
107
+ windowCheckIntervalRef.current = null;
108
+ }
109
+ startFallbackPolling();
110
+ }
111
+ }, 500);
112
+ },
113
+ [checkStatus, provider, startFallbackPolling],
114
+ );
115
+
116
+ const openOAuthWindow = useCallback(
117
+ (authorizeUrl: string) => {
118
+ cleanup();
119
+ setIsWaitingAuth(true);
120
+
121
+ const oauthWindow = window.open(authorizeUrl, '_blank', 'width=600,height=700');
122
+ if (oauthWindow) {
123
+ oauthWindowRef.current = oauthWindow;
124
+ startWindowMonitor(oauthWindow);
125
+ } else {
126
+ startFallbackPolling();
127
+ }
128
+ },
129
+ [cleanup, startWindowMonitor, startFallbackPolling],
130
+ );
131
+
132
+ const pluginId = server ? server.identifier : '';
133
+ const [checked, togglePlugin] = useAgentStore((s) => [
134
+ agentSelectors.currentAgentPlugins(s).includes(pluginId),
135
+ s.togglePlugin,
136
+ ]);
137
+
138
+ const handleConnect = async () => {
139
+ // 只有已连接状态才阻止重新连接
140
+ if (server?.isConnected) return;
141
+
142
+ setIsConnecting(true);
143
+ try {
144
+ const { authorizeUrl } = await getAuthorizeUrl(provider);
145
+ openOAuthWindow(authorizeUrl);
146
+ } catch (error) {
147
+ console.error('[LobehubSkill] Failed to get authorize URL:', error);
148
+ } finally {
149
+ setIsConnecting(false);
150
+ }
151
+ };
152
+
153
+ const handleToggle = async () => {
154
+ if (!server) return;
155
+ setIsToggling(true);
156
+ await togglePlugin(pluginId);
157
+ setIsToggling(false);
158
+ };
159
+
160
+ const handleDisconnect = async () => {
161
+ if (!server) return;
162
+ setIsToggling(true);
163
+ if (checked) {
164
+ await togglePlugin(pluginId);
165
+ }
166
+ await revokeConnect(server.identifier);
167
+ setIsToggling(false);
168
+ };
169
+
170
+ const renderRightControl = () => {
171
+ if (isConnecting) {
172
+ return (
173
+ <Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
174
+ <Icon icon={Loader2} spin />
175
+ </Flexbox>
176
+ );
177
+ }
178
+
179
+ if (!server) {
180
+ return (
181
+ <Flexbox
182
+ align="center"
183
+ gap={4}
184
+ horizontal
185
+ onClick={(e) => {
186
+ e.stopPropagation();
187
+ handleConnect();
188
+ }}
189
+ style={{ cursor: 'pointer', opacity: 0.65 }}
190
+ >
191
+ {t('tools.lobehubSkill.connect', { defaultValue: 'Connect' })}
192
+ <Icon icon={SquareArrowOutUpRight} size="small" />
193
+ </Flexbox>
194
+ );
195
+ }
196
+
197
+ switch (server.status) {
198
+ case LobehubSkillStatus.CONNECTED: {
199
+ if (isToggling) {
200
+ return <Icon icon={Loader2} spin />;
201
+ }
202
+ return (
203
+ <Flexbox align="center" gap={8} horizontal>
204
+ <Icon
205
+ icon={Unplug}
206
+ onClick={(e) => {
207
+ e.stopPropagation();
208
+ handleDisconnect();
209
+ }}
210
+ size="small"
211
+ style={{ cursor: 'pointer', opacity: 0.5 }}
212
+ />
213
+ <Checkbox
214
+ checked={checked}
215
+ onClick={(e) => {
216
+ e.stopPropagation();
217
+ handleToggle();
218
+ }}
219
+ />
220
+ </Flexbox>
221
+ );
222
+ }
223
+ case LobehubSkillStatus.CONNECTING: {
224
+ if (isWaitingAuth) {
225
+ return (
226
+ <Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
227
+ <Icon icon={Loader2} spin />
228
+ </Flexbox>
229
+ );
230
+ }
231
+ return (
232
+ <Flexbox
233
+ align="center"
234
+ gap={4}
235
+ horizontal
236
+ onClick={async (e) => {
237
+ e.stopPropagation();
238
+ try {
239
+ const { authorizeUrl } = await getAuthorizeUrl(provider);
240
+ openOAuthWindow(authorizeUrl);
241
+ } catch (error) {
242
+ console.error('[LobehubSkill] Failed to get authorize URL:', error);
243
+ }
244
+ }}
245
+ style={{ cursor: 'pointer', opacity: 0.65 }}
246
+ >
247
+ {t('tools.lobehubSkill.authorize', { defaultValue: 'Authorize' })}
248
+ <Icon icon={SquareArrowOutUpRight} size="small" />
249
+ </Flexbox>
250
+ );
251
+ }
252
+ case LobehubSkillStatus.NOT_CONNECTED: {
253
+ return (
254
+ <Flexbox
255
+ align="center"
256
+ gap={4}
257
+ horizontal
258
+ onClick={(e) => {
259
+ e.stopPropagation();
260
+ handleConnect();
261
+ }}
262
+ style={{ cursor: 'pointer', opacity: 0.65 }}
263
+ >
264
+ {t('tools.lobehubSkill.connect', { defaultValue: 'Connect' })}
265
+ <Icon icon={SquareArrowOutUpRight} size="small" />
266
+ </Flexbox>
267
+ );
268
+ }
269
+ case LobehubSkillStatus.ERROR: {
270
+ return (
271
+ <span style={{ color: 'red', fontSize: 12 }}>
272
+ {t('tools.lobehubSkill.error', { defaultValue: 'Error' })}
273
+ </span>
274
+ );
275
+ }
276
+ default: {
277
+ return null;
278
+ }
279
+ }
280
+ };
281
+
282
+ return (
283
+ <Flexbox
284
+ align={'center'}
285
+ gap={24}
286
+ horizontal
287
+ justify={'space-between'}
288
+ onClick={(e) => {
289
+ e.stopPropagation();
290
+ if (server?.status === LobehubSkillStatus.CONNECTED) {
291
+ handleToggle();
292
+ }
293
+ }}
294
+ style={{ paddingLeft: 8 }}
295
+ >
296
+ <Flexbox align={'center'} gap={8} horizontal>
297
+ {label}
298
+ </Flexbox>
299
+ {renderRightControl()}
300
+ </Flexbox>
301
+ );
302
+ });
303
+
304
+ export default LobehubSkillServerItem;
@@ -1,4 +1,9 @@
1
- import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
1
+ import {
2
+ KLAVIS_SERVER_TYPES,
3
+ type KlavisServerType,
4
+ LOBEHUB_SKILL_PROVIDERS,
5
+ type LobehubSkillProviderType,
6
+ } from '@lobechat/const';
2
7
  import { Avatar, Flexbox, Icon, Image, type ItemType } from '@lobehub/ui';
3
8
  import { cssVar } from 'antd-style';
4
9
  import isEqual from 'fast-deep-equal';
@@ -16,11 +21,13 @@ import { useToolStore } from '@/store/tool';
16
21
  import {
17
22
  builtinToolSelectors,
18
23
  klavisStoreSelectors,
24
+ lobehubSkillStoreSelectors,
19
25
  pluginSelectors,
20
26
  } from '@/store/tool/selectors';
21
27
 
22
28
  import { useAgentId } from '../../hooks/useAgentId';
23
29
  import KlavisServerItem from './KlavisServerItem';
30
+ import LobehubSkillServerItem from './LobehubSkillServerItem';
24
31
  import ToolItem from './ToolItem';
25
32
 
26
33
  /**
@@ -39,6 +46,21 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
39
46
 
40
47
  KlavisIcon.displayName = 'KlavisIcon';
41
48
 
49
+ /**
50
+ * LobeHub Skill Provider 图标组件
51
+ */
52
+ const LobehubSkillIcon = memo<Pick<LobehubSkillProviderType, 'icon' | 'label'>>(
53
+ ({ icon, label }) => {
54
+ if (typeof icon === 'string') {
55
+ return <Image alt={label} height={18} src={icon} style={{ flex: 'none' }} width={18} />;
56
+ }
57
+
58
+ return <Icon fill={cssVar.colorText} icon={icon} size={18} />;
59
+ },
60
+ );
61
+
62
+ LobehubSkillIcon.displayName = 'LobehubSkillIcon';
63
+
42
64
  export const useControls = ({
43
65
  setModalOpen,
44
66
  setUpdating,
@@ -66,10 +88,16 @@ export const useControls = ({
66
88
  const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
67
89
  const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
68
90
 
69
- const [useFetchPluginStore, useFetchUserKlavisServers] = useToolStore((s) => [
70
- s.useFetchPluginStore,
71
- s.useFetchUserKlavisServers,
72
- ]);
91
+ // LobeHub Skill 相关状态
92
+ const allLobehubSkillServers = useToolStore(lobehubSkillStoreSelectors.getServers, isEqual);
93
+ const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
94
+
95
+ const [useFetchPluginStore, useFetchUserKlavisServers, useFetchLobehubSkillConnections] =
96
+ useToolStore((s) => [
97
+ s.useFetchPluginStore,
98
+ s.useFetchUserKlavisServers,
99
+ s.useFetchLobehubSkillConnections,
100
+ ]);
73
101
 
74
102
  useFetchPluginStore();
75
103
  useFetchInstalledPlugins();
@@ -78,6 +106,9 @@ export const useControls = ({
78
106
  // 使用 SWR 加载用户的 Klavis 集成(从数据库)
79
107
  useFetchUserKlavisServers(isKlavisEnabledInEnv);
80
108
 
109
+ // 使用 SWR 加载用户的 LobeHub Skill 连接
110
+ useFetchLobehubSkillConnections(isLobehubSkillEnabled);
111
+
81
112
  // 根据 identifier 获取已连接的服务器
82
113
  const getServerByName = (identifier: string) => {
83
114
  return allKlavisServers.find((server) => server.identifier === identifier);
@@ -118,7 +149,20 @@ export const useControls = ({
118
149
  [isKlavisEnabledInEnv, allKlavisServers],
119
150
  );
120
151
 
121
- // 合并 builtin 工具和 Klavis 服务器
152
+ // LobeHub Skill Provider 列表项
153
+ const lobehubSkillItems = useMemo(
154
+ () =>
155
+ isLobehubSkillEnabled
156
+ ? LOBEHUB_SKILL_PROVIDERS.map((provider) => ({
157
+ icon: <LobehubSkillIcon icon={provider.icon} label={provider.label} />,
158
+ key: provider.id, // 使用 provider.id 作为 key,与 pluginId 保持一致
159
+ label: <LobehubSkillServerItem label={provider.label} provider={provider.id} />,
160
+ }))
161
+ : [],
162
+ [isLobehubSkillEnabled, allLobehubSkillServers],
163
+ );
164
+
165
+ // 合并 builtin 工具、Klavis 服务器和 LobeHub Skill Provider
122
166
  const builtinItems = useMemo(
123
167
  () => [
124
168
  // 原有的 builtin 工具
@@ -140,10 +184,12 @@ export const useControls = ({
140
184
  />
141
185
  ),
142
186
  })),
187
+ // LobeHub Skill Providers
188
+ ...lobehubSkillItems,
143
189
  // Klavis 服务器
144
190
  ...klavisServerItems,
145
191
  ],
146
- [filteredBuiltinList, klavisServerItems, checked, togglePlugin, setUpdating],
192
+ [filteredBuiltinList, klavisServerItems, lobehubSkillItems, checked, togglePlugin, setUpdating],
147
193
  );
148
194
 
149
195
  // 市场 tab 的 items
@@ -233,8 +279,17 @@ export const useControls = ({
233
279
  checked.includes(item.key as string),
234
280
  );
235
281
 
236
- // 合并 builtin Klavis
237
- const allBuiltinItems = [...enabledBuiltinItems, ...connectedKlavisItems];
282
+ // 已连接的 LobeHub Skill Providers
283
+ const connectedLobehubSkillItems = lobehubSkillItems.filter((item) =>
284
+ checked.includes(item.key as string),
285
+ );
286
+
287
+ // 合并 builtin、Klavis 和 LobeHub Skill
288
+ const allBuiltinItems = [
289
+ ...enabledBuiltinItems,
290
+ ...connectedKlavisItems,
291
+ ...connectedLobehubSkillItems,
292
+ ];
238
293
 
239
294
  if (allBuiltinItems.length > 0) {
240
295
  installedItems.push({
@@ -279,7 +334,16 @@ export const useControls = ({
279
334
  }
280
335
 
281
336
  return installedItems;
282
- }, [filteredBuiltinList, list, klavisServerItems, checked, togglePlugin, setUpdating, t]);
337
+ }, [
338
+ filteredBuiltinList,
339
+ list,
340
+ klavisServerItems,
341
+ lobehubSkillItems,
342
+ checked,
343
+ togglePlugin,
344
+ setUpdating,
345
+ t,
346
+ ]);
283
347
 
284
348
  return { installedPluginItems, marketItems };
285
349
  };
@@ -38,6 +38,15 @@ const ToolTitle = memo<ToolTitleProps>(({ identifier, apiName, isLoading, isAbor
38
38
  const isBuiltinPlugin = builtinToolIdentifiers.includes(identifier);
39
39
  const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
40
40
 
41
+ // Debug logging for LobeHub Skill title issue
42
+ console.log('[ToolTitle Debug]', {
43
+ apiName,
44
+ identifier,
45
+ isBuiltinPlugin,
46
+ pluginMeta,
47
+ pluginTitle,
48
+ });
49
+
41
50
  return (
42
51
  <div
43
52
  className={cx(