@lobehub/chat 1.61.3 → 1.61.5
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 +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/auth.json +10 -1
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/auth.json +10 -1
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/auth.json +10 -1
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/auth.json +10 -1
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/auth.json +10 -1
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/auth.json +10 -1
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/auth.json +10 -1
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/auth.json +10 -1
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/auth.json +10 -1
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/auth.json +10 -1
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/auth.json +10 -1
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/auth.json +10 -1
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/auth.json +10 -1
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/auth.json +10 -1
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/auth.json +10 -1
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/auth.json +10 -1
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/auth.json +9 -0
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/auth.json +10 -1
- package/locales/zh-TW/models.json +3 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/profile/(home)/Client.tsx +9 -0
- package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/AuthIcons.tsx +37 -0
- package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +93 -0
- package/src/config/aiModels/perplexity.ts +16 -6
- package/src/config/modelProviders/perplexity.ts +9 -6
- package/src/database/server/models/user.ts +24 -1
- package/src/libs/agent-runtime/perplexity/index.test.ts +10 -222
- package/src/locales/default/auth.ts +10 -0
- package/src/server/routers/lambda/user.ts +32 -2
- package/src/services/user/_deprecated.ts +9 -0
- package/src/services/user/client.ts +9 -0
- package/src/services/user/server.ts +11 -0
- package/src/services/user/type.ts +3 -0
- package/src/types/user/index.ts +5 -0
@@ -1577,6 +1577,9 @@
|
|
1577
1577
|
"sonar-reasoning": {
|
1578
1578
|
"description": "由 DeepSeek 推理模型提供支持的新 API 产品。"
|
1579
1579
|
},
|
1580
|
+
"sonar-reasoning-pro": {
|
1581
|
+
"description": "由 DeepSeek 推理模型提供支持的新 API 产品。"
|
1582
|
+
},
|
1580
1583
|
"step-1-128k": {
|
1581
1584
|
"description": "平衡性能与成本,适合一般场景。"
|
1582
1585
|
},
|
package/locales/zh-TW/auth.json
CHANGED
@@ -34,6 +34,15 @@
|
|
34
34
|
"profile": {
|
35
35
|
"avatar": "頭像",
|
36
36
|
"email": "電子郵件地址",
|
37
|
+
"sso": {
|
38
|
+
"loading": "正在載入已綁定的第三方帳戶",
|
39
|
+
"providers": "連結的帳戶",
|
40
|
+
"unlink": {
|
41
|
+
"description": "解除綁定後,您將無法使用 {{provider}} 帳戶「{{providerAccountId}}」登入。如果您需要重新綁定 {{provider}} 帳戶到當前帳戶,請確保 {{provider}} 帳戶的電子郵件地址為 {{email}},我們會在登入時為您自動綁定到當前登入帳戶。",
|
42
|
+
"forbidden": "您至少需要保留一個第三方帳戶綁定。",
|
43
|
+
"title": "是否解除綁定該第三方帳戶 {{provider}} ?"
|
44
|
+
}
|
45
|
+
},
|
37
46
|
"username": "用戶名"
|
38
47
|
},
|
39
48
|
"signout": "登出",
|
@@ -84,4 +93,4 @@
|
|
84
93
|
"security": "安全",
|
85
94
|
"stats": "數據統計"
|
86
95
|
}
|
87
|
-
}
|
96
|
+
}
|
@@ -1577,6 +1577,9 @@
|
|
1577
1577
|
"sonar-reasoning": {
|
1578
1578
|
"description": "由 DeepSeek 推理模型提供支持的新 API 產品。"
|
1579
1579
|
},
|
1580
|
+
"sonar-reasoning-pro": {
|
1581
|
+
"description": "由 DeepSeek 推理模型提供支援的新 API 產品。"
|
1582
|
+
},
|
1580
1583
|
"step-1-128k": {
|
1581
1584
|
"description": "平衡性能與成本,適合一般場景。"
|
1582
1585
|
},
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.61.
|
3
|
+
"version": "1.61.5",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -11,6 +11,8 @@ import UserAvatar from '@/features/User/UserAvatar';
|
|
11
11
|
import { useUserStore } from '@/store/user';
|
12
12
|
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
|
13
13
|
|
14
|
+
import SSOProvidersList from './features/SSOProvidersList';
|
15
|
+
|
14
16
|
type SettingItemGroup = ItemGroup;
|
15
17
|
|
16
18
|
const Client = memo<{ mobile?: boolean }>(() => {
|
@@ -42,6 +44,13 @@ const Client = memo<{ mobile?: boolean }>(() => {
|
|
42
44
|
label: t('profile.email'),
|
43
45
|
minWidth: undefined,
|
44
46
|
},
|
47
|
+
{
|
48
|
+
children: <SSOProvidersList />,
|
49
|
+
hidden: !isLoginWithNextAuth,
|
50
|
+
label: t('profile.sso.providers'),
|
51
|
+
layout: 'vertical',
|
52
|
+
minWidth: undefined,
|
53
|
+
},
|
45
54
|
],
|
46
55
|
title: t('tab.profile'),
|
47
56
|
};
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import {
|
2
|
+
Auth0,
|
3
|
+
Authelia,
|
4
|
+
Authentik,
|
5
|
+
Casdoor,
|
6
|
+
Cloudflare,
|
7
|
+
Github,
|
8
|
+
Logto,
|
9
|
+
MicrosoftEntra,
|
10
|
+
NextAuth,
|
11
|
+
Zitadel,
|
12
|
+
} from '@lobehub/ui/icons';
|
13
|
+
import React from 'react';
|
14
|
+
|
15
|
+
const iconProps = {
|
16
|
+
size: 32,
|
17
|
+
};
|
18
|
+
|
19
|
+
const iconComponents: { [key: string]: React.ElementType } = {
|
20
|
+
'auth0': Auth0,
|
21
|
+
'authelia': Authelia.Color,
|
22
|
+
'authentik': Authentik.Color,
|
23
|
+
'casdoor': Casdoor.Color,
|
24
|
+
'cloudflare': Cloudflare.Color,
|
25
|
+
'default': NextAuth.Color,
|
26
|
+
'github': Github,
|
27
|
+
'logto': Logto.Color,
|
28
|
+
'microsoft-entra-id': MicrosoftEntra.Color,
|
29
|
+
'zitadel': Zitadel.Color,
|
30
|
+
};
|
31
|
+
|
32
|
+
const AuthIcons = (id: string) => {
|
33
|
+
const IconComponent = iconComponents[id] || iconComponents.default;
|
34
|
+
return <IconComponent {...iconProps} />;
|
35
|
+
};
|
36
|
+
|
37
|
+
export default AuthIcons;
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { ActionIcon, CopyButton, List } from '@lobehub/ui';
|
2
|
+
import { RotateCw, Unlink } from 'lucide-react';
|
3
|
+
import { CSSProperties, memo, useState } from 'react';
|
4
|
+
import { useTranslation } from 'react-i18next';
|
5
|
+
import { Flexbox } from 'react-layout-kit';
|
6
|
+
|
7
|
+
import { modal, notification } from '@/components/AntdStaticMethods';
|
8
|
+
import { useOnlyFetchOnceSWR } from '@/libs/swr';
|
9
|
+
import { userService } from '@/services/user';
|
10
|
+
import { useUserStore } from '@/store/user';
|
11
|
+
import { userProfileSelectors } from '@/store/user/selectors';
|
12
|
+
|
13
|
+
import AuthIcons from './AuthIcons';
|
14
|
+
|
15
|
+
const { Item } = List;
|
16
|
+
|
17
|
+
const providerNameStyle: CSSProperties = {
|
18
|
+
textTransform: 'capitalize',
|
19
|
+
};
|
20
|
+
|
21
|
+
export const SSOProvidersList = memo(() => {
|
22
|
+
const [userProfile] = useUserStore((s) => [userProfileSelectors.userProfile(s)]);
|
23
|
+
const { t } = useTranslation('auth');
|
24
|
+
|
25
|
+
const [allowUnlink, setAllowUnlink] = useState<boolean>(false);
|
26
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
27
|
+
|
28
|
+
const { data, isLoading, mutate } = useOnlyFetchOnceSWR('profile-sso-providers', async () => {
|
29
|
+
const list = await userService.getUserSSOProviders();
|
30
|
+
setAllowUnlink(list?.length > 1);
|
31
|
+
return list;
|
32
|
+
});
|
33
|
+
|
34
|
+
const handleUnlinkSSO = async (provider: string, providerAccountId: string) => {
|
35
|
+
if (data?.length === 1 || !data) {
|
36
|
+
// At least one SSO provider should be linked
|
37
|
+
notification.error({
|
38
|
+
message: t('profile.sso.unlink.forbidden'),
|
39
|
+
});
|
40
|
+
return;
|
41
|
+
}
|
42
|
+
modal.confirm({
|
43
|
+
content: t('profile.sso.unlink.description', {
|
44
|
+
email: userProfile?.email || 'None',
|
45
|
+
provider,
|
46
|
+
providerAccountId,
|
47
|
+
}),
|
48
|
+
okButtonProps: {
|
49
|
+
danger: true,
|
50
|
+
},
|
51
|
+
onOk: async () => {
|
52
|
+
await userService.unlinkSSOProvider(provider, providerAccountId);
|
53
|
+
mutate();
|
54
|
+
},
|
55
|
+
title: <span style={providerNameStyle}>{t('profile.sso.unlink.title', { provider })}</span>,
|
56
|
+
});
|
57
|
+
};
|
58
|
+
|
59
|
+
return isLoading ? (
|
60
|
+
<Flexbox align={'center'} gap={4} horizontal>
|
61
|
+
<ActionIcon icon={RotateCw} spin />
|
62
|
+
{t('profile.sso.loading')}
|
63
|
+
</Flexbox>
|
64
|
+
) : (
|
65
|
+
<Flexbox>
|
66
|
+
{data?.map((item, index) => (
|
67
|
+
<Item
|
68
|
+
actions={
|
69
|
+
<Flexbox gap={4} horizontal>
|
70
|
+
<CopyButton content={item.providerAccountId} size={'small'} />
|
71
|
+
<ActionIcon
|
72
|
+
disable={!allowUnlink}
|
73
|
+
icon={Unlink}
|
74
|
+
onClick={() => handleUnlinkSSO(item.provider, item.providerAccountId)}
|
75
|
+
size={'small'}
|
76
|
+
/>
|
77
|
+
</Flexbox>
|
78
|
+
}
|
79
|
+
avatar={AuthIcons(item.provider)}
|
80
|
+
date={item.expires_at}
|
81
|
+
description={item.providerAccountId}
|
82
|
+
key={index}
|
83
|
+
onMouseEnter={() => setHoveredIndex(index)}
|
84
|
+
onMouseLeave={() => setHoveredIndex(null)}
|
85
|
+
showAction={hoveredIndex === index}
|
86
|
+
title={<span style={providerNameStyle}>{item.provider}</span>}
|
87
|
+
/>
|
88
|
+
))}
|
89
|
+
</Flexbox>
|
90
|
+
);
|
91
|
+
});
|
92
|
+
|
93
|
+
export default SSOProvidersList;
|
@@ -6,17 +6,28 @@ const perplexityChatModels: AIChatModelCard[] = [
|
|
6
6
|
reasoning: true,
|
7
7
|
},
|
8
8
|
contextWindowTokens: 127_072,
|
9
|
-
description:
|
10
|
-
|
9
|
+
description: '由 DeepSeek 推理模型提供支持的新 API 产品。',
|
10
|
+
displayName: 'Sonar Reasoning Pro',
|
11
|
+
enabled: true,
|
12
|
+
id: 'sonar-reasoning-pro',
|
13
|
+
maxOutput: 8192,
|
14
|
+
type: 'chat',
|
15
|
+
},
|
16
|
+
{
|
17
|
+
abilities: {
|
18
|
+
reasoning: true,
|
19
|
+
},
|
20
|
+
contextWindowTokens: 127_072,
|
21
|
+
description: '由 DeepSeek 推理模型提供支持的新 API 产品。',
|
11
22
|
displayName: 'Sonar Reasoning',
|
12
23
|
enabled: true,
|
13
24
|
id: 'sonar-reasoning',
|
25
|
+
maxOutput: 8192,
|
14
26
|
type: 'chat',
|
15
27
|
},
|
16
28
|
{
|
17
29
|
contextWindowTokens: 200_000,
|
18
|
-
description:
|
19
|
-
'支持搜索上下文的高级搜索产品,支持高级查询和跟进。',
|
30
|
+
description: '支持搜索上下文的高级搜索产品,支持高级查询和跟进。',
|
20
31
|
displayName: 'Sonar Pro',
|
21
32
|
enabled: true,
|
22
33
|
id: 'sonar-pro',
|
@@ -24,8 +35,7 @@ const perplexityChatModels: AIChatModelCard[] = [
|
|
24
35
|
},
|
25
36
|
{
|
26
37
|
contextWindowTokens: 127_072,
|
27
|
-
description:
|
28
|
-
'基于搜索上下文的轻量级搜索产品,比 Sonar Pro 更快、更便宜。',
|
38
|
+
description: '基于搜索上下文的轻量级搜索产品,比 Sonar Pro 更快、更便宜。',
|
29
39
|
displayName: 'Sonar',
|
30
40
|
enabled: true,
|
31
41
|
id: 'sonar',
|
@@ -5,24 +5,21 @@ const Perplexity: ModelProviderCard = {
|
|
5
5
|
chatModels: [
|
6
6
|
{
|
7
7
|
contextWindowTokens: 127_072,
|
8
|
-
description:
|
9
|
-
'由 DeepSeek 推理模型提供支持的新 API 产品。',
|
8
|
+
description: '由 DeepSeek 推理模型提供支持的新 API 产品。',
|
10
9
|
displayName: 'Sonar Reasoning',
|
11
10
|
enabled: true,
|
12
11
|
id: 'sonar-reasoning',
|
13
12
|
},
|
14
13
|
{
|
15
14
|
contextWindowTokens: 200_000,
|
16
|
-
description:
|
17
|
-
'支持搜索上下文的高级搜索产品,支持高级查询和跟进。',
|
15
|
+
description: '支持搜索上下文的高级搜索产品,支持高级查询和跟进。',
|
18
16
|
displayName: 'Sonar Pro',
|
19
17
|
enabled: true,
|
20
18
|
id: 'sonar-pro',
|
21
19
|
},
|
22
20
|
{
|
23
21
|
contextWindowTokens: 127_072,
|
24
|
-
description:
|
25
|
-
'基于搜索上下文的轻量级搜索产品,比 Sonar Pro 更快、更便宜。',
|
22
|
+
description: '基于搜索上下文的轻量级搜索产品,比 Sonar Pro 更快、更便宜。',
|
26
23
|
displayName: 'Sonar',
|
27
24
|
enabled: true,
|
28
25
|
id: 'sonar',
|
@@ -60,10 +57,16 @@ const Perplexity: ModelProviderCard = {
|
|
60
57
|
placeholder: 'https://api.perplexity.ai',
|
61
58
|
},
|
62
59
|
settings: {
|
60
|
+
// perplexity doesn't support CORS
|
61
|
+
disableBrowserRequest: true,
|
63
62
|
proxyUrl: {
|
64
63
|
placeholder: 'https://api.perplexity.ai',
|
65
64
|
},
|
66
65
|
sdkType: 'openai',
|
66
|
+
smoothing: {
|
67
|
+
speed: 2,
|
68
|
+
text: true,
|
69
|
+
},
|
67
70
|
},
|
68
71
|
url: 'https://www.perplexity.ai',
|
69
72
|
};
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
2
2
|
import dayjs from 'dayjs';
|
3
3
|
import { eq } from 'drizzle-orm/expressions';
|
4
|
+
import type { AdapterAccount } from 'next-auth/adapters';
|
4
5
|
import { DeepPartial } from 'utility-types';
|
5
6
|
|
6
7
|
import { LobeChatDatabase } from '@/database/type';
|
@@ -9,7 +10,14 @@ import { UserKeyVaults, UserSettings } from '@/types/user/settings';
|
|
9
10
|
import { merge } from '@/utils/merge';
|
10
11
|
import { today } from '@/utils/time';
|
11
12
|
|
12
|
-
import {
|
13
|
+
import {
|
14
|
+
NewUser,
|
15
|
+
UserItem,
|
16
|
+
UserSettingsItem,
|
17
|
+
nextauthAccounts,
|
18
|
+
userSettings,
|
19
|
+
users,
|
20
|
+
} from '../../schemas';
|
13
21
|
|
14
22
|
type DecryptUserKeyVaults = (
|
15
23
|
encryptKeyVaultsStr: string | null,
|
@@ -96,6 +104,21 @@ export class UserModel {
|
|
96
104
|
};
|
97
105
|
};
|
98
106
|
|
107
|
+
getUserSSOProviders = async () => {
|
108
|
+
const result = await this.db
|
109
|
+
.select({
|
110
|
+
expiresAt: nextauthAccounts.expires_at,
|
111
|
+
provider: nextauthAccounts.provider,
|
112
|
+
providerAccountId: nextauthAccounts.providerAccountId,
|
113
|
+
scope: nextauthAccounts.scope,
|
114
|
+
type: nextauthAccounts.type,
|
115
|
+
userId: nextauthAccounts.userId,
|
116
|
+
})
|
117
|
+
.from(nextauthAccounts)
|
118
|
+
.where(eq(nextauthAccounts.userId, this.userId));
|
119
|
+
return result as unknown as AdapterAccount[];
|
120
|
+
};
|
121
|
+
|
99
122
|
getUserSettings = async () => {
|
100
123
|
return this.db.query.userSettings.findFirst({ where: eq(userSettings.id, this.userId) });
|
101
124
|
};
|
@@ -1,16 +1,18 @@
|
|
1
1
|
// @vitest-environment node
|
2
|
-
import
|
3
|
-
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
4
3
|
|
5
|
-
import {
|
4
|
+
import { LobeOpenAICompatibleRuntime, ModelProvider } from '@/libs/agent-runtime';
|
5
|
+
import { testProvider } from '@/libs/agent-runtime/providerTestUtils';
|
6
6
|
|
7
|
-
import * as debugStreamModule from '../utils/debugStream';
|
8
7
|
import { LobePerplexityAI } from './index';
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
testProvider({
|
10
|
+
Runtime: LobePerplexityAI,
|
11
|
+
provider: ModelProvider.Perplexity,
|
12
|
+
defaultBaseURL: 'https://api.perplexity.ai',
|
13
|
+
chatDebugEnv: 'DEBUG_PERPLEXITY_CHAT_COMPLETION',
|
14
|
+
chatModel: 'sonar',
|
15
|
+
});
|
14
16
|
|
15
17
|
// Mock the console.error to avoid polluting test output
|
16
18
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
@@ -31,221 +33,7 @@ afterEach(() => {
|
|
31
33
|
});
|
32
34
|
|
33
35
|
describe('LobePerplexityAI', () => {
|
34
|
-
describe('init', () => {
|
35
|
-
it('should correctly initialize with an API key', async () => {
|
36
|
-
const instance = new LobePerplexityAI({ apiKey: 'test_api_key' });
|
37
|
-
expect(instance).toBeInstanceOf(LobePerplexityAI);
|
38
|
-
expect(instance.baseURL).toEqual(defaultBaseURL);
|
39
|
-
});
|
40
|
-
});
|
41
|
-
|
42
36
|
describe('chat', () => {
|
43
|
-
describe('Error', () => {
|
44
|
-
it('should return OpenAIBizError with an openai error response when OpenAI.APIError is thrown', async () => {
|
45
|
-
// Arrange
|
46
|
-
const apiError = new OpenAI.APIError(
|
47
|
-
400,
|
48
|
-
{
|
49
|
-
status: 400,
|
50
|
-
error: {
|
51
|
-
message: 'Bad Request',
|
52
|
-
},
|
53
|
-
},
|
54
|
-
'Error message',
|
55
|
-
{},
|
56
|
-
);
|
57
|
-
|
58
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
59
|
-
|
60
|
-
// Act
|
61
|
-
try {
|
62
|
-
await instance.chat({
|
63
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
64
|
-
model: 'text-davinci-003',
|
65
|
-
temperature: 0,
|
66
|
-
});
|
67
|
-
} catch (e) {
|
68
|
-
expect(e).toEqual({
|
69
|
-
endpoint: defaultBaseURL,
|
70
|
-
error: {
|
71
|
-
error: { message: 'Bad Request' },
|
72
|
-
status: 400,
|
73
|
-
},
|
74
|
-
errorType: bizErrorType,
|
75
|
-
provider,
|
76
|
-
});
|
77
|
-
}
|
78
|
-
});
|
79
|
-
|
80
|
-
it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => {
|
81
|
-
try {
|
82
|
-
new LobePerplexityAI({});
|
83
|
-
} catch (e) {
|
84
|
-
expect(e).toEqual({ errorType: invalidErrorType });
|
85
|
-
}
|
86
|
-
});
|
87
|
-
|
88
|
-
it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
|
89
|
-
// Arrange
|
90
|
-
const errorInfo = {
|
91
|
-
stack: 'abc',
|
92
|
-
cause: {
|
93
|
-
message: 'api is undefined',
|
94
|
-
},
|
95
|
-
};
|
96
|
-
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
97
|
-
|
98
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
99
|
-
|
100
|
-
// Act
|
101
|
-
try {
|
102
|
-
await instance.chat({
|
103
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
104
|
-
model: 'text-davinci-003',
|
105
|
-
temperature: 0,
|
106
|
-
});
|
107
|
-
} catch (e) {
|
108
|
-
expect(e).toEqual({
|
109
|
-
endpoint: defaultBaseURL,
|
110
|
-
error: {
|
111
|
-
cause: { message: 'api is undefined' },
|
112
|
-
stack: 'abc',
|
113
|
-
},
|
114
|
-
errorType: bizErrorType,
|
115
|
-
provider,
|
116
|
-
});
|
117
|
-
}
|
118
|
-
});
|
119
|
-
|
120
|
-
it('should return OpenAIBizError with an cause response with desensitize Url', async () => {
|
121
|
-
// Arrange
|
122
|
-
const errorInfo = {
|
123
|
-
stack: 'abc',
|
124
|
-
cause: { message: 'api is undefined' },
|
125
|
-
};
|
126
|
-
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
|
127
|
-
|
128
|
-
instance = new LobePerplexityAI({
|
129
|
-
apiKey: 'test',
|
130
|
-
|
131
|
-
baseURL: 'https://api.abc.com/v1',
|
132
|
-
});
|
133
|
-
|
134
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
|
135
|
-
|
136
|
-
// Act
|
137
|
-
try {
|
138
|
-
await instance.chat({
|
139
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
140
|
-
model: 'gpt-3.5-turbo',
|
141
|
-
temperature: 0,
|
142
|
-
});
|
143
|
-
} catch (e) {
|
144
|
-
expect(e).toEqual({
|
145
|
-
endpoint: 'https://api.***.com/v1',
|
146
|
-
error: {
|
147
|
-
cause: { message: 'api is undefined' },
|
148
|
-
stack: 'abc',
|
149
|
-
},
|
150
|
-
errorType: bizErrorType,
|
151
|
-
provider,
|
152
|
-
});
|
153
|
-
}
|
154
|
-
});
|
155
|
-
|
156
|
-
it('should throw an InvalidMoonshotAPIKey error type on 401 status code', async () => {
|
157
|
-
// Mock the API call to simulate a 401 error
|
158
|
-
const error = new Error('Unauthorized') as any;
|
159
|
-
error.status = 401;
|
160
|
-
vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error);
|
161
|
-
|
162
|
-
try {
|
163
|
-
await instance.chat({
|
164
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
165
|
-
model: 'gpt-3.5-turbo',
|
166
|
-
temperature: 0,
|
167
|
-
});
|
168
|
-
} catch (e) {
|
169
|
-
// Expect the chat method to throw an error with InvalidMoonshotAPIKey
|
170
|
-
expect(e).toEqual({
|
171
|
-
endpoint: defaultBaseURL,
|
172
|
-
error: new Error('Unauthorized'),
|
173
|
-
errorType: invalidErrorType,
|
174
|
-
provider,
|
175
|
-
});
|
176
|
-
}
|
177
|
-
});
|
178
|
-
|
179
|
-
it('should return AgentRuntimeError for non-OpenAI errors', async () => {
|
180
|
-
// Arrange
|
181
|
-
const genericError = new Error('Generic Error');
|
182
|
-
|
183
|
-
vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);
|
184
|
-
|
185
|
-
// Act
|
186
|
-
try {
|
187
|
-
await instance.chat({
|
188
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
189
|
-
model: 'text-davinci-003',
|
190
|
-
temperature: 0,
|
191
|
-
});
|
192
|
-
} catch (e) {
|
193
|
-
expect(e).toEqual({
|
194
|
-
endpoint: defaultBaseURL,
|
195
|
-
errorType: 'AgentRuntimeError',
|
196
|
-
provider,
|
197
|
-
error: {
|
198
|
-
name: genericError.name,
|
199
|
-
cause: genericError.cause,
|
200
|
-
message: genericError.message,
|
201
|
-
stack: genericError.stack,
|
202
|
-
},
|
203
|
-
});
|
204
|
-
}
|
205
|
-
});
|
206
|
-
});
|
207
|
-
|
208
|
-
describe('DEBUG', () => {
|
209
|
-
it('should call debugStream and return StreamingTextResponse when DEBUG_PERPLEXITY_CHAT_COMPLETION is 1', async () => {
|
210
|
-
// Arrange
|
211
|
-
const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
|
212
|
-
const mockDebugStream = new ReadableStream({
|
213
|
-
start(controller) {
|
214
|
-
controller.enqueue('Debug stream content');
|
215
|
-
controller.close();
|
216
|
-
},
|
217
|
-
}) as any;
|
218
|
-
mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法
|
219
|
-
|
220
|
-
// 模拟 chat.completions.create 返回值,包括模拟的 tee 方法
|
221
|
-
(instance['client'].chat.completions.create as Mock).mockResolvedValue({
|
222
|
-
tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
|
223
|
-
});
|
224
|
-
|
225
|
-
// 保存原始环境变量值
|
226
|
-
const originalDebugValue = process.env.DEBUG_PERPLEXITY_CHAT_COMPLETION;
|
227
|
-
|
228
|
-
// 模拟环境变量
|
229
|
-
process.env.DEBUG_PERPLEXITY_CHAT_COMPLETION = '1';
|
230
|
-
vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
|
231
|
-
|
232
|
-
// 执行测试
|
233
|
-
// 运行你的测试函数,确保它会在条件满足时调用 debugStream
|
234
|
-
// 假设的测试函数调用,你可能需要根据实际情况调整
|
235
|
-
await instance.chat({
|
236
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
237
|
-
model: 'text-davinci-003',
|
238
|
-
temperature: 0,
|
239
|
-
});
|
240
|
-
|
241
|
-
// 验证 debugStream 被调用
|
242
|
-
expect(debugStreamModule.debugStream).toHaveBeenCalled();
|
243
|
-
|
244
|
-
// 恢复原始环境变量值
|
245
|
-
process.env.DEBUG_PERPLEXITY_CHAT_COMPLETION = originalDebugValue;
|
246
|
-
});
|
247
|
-
});
|
248
|
-
|
249
37
|
it('should call chat method with temperature', async () => {
|
250
38
|
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
251
39
|
new ReadableStream() as any,
|
@@ -34,6 +34,16 @@ export default {
|
|
34
34
|
profile: {
|
35
35
|
avatar: '头像',
|
36
36
|
email: '电子邮件地址',
|
37
|
+
sso: {
|
38
|
+
loading: '正在加载已绑定的第三方账户',
|
39
|
+
providers: '连接的帐户',
|
40
|
+
unlink: {
|
41
|
+
description:
|
42
|
+
'解绑后,您将无法使用 {{provider}} 账户“{{providerAccountId}}”登录。如果您需要重新绑定 {{provider}} 账户到当前账户,请确保 {{provider}} 账户的邮件地址为 {{email}} ,我们会在登陆时为你自动绑定到当前登录账户。',
|
43
|
+
forbidden: '您至少需要保留一个第三方账户绑定。',
|
44
|
+
title: '是否解绑该第三方账户 {{provider}} ?',
|
45
|
+
},
|
46
|
+
},
|
37
47
|
username: '用户名',
|
38
48
|
},
|
39
49
|
signout: '退出登录',
|
@@ -7,15 +7,24 @@ import { serverDB } from '@/database/server';
|
|
7
7
|
import { MessageModel } from '@/database/server/models/message';
|
8
8
|
import { SessionModel } from '@/database/server/models/session';
|
9
9
|
import { UserModel, UserNotFoundError } from '@/database/server/models/user';
|
10
|
+
import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
|
10
11
|
import { authedProcedure, router } from '@/libs/trpc';
|
11
12
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
12
13
|
import { UserService } from '@/server/services/user';
|
13
|
-
import {
|
14
|
+
import {
|
15
|
+
NextAuthAccountSchame,
|
16
|
+
UserGuideSchema,
|
17
|
+
UserInitializationState,
|
18
|
+
UserPreference,
|
19
|
+
} from '@/types/user';
|
14
20
|
import { UserSettings } from '@/types/user/settings';
|
15
21
|
|
16
22
|
const userProcedure = authedProcedure.use(async (opts) => {
|
17
23
|
return opts.next({
|
18
|
-
ctx: {
|
24
|
+
ctx: {
|
25
|
+
nextAuthDbAdapter: LobeNextAuthDbAdapter(serverDB),
|
26
|
+
userModel: new UserModel(serverDB, opts.ctx.userId),
|
27
|
+
},
|
19
28
|
});
|
20
29
|
});
|
21
30
|
|
@@ -24,6 +33,10 @@ export const userRouter = router({
|
|
24
33
|
return ctx.userModel.getUserRegistrationDuration();
|
25
34
|
}),
|
26
35
|
|
36
|
+
getUserSSOProviders: userProcedure.query(async ({ ctx }) => {
|
37
|
+
return ctx.userModel.getUserSSOProviders();
|
38
|
+
}),
|
39
|
+
|
27
40
|
getUserState: userProcedure.query(async ({ ctx }): Promise<UserInitializationState> => {
|
28
41
|
let state: Awaited<ReturnType<UserModel['getUserState']>> | undefined;
|
29
42
|
|
@@ -92,6 +105,23 @@ export const userRouter = router({
|
|
92
105
|
return ctx.userModel.deleteSetting();
|
93
106
|
}),
|
94
107
|
|
108
|
+
unlinkSSOProvider: userProcedure.input(NextAuthAccountSchame).mutation(async ({ ctx, input }) => {
|
109
|
+
const { provider, providerAccountId } = input;
|
110
|
+
if (
|
111
|
+
ctx.nextAuthDbAdapter?.unlinkAccount &&
|
112
|
+
typeof ctx.nextAuthDbAdapter.unlinkAccount === 'function' &&
|
113
|
+
ctx.nextAuthDbAdapter?.getAccount &&
|
114
|
+
typeof ctx.nextAuthDbAdapter.getAccount === 'function'
|
115
|
+
) {
|
116
|
+
const account = await ctx.nextAuthDbAdapter.getAccount(providerAccountId, provider);
|
117
|
+
// The userId can either get from ctx.nextAuth?.id or ctx.userId
|
118
|
+
if (!account || account.userId !== ctx.userId) throw new Error('The account does not exist');
|
119
|
+
await ctx.nextAuthDbAdapter.unlinkAccount({ provider, providerAccountId });
|
120
|
+
} else {
|
121
|
+
throw new Error('The method in LobeNextAuthDbAdapter `unlinkAccount` is not implemented');
|
122
|
+
}
|
123
|
+
}),
|
124
|
+
|
95
125
|
updateGuide: userProcedure.input(UserGuideSchema).mutation(async ({ ctx, input }) => {
|
96
126
|
return ctx.userModel.updateGuide(input);
|
97
127
|
}),
|