@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/auth.json +10 -1
  4. package/locales/ar/models.json +3 -0
  5. package/locales/bg-BG/auth.json +10 -1
  6. package/locales/bg-BG/models.json +3 -0
  7. package/locales/de-DE/auth.json +10 -1
  8. package/locales/de-DE/models.json +3 -0
  9. package/locales/en-US/auth.json +10 -1
  10. package/locales/en-US/models.json +3 -0
  11. package/locales/es-ES/auth.json +10 -1
  12. package/locales/es-ES/models.json +3 -0
  13. package/locales/fa-IR/auth.json +10 -1
  14. package/locales/fa-IR/models.json +3 -0
  15. package/locales/fr-FR/auth.json +10 -1
  16. package/locales/fr-FR/models.json +3 -0
  17. package/locales/it-IT/auth.json +10 -1
  18. package/locales/it-IT/models.json +3 -0
  19. package/locales/ja-JP/auth.json +10 -1
  20. package/locales/ja-JP/models.json +3 -0
  21. package/locales/ko-KR/auth.json +10 -1
  22. package/locales/ko-KR/models.json +3 -0
  23. package/locales/nl-NL/auth.json +10 -1
  24. package/locales/nl-NL/models.json +3 -0
  25. package/locales/pl-PL/auth.json +10 -1
  26. package/locales/pl-PL/models.json +3 -0
  27. package/locales/pt-BR/auth.json +10 -1
  28. package/locales/pt-BR/models.json +3 -0
  29. package/locales/ru-RU/auth.json +10 -1
  30. package/locales/ru-RU/models.json +3 -0
  31. package/locales/tr-TR/auth.json +10 -1
  32. package/locales/tr-TR/models.json +3 -0
  33. package/locales/vi-VN/auth.json +10 -1
  34. package/locales/vi-VN/models.json +3 -0
  35. package/locales/zh-CN/auth.json +9 -0
  36. package/locales/zh-CN/models.json +3 -0
  37. package/locales/zh-TW/auth.json +10 -1
  38. package/locales/zh-TW/models.json +3 -0
  39. package/package.json +1 -1
  40. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +9 -0
  41. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/AuthIcons.tsx +37 -0
  42. package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +93 -0
  43. package/src/config/aiModels/perplexity.ts +16 -6
  44. package/src/config/modelProviders/perplexity.ts +9 -6
  45. package/src/database/server/models/user.ts +24 -1
  46. package/src/libs/agent-runtime/perplexity/index.test.ts +10 -222
  47. package/src/locales/default/auth.ts +10 -0
  48. package/src/server/routers/lambda/user.ts +32 -2
  49. package/src/services/user/_deprecated.ts +9 -0
  50. package/src/services/user/client.ts +9 -0
  51. package/src/services/user/server.ts +11 -0
  52. package/src/services/user/type.ts +3 -0
  53. 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
  },
@@ -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",
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
- ' DeepSeek 推理模型提供支持的新 API 产品。',
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 { NewUser, UserItem, UserSettingsItem, userSettings, users } from '../../schemas';
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 OpenAI from 'openai';
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 { ChatStreamCallbacks, LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime';
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
- const provider = 'perplexity';
11
- const defaultBaseURL = 'https://api.perplexity.ai';
12
- const bizErrorType = 'ProviderBizError';
13
- const invalidErrorType = 'InvalidProviderAPIKey';
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 { UserGuideSchema, UserInitializationState, UserPreference } from '@/types/user';
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: { userModel: new UserModel(serverDB, opts.ctx.userId) },
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
  }),