@lobehub/chat 1.84.0 → 1.84.2

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 (47) hide show
  1. package/.env.desktop +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v1.json +14 -0
  4. package/locales/ar/models.json +6 -6
  5. package/locales/ar/plugin.json +10 -1
  6. package/locales/bg-BG/models.json +6 -6
  7. package/locales/bg-BG/plugin.json +10 -1
  8. package/locales/de-DE/models.json +6 -6
  9. package/locales/de-DE/plugin.json +10 -1
  10. package/locales/en-US/models.json +6 -6
  11. package/locales/en-US/plugin.json +10 -1
  12. package/locales/es-ES/models.json +6 -6
  13. package/locales/es-ES/plugin.json +10 -1
  14. package/locales/fa-IR/models.json +6 -6
  15. package/locales/fa-IR/plugin.json +10 -1
  16. package/locales/fr-FR/models.json +6 -6
  17. package/locales/fr-FR/plugin.json +10 -1
  18. package/locales/it-IT/models.json +6 -6
  19. package/locales/it-IT/plugin.json +10 -1
  20. package/locales/ja-JP/models.json +6 -6
  21. package/locales/ja-JP/plugin.json +10 -1
  22. package/locales/ko-KR/models.json +6 -6
  23. package/locales/ko-KR/plugin.json +10 -1
  24. package/locales/nl-NL/models.json +6 -6
  25. package/locales/nl-NL/plugin.json +10 -1
  26. package/locales/pl-PL/models.json +6 -6
  27. package/locales/pl-PL/plugin.json +10 -1
  28. package/locales/pt-BR/models.json +6 -6
  29. package/locales/pt-BR/plugin.json +10 -1
  30. package/locales/ru-RU/models.json +6 -6
  31. package/locales/ru-RU/plugin.json +10 -1
  32. package/locales/tr-TR/models.json +6 -6
  33. package/locales/tr-TR/plugin.json +10 -1
  34. package/locales/vi-VN/models.json +6 -6
  35. package/locales/vi-VN/plugin.json +10 -1
  36. package/locales/zh-CN/models.json +6 -6
  37. package/locales/zh-CN/plugin.json +10 -1
  38. package/locales/zh-TW/models.json +6 -6
  39. package/locales/zh-TW/plugin.json +10 -1
  40. package/package.json +1 -1
  41. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +15 -5
  42. package/src/const/hotkeys.ts +1 -1
  43. package/src/features/ModelSwitchPanel/index.tsx +6 -0
  44. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +227 -0
  45. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +14 -0
  46. package/src/locales/default/plugin.ts +10 -1
  47. package/src/server/globalConfig/index.ts +3 -0
