@lobehub/chat 1.109.1 → 1.110.1

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 (57) hide show
  1. package/.cursor/rules/i18n.mdc +1 -2
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +1 -1
  4. package/README.zh-CN.md +1 -1
  5. package/apps/desktop/electron-builder.js +22 -0
  6. package/apps/desktop/src/main/controllers/McpInstallCtr.ts +153 -0
  7. package/apps/desktop/src/main/controllers/index.ts +19 -0
  8. package/apps/desktop/src/main/core/App.ts +46 -0
  9. package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +4 -0
  10. package/apps/desktop/src/main/core/infrastructure/ProtocolManager.ts +256 -0
  11. package/apps/desktop/src/main/types/protocol.ts +60 -0
  12. package/apps/desktop/src/main/utils/__tests__/protocol.test.ts +203 -0
  13. package/apps/desktop/src/main/utils/protocol.ts +210 -0
  14. package/changelog/v1.json +18 -0
  15. package/locales/ar/plugin.json +196 -136
  16. package/locales/bg-BG/plugin.json +204 -144
  17. package/locales/de-DE/plugin.json +176 -116
  18. package/locales/en-US/plugin.json +192 -132
  19. package/locales/es-ES/plugin.json +203 -143
  20. package/locales/fa-IR/plugin.json +155 -95
  21. package/locales/fr-FR/plugin.json +161 -101
  22. package/locales/it-IT/plugin.json +193 -133
  23. package/locales/ja-JP/plugin.json +195 -135
  24. package/locales/ko-KR/plugin.json +163 -103
  25. package/locales/nl-NL/plugin.json +211 -151
  26. package/locales/pl-PL/plugin.json +171 -111
  27. package/locales/pt-BR/plugin.json +180 -120
  28. package/locales/ru-RU/plugin.json +191 -131
  29. package/locales/tr-TR/plugin.json +187 -127
  30. package/locales/vi-VN/plugin.json +152 -92
  31. package/locales/zh-CN/plugin.json +60 -0
  32. package/locales/zh-TW/plugin.json +157 -97
  33. package/package.json +2 -1
  34. package/packages/electron-client-ipc/src/events/index.ts +5 -2
  35. package/packages/electron-client-ipc/src/events/protocol.ts +29 -0
  36. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  37. package/packages/electron-client-ipc/src/types/mcpInstall.ts +19 -0
  38. package/packages/types/src/plugins/mcp.ts +38 -1
  39. package/packages/types/src/plugins/protocol.ts +166 -0
  40. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +4 -1
  41. package/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/ActionButton/index.tsx +1 -2
  42. package/src/components/KeyValueEditor/index.tsx +4 -2
  43. package/src/features/ChatItem/index.tsx +25 -2
  44. package/src/features/MCP/MCPInstallProgress/index.tsx +1 -1
  45. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +30 -36
  46. package/src/features/PluginStore/McpList/List/Item.tsx +1 -1
  47. package/src/features/ProtocolUrlHandler/InstallPlugin/ConfigDisplay.tsx +211 -0
  48. package/src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx +228 -0
  49. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/Detail.tsx +44 -0
  50. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +105 -0
  51. package/src/features/ProtocolUrlHandler/InstallPlugin/index.tsx +55 -0
  52. package/src/features/ProtocolUrlHandler/InstallPlugin/types.ts +45 -0
  53. package/src/features/ProtocolUrlHandler/index.tsx +30 -0
  54. package/src/locales/default/plugin.ts +60 -0
  55. package/src/store/tool/slices/mcpStore/action.ts +127 -1
  56. package/src/store/tool/slices/mcpStore/initialState.ts +8 -13
  57. package/src/store/tool/slices/mcpStore/selectors.ts +13 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 协议来源类型
3
+ */
4
+ export enum ProtocolSource {
5
+ /** 社区贡献 */
6
+ COMMUNITY = 'community',
7
+ /** 开发者自定义 */
8
+ DEVELOPER = 'developer',
9
+ /** GitHub 官方 */
10
+ GITHUB_OFFICIAL = 'github_official',
11
+ /** 官方LobeHub市场 */
12
+ OFFICIAL = 'official',
13
+ /** 第三方市场 */
14
+ THIRD_PARTY = 'third_party',
15
+ }
16
+
17
+ /**
18
+ * MCP Schema - stdio 配置类型
19
+ */
20
+ export interface McpStdioConfig {
21
+ args?: string[];
22
+ command: string;
23
+ env?: Record<string, string>;
24
+ type: 'stdio';
25
+ }
26
+
27
+ /**
28
+ * MCP Schema - http 配置类型
29
+ */
30
+ export interface McpHttpConfig {
31
+ headers?: Record<string, string>;
32
+ type: 'http';
33
+ url: string;
34
+ }
35
+
36
+ /**
37
+ * MCP Schema 配置类型
38
+ */
39
+ export type McpConfig = McpStdioConfig | McpHttpConfig;
40
+
41
+ /**
42
+ * MCP Schema 对象
43
+ * 符合 RFC 0001 定义
44
+ */
45
+ export interface McpSchema {
46
+ /** 插件作者 */
47
+ author: string;
48
+ /** 插件配置 */
49
+ config: McpConfig;
50
+ /** 插件描述 */
51
+ description: string;
52
+ /** 插件主页 */
53
+ homepage?: string;
54
+ /** 插件图标 */
55
+ icon?: string;
56
+ /** 插件唯一标识符,必须与URL中的id参数匹配 */
57
+ identifier: string;
58
+ /** 插件名称 */
59
+ name: string;
60
+ /** 插件版本 (semver) */
61
+ version: string;
62
+ }
63
+
64
+ /**
65
+ * RFC 0001 协议参数
66
+ * lobehub://plugin/install?id=xxx&schema=xxx&marketId=xxx&meta_*=xxx
67
+ */
68
+ export interface McpInstallProtocolParamsRFC {
69
+ /** 可选的 UI 显示元数据,以 meta_ 为前缀 */
70
+ [key: `meta_${string}`]: string | undefined;
71
+ /** 插件的唯一标识符 */
72
+ id: string;
73
+ /** 提供该插件的 Marketplace 的唯一标识符 */
74
+ marketId?: string;
75
+ /** Base64URL 编码的 MCP Schema 对象 */
76
+ schema: string;
77
+ /** 插件类型,对于 MCP 固定为 'mcp' */
78
+ type: 'mcp';
79
+ }
80
+
81
+ /**
82
+ * 协议URL解析结果
83
+ */
84
+ export interface ProtocolUrlParsed {
85
+ /** 操作类型 (如: 'install') */
86
+ action: 'install' | 'configure' | 'update';
87
+ /** 解析后的参数 */
88
+ params: {
89
+ id: string;
90
+ marketId?: string;
91
+ type: string;
92
+ };
93
+ /** MCP Schema 对象 */
94
+ schema: McpSchema;
95
+ /** 协议来源 */
96
+ source: ProtocolSource;
97
+ /** 插件类型 (如: 'mcp') */
98
+ type: 'mcp' | 'plugin';
99
+ /** URL类型 (如: 'plugin') */
100
+ urlType: string;
101
+ }
102
+
103
+ /**
104
+ * 安装确认弹窗信息
105
+ */
106
+ export interface InstallConfirmationInfo {
107
+ dependencies?: string[];
108
+ permissions?: {
109
+ filesystem?: boolean;
110
+ network?: boolean;
111
+ system?: boolean;
112
+ };
113
+ pluginInfo: {
114
+ author?: string;
115
+ description: string;
116
+ homepage?: string;
117
+ icon?: string;
118
+ identifier: string;
119
+ name: string;
120
+ version: string;
121
+ };
122
+ source: {
123
+ platform?: {
124
+ name: string;
125
+ url?: string;
126
+ };
127
+ type: ProtocolSource;
128
+ verified: boolean; // 是否为验证来源
129
+ };
130
+ }
131
+
132
+ /**
133
+ * 协议处理器接口
134
+ */
135
+ export interface ProtocolHandler {
136
+ /**
137
+ * 处理协议URL
138
+ */
139
+ handle(
140
+ parsed: ProtocolUrlParsed,
141
+ ): Promise<{ error?: string; success: boolean; targetWindow?: string }>;
142
+
143
+ /**
144
+ * 支持的操作
145
+ */
146
+ readonly supportedActions: string[];
147
+
148
+ /**
149
+ * 协议类型
150
+ */
151
+ readonly type: string;
152
+ }
153
+
154
+ /**
155
+ * 协议路由配置
156
+ */
157
+ export interface ProtocolRouteConfig {
158
+ /** 操作类型 */
159
+ action: string;
160
+ /** 目标路径(相对于窗口base路径) */
161
+ targetPath?: string;
162
+ /** 目标窗口 */
163
+ targetWindow: 'chat' | 'settings';
164
+ /** 协议类型 */
165
+ type: string;
166
+ }
@@ -1,7 +1,9 @@
1
1
  import { Suspense } from 'react';
2
2
  import { Flexbox } from 'react-layout-kit';
3
3
 
4
+ import { isDesktop } from '@/const/version';
4
5
  import InitClientDB from '@/features/InitClientDB';
6
+ import ProtocolUrlHandler from '@/features/ProtocolUrlHandler';
5
7
 
6
8
  import { LayoutProps } from '../type';
7
9
  import RegisterHotkeys from './RegisterHotkeys';
@@ -20,13 +22,14 @@ const Layout = ({ children, session }: LayoutProps) => {
20
22
  <SessionPanel>{session}</SessionPanel>
21
23
  <Workspace>{children}</Workspace>
22
24
  </Flexbox>
23
- <InitClientDB bottom={60} />
25
+ {!isDesktop && <InitClientDB bottom={60} />}
24
26
  {/* ↓ cloud slot ↓ */}
25
27
 
26
28
  {/* ↑ cloud slot ↑ */}
27
29
  <Suspense>
28
30
  <RegisterHotkeys />
29
31
  </Suspense>
32
+ {isDesktop && <ProtocolUrlHandler />}
30
33
  </>
31
34
  );
32
35
  };
@@ -22,6 +22,7 @@ const useStyles = createStyles(({ css }) => ({
22
22
  }));
23
23
 
24
24
  const ActionButton = memo(() => {
25
+ const { t } = useTranslation(['discover', 'plugin']);
25
26
  const { identifier } = useDetailContext();
26
27
  const { styles } = useStyles();
27
28
  const [isLoading, setIsLoading] = useState(false);
@@ -32,8 +33,6 @@ const ActionButton = memo(() => {
32
33
  s.uninstallMCPPlugin,
33
34
  ]);
34
35
 
35
- const { t } = useTranslation(['discover', 'plugin']);
36
-
37
36
  const installPlugin = async () => {
38
37
  if (!identifier) return;
39
38
  setIsLoading(true);
@@ -3,7 +3,7 @@ import { Button } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import fastDeepEqual from 'fast-deep-equal';
5
5
  import { LucidePlus, LucideTrash } from 'lucide-react';
6
- import { memo, useEffect, useRef, useState } from 'react';
6
+ import { CSSProperties, memo, useEffect, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
  import { v4 as uuidv4 } from 'uuid';
@@ -44,6 +44,7 @@ export interface KeyValueEditorProps {
44
44
  duplicateKeyErrorText?: string;
45
45
  keyPlaceholder?: string;
46
46
  onChange?: (value: Record<string, string>) => void;
47
+ style?: CSSProperties;
47
48
  value?: Record<string, string>;
48
49
  valuePlaceholder?: string;
49
50
  }
@@ -57,6 +58,7 @@ const KeyValueEditor = memo<KeyValueEditorProps>(
57
58
  addButtonText,
58
59
  duplicateKeyErrorText,
59
60
  deleteTooltip,
61
+ style,
60
62
  }) => {
61
63
  const { styles } = useStyles();
62
64
  const { t } = useTranslation('components');
@@ -125,7 +127,7 @@ const KeyValueEditor = memo<KeyValueEditorProps>(
125
127
  const duplicateKeys = getDuplicateKeys(items);
126
128
 
127
129
  return (
128
- <div className={styles.container}>
130
+ <div className={styles.container} style={style}>
129
131
  <Flexbox className={styles.title} gap={8} horizontal>
130
132
  <Flexbox flex={1}>{keyPlaceholder || t('KeyValueEditor.keyPlaceholder')}</Flexbox>
131
133
  <Flexbox flex={2}>{valuePlaceholder || t('KeyValueEditor.valuePlaceholder')}</Flexbox>
@@ -2,16 +2,39 @@
2
2
 
3
3
  import { ChatItemProps, ChatItem as ChatItemRaw } from '@lobehub/ui/chat';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { memo } from 'react';
5
+ import { memo, useMemo } from 'react';
6
6
 
7
+ import { isDesktop } from '@/const/version';
8
+ import { useElectronStore } from '@/store/electron';
9
+ import { electronSyncSelectors } from '@/store/electron/selectors';
7
10
  import { useUserStore } from '@/store/user';
8
11
  import { settingsSelectors } from '@/store/user/selectors';
9
12
 
10
- const ChatItem = memo<ChatItemProps>(({ markdownProps = {}, ...rest }) => {
13
+ const ChatItem = memo<ChatItemProps>(({ markdownProps = {}, avatar, ...rest }) => {
11
14
  const { componentProps, ...restMarkdown } = markdownProps;
12
15
  const { general } = useUserStore(settingsSelectors.currentSettings, isEqual);
16
+
17
+ const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
18
+ const processedAvatar = useMemo(() => {
19
+ // only process avatar in desktop environment and when avatar url starts with /
20
+ if (
21
+ !isDesktop ||
22
+ !remoteServerUrl ||
23
+ !avatar.avatar ||
24
+ typeof avatar.avatar !== 'string' ||
25
+ !avatar.avatar.startsWith('/')
26
+ )
27
+ return avatar;
28
+
29
+ return {
30
+ ...avatar,
31
+ avatar: remoteServerUrl + avatar.avatar, // prepend the remote server URL
32
+ };
33
+ }, [avatar, remoteServerUrl]);
34
+
13
35
  return (
14
36
  <ChatItemRaw
37
+ avatar={processedAvatar}
15
38
  fontSize={general.fontSize}
16
39
  markdownProps={{
17
40
  ...restMarkdown,
@@ -9,7 +9,7 @@ import { Flexbox } from 'react-layout-kit';
9
9
 
10
10
  import { useToolStore } from '@/store/tool';
11
11
  import { mcpStoreSelectors } from '@/store/tool/selectors';
12
- import { MCPInstallStep } from '@/store/tool/slices/mcpStore';
12
+ import { MCPInstallStep } from '@/types/plugins';
13
13
 
14
14
  import InstallError from './InstallError';
15
15
  import MCPConfigForm from './MCPConfigForm';
@@ -1,15 +1,14 @@
1
- import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
1
  import { Alert, FormItem, Input, InputPassword } from '@lobehub/ui';
3
2
  import { Button, Divider, Form, FormInstance, Radio } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
4
  import { useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import KeyValueEditor from '@/components/KeyValueEditor';
9
9
  import MCPStdioCommandInput from '@/components/MCPStdioCommandInput';
10
- import { mcpService } from '@/services/mcp';
11
10
  import { useToolStore } from '@/store/tool';
12
- import { pluginSelectors } from '@/store/tool/selectors';
11
+ import { mcpStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
13
12
 
14
13
  import ArgsInput from './ArgsInput';
15
14
  import CollapsibleSection from './CollapsibleSection';
@@ -40,6 +39,13 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
40
39
 
41
40
  const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
42
41
  const [isTesting, setIsTesting] = useState(false);
42
+ const testMcpConnection = useToolStore((s) => s.testMcpConnection);
43
+
44
+ // 使用 identifier 来跟踪测试状态(如果表单中有的话)
45
+ const formValues = form.getFieldsValue();
46
+ const identifier = formValues?.identifier || 'temp-test-id';
47
+ const testState = useToolStore(mcpStoreSelectors.getMCPConnectionTestState(identifier), isEqual);
48
+
43
49
  const [connectionError, setConnectionError] = useState<string | null>(null);
44
50
 
45
51
  const handleTestConnection = async () => {
@@ -80,43 +86,31 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
80
86
  const description = values.customParams?.description;
81
87
  const avatar = values.customParams?.avatar;
82
88
 
83
- let data: LobeChatPluginManifest;
89
+ // 使用 mcpStore 的 testMcpConnection 方法
90
+ const result = await testMcpConnection({
91
+ connection: mcp,
92
+ identifier: id,
93
+ metadata: { avatar, description },
94
+ });
84
95
 
85
- if (mcp.type === 'http') {
86
- if (!mcp.url) throw new Error(t('dev.mcp.url.required'));
87
- data = await mcpService.getStreamableMcpServerManifest({
88
- auth: mcp.auth,
89
- headers: mcp.headers,
90
- identifier: id,
91
- metadata: { avatar, description },
92
- url: mcp.url,
96
+ if (result.success && result.manifest) {
97
+ // Optionally update form if manifest ID differs or to store the fetched manifest
98
+ // Be careful about overwriting user input if not desired
99
+ form.setFieldsValue({ manifest: result.manifest });
100
+ setConnectionError(null); // 清除本地错误状态
101
+ } else if (result.error) {
102
+ // Store 已经处理了错误状态,这里可以选择显示额外的用户友好提示
103
+ const errorMessage = t('error.testConnectionFailed', {
104
+ error: result.error,
93
105
  });
94
- } else if (mcp.type === 'stdio') {
95
- if (!mcp.command) throw new Error(t('dev.mcp.command.required'));
96
- if (!mcp.args) throw new Error(t('dev.mcp.args.required'));
97
- data = await mcpService.getStdioMcpServerManifest(
98
- { ...mcp, name: id },
99
- { avatar, description },
100
- );
101
- } else {
102
- throw new Error('Invalid MCP type'); // Internal error
106
+ setConnectionError(errorMessage);
103
107
  }
104
-
105
- // Optionally update form if manifest ID differs or to store the fetched manifest
106
- // Be careful about overwriting user input if not desired
107
- form.setFieldsValue({ manifest: data });
108
108
  } catch (error) {
109
- // Check if error is a validation error object (from validateFields)
110
-
111
- // Handle API call errors or other errors
112
- const err = error as Error; // Assuming PluginInstallError or similar structure
113
- // Use the error message directly if it's a simple string error, otherwise try translation
114
- // highlight-start
109
+ // Handle unexpected errors
110
+ const err = error as Error;
115
111
  const errorMessage = t('error.testConnectionFailed', {
116
- error: err.cause || err.message || t('unknownError'),
112
+ error: err.message || t('unknownError'),
117
113
  });
118
- // highlight-end
119
-
120
114
  setConnectionError(errorMessage);
121
115
  } finally {
122
116
  setIsTesting(false);
@@ -274,10 +268,10 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
274
268
  </Button>
275
269
  </Flexbox>
276
270
  </FormItem>
277
- {connectionError && (
271
+ {(connectionError || testState.error) && (
278
272
  <Alert
279
273
  closable
280
- message={connectionError}
274
+ message={connectionError || testState.error}
281
275
  onClose={() => setConnectionError(null)}
282
276
  showIcon
283
277
  style={{ marginBottom: 16 }}
@@ -7,8 +7,8 @@ import PluginAvatar from '@/components/Plugins/PluginAvatar';
7
7
  import MCPInstallProgress from '@/features/MCP/MCPInstallProgress';
8
8
  import { useToolStore } from '@/store/tool';
9
9
  import { mcpStoreSelectors } from '@/store/tool/selectors';
10
- import { MCPInstallStep } from '@/store/tool/slices/mcpStore/initialState';
11
10
  import { DiscoverMcpItem } from '@/types/discover';
11
+ import { MCPInstallStep } from '@/types/plugins';
12
12
  import { LobeToolType } from '@/types/tool/tool';
13
13
 
14
14
  import Actions from './Action';
@@ -0,0 +1,211 @@
1
+ 'use client';
2
+
3
+ import { McpInstallSchema } from '@lobechat/electron-client-ipc';
4
+ import { Block, Text } from '@lobehub/ui';
5
+ import { createStyles } from 'antd-style';
6
+ import { LinkIcon, Settings2Icon } from 'lucide-react';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
10
+
11
+ import KeyValueEditor from '@/components/KeyValueEditor';
12
+
13
+ const useStyles = createStyles(({ css, token }) => ({
14
+ configEditor: css`
15
+ margin-block-start: ${token.marginSM}px;
16
+ `,
17
+ configSection: css`
18
+ margin-block-end: ${token.marginLG}px;
19
+ padding: ${token.paddingSM}px;
20
+ border-radius: ${token.borderRadius}px;
21
+ `,
22
+ configTitle: css`
23
+ display: flex;
24
+ gap: ${token.marginXS}px;
25
+ align-items: center;
26
+
27
+ height: 24px;
28
+
29
+ font-weight: 600;
30
+ color: ${token.colorTextHeading};
31
+ `,
32
+
33
+ previewContainer: css`
34
+ padding-inline: ${token.paddingXS}px;
35
+ `,
36
+
37
+ previewItem: css`
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+
42
+ padding-block: ${token.paddingXS}px;
43
+ padding-inline: 0;
44
+
45
+ &:not(:last-child) {
46
+ border-block-end: 1px solid ${token.colorBorderSecondary};
47
+ }
48
+ `,
49
+
50
+ previewLabel: css`
51
+ display: flex;
52
+ gap: ${token.marginXS}px;
53
+ align-items: center;
54
+
55
+ font-size: ${token.fontSizeSM}px;
56
+ font-weight: 500;
57
+ color: ${token.colorTextSecondary};
58
+ `,
59
+
60
+ previewValue: css`
61
+ padding-block: ${token.paddingXXS}px;
62
+ padding-inline: ${token.paddingXS}px;
63
+ border-radius: ${token.borderRadiusSM}px;
64
+
65
+ font-family: ${token.fontFamilyCode};
66
+ font-size: ${token.fontSizeSM}px;
67
+ font-weight: 600;
68
+ color: ${token.colorText};
69
+
70
+ background: ${token.colorFillQuaternary};
71
+ `,
72
+
73
+ typeValue: css`
74
+ display: flex;
75
+ gap: ${token.marginXS}px;
76
+ align-items: center;
77
+ `,
78
+
79
+ urlValue: css`
80
+ max-width: 300px;
81
+ padding-block: ${token.paddingXS}px;
82
+ padding-inline: ${token.paddingSM}px;
83
+ border: 1px solid ${token.colorBorder};
84
+ border-radius: ${token.borderRadius}px;
85
+
86
+ font-family: ${token.fontFamilyCode};
87
+ font-size: ${token.fontSizeSM}px;
88
+ font-weight: 500;
89
+ word-break: auto-phrase;
90
+
91
+ background: ${token.colorBgElevated};
92
+ `,
93
+ }));
94
+
95
+ interface ConfigDisplayProps {
96
+ onConfigUpdate?: (updatedConfig: {
97
+ env?: Record<string, string>;
98
+ headers?: Record<string, string>;
99
+ }) => void;
100
+ schema: McpInstallSchema;
101
+ }
102
+
103
+ const ConfigDisplay = memo<ConfigDisplayProps>(({ schema, onConfigUpdate }) => {
104
+ const { t } = useTranslation('plugin');
105
+ const { styles } = useStyles();
106
+
107
+ // 本地状态管理配置数据
108
+ const [currentEnv, setCurrentEnv] = useState<Record<string, string>>(schema.config.env || {});
109
+ const [currentHeaders, setCurrentHeaders] = useState<Record<string, string>>(
110
+ schema.config.headers || {},
111
+ );
112
+
113
+ // 处理环境变量更新
114
+ const handleEnvUpdate = (newEnv: Record<string, string>) => {
115
+ setCurrentEnv(newEnv);
116
+ onConfigUpdate?.({ env: newEnv, headers: currentHeaders });
117
+ };
118
+
119
+ // 处理 Headers 更新
120
+ const handleHeadersUpdate = (newHeaders: Record<string, string>) => {
121
+ setCurrentHeaders(newHeaders);
122
+ onConfigUpdate?.({ env: currentEnv, headers: newHeaders });
123
+ };
124
+
125
+ return (
126
+ <Flexbox gap={16}>
127
+ {/* 安装信息 */}
128
+ <Block className={styles.configSection} variant={'outlined'}>
129
+ <div className={styles.configTitle}>
130
+ <LinkIcon size={14} />
131
+ {t('protocolInstall.install.title', { defaultValue: '安装信息' })}
132
+ </div>
133
+
134
+ <div className={styles.previewContainer}>
135
+ {/* 连接类型 */}
136
+ <div className={styles.previewItem}>
137
+ <span className={styles.previewLabel}>{t('protocolInstall.config.type.label')}</span>
138
+ <div className={styles.typeValue}>
139
+ <Text className={styles.previewValue}>
140
+ {schema.config.type === 'stdio' ? 'STDIO' : 'HTTP'}
141
+ </Text>
142
+ </div>
143
+ </div>
144
+
145
+ {/* HTTP 类型显示 URL */}
146
+ {schema.config.type === 'http' && schema.config.url && (
147
+ <div className={styles.previewItem}>
148
+ <span className={styles.previewLabel}>{t('protocolInstall.config.url')}</span>
149
+ <div className={styles.urlValue}>{schema.config.url}</div>
150
+ </div>
151
+ )}
152
+
153
+ {/* STDIO 类型显示命令和参数 */}
154
+ {schema.config.type === 'stdio' && (
155
+ <>
156
+ {schema.config.command && (
157
+ <div className={styles.previewItem}>
158
+ <span className={styles.previewLabel}>{t('protocolInstall.config.command')}</span>
159
+ <span className={styles.previewValue}>{schema.config.command}</span>
160
+ </div>
161
+ )}
162
+
163
+ {schema.config.args && schema.config.args.length > 0 && (
164
+ <div className={styles.previewItem}>
165
+ <span className={styles.previewLabel}>{t('protocolInstall.config.args')}</span>
166
+ <span className={styles.previewValue}>{schema.config.args.join(' ')}</span>
167
+ </div>
168
+ )}
169
+ </>
170
+ )}
171
+ </div>
172
+ </Block>
173
+
174
+ {/* 配置信息 - 直接使用 KeyValueEditor */}
175
+ <Block className={styles.configSection} variant={'outlined'}>
176
+ <div className={styles.configTitle}>
177
+ <Settings2Icon size={14} />
178
+ {schema.config.type === 'stdio'
179
+ ? t('protocolInstall.config.env', { defaultValue: '环境变量' })
180
+ : t('protocolInstall.config.headers', { defaultValue: '请求头' })}
181
+ </div>
182
+
183
+ <div className={styles.configEditor}>
184
+ {/* HTTP 类型显示 Headers */}
185
+ {schema.config.type === 'http' && (
186
+ <KeyValueEditor
187
+ addButtonText={t('protocolInstall.config.addHeaders', { defaultValue: '添加请求头' })}
188
+ onChange={handleHeadersUpdate}
189
+ style={{ border: 'none' }}
190
+ value={currentHeaders}
191
+ />
192
+ )}
193
+
194
+ {/* STDIO 类型显示环境变量 */}
195
+ {schema.config.type === 'stdio' && (
196
+ <KeyValueEditor
197
+ addButtonText={t('protocolInstall.config.addEnv', { defaultValue: '添加环境变量' })}
198
+ onChange={handleEnvUpdate}
199
+ style={{ border: 'none' }}
200
+ value={currentEnv}
201
+ />
202
+ )}
203
+ </div>
204
+ </Block>
205
+ </Flexbox>
206
+ );
207
+ });
208
+
209
+ ConfigDisplay.displayName = 'ConfigDisplay';
210
+
211
+ export default ConfigDisplay;