@lobehub/lobehub 2.0.0-next.232 → 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 (112) hide show
  1. package/.github/workflows/bundle-analyzer.yml +1 -1
  2. package/.github/workflows/e2e.yml +62 -53
  3. package/.github/workflows/manual-build-desktop.yml +5 -5
  4. package/.github/workflows/pr-build-desktop.yml +4 -4
  5. package/.github/workflows/pr-build-docker.yml +2 -2
  6. package/.github/workflows/release-desktop-beta.yml +4 -4
  7. package/.github/workflows/release-docker.yml +2 -2
  8. package/.github/workflows/test.yml +44 -7
  9. package/CHANGELOG.md +59 -0
  10. package/CLAUDE.md +1 -1
  11. package/changelog/v1.json +14 -0
  12. package/docs/development/basic/feature-development.mdx +4 -5
  13. package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
  14. package/docs/self-hosting/environment-variables/auth.mdx +7 -0
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
  16. package/e2e/README.md +6 -6
  17. package/e2e/src/features/community/detail-pages.feature +9 -9
  18. package/e2e/src/features/community/interactions.feature +13 -13
  19. package/e2e/src/features/community/smoke.feature +6 -6
  20. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
  21. package/e2e/src/steps/agent/conversation.steps.ts +58 -0
  22. package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
  23. package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
  24. package/e2e/src/steps/community/interactions.steps.ts +145 -32
  25. package/e2e/src/steps/hooks.ts +12 -2
  26. package/locales/en-US/setting.json +3 -0
  27. package/locales/zh-CN/file.json +4 -0
  28. package/locales/zh-CN/setting.json +3 -0
  29. package/package.json +5 -5
  30. package/packages/business/config/src/llm.ts +6 -1
  31. package/packages/const/src/index.ts +1 -0
  32. package/packages/const/src/lobehubSkill.ts +55 -0
  33. package/packages/const/src/settings/image.ts +1 -1
  34. package/packages/model-bank/src/aiModels/azure.ts +2 -2
  35. package/packages/model-bank/src/aiModels/google.ts +1 -0
  36. package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
  37. package/packages/model-bank/src/aiModels/openai.ts +21 -4
  38. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
  39. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
  40. package/packages/ssrf-safe-fetch/index.test.ts +5 -34
  41. package/packages/ssrf-safe-fetch/index.ts +12 -2
  42. package/packages/types/package.json +1 -1
  43. package/packages/types/src/files/upload.ts +11 -1
  44. package/packages/types/src/message/common/tools.ts +1 -1
  45. package/packages/types/src/serverConfig.ts +1 -0
  46. package/public/not-compatible.html +1296 -0
  47. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
  48. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
  49. package/src/app/[variants]/(main)/image/index.tsx +1 -1
  50. package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
  51. package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
  52. package/src/app/[variants]/layout.tsx +50 -1
  53. package/src/envs/auth.ts +15 -0
  54. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
  55. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
  56. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
  57. package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
  58. package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
  59. package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
  60. package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
  61. package/src/features/FileViewer/index.tsx +135 -24
  62. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
  63. package/src/features/PageEditor/store/initialState.ts +2 -1
  64. package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
  65. package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
  66. package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
  67. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
  68. package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
  69. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
  70. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
  71. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
  72. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
  73. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
  74. package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
  75. package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
  76. package/src/features/ResourceManager/index.tsx +1 -0
  77. package/src/helpers/toolEngineering/index.test.ts +3 -0
  78. package/src/helpers/toolEngineering/index.ts +12 -1
  79. package/src/hooks/useFetchAiImageConfig.ts +54 -10
  80. package/src/libs/trpc/utils/internalJwt.ts +2 -2
  81. package/src/locales/default/file.ts +4 -0
  82. package/src/locales/default/setting.ts +3 -0
  83. package/src/server/globalConfig/index.ts +1 -0
  84. package/src/server/modules/ModelRuntime/index.test.ts +214 -1
  85. package/src/server/modules/ModelRuntime/index.ts +43 -7
  86. package/src/server/routers/lambda/document.ts +44 -0
  87. package/src/server/routers/tools/market.ts +261 -0
  88. package/src/server/services/document/index.ts +22 -0
  89. package/src/services/document/index.ts +4 -0
  90. package/src/services/upload.ts +22 -2
  91. package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
  92. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
  93. package/src/store/file/slices/fileManager/action.test.ts +9 -3
  94. package/src/store/file/slices/fileManager/action.ts +165 -70
  95. package/src/store/file/slices/upload/action.ts +3 -0
  96. package/src/store/global/actions/general.ts +15 -0
  97. package/src/store/global/initialState.ts +13 -0
  98. package/src/store/image/slices/generationConfig/initialState.ts +5 -5
  99. package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
  100. package/src/store/serverConfig/selectors.ts +1 -0
  101. package/src/store/tool/initialState.ts +11 -2
  102. package/src/store/tool/selectors/index.ts +1 -0
  103. package/src/store/tool/selectors/tool.ts +3 -1
  104. package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
  105. package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
  106. package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
  107. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
  108. package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
  109. package/src/store/tool/store.ts +8 -2
  110. package/vitest.config.mts +11 -6
  111. package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
  112. package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
@@ -93,8 +93,8 @@ const styles = createStaticStyles(({ css }) => {
93
93
 
94
94
  overflow: hidden;
95
95
 
96
- width: ${thumbnailSize};
97
- height: ${thumbnailSize};
96
+ width: ${thumbnailSize}px;
97
+ height: ${thumbnailSize}px;
98
98
  border-radius: ${cssVar.borderRadius};
99
99
 
100
100
  background: ${cssVar.colorBgContainer};
@@ -112,7 +112,7 @@ const styles = createStaticStyles(({ css }) => {
112
112
  gap: 8px;
113
113
 
114
114
  width: 100%;
115
- height: ${thumbnailSize};
115
+ height: ${thumbnailSize}px;
116
116
  padding: 0;
117
117
  border-radius: ${cssVar.borderRadiusLG};
118
118
 
@@ -77,15 +77,8 @@ const GenerationFeed = memo(() => {
77
77
  }
78
78
 
79
79
  return (
80
- <>
81
- <Flexbox
82
- gap={16}
83
- ref={parent}
84
- style={{
85
- minHeight: 'calc(100vh - 180px)',
86
- }}
87
- width="100%"
88
- >
80
+ <Flexbox flex={1}>
81
+ <Flexbox gap={16} ref={parent} width="100%">
89
82
  {currentGenerationBatches.map((batch, index) => (
90
83
  <Fragment key={batch.id}>
91
84
  {Boolean(index !== 0) && <Divider dashed style={{ margin: 0 }} />}
@@ -95,7 +88,7 @@ const GenerationFeed = memo(() => {
95
88
  </Flexbox>
96
89
  {/* Invisible element for scroll target */}
97
90
  <div ref={containerRef} style={{ height: 1 }} />
98
- </>
91
+ </Flexbox>
99
92
  );
100
93
  });
101
94
 
@@ -15,7 +15,7 @@ const DesktopImagePage = memo(() => {
15
15
  <>
16
16
  <NavHeader right={<WideScreenButton />} />
17
17
  <Flexbox height={'100%'} style={{ overflowY: 'auto', position: 'relative' }} width={'100%'}>
18
- <WideScreenContainer>
18
+ <WideScreenContainer height={'100%'} wrapperStyle={{ height: '100%' }}>
19
19
  <Suspense fallback={<SkeletonList />}>
20
20
  <ImageWorkspace />
21
21
  </Suspense>
@@ -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 }[] = [];
package/src/envs/auth.ts CHANGED
@@ -158,6 +158,15 @@ declare global {
158
158
  * Can be generated using `node scripts/generate-oidc-jwk.mjs`.
159
159
  */
160
160
  JWKS_KEY?: string;
161
+
162
+ /**
163
+ * Internal JWT expiration time for lambda → async calls.
164
+ * Format: number followed by unit (s=seconds, m=minutes, h=hours)
165
+ * Examples: '10s', '1m', '1h'
166
+ * Should be as short as possible for security, but long enough to account for network latency and server processing time.
167
+ * @default '30s'
168
+ */
169
+ INTERNAL_JWT_EXPIRATION?: string;
161
170
  }
162
171
  }
163
172
  }
@@ -285,6 +294,9 @@ export const getAuthConfig = () => {
285
294
  // Generic JWKS key for signing/verifying JWTs
286
295
  JWKS_KEY: z.string().optional(),
287
296
  ENABLE_OIDC: z.boolean(),
297
+
298
+ // Internal JWT expiration time (e.g., '10s', '1m', '1h')
299
+ INTERNAL_JWT_EXPIRATION: z.string().default('30s'),
288
300
  },
289
301
 
290
302
  runtimeEnv: {
@@ -415,6 +427,9 @@ export const getAuthConfig = () => {
415
427
  // Generic JWKS key (fallback to OIDC_JWKS_KEY for backward compatibility)
416
428
  JWKS_KEY: process.env.JWKS_KEY || process.env.OIDC_JWKS_KEY,
417
429
  ENABLE_OIDC: !!(process.env.JWKS_KEY || process.env.OIDC_JWKS_KEY),
430
+
431
+ // Internal JWT expiration time
432
+ INTERNAL_JWT_EXPIRATION: process.env.INTERNAL_JWT_EXPIRATION,
418
433
  },
419
434
  });
420
435
  };
@@ -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;