@@ -0,0 +1,227 @@
1
+ import { ActionIcon, Icon } from '@lobehub/ui';
2
+ import { Button } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import fastDeepEqual from 'fast-deep-equal';
5
+ import { LucidePlus, LucideTrash } from 'lucide-react';
6
+ import { memo, useEffect, useRef, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+ import { v4 as uuidv4 } from 'uuid';
10
+
11
+ import { FormInput } from '@/components/FormInput';
12
+
13
+ const useStyles = createStyles(({ css, token }) => ({
14
+ container: css`
15
+ position: relative;
16
+
17
+ width: 100%;
18
+ padding: 12px;
19
+ border: 1px solid ${token.colorBorderSecondary};
20
+ border-radius: ${token.borderRadiusLG}px;
21
+ `,
22
+ input: css`
23
+ font-family: ${token.fontFamilyCode};
24
+ font-size: 12px;
25
+ `,
26
+ row: css`
27
+ margin-block-end: 8px;
28
+
29
+ &:last-child {
30
+ margin-block-end: 0;
31
+ }
32
+ `,
33
+ title: css`
34
+ margin-block-end: 8px;
35
+ color: ${token.colorTextTertiary};
36
+ `,
37
+ }));
38
+
39
+ interface KeyValueItem {
40
+ id: string;
41
+ key: string;
42
+ value: string;
43
+ }
44
+
45
+ export interface EnvEditorProps {
46
+ onChange?: (value: Record<string, string>) => void;
47
+ value?: Record<string, string>;
48
+ }
49
+
50
+ const recordToLocalList = (
51
+ record: Record<string, string> | undefined | null = {},
52
+ ): KeyValueItem[] =>
53
+ Object.entries(record || {}).map(([key, val]) => ({
54
+ id: uuidv4(),
55
+ key,
56
+ value: typeof val === 'string' ? val : '',
57
+ }));
58
+
59
+ const localListToRecord = (
60
+ list: KeyValueItem[] | undefined | null = [],
61
+ ): Record<string, string> => {
62
+ const record: Record<string, string> = {};
63
+ const keys = new Set<string>();
64
+ (list || [])
65
+ .slice()
66
+ .reverse()
67
+ .forEach((item) => {
68
+ const trimmedKey = item.key.trim();
69
+ if (trimmedKey && !keys.has(trimmedKey)) {
70
+ record[trimmedKey] = typeof item.value === 'string' ? item.value : '';
71
+ keys.add(trimmedKey);
72
+ }
73
+ });
74
+ return Object.keys(record)
75
+ .reverse()
76
+ .reduce(
77
+ (acc, key) => {
78
+ acc[key] = record[key];
79
+ return acc;
80
+ },
81
+ {} as Record<string, string>,
82
+ );
83
+ };
84
+
85
+ const EnvEditor = memo<EnvEditorProps>(({ value, onChange }) => {
86
+ const { styles } = useStyles();
87
+ const { t } = useTranslation(['plugin', 'common']);
88
+ const [items, setItems] = useState<KeyValueItem[]>(() => recordToLocalList(value));
89
+ const prevValueRef = useRef<Record<string, string> | undefined>(undefined);
90
+
91
+ useEffect(() => {
92
+ const externalRecord = value || {};
93
+ if (!fastDeepEqual(externalRecord, prevValueRef.current)) {
94
+ setItems(recordToLocalList(externalRecord));
95
+ prevValueRef.current = externalRecord;
96
+ }
97
+ }, [value]);
98
+
99
+ const triggerChange = (newItems: KeyValueItem[]) => {
100
+ const keysCount: Record<string, number> = {};
101
+ newItems.forEach((item) => {
102
+ const trimmedKey = item.key.trim();
103
+ if (trimmedKey) {
104
+ keysCount[trimmedKey] = (keysCount[trimmedKey] || 0) + 1;
105
+ }
106
+ });
107
+ setItems(
108
+ newItems.map((item) => ({
109
+ ...item,
110
+ })),
111
+ );
112
+ onChange?.(localListToRecord(newItems));
113
+ };
114
+
115
+ const handleAdd = () => {
116
+ const newItems = [...items, { id: uuidv4(), key: '', value: '' }];
117
+ triggerChange(newItems);
118
+ };
119
+
120
+ const handleRemove = (id: string) => {
121
+ const newItems = items.filter((item) => item.id !== id);
122
+ triggerChange(newItems);
123
+ };
124
+
125
+ const handleKeyChange = (id: string, newKey: string) => {
126
+ const newItems = items.map((item) => (item.id === id ? { ...item, key: newKey } : item));
127
+ triggerChange(newItems);
128
+ };
129
+
130
+ const handleValueChange = (id: string, newValue: string) => {
131
+ const newItems = items.map((item) => (item.id === id ? { ...item, value: newValue } : item));
132
+ triggerChange(newItems);
133
+ };
134
+
135
+ const getDuplicateKeys = (currentItems: KeyValueItem[]): Set<string> => {
136
+ const keys = new Set<string>();
137
+ const duplicates = new Set<string>();
138
+ currentItems.forEach((item) => {
139
+ const trimmedKey = item.key.trim();
140
+ if (trimmedKey) {
141
+ if (keys.has(trimmedKey)) {
142
+ duplicates.add(trimmedKey);
143
+ } else {
144
+ keys.add(trimmedKey);
145
+ }
146
+ }
147
+ });
148
+ return duplicates;
149
+ };
150
+ const duplicateKeys = getDuplicateKeys(items);
151
+
152
+ return (
153
+ <div className={styles.container}>
154
+ <Flexbox className={styles.title} gap={8} horizontal>
155
+ <Flexbox flex={1}>key</Flexbox>
156
+ <Flexbox flex={2}>value</Flexbox>
157
+ <Flexbox style={{ width: 30 }} />
158
+ </Flexbox>
159
+ <Flexbox width={'100%'}>
160
+ {items.map((item) => {
161
+ const isDuplicate = item.key.trim() && duplicateKeys.has(item.key.trim());
162
+ return (
163
+ <Flexbox
164
+ align="flex-start"
165
+ className={styles.row}
166
+ gap={8}
167
+ horizontal
168
+ key={item.id}
169
+ width={'100%'}
170
+ >
171
+ <Flexbox flex={1} style={{ position: 'relative' }}>
172
+ <FormInput
173
+ className={styles.input}
174
+ onChange={(e) => handleKeyChange(item.id, e)}
175
+ placeholder={'key'}
176
+ status={isDuplicate ? 'error' : undefined}
177
+ value={item.key}
178
+ variant={'filled'}
179
+ />
180
+ {isDuplicate && (
181
+ <div
182
+ style={{
183
+ bottom: '-16px',
184
+ color: 'red',
185
+ fontSize: '12px',
186
+ position: 'absolute',
187
+ }}
188
+ >
189
+ {t('dev.mcp.env.duplicateKeyError')}
190
+ </div>
191
+ )}
192
+ </Flexbox>
193
+ <Flexbox flex={2}>
194
+ <FormInput
195
+ className={styles.input}
196
+ onChange={(value) => handleValueChange(item.id, value)}
197
+ placeholder={'value'}
198
+ value={item.value}
199
+ variant={'filled'}
200
+ />
201
+ </Flexbox>
202
+ <ActionIcon
203
+ icon={LucideTrash}
204
+ onClick={() => handleRemove(item.id)}
205
+ size={'small'}
206
+ style={{ marginTop: 4 }}
207
+ title={t('delete', { ns: 'common' })}
208
+ />
209
+ </Flexbox>
210
+ );
211
+ })}
212
+ <Button
213
+ block
214
+ icon={<Icon icon={LucidePlus} />}
215
+ onClick={handleAdd}
216
+ size={'small'}
217
+ style={{ marginTop: items.length > 0 ? 16 : 8 }}
218
+ type="dashed"
219
+ >
220
+ {t('dev.mcp.env.add')}
221
+ </Button>
222
+ </Flexbox>
223
+ </div>
224
+ );
225
+ });
226
+
227
+ export default EnvEditor;
@@ -21,6 +21,7 @@ import { useToolStore } from '@/store/tool';
21
21
  import { pluginSelectors } from '@/store/tool/selectors';
22
22
 
23
23
  import ArgsInput from './ArgsInput';
24
+ import EnvEditor from './EnvEditor';
24
25
  import MCPTypeSelect from './MCPTypeSelect';
25
26
  import { parseMcpInput } from './utils';
26
27
 
@@ -51,6 +52,7 @@ const STDIO_COMMAND_OPTIONS: {
51
52
  const HTTP_URL_KEY = ['customParams', 'mcp', 'url'];
52
53
  const STDIO_COMMAND = ['customParams', 'mcp', 'command'];
53
54
  const STDIO_ARGS = ['customParams', 'mcp', 'args'];
55
+ const STDIO_ENV = ['customParams', 'mcp', 'env'];
54
56
  const MCP_TYPE = ['customParams', 'mcp', 'type'];
55
57
 
56
58
  const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
@@ -199,6 +201,7 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
199
201
  },
200
202
  },
201
203
  ]}
204
+ tag={'identifier'}
202
205
  >
203
206
  <Input
204
207
  onChange={handleIdentifierChange}
@@ -215,6 +218,7 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
215
218
  { message: t('dev.mcp.url.required'), required: true },
216
219
  { message: t('dev.mcp.url.invalid'), type: 'url' },
217
220
  ]}
221
+ tag={'url'}
218
222
  >
219
223
  <Input placeholder="https://mcp.higress.ai/mcp-github/xxxxx" />
220
224
  </FormItem>
@@ -227,6 +231,7 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
227
231
  label={t('dev.mcp.command.label')}
228
232
  name={STDIO_COMMAND}
229
233
  rules={[{ message: t('dev.mcp.command.required'), required: true }]}
234
+ tag={'command'}
230
235
  >
231
236
  <AutoComplete
232
237
  options={STDIO_COMMAND_OPTIONS.map(({ value, icon: Icon, color }) => ({
@@ -246,9 +251,18 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
246
251
  label={t('dev.mcp.args.label')}
247
252
  name={STDIO_ARGS}
248
253
  rules={[{ message: t('dev.mcp.args.required'), required: true }]}
254
+ tag={'args'}
249
255
  >
250
256
  <ArgsInput placeholder={t('dev.mcp.args.placeholder')} />
251
257
  </FormItem>
258
+ <FormItem
259
+ extra={t('dev.mcp.env.desc')}
260
+ label={t('dev.mcp.env.label')}
261
+ name={STDIO_ENV}
262
+ tag={'env'}
263
+ >
264
+ <EnvEditor />
265
+ </FormItem>
252
266
  </>
253
267
  )}
254
268
  <FormItem colon={false} label={t('dev.mcp.testConnectionTip')} layout={'horizontal'}>
@@ -48,7 +48,7 @@ export default {
48
48
  },
49
49
  mcp: {
50
50
  args: {
51
- desc: '传递给 STDIO 命令的参数列表,一般在这里输入 MCP 服务器名称',
51
+ desc: '传递给执行命令的参数列表,一般在这里输入 MCP 服务器名称,或启动脚本路径',
52
52
  label: '命令参数',
53
53
  placeholder: '例如:mcp-hello-world',
54
54
  required: '请输入启动参数',
@@ -63,6 +63,15 @@ export default {
63
63
  desc: '输入你的 MCP Streamable HTTP Server 的地址',
64
64
  label: 'MCP Endpoint URL',
65
65
  },
66
+ env: {
67
+ add: '新增一行',
68
+ desc: '输入你的 MCP Server 所需要的环境变量',
69
+ duplicateKeyError: '字段键必须唯一',
70
+ formValidationFailed: '表单验证失败,请检查参数格式',
71
+ keyRequired: '字段键不能为空',
72
+ label: 'MCP Server 环境变量',
73
+ stringifyError: '无法序列化参数,请检查参数格式',
74
+ },
66
75
  identifier: {
67
76
  desc: '为你的 MCP 插件指定一个名称,需要使用英文字符',
68
77
  invalid: '标识符只能包含字母、数字、连字符和下划线',
@@ -33,6 +33,9 @@ export const getServerGlobalConfig = async () => {
33
33
  enabledKey: 'ENABLED_GITEE_AI',
34
34
  modelListKey: 'GITEE_AI_MODEL_LIST',
35
35
  },
36
+ lmstudio: {
37
+ fetchOnClient: isDesktop ? false : undefined,
38
+ },
36
39
  /* ↓ cloud slot ↓ */
37
40
 
38
41
  /* ↑ cloud slot ↑ */