@lobehub/chat 0.162.23 → 0.162.25
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.
- package/CHANGELOG.md +51 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/docs/self-hosting/platform/zeabur.mdx +1 -1
- package/docs/self-hosting/platform/zeabur.zh-CN.mdx +1 -1
- package/locales/ar/common.json +28 -1
- package/locales/bg-BG/common.json +28 -1
- package/locales/de-DE/common.json +28 -1
- package/locales/en-US/common.json +28 -1
- package/locales/es-ES/common.json +28 -1
- package/locales/fr-FR/common.json +28 -1
- package/locales/it-IT/common.json +28 -1
- package/locales/ja-JP/common.json +28 -1
- package/locales/ko-KR/common.json +28 -1
- package/locales/nl-NL/common.json +28 -1
- package/locales/pl-PL/common.json +28 -1
- package/locales/pt-BR/common.json +28 -1
- package/locales/ru-RU/common.json +28 -1
- package/locales/tr-TR/common.json +28 -1
- package/locales/vi-VN/common.json +28 -1
- package/locales/zh-CN/common.json +28 -1
- package/locales/zh-TW/common.json +28 -1
- package/package.json +2 -2
- package/src/app/(main)/settings/@category/default.tsx +1 -2
- package/src/app/(main)/settings/_layout/Desktop/index.tsx +4 -1
- package/src/app/(main)/settings/_layout/Mobile/index.tsx +7 -1
- package/src/app/(main)/settings/about/features/AboutList.tsx +13 -120
- package/src/app/(main)/settings/about/features/Analytics.tsx +1 -1
- package/src/app/(main)/settings/about/features/ItemCard.tsx +45 -0
- package/src/app/(main)/settings/about/features/ItemLink.tsx +32 -0
- package/src/app/(main)/settings/about/features/Version.tsx +75 -0
- package/src/app/(main)/settings/about/index.tsx +118 -25
- package/src/app/(main)/settings/features/Footer.tsx +80 -10
- package/src/app/(main)/settings/llm/components/Footer.tsx +14 -5
- package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +11 -1
- package/src/app/(main)/settings/llm/index.tsx +3 -0
- package/src/app/(main)/settings/system-agent/page.tsx +11 -11
- package/src/app/@modal/_layout/SettingModalLayout.tsx +4 -2
- package/src/app/api/middleware/auth/index.ts +1 -1
- package/src/components/GuideModal/index.tsx +77 -0
- package/src/components/GuideVideo/index.tsx +30 -0
- package/src/const/url.ts +6 -1
- package/src/features/AgentSetting/AgentModal/index.tsx +6 -7
- package/src/features/Conversation/Error/OllamaBizError/SetupGuide.tsx +177 -173
- package/src/features/Conversation/Error/style.tsx +56 -31
- package/src/features/User/UserPanel/useMenu.tsx +2 -2
- package/src/locales/default/common.ts +27 -2
- package/src/services/ollama.ts +2 -2
- package/src/store/user/slices/modelList/selectors/keyVaults.test.ts +201 -0
- package/src/store/user/slices/modelList/selectors/keyVaults.ts +15 -3
- package/src/store/user/slices/modelList/selectors/modelConfig.test.ts +29 -1
- package/src/store/user/slices/modelList/selectors/modelConfig.ts +21 -1
- package/src/types/user/settings/keyVaults.ts +1 -1
- package/src/app/(main)/settings/about/features/Item.tsx +0 -50
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Avatar } from '@lobehub/ui';
|
|
2
2
|
import { createStyles } from 'antd-style';
|
|
3
3
|
import { ReactNode, memo } from 'react';
|
|
4
|
-
import { Center, Flexbox } from 'react-layout-kit';
|
|
4
|
+
import { Center, CenterProps, Flexbox } from 'react-layout-kit';
|
|
5
5
|
|
|
6
6
|
export const useStyles = createStyles(({ css, token }) => ({
|
|
7
7
|
container: css`
|
|
@@ -14,38 +14,63 @@ export const useStyles = createStyles(({ css, token }) => ({
|
|
|
14
14
|
color: ${token.colorTextTertiary};
|
|
15
15
|
text-align: center;
|
|
16
16
|
`,
|
|
17
|
+
form: css`
|
|
18
|
+
width: 100%;
|
|
19
|
+
max-width: 300px;
|
|
20
|
+
`,
|
|
17
21
|
}));
|
|
18
22
|
|
|
19
|
-
export const ErrorActionContainer = memo<
|
|
20
|
-
|
|
23
|
+
export const ErrorActionContainer = memo<CenterProps>(
|
|
24
|
+
({ children, className, gap = 24, padding = 24, ...rest }) => {
|
|
25
|
+
const { cx, styles } = useStyles();
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
27
|
+
return (
|
|
28
|
+
<Center className={cx(styles.container, className)} gap={gap} padding={padding} {...rest}>
|
|
29
|
+
{children}
|
|
30
|
+
</Center>
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
28
34
|
|
|
29
|
-
export const FormAction = memo<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
export const FormAction = memo<
|
|
36
|
+
{
|
|
37
|
+
animation?: boolean;
|
|
38
|
+
avatar: ReactNode;
|
|
39
|
+
background?: string;
|
|
40
|
+
description: string;
|
|
41
|
+
title: string;
|
|
42
|
+
} & CenterProps
|
|
43
|
+
>(
|
|
44
|
+
({
|
|
45
|
+
children,
|
|
46
|
+
background,
|
|
47
|
+
title,
|
|
48
|
+
description,
|
|
49
|
+
avatar,
|
|
50
|
+
animation,
|
|
51
|
+
className,
|
|
52
|
+
gap = 16,
|
|
53
|
+
...rest
|
|
54
|
+
}) => {
|
|
55
|
+
const { cx, styles, theme } = useStyles();
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
57
|
+
return (
|
|
58
|
+
<Center className={cx(styles.form, className)} gap={gap} {...rest}>
|
|
59
|
+
<Avatar
|
|
60
|
+
animation={animation}
|
|
61
|
+
avatar={avatar}
|
|
62
|
+
background={background ?? theme.colorFillContent}
|
|
63
|
+
gap={12}
|
|
64
|
+
size={80}
|
|
65
|
+
/>
|
|
66
|
+
<Flexbox gap={8} width={'100%'}>
|
|
67
|
+
<Flexbox style={{ fontSize: 18, fontWeight: 'bold', textAlign: 'center' }}>
|
|
68
|
+
{title}
|
|
69
|
+
</Flexbox>
|
|
70
|
+
<Flexbox className={styles.desc}>{description}</Flexbox>
|
|
71
|
+
</Flexbox>
|
|
72
|
+
{children}
|
|
73
|
+
</Center>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
@@ -20,7 +20,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
20
20
|
import urlJoin from 'url-join';
|
|
21
21
|
|
|
22
22
|
import type { MenuProps } from '@/components/Menu';
|
|
23
|
-
import { DISCORD, DOCUMENTS, EMAIL_SUPPORT, GITHUB_ISSUES } from '@/const/url';
|
|
23
|
+
import { DISCORD, DOCUMENTS, EMAIL_SUPPORT, GITHUB_ISSUES, mailTo } from '@/const/url';
|
|
24
24
|
import DataImporter from '@/features/DataImporter';
|
|
25
25
|
import { useOpenSettings } from '@/hooks/useInterceptingRoutes';
|
|
26
26
|
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
|
@@ -190,7 +190,7 @@ export const useMenu = () => {
|
|
|
190
190
|
icon: <Icon icon={Mail} />,
|
|
191
191
|
key: 'email',
|
|
192
192
|
label: (
|
|
193
|
-
<Link href={
|
|
193
|
+
<Link href={mailTo(EMAIL_SUPPORT)} target={'_blank'}>
|
|
194
194
|
{t('userPanel.email')}
|
|
195
195
|
</Link>
|
|
196
196
|
),
|
|
@@ -9,6 +9,7 @@ export default {
|
|
|
9
9
|
cancel: '取消',
|
|
10
10
|
changelog: '更新日志',
|
|
11
11
|
close: '关闭',
|
|
12
|
+
contact: '联系我们',
|
|
12
13
|
copy: '复制',
|
|
13
14
|
copyFail: '复制失败',
|
|
14
15
|
copySuccess: '复制成功',
|
|
@@ -35,8 +36,26 @@ export default {
|
|
|
35
36
|
},
|
|
36
37
|
feedback: '反馈与建议',
|
|
37
38
|
follow: '在 {{name}} 上关注我们',
|
|
39
|
+
footer: {
|
|
40
|
+
action: {
|
|
41
|
+
feedback: '分享您宝贵的建议',
|
|
42
|
+
star: '在 GitHub 给添加星标',
|
|
43
|
+
},
|
|
44
|
+
and: '并',
|
|
45
|
+
feedback: {
|
|
46
|
+
action: '分享反馈',
|
|
47
|
+
desc: '您的每一个想法和建议对我们来说都弥足珍贵,我们迫不及待地想知道您的看法!欢迎联系我们提供产品功能和使用体验反馈,帮助我们将 LobeChat 建设得更好。',
|
|
48
|
+
title: '在 GitHub 分享您宝贵的反馈',
|
|
49
|
+
},
|
|
50
|
+
later: '稍后',
|
|
51
|
+
star: {
|
|
52
|
+
action: '点亮星标',
|
|
53
|
+
desc: '如果您喜爱我们的产品,并希望支持我们,可以去 GitHub 给我们点一颗星吗?这个小小的动作对我们来说意义重大,能激励我们为您持续提供特性体验。',
|
|
54
|
+
title: '在 GitHub 为我们点亮星标',
|
|
55
|
+
},
|
|
56
|
+
title: '喜欢我们的产品?',
|
|
57
|
+
},
|
|
38
58
|
fullscreen: '全屏模式',
|
|
39
|
-
|
|
40
59
|
historyRange: '历史范围',
|
|
41
60
|
import: '导入配置',
|
|
42
61
|
importModal: {
|
|
@@ -69,6 +88,7 @@ export default {
|
|
|
69
88
|
speed: '上传速度',
|
|
70
89
|
},
|
|
71
90
|
},
|
|
91
|
+
information: '社区与资讯',
|
|
72
92
|
installPWA: '安装浏览器应用 (PWA)',
|
|
73
93
|
lang: {
|
|
74
94
|
'ar': '阿拉伯语',
|
|
@@ -108,6 +128,11 @@ export default {
|
|
|
108
128
|
'zh-TW': '繁体中文',
|
|
109
129
|
},
|
|
110
130
|
layoutInitializing: '正在加载布局...',
|
|
131
|
+
legal: '法律声明',
|
|
132
|
+
mail: {
|
|
133
|
+
business: '商务合作',
|
|
134
|
+
support: '邮件支持',
|
|
135
|
+
},
|
|
111
136
|
noDescription: '暂无描述',
|
|
112
137
|
oauth: 'SSO 登录',
|
|
113
138
|
officialSite: '官方网站',
|
|
@@ -149,7 +174,6 @@ export default {
|
|
|
149
174
|
title: '同步状态',
|
|
150
175
|
unconnected: { tip: '信令服务器连接失败,将无法建立点对点通信频道,请检查网络后重试' },
|
|
151
176
|
},
|
|
152
|
-
|
|
153
177
|
tab: {
|
|
154
178
|
chat: '会话',
|
|
155
179
|
market: '发现',
|
|
@@ -188,4 +212,5 @@ export default {
|
|
|
188
212
|
setting: '应用设置',
|
|
189
213
|
usages: '用量统计',
|
|
190
214
|
},
|
|
215
|
+
version: '版本',
|
|
191
216
|
};
|
package/src/services/ollama.ts
CHANGED
|
@@ -42,8 +42,8 @@ export class OllamaService {
|
|
|
42
42
|
this._client.abort();
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
pullModel = async (model: string): Promise<
|
|
46
|
-
let response: Response |
|
|
45
|
+
pullModel = async (model: string): Promise<AsyncIterable<ProgressResponse>> => {
|
|
46
|
+
let response: Response | AsyncIterable<ProgressResponse>;
|
|
47
47
|
try {
|
|
48
48
|
response = await this.getOllamaClient().pull({ insecure: true, model, stream: true });
|
|
49
49
|
return response;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { UserStore } from '@/store/user';
|
|
4
|
+
import {
|
|
5
|
+
AWSBedrockKeyVault,
|
|
6
|
+
AzureOpenAIKeyVault,
|
|
7
|
+
OpenAICompatibleKeyVault,
|
|
8
|
+
} from '@/types/user/settings';
|
|
9
|
+
import { merge } from '@/utils/merge';
|
|
10
|
+
|
|
11
|
+
import { initialSettingsState } from '../../settings/initialState';
|
|
12
|
+
import { keyVaultsConfigSelectors } from './keyVaults';
|
|
13
|
+
|
|
14
|
+
describe('keyVaultsConfigSelectors', () => {
|
|
15
|
+
describe('isProviderEndpointNotEmpty', () => {
|
|
16
|
+
describe('OpenAICompatibleKeyVault', () => {
|
|
17
|
+
it('should return true if provider endpoint is not empty', () => {
|
|
18
|
+
const s = merge(initialSettingsState, {
|
|
19
|
+
settings: {
|
|
20
|
+
keyVaults: {
|
|
21
|
+
openai: {
|
|
22
|
+
endpoint: 'endpoint',
|
|
23
|
+
} as OpenAICompatibleKeyVault,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}) as unknown as UserStore;
|
|
27
|
+
expect(keyVaultsConfigSelectors.isProviderEndpointNotEmpty('openai')(s)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return false if provider endpoint is empty', () => {
|
|
31
|
+
const s = merge(initialSettingsState, {
|
|
32
|
+
settings: {
|
|
33
|
+
keyVaults: {
|
|
34
|
+
openai: {
|
|
35
|
+
endpoint: undefined,
|
|
36
|
+
} as OpenAICompatibleKeyVault,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}) as unknown as UserStore;
|
|
40
|
+
expect(keyVaultsConfigSelectors.isProviderEndpointNotEmpty('openai')(s)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('AzureOpenAIKeyVault', () => {
|
|
45
|
+
it('should return true if provider endpoint is not empty', () => {
|
|
46
|
+
const s = merge(initialSettingsState, {
|
|
47
|
+
settings: {
|
|
48
|
+
keyVaults: {
|
|
49
|
+
azure: {
|
|
50
|
+
baseURL: 'baseURL',
|
|
51
|
+
} as AzureOpenAIKeyVault,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}) as unknown as UserStore;
|
|
55
|
+
expect(keyVaultsConfigSelectors.isProviderEndpointNotEmpty('azure')(s)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return false if provider endpoint is empty', () => {
|
|
59
|
+
const s = merge(initialSettingsState, {
|
|
60
|
+
settings: {
|
|
61
|
+
keyVaults: {
|
|
62
|
+
azure: {
|
|
63
|
+
baseURL: undefined,
|
|
64
|
+
} as AzureOpenAIKeyVault,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}) as unknown as UserStore;
|
|
68
|
+
expect(keyVaultsConfigSelectors.isProviderEndpointNotEmpty('azure')(s)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Always return false for AWSBedrockKeyVault
|
|
73
|
+
describe('AWSBedrockKeyVault', () => {
|
|
74
|
+
it('should return false if provider region is not empty for AWSBedrockKeyVault', () => {
|
|
75
|
+
const s = merge(initialSettingsState, {
|
|
76
|
+
settings: {
|
|
77
|
+
keyVaults: {
|
|
78
|
+
bedrock: {
|
|
79
|
+
region: 'region',
|
|
80
|
+
} as AWSBedrockKeyVault,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}) as unknown as UserStore;
|
|
84
|
+
expect(keyVaultsConfigSelectors.isProviderEndpointNotEmpty('bedrock')(s)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should return false if provider region is empty for AWSBedrockKeyVault', () => {
|
|
88
|
+
const s = merge(initialSettingsState, {
|
|
89
|
+
settings: {
|
|
90
|
+
keyVaults: {
|
|
91
|
+
bedrock: {
|
|
92
|
+
region: undefined,
|
|
93
|
+
} as AWSBedrockKeyVault,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}) as unknown as UserStore;
|
|
97
|
+
expect(keyVaultsConfigSelectors.isProviderEndpointNotEmpty('bedrock')(s)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('isProviderApiKeyNotEmpty', () => {
|
|
103
|
+
describe('OpenAICompatibleKeyVault', () => {
|
|
104
|
+
it('should return true if provider apikey is not empty', () => {
|
|
105
|
+
const s = merge(initialSettingsState, {
|
|
106
|
+
settings: {
|
|
107
|
+
keyVaults: {
|
|
108
|
+
openai: {
|
|
109
|
+
apiKey: 'apikey',
|
|
110
|
+
} as OpenAICompatibleKeyVault,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}) as unknown as UserStore;
|
|
114
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('openai')(s)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return false if provider apikey is empty', () => {
|
|
118
|
+
const s = merge(initialSettingsState, {
|
|
119
|
+
settings: {
|
|
120
|
+
keyVaults: {
|
|
121
|
+
openai: {
|
|
122
|
+
apiKey: undefined,
|
|
123
|
+
} as OpenAICompatibleKeyVault,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}) as unknown as UserStore;
|
|
127
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('openai')(s)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('AzureOpenAIKeyVault', () => {
|
|
132
|
+
it('should return true if provider apikey is not empty', () => {
|
|
133
|
+
const s = merge(initialSettingsState, {
|
|
134
|
+
settings: {
|
|
135
|
+
keyVaults: {
|
|
136
|
+
azure: {
|
|
137
|
+
apiKey: 'apikey',
|
|
138
|
+
} as AzureOpenAIKeyVault,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}) as unknown as UserStore;
|
|
142
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('azure')(s)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return false if provider apikey is empty', () => {
|
|
146
|
+
const s = merge(initialSettingsState, {
|
|
147
|
+
settings: {
|
|
148
|
+
keyVaults: {
|
|
149
|
+
azure: {
|
|
150
|
+
apiKey: undefined,
|
|
151
|
+
} as AzureOpenAIKeyVault,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}) as unknown as UserStore;
|
|
155
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('azure')(s)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('AWSBedrockKeyVault', () => {
|
|
160
|
+
it('should return true if provider accessKeyId is not empty for AWSBedrockKeyVault', () => {
|
|
161
|
+
const s = merge(initialSettingsState, {
|
|
162
|
+
settings: {
|
|
163
|
+
keyVaults: {
|
|
164
|
+
bedrock: {
|
|
165
|
+
accessKeyId: 'accessKeyId',
|
|
166
|
+
} as AWSBedrockKeyVault,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
}) as unknown as UserStore;
|
|
170
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('bedrock')(s)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should return true if provider secretAccessKey is not empty for AWSBedrockKeyVault', () => {
|
|
174
|
+
const s = merge(initialSettingsState, {
|
|
175
|
+
settings: {
|
|
176
|
+
keyVaults: {
|
|
177
|
+
bedrock: {
|
|
178
|
+
secretAccessKey: 'secretAccessKey',
|
|
179
|
+
} as AWSBedrockKeyVault,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}) as unknown as UserStore;
|
|
183
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('bedrock')(s)).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return false if provider accessKeyId and secretAccessKey are both empty for AWSBedrockKeyVault', () => {
|
|
187
|
+
const s = merge(initialSettingsState, {
|
|
188
|
+
settings: {
|
|
189
|
+
keyVaults: {
|
|
190
|
+
bedrock: {
|
|
191
|
+
accessKeyId: undefined,
|
|
192
|
+
secretAccessKey: undefined,
|
|
193
|
+
} as AWSBedrockKeyVault,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}) as unknown as UserStore;
|
|
197
|
+
expect(keyVaultsConfigSelectors.isProviderApiKeyNotEmpty('bedrock')(s)).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { UserStore } from '@/store/user';
|
|
2
2
|
import {
|
|
3
|
+
AWSBedrockKeyVault,
|
|
4
|
+
AzureOpenAIKeyVault,
|
|
3
5
|
GlobalLLMProviderKey,
|
|
4
6
|
OpenAICompatibleKeyVault,
|
|
5
7
|
UserKeyVaults,
|
|
@@ -15,10 +17,19 @@ const bedrockConfig = (s: UserStore) => keyVaultsSettings(s).bedrock || {};
|
|
|
15
17
|
const ollamaConfig = (s: UserStore) => keyVaultsSettings(s).ollama || {};
|
|
16
18
|
const azureConfig = (s: UserStore) => keyVaultsSettings(s).azure || {};
|
|
17
19
|
const getVaultByProvider = (provider: GlobalLLMProviderKey) => (s: UserStore) =>
|
|
18
|
-
(keyVaultsSettings(s)[provider] || {}) as OpenAICompatibleKeyVault
|
|
20
|
+
(keyVaultsSettings(s)[provider] || {}) as OpenAICompatibleKeyVault &
|
|
21
|
+
AzureOpenAIKeyVault &
|
|
22
|
+
AWSBedrockKeyVault;
|
|
19
23
|
|
|
20
|
-
const isProviderEndpointNotEmpty = (provider: string) => (s: UserStore) =>
|
|
21
|
-
|
|
24
|
+
const isProviderEndpointNotEmpty = (provider: string) => (s: UserStore) => {
|
|
25
|
+
const vault = getVaultByProvider(provider as GlobalLLMProviderKey)(s);
|
|
26
|
+
return !!vault?.baseURL || !!vault?.endpoint;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const isProviderApiKeyNotEmpty = (provider: string) => (s: UserStore) => {
|
|
30
|
+
const vault = getVaultByProvider(provider as GlobalLLMProviderKey)(s);
|
|
31
|
+
return !!vault?.apiKey || !!vault?.accessKeyId || !!vault?.secretAccessKey;
|
|
32
|
+
};
|
|
22
33
|
|
|
23
34
|
const password = (s: UserStore) => keyVaultsSettings(s).password || '';
|
|
24
35
|
|
|
@@ -26,6 +37,7 @@ export const keyVaultsConfigSelectors = {
|
|
|
26
37
|
azureConfig,
|
|
27
38
|
bedrockConfig,
|
|
28
39
|
getVaultByProvider,
|
|
40
|
+
isProviderApiKeyNotEmpty,
|
|
29
41
|
isProviderEndpointNotEmpty,
|
|
30
42
|
ollamaConfig,
|
|
31
43
|
openAIConfig,
|
|
@@ -35,6 +35,7 @@ describe('modelConfigSelectors', () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
describe('isProviderFetchOnClient', () => {
|
|
38
|
+
// The next 4 case are base on the rules on https://github.com/lobehub/lobe-chat/pull/2753
|
|
38
39
|
it('client fetch should disabled on default', () => {
|
|
39
40
|
const s = merge(initialSettingsState, {
|
|
40
41
|
settings: {
|
|
@@ -46,16 +47,43 @@ describe('modelConfigSelectors', () => {
|
|
|
46
47
|
},
|
|
47
48
|
},
|
|
48
49
|
} as UserSettingsState) as unknown as UserStore;
|
|
50
|
+
expect(modelConfigSelectors.isProviderFetchOnClient('azure')(s)).toBe(false);
|
|
51
|
+
});
|
|
49
52
|
|
|
53
|
+
it('client fetch should disabled if no apikey or endpoint provided even user set it enabled', () => {
|
|
54
|
+
const s = merge(initialSettingsState, {
|
|
55
|
+
settings: {
|
|
56
|
+
languageModel: {
|
|
57
|
+
azure: { fetchOnClient: true },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
} as UserSettingsState) as unknown as UserStore;
|
|
50
61
|
expect(modelConfigSelectors.isProviderFetchOnClient('azure')(s)).toBe(false);
|
|
51
62
|
});
|
|
52
63
|
|
|
53
|
-
it('client fetch should
|
|
64
|
+
it('client fetch should enable if only endpoint provided', () => {
|
|
65
|
+
const s = merge(initialSettingsState, {
|
|
66
|
+
settings: {
|
|
67
|
+
languageModel: {
|
|
68
|
+
azure: { fetchOnClient: false },
|
|
69
|
+
},
|
|
70
|
+
keyVaults: {
|
|
71
|
+
azure: { endpoint: 'https://example.com' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
} as UserSettingsState) as unknown as UserStore;
|
|
75
|
+
expect(modelConfigSelectors.isProviderFetchOnClient('azure')(s)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('client fetch should control by user when a apikey or endpoint provided', () => {
|
|
54
79
|
const s = merge(initialSettingsState, {
|
|
55
80
|
settings: {
|
|
56
81
|
languageModel: {
|
|
57
82
|
azure: { fetchOnClient: true },
|
|
58
83
|
},
|
|
84
|
+
keyVaults: {
|
|
85
|
+
azure: { apiKey: 'some-key' },
|
|
86
|
+
},
|
|
59
87
|
},
|
|
60
88
|
} as UserSettingsState) as unknown as UserStore;
|
|
61
89
|
expect(modelConfigSelectors.isProviderFetchOnClient('azure')(s)).toBe(true);
|
|
@@ -1,15 +1,35 @@
|
|
|
1
|
+
import { UserStore } from '@/store/user';
|
|
1
2
|
import { GlobalLLMProviderKey } from '@/types/user/settings';
|
|
2
3
|
|
|
3
|
-
import { UserStore } from '../../../store';
|
|
4
4
|
import { currentLLMSettings, getProviderConfigById } from '../../settings/selectors/settings';
|
|
5
|
+
import { keyVaultsConfigSelectors } from './keyVaults';
|
|
5
6
|
|
|
6
7
|
const isProviderEnabled = (provider: GlobalLLMProviderKey) => (s: UserStore) =>
|
|
7
8
|
getProviderConfigById(provider)(s)?.enabled || false;
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @description The conditions to enable client fetch
|
|
12
|
+
* 1. If no baseUrl and apikey input, force on Server.
|
|
13
|
+
* 2. If only contains baseUrl, force on Client
|
|
14
|
+
* 3. Follow the user settings.
|
|
15
|
+
* 4. On Server, by default.
|
|
16
|
+
*/
|
|
9
17
|
const isProviderFetchOnClient = (provider: GlobalLLMProviderKey | string) => (s: UserStore) => {
|
|
10
18
|
const config = getProviderConfigById(provider)(s);
|
|
19
|
+
|
|
20
|
+
// 1. If no baseUrl and apikey input, force on Server.
|
|
21
|
+
const isProviderEndpointNotEmpty =
|
|
22
|
+
keyVaultsConfigSelectors.isProviderEndpointNotEmpty(provider)(s);
|
|
23
|
+
const isProviderApiKeyNotEmpty = keyVaultsConfigSelectors.isProviderApiKeyNotEmpty(provider)(s);
|
|
24
|
+
if (!isProviderEndpointNotEmpty && !isProviderApiKeyNotEmpty) return false;
|
|
25
|
+
|
|
26
|
+
// 2. If only contains baseUrl, force on Client
|
|
27
|
+
if (isProviderEndpointNotEmpty && !isProviderApiKeyNotEmpty) return true;
|
|
28
|
+
|
|
29
|
+
// 3. Follow the user settings.
|
|
11
30
|
if (typeof config?.fetchOnClient !== 'undefined') return config?.fetchOnClient;
|
|
12
31
|
|
|
32
|
+
// 4. On Server, by default.
|
|
13
33
|
return false;
|
|
14
34
|
};
|
|
15
35
|
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Icon, List } from '@lobehub/ui';
|
|
2
|
-
import { createStyles, useResponsive } from 'antd-style';
|
|
3
|
-
import { ChevronRight, type LucideIcon } from 'lucide-react';
|
|
4
|
-
import { CSSProperties, ReactNode, memo } from 'react';
|
|
5
|
-
|
|
6
|
-
const { Item } = List;
|
|
7
|
-
|
|
8
|
-
const useStyles = createStyles(({ css, token, responsive }) => ({
|
|
9
|
-
container: css`
|
|
10
|
-
position: relative;
|
|
11
|
-
padding-top: 16px;
|
|
12
|
-
padding-bottom: 16px;
|
|
13
|
-
border-radius: ${token.borderRadius}px;
|
|
14
|
-
${responsive.mobile} {
|
|
15
|
-
border-radius: 0;
|
|
16
|
-
}
|
|
17
|
-
`,
|
|
18
|
-
noHover: css`
|
|
19
|
-
pointer-events: none;
|
|
20
|
-
`,
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
export interface ItemProps {
|
|
24
|
-
active?: boolean;
|
|
25
|
-
className?: string;
|
|
26
|
-
hoverable?: boolean;
|
|
27
|
-
icon: LucideIcon;
|
|
28
|
-
label: ReactNode;
|
|
29
|
-
style?: CSSProperties;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const SettingItem = memo<ItemProps>(
|
|
33
|
-
({ label, icon, hoverable = true, active = false, style, className }) => {
|
|
34
|
-
const { cx, styles } = useStyles();
|
|
35
|
-
const { mobile } = useResponsive();
|
|
36
|
-
return (
|
|
37
|
-
<Item
|
|
38
|
-
active={active}
|
|
39
|
-
avatar={<Icon icon={icon} size={{ fontSize: 20 }} />}
|
|
40
|
-
className={cx(styles.container, !hoverable && styles.noHover, className)}
|
|
41
|
-
style={style}
|
|
42
|
-
title={label as string}
|
|
43
|
-
>
|
|
44
|
-
{mobile && <Icon icon={ChevronRight} size={{ fontSize: 16 }} />}
|
|
45
|
-
</Item>
|
|
46
|
-
);
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
export default SettingItem;
|