@lobehub/chat 1.109.0 → 1.110.0

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 (100) hide show
  1. package/.cursor/rules/i18n.mdc +1 -2
  2. package/CHANGELOG.md +59 -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 +21 -0
  15. package/locales/ar/models.json +6 -0
  16. package/locales/ar/plugin.json +196 -136
  17. package/locales/ar/providers.json +3 -0
  18. package/locales/bg-BG/models.json +6 -0
  19. package/locales/bg-BG/plugin.json +204 -144
  20. package/locales/bg-BG/providers.json +3 -0
  21. package/locales/de-DE/models.json +6 -0
  22. package/locales/de-DE/plugin.json +176 -116
  23. package/locales/de-DE/providers.json +3 -0
  24. package/locales/en-US/models.json +6 -0
  25. package/locales/en-US/plugin.json +192 -132
  26. package/locales/en-US/providers.json +3 -0
  27. package/locales/es-ES/models.json +6 -0
  28. package/locales/es-ES/plugin.json +203 -143
  29. package/locales/es-ES/providers.json +3 -0
  30. package/locales/fa-IR/models.json +6 -0
  31. package/locales/fa-IR/plugin.json +155 -95
  32. package/locales/fa-IR/providers.json +3 -0
  33. package/locales/fr-FR/models.json +6 -0
  34. package/locales/fr-FR/plugin.json +161 -101
  35. package/locales/fr-FR/providers.json +3 -0
  36. package/locales/it-IT/models.json +6 -0
  37. package/locales/it-IT/plugin.json +193 -133
  38. package/locales/it-IT/providers.json +3 -0
  39. package/locales/ja-JP/models.json +6 -0
  40. package/locales/ja-JP/plugin.json +195 -135
  41. package/locales/ja-JP/providers.json +3 -0
  42. package/locales/ko-KR/models.json +6 -0
  43. package/locales/ko-KR/plugin.json +163 -103
  44. package/locales/ko-KR/providers.json +3 -0
  45. package/locales/nl-NL/models.json +6 -0
  46. package/locales/nl-NL/plugin.json +211 -151
  47. package/locales/nl-NL/providers.json +3 -0
  48. package/locales/pl-PL/models.json +6 -0
  49. package/locales/pl-PL/plugin.json +171 -111
  50. package/locales/pl-PL/providers.json +3 -0
  51. package/locales/pt-BR/models.json +6 -0
  52. package/locales/pt-BR/plugin.json +180 -120
  53. package/locales/pt-BR/providers.json +3 -0
  54. package/locales/ru-RU/models.json +6 -0
  55. package/locales/ru-RU/plugin.json +191 -131
  56. package/locales/ru-RU/providers.json +3 -0
  57. package/locales/tr-TR/models.json +6 -0
  58. package/locales/tr-TR/plugin.json +187 -127
  59. package/locales/tr-TR/providers.json +3 -0
  60. package/locales/vi-VN/models.json +6 -0
  61. package/locales/vi-VN/plugin.json +152 -92
  62. package/locales/vi-VN/providers.json +3 -0
  63. package/locales/zh-CN/models.json +6 -0
  64. package/locales/zh-CN/plugin.json +60 -0
  65. package/locales/zh-CN/providers.json +3 -0
  66. package/locales/zh-TW/models.json +6 -0
  67. package/locales/zh-TW/plugin.json +157 -97
  68. package/locales/zh-TW/providers.json +3 -0
  69. package/package.json +2 -1
  70. package/packages/electron-client-ipc/src/events/index.ts +5 -2
  71. package/packages/electron-client-ipc/src/events/protocol.ts +29 -0
  72. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  73. package/packages/electron-client-ipc/src/types/mcpInstall.ts +19 -0
  74. package/packages/types/src/plugins/mcp.ts +38 -1
  75. package/packages/types/src/plugins/protocol.ts +166 -0
  76. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +4 -1
  77. package/src/app/[variants]/(main)/discover/(detail)/mcp/[slug]/features/Sidebar/ActionButton/index.tsx +1 -2
  78. package/src/components/KeyValueEditor/index.tsx +4 -2
  79. package/src/config/aiModels/aihubmix.ts +465 -30
  80. package/src/config/aiModels/anthropic.ts +27 -1
  81. package/src/config/aiModels/groq.ts +40 -4
  82. package/src/config/aiModels/qwen.ts +24 -2
  83. package/src/features/MCP/MCPInstallProgress/index.tsx +1 -1
  84. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +30 -36
  85. package/src/features/PluginStore/McpList/List/Item.tsx +1 -1
  86. package/src/features/ProtocolUrlHandler/InstallPlugin/ConfigDisplay.tsx +211 -0
  87. package/src/features/ProtocolUrlHandler/InstallPlugin/CustomPluginInstallModal.tsx +228 -0
  88. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/Detail.tsx +44 -0
  89. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +105 -0
  90. package/src/features/ProtocolUrlHandler/InstallPlugin/index.tsx +55 -0
  91. package/src/features/ProtocolUrlHandler/InstallPlugin/types.ts +45 -0
  92. package/src/features/ProtocolUrlHandler/index.tsx +30 -0
  93. package/src/libs/model-runtime/anthropic/index.ts +15 -2
  94. package/src/libs/model-runtime/utils/modelParse.ts +2 -2
  95. package/src/libs/model-runtime/utils/streams/ollama.test.ts +97 -51
  96. package/src/libs/model-runtime/utils/streams/ollama.ts +4 -0
  97. package/src/locales/default/plugin.ts +60 -0
  98. package/src/store/tool/slices/mcpStore/action.ts +127 -1
  99. package/src/store/tool/slices/mcpStore/initialState.ts +8 -13
  100. package/src/store/tool/slices/mcpStore/selectors.ts +13 -0
@@ -5,7 +5,7 @@ import { AIChatModelCard } from '@/types/aiModel';
5
5
 
6
6
  const groqChatModels: AIChatModelCard[] = [
7
7
  {
8
- contextWindowTokens: 131_072,
8
+ contextWindowTokens: 8192,
9
9
  description:
10
10
  'Compound-beta 是一个复合 AI 系统,由 GroqCloud 中已经支持的多个开放可用的模型提供支持,可以智能地、有选择地使用工具来回答用户查询。',
11
11
  displayName: 'Compound Beta',
@@ -15,7 +15,7 @@ const groqChatModels: AIChatModelCard[] = [
15
15
  type: 'chat',
16
16
  },
17
17
  {
18
- contextWindowTokens: 131_072,
18
+ contextWindowTokens: 8192,
19
19
  description:
20
20
  'Compound-beta-mini 是一个复合 AI 系统,由 GroqCloud 中已经支持的公开可用模型提供支持,可以智能地、有选择地使用工具来回答用户查询。',
21
21
  displayName: 'Compound Beta Mini',
@@ -23,6 +23,42 @@ const groqChatModels: AIChatModelCard[] = [
23
23
  maxOutput: 8192,
24
24
  type: 'chat',
25
25
  },
26
+ {
27
+ abilities: {
28
+ functionCall: true,
29
+ reasoning: true,
30
+ },
31
+ contextWindowTokens: 131_072,
32
+ description:
33
+ 'OpenAI GPT-OSS 120B 是一款拥有 1200 亿参数的顶尖语言模型,内置浏览器搜索和代码执行功能,并具备推理能力。',
34
+ displayName: 'GPT OSS 120B',
35
+ id: 'openai/gpt-oss-120b',
36
+ maxOutput: 32_768,
37
+ pricing: {
38
+ input: 0.15,
39
+ output: 0.75,
40
+ },
41
+ releasedAt: '2025-08-06',
42
+ type: 'chat',
43
+ },
44
+ {
45
+ abilities: {
46
+ functionCall: true,
47
+ reasoning: true,
48
+ },
49
+ contextWindowTokens: 131_072,
50
+ description:
51
+ 'OpenAI GPT-OSS 20B 是一款拥有 200 亿参数的顶尖语言模型,内置浏览器搜索和代码执行功能,并具备推理能力。',
52
+ displayName: 'GPT OSS 20B',
53
+ id: 'openai/gpt-oss-20b',
54
+ maxOutput: 32_768,
55
+ pricing: {
56
+ input: 0.1,
57
+ output: 0.5,
58
+ },
59
+ releasedAt: '2025-08-06',
60
+ type: 'chat',
61
+ },
26
62
  {
27
63
  abilities: {
28
64
  functionCall: true,
@@ -75,7 +111,7 @@ const groqChatModels: AIChatModelCard[] = [
75
111
  contextWindowTokens: 131_072,
76
112
  displayName: 'Qwen3 32B',
77
113
  id: 'qwen/qwen3-32b',
78
- maxOutput: 40_960,
114
+ maxOutput: 131_072,
79
115
  pricing: {
80
116
  input: 0.29,
81
117
  output: 0.59,
@@ -132,7 +168,7 @@ const groqChatModels: AIChatModelCard[] = [
132
168
  abilities: {
133
169
  functionCall: true,
134
170
  },
135
- contextWindowTokens: 131_072,
171
+ contextWindowTokens: 32_768,
136
172
  description:
137
173
  'Meta Llama 3.3 多语言大语言模型 ( LLM ) 是 70B(文本输入/文本输出)中的预训练和指令调整生成模型。 Llama 3.3 指令调整的纯文本模型针对多语言对话用例进行了优化,并且在常见行业基准上优于许多可用的开源和封闭式聊天模型。',
138
174
  displayName: 'Llama 3.3 70B Versatile',
@@ -34,7 +34,7 @@ const qwenChatModels: AIChatModelCard[] = [
34
34
  deploymentName: 'qwen3-coder-plus',
35
35
  },
36
36
  contextWindowTokens: 1_048_576,
37
- description: '通义千问代码模型。最新的 Qwen3-Coder-Plus 系列模型是基于 Qwen3 的代码生成模型,具有强大的Coding Agent能力,擅长工具调用和环境交互,能够实现自主编程,代码能力卓越的同时兼具通用能力。',
37
+ description: '通义千问代码模型。最新的 Qwen3-Coder 系列模型是基于 Qwen3 的代码生成模型,具有强大的Coding Agent能力,擅长工具调用和环境交互,能够实现自主编程,代码能力卓越的同时兼具通用能力。',
38
38
  displayName: 'Qwen3 Coder Plus',
39
39
  id: 'qwen3-coder-plus',
40
40
  maxOutput: 65_536,
@@ -45,7 +45,29 @@ const qwenChatModels: AIChatModelCard[] = [
45
45
  input: 6,
46
46
  output: 24,
47
47
  },
48
- releasedAt: '2025-07-23',
48
+ releasedAt: '2025-07-22',
49
+ type: 'chat',
50
+ },
51
+ {
52
+ abilities: {
53
+ functionCall: true,
54
+ },
55
+ config: {
56
+ deploymentName: 'qwen3-coder-flash',
57
+ },
58
+ contextWindowTokens: 1_048_576,
59
+ description: '通义千问代码模型。最新的 Qwen3-Coder 系列模型是基于 Qwen3 的代码生成模型,具有强大的Coding Agent能力,擅长工具调用和环境交互,能够实现自主编程,代码能力卓越的同时兼具通用能力。',
60
+ displayName: 'Qwen3 Coder Flash',
61
+ id: 'qwen3-coder-flash',
62
+ maxOutput: 65_536,
63
+ organization: 'Qwen',
64
+ pricing: {
65
+ cachedInput: 0.6, // tokens 32K ~ 128K
66
+ currency: 'CNY',
67
+ input: 1.5,
68
+ output: 6,
69
+ },
70
+ releasedAt: '2025-07-28',
49
71
  type: 'chat',
50
72
  },
51
73
  {
@@ -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;
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import { Alert, Block, Modal, Text } from '@lobehub/ui';
4
+ import { App } from 'antd';
5
+ import { memo, useCallback, useEffect, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import PluginAvatar from '@/components/Plugins/PluginAvatar';
10
+ import PluginTag from '@/components/Plugins/PluginTag';
11
+ import { useAgentStore } from '@/store/agent';
12
+ import { useToolStore } from '@/store/tool';
13
+ import { mcpStoreSelectors } from '@/store/tool/selectors';
14
+ import { McpConnectionParams } from '@/types/plugins';
15
+ import { LobeToolCustomPlugin } from '@/types/tool/plugin';
16
+
17
+ import ConfigDisplay from './ConfigDisplay';
18
+ import { McpInstallRequest, TRUSTED_MARKETPLACES, TrustedMarketplaceId } from './types';
19
+
20
+ interface CustomPluginInstallModalProps {
21
+ installRequest: McpInstallRequest | null;
22
+ isMarketplace?: boolean;
23
+ onComplete?: () => void;
24
+ }
25
+
26
+ const CustomPluginInstallModal = memo<CustomPluginInstallModalProps>(
27
+ ({ installRequest, isMarketplace = false, onComplete }) => {
28
+ const { message } = App.useApp();
29
+ const { t } = useTranslation('plugin');
30
+ const [loading, setLoading] = useState(false);
31
+
32
+ // 跟踪配置更新
33
+ const [updatedConfig, setUpdatedConfig] = useState<{
34
+ env?: Record<string, string>;
35
+ headers?: Record<string, string>;
36
+ }>({});
37
+
38
+ const [installCustomPlugin] = useToolStore((s) => [s.installCustomPlugin]);
39
+ const testMcpConnection = useToolStore((s) => s.testMcpConnection);
40
+ const togglePlugin = useAgentStore((s) => s.togglePlugin);
41
+
42
+ // 为自定义插件测试连接生成唯一标识符
43
+ const identifier = installRequest?.schema?.identifier || '';
44
+ const testState = useToolStore(mcpStoreSelectors.getMCPConnectionTestState(identifier));
45
+
46
+ const schema = installRequest?.schema;
47
+ const marketId = installRequest?.marketId;
48
+ const marketplace =
49
+ isMarketplace && marketId ? TRUSTED_MARKETPLACES[marketId as TrustedMarketplaceId] : null;
50
+
51
+ // 重置加载状态和配置
52
+ useEffect(() => {
53
+ if (!installRequest) {
54
+ setLoading(false);
55
+ setUpdatedConfig({});
56
+ }
57
+ }, [installRequest]);
58
+
59
+ const handleConfirm = useCallback(async () => {
60
+ if (!installRequest || !schema) return;
61
+
62
+ setLoading(true);
63
+ try {
64
+ // 第三方市场和自定义插件:构建自定义插件数据
65
+ let customPlugin: LobeToolCustomPlugin;
66
+
67
+ // 合并原始配置和用户更新的配置
68
+ const finalConfig = {
69
+ ...schema.config,
70
+ env: updatedConfig.env || schema.config.env,
71
+ headers: updatedConfig.headers || schema.config.headers,
72
+ };
73
+
74
+ // 自定义插件:先测试连接获取真实的 manifest
75
+ const testParams: McpConnectionParams = {
76
+ connection: finalConfig,
77
+ identifier: identifier,
78
+ metadata: {
79
+ avatar: schema.icon,
80
+ description: schema.description,
81
+ },
82
+ };
83
+ console.log('testParams:', testParams);
84
+
85
+ const testResult = await testMcpConnection(testParams);
86
+
87
+ if (!testResult.success) {
88
+ throw new Error(testResult.error || t('protocolInstall.messages.connectionTestFailed'));
89
+ }
90
+
91
+ if (!testResult.manifest) {
92
+ throw new Error(t('protocolInstall.messages.manifestNotFound'));
93
+ }
94
+
95
+ // 使用测试连接获取的真实 manifest
96
+ customPlugin = {
97
+ customParams: {
98
+ avatar: schema.icon,
99
+ description: schema.description,
100
+ mcp: {
101
+ ...finalConfig, // 使用合并后的配置
102
+ headers: finalConfig.type === 'http' ? finalConfig.headers : undefined,
103
+ },
104
+ },
105
+ identifier: schema.identifier,
106
+ manifest: testResult.manifest, // 使用真实的 manifest
107
+ type: 'customPlugin',
108
+ };
109
+
110
+ await installCustomPlugin(customPlugin);
111
+ await togglePlugin(schema.identifier);
112
+ message.success(t('protocolInstall.messages.installSuccess', { name: schema.name }));
113
+
114
+ onComplete?.();
115
+ } catch (error) {
116
+ console.error('Plugin installation error:', error);
117
+ message.error(t('protocolInstall.messages.installError'));
118
+ setLoading(false);
119
+ }
120
+ }, [
121
+ installRequest,
122
+ schema,
123
+ updatedConfig,
124
+ onComplete,
125
+ installCustomPlugin,
126
+ testMcpConnection,
127
+ togglePlugin,
128
+ message,
129
+ t,
130
+ identifier,
131
+ ]);
132
+
133
+ const handleCancel = useCallback(() => {
134
+ onComplete?.();
135
+ }, [onComplete]);
136
+
137
+ if (!installRequest || !schema) return null;
138
+
139
+ // 根据类型渲染不同的 Alert 组件
140
+ const renderAlert = () => {
141
+ if (!isMarketplace) {
142
+ return (
143
+ <Alert
144
+ message={t('protocolInstall.custom.security.description')}
145
+ showIcon
146
+ type="warning"
147
+ variant={'borderless'}
148
+ />
149
+ );
150
+ }
151
+
152
+ // marketplace 类型
153
+ return marketplace ? (
154
+ <Alert
155
+ message={t('protocolInstall.marketplace.trustedBy', { name: marketplace.name })}
156
+ showIcon
157
+ type="success"
158
+ variant={'borderless'}
159
+ />
160
+ ) : (
161
+ <Alert
162
+ message={t('protocolInstall.marketplace.unverified.warning')}
163
+ showIcon
164
+ type="warning"
165
+ variant={'borderless'}
166
+ />
167
+ );
168
+ };
169
+
170
+ const modalTitle = isMarketplace
171
+ ? t('protocolInstall.marketplace.title')
172
+ : t('protocolInstall.custom.title');
173
+
174
+ const okText = isMarketplace
175
+ ? t('protocolInstall.actions.install')
176
+ : t('protocolInstall.actions.installAnyway');
177
+
178
+ return (
179
+ <Modal
180
+ confirmLoading={loading || testState.loading}
181
+ okText={okText}
182
+ onCancel={handleCancel}
183
+ onOk={handleConfirm}
184
+ open
185
+ title={modalTitle}
186
+ width={680}
187
+ >
188
+ <Flexbox gap={24}>
189
+ {renderAlert()}
190
+
191
+ <Block gap={16} horizontal justify={'space-between'} padding={16} variant={'outlined'}>
192
+ <Flexbox gap={16} horizontal>
193
+ <PluginAvatar avatar={schema.icon} size={40} />
194
+ <Flexbox gap={2}>
195
+ <Flexbox align={'center'} gap={8} horizontal>
196
+ {schema.name}
197
+ <PluginTag type={'customPlugin'} />
198
+ </Flexbox>
199
+ <Text style={{ fontSize: 12 }} type={'secondary'}>
200
+ {schema.description}
201
+ </Text>
202
+ </Flexbox>
203
+ </Flexbox>
204
+ </Block>
205
+
206
+ <Flexbox>
207
+ <ConfigDisplay onConfigUpdate={setUpdatedConfig} schema={schema} />
208
+ {/* 显示测试连接错误 */}
209
+ {testState.error && (
210
+ <Alert
211
+ closable
212
+ description={testState.error}
213
+ message={t('protocolInstall.messages.connectionTestFailed')}
214
+ showIcon
215
+ type="error"
216
+ variant={'filled'}
217
+ />
218
+ )}
219
+ </Flexbox>
220
+ </Flexbox>
221
+ </Modal>
222
+ );
223
+ },
224
+ );
225
+
226
+ CustomPluginInstallModal.displayName = 'CustomPluginInstallModal';
227
+
228
+ export default CustomPluginInstallModal;