@lobehub/chat 1.80.1 → 1.80.3

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 (37) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/next.config.ts +5 -1
  4. package/package.json +1 -1
  5. package/src/app/[variants]/oauth/consent/[uid]/Client.tsx +36 -23
  6. package/src/app/[variants]/oauth/consent/[uid]/page.tsx +2 -0
  7. package/src/config/aiModels/azure.ts +79 -1
  8. package/src/config/aiModels/azureai.ts +181 -0
  9. package/src/config/aiModels/google.ts +36 -2
  10. package/src/config/aiModels/groq.ts +31 -3
  11. package/src/config/aiModels/hunyuan.ts +54 -18
  12. package/src/config/aiModels/moonshot.ts +17 -17
  13. package/src/config/aiModels/novita.ts +25 -30
  14. package/src/config/aiModels/siliconcloud.ts +80 -2
  15. package/src/config/aiModels/stepfun.ts +40 -31
  16. package/src/config/aiModels/tencentcloud.ts +7 -6
  17. package/src/config/aiModels/volcengine.ts +1 -0
  18. package/src/config/aiModels/zhipu.ts +91 -27
  19. package/src/const/settings/knowledge.ts +2 -2
  20. package/src/database/models/user.ts +13 -1
  21. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +18 -11
  22. package/src/libs/oidc-provider/adapter.ts +5 -6
  23. package/src/libs/oidc-provider/config.ts +0 -3
  24. package/src/libs/oidc-provider/provider.ts +1 -0
  25. package/src/libs/trpc/edge/index.ts +0 -4
  26. package/src/libs/trpc/lambda/context.ts +90 -6
  27. package/src/libs/trpc/lambda/index.ts +2 -1
  28. package/src/libs/trpc/lambda/middleware/oidcAuth.ts +14 -0
  29. package/src/libs/trpc/middleware/userAuth.ts +2 -4
  30. package/src/server/routers/lambda/user.ts +9 -2
  31. package/src/server/services/oidc/index.ts +71 -0
  32. package/src/services/user/client.ts +5 -2
  33. package/src/store/user/slices/common/action.ts +9 -2
  34. package/src/types/user/index.ts +6 -1
  35. package/src/utils/parseModels.test.ts +19 -3
  36. package/src/utils/server/__tests__/auth.test.ts +45 -1
  37. package/src/utils/server/auth.ts +26 -2
@@ -1,3 +1,4 @@
1
+ import { TRPCError } from '@trpc/server';
1
2
  import debug from 'debug';
2
3
 
3
4
  import { createContextForInteractionDetails } from '@/libs/oidc-provider/http-adapter';
@@ -19,6 +20,76 @@ export class OIDCService {
19
20
  return new OIDCService(provider);
20
21
  }
21
22
 
23
+ /**
24
+ * 验证 OIDC Bearer Token 并返回用户信息
25
+ * 使用 oidc-provider 实例的 AccessToken.find 方法验证 token
26
+ *
27
+ * @param token - Bearer Token
28
+ * @returns 包含用户ID和Token数据的对象
29
+ * @throws 如果token无效或OIDC未启用则抛出 TRPCError
30
+ */
31
+ async validateToken(token: string) {
32
+ try {
33
+ log('Validating access token using AccessToken.find');
34
+
35
+ // 使用 oidc-provider 的 AccessToken 查找和验证方法
36
+ const accessToken = await this.provider.AccessToken.find(token);
37
+
38
+ if (!accessToken) {
39
+ log('Access token not found, expired, or consumed');
40
+ throw new TRPCError({
41
+ code: 'UNAUTHORIZED',
42
+ message: 'Access token 无效、已过期或已被使用',
43
+ });
44
+ }
45
+
46
+ // 从 accessToken 实例中获取必要的数据
47
+ // 注意:accessToken 没有 payload() 方法,而是直接访问其属性
48
+ const userId = accessToken.accountId; // 用户 ID 通常存储在 accountId 属性中
49
+ const clientId = accessToken.clientId;
50
+
51
+ // 如果需要更多的声明信息,可以从 accessToken 的其他属性中获取
52
+ // 例如,scopes、claims、exp 等
53
+ const tokenData = {
54
+ client_id: clientId,
55
+ exp: accessToken.exp,
56
+ iat: accessToken.iat,
57
+ jti: accessToken.jti,
58
+ scope: accessToken.scope,
59
+ // OIDC 标准中,sub 字段表示用户 ID
60
+ sub: userId,
61
+ };
62
+
63
+ if (!userId) {
64
+ log('Access token does not contain user ID (accountId)');
65
+ throw new TRPCError({
66
+ code: 'UNAUTHORIZED',
67
+ message: 'Access token 中未包含用户 ID',
68
+ });
69
+ }
70
+
71
+ log('Access token validated successfully for user: %s', userId);
72
+ return {
73
+ // 包含 token 原始数据,可用于获取更多信息
74
+ accessToken,
75
+ // 构建的 token 数据对象
76
+ tokenData,
77
+ // 用户 ID
78
+ userId,
79
+ };
80
+ } catch (error) {
81
+ if (error instanceof TRPCError) throw error;
82
+
83
+ // AccessToken.find 可能抛出特定错误
84
+ log('Error validating access token with AccessToken.find: %O', error);
85
+ console.error('OIDC 令牌验证错误:', error);
86
+ throw new TRPCError({
87
+ code: 'UNAUTHORIZED',
88
+ message: `OIDC 认证失败: ${(error as Error).message}`,
89
+ });
90
+ }
91
+ }
92
+
22
93
  async getInteractionDetails(uid: string) {
23
94
  const { req, res } = await createContextForInteractionDetails(uid);
24
95
  return this.provider.interactionDetails(req, res);
@@ -39,18 +39,21 @@ export class ClientService extends BaseClientService implements IUserService {
39
39
  encryptKeyVaultsStr ? JSON.parse(encryptKeyVaultsStr) : {},
40
40
  );
41
41
 
42
- const user = await UserModel.findById(clientDB as any, this.userId);
43
42
  const messageCount = await this.messageModel.count();
44
43
  const sessionCount = await this.sessionModel.count();
45
44
 
46
45
  return {
47
46
  ...state,
48
- avatar: user?.avatar as string,
47
+ avatar: state.avatar ?? '',
49
48
  canEnablePWAGuide: messageCount >= 4,
50
49
  canEnableTrace: messageCount >= 4,
50
+ firstName: state.firstName,
51
+ fullName: state.fullName,
51
52
  hasConversation: messageCount > 0 || sessionCount > 0,
52
53
  isOnboard: true,
54
+ lastName: state.lastName,
53
55
  preference: await this.preferenceStorage.getFromLocalStorage(),
56
+ username: state.username,
54
57
  };
55
58
  };
56
59
 
@@ -7,7 +7,7 @@ import { useOnlyFetchOnceSWR } from '@/libs/swr';
7
7
  import { userService } from '@/services/user';
8
8
  import type { UserStore } from '@/store/user';
9
9
  import type { GlobalServerConfig } from '@/types/serverConfig';
10
- import { UserInitializationState } from '@/types/user';
10
+ import { LobeUser, UserInitializationState } from '@/types/user';
11
11
  import type { UserSettings } from '@/types/user/settings';
12
12
  import { merge } from '@/utils/merge';
13
13
  import { setNamespace } from '@/utils/storeDebug';
@@ -91,7 +91,14 @@ export const createCommonSlice: StateCreator<
91
91
  // if there is avatar or userId (from client DB), update it into user
92
92
  const user =
93
93
  data.avatar || data.userId
94
- ? merge(get().user, { avatar: data.avatar, id: data.userId })
94
+ ? merge(get().user, {
95
+ avatar: data.avatar,
96
+ firstName: data.firstName,
97
+ fullName: data.fullName,
98
+ id: data.userId,
99
+ latestName: data.lastName,
100
+ username: data.username,
101
+ } as LobeUser)
95
102
  : get().user;
96
103
 
97
104
  set(
@@ -48,14 +48,19 @@ export interface UserInitializationState {
48
48
  avatar?: string;
49
49
  canEnablePWAGuide?: boolean;
50
50
  canEnableTrace?: boolean;
51
+ email?: string;
52
+ firstName?: string;
53
+ fullName?: string;
51
54
  hasConversation?: boolean;
52
55
  isOnboard?: boolean;
56
+ lastName?: string;
53
57
  preference: UserPreference;
54
58
  settings: DeepPartial<UserSettings>;
55
59
  userId?: string;
60
+ username?: string;
56
61
  }
57
62
 
58
63
  export const NextAuthAccountSchame = z.object({
59
64
  provider: z.string(),
60
65
  providerAccountId: z.string(),
61
- });
66
+ });
@@ -87,7 +87,9 @@ describe('parseModelString', () => {
87
87
  });
88
88
 
89
89
  it('token and image output', () => {
90
- const result = parseModelString('gemini-2.0-flash-exp-image-generation=Gemini 2.0 Flash (Image Generation) Experimental<32768:imageOutput>');
90
+ const result = parseModelString(
91
+ 'gemini-2.0-flash-exp-image-generation=Gemini 2.0 Flash (Image Generation) Experimental<32768:imageOutput>',
92
+ );
91
93
 
92
94
  expect(result.add[0]).toEqual({
93
95
  displayName: 'Gemini 2.0 Flash (Image Generation) Experimental',
@@ -565,7 +567,12 @@ describe('transformToChatModelCards', () => {
565
567
  displayName: 'GPT-4o',
566
568
  enabled: true,
567
569
  id: 'gpt-4o',
568
- pricing: { input: 2.5, output: 10 },
570
+ maxOutput: 4096,
571
+ pricing: {
572
+ cachedInput: 1.25,
573
+ input: 2.5,
574
+ output: 10,
575
+ },
569
576
  providerId: 'azure',
570
577
  releasedAt: '2024-05-13',
571
578
  source: 'builtin',
@@ -582,6 +589,11 @@ describe('transformToChatModelCards', () => {
582
589
  enabled: true,
583
590
  id: 'gpt-4o-mini',
584
591
  maxOutput: 4096,
592
+ pricing: {
593
+ cachedInput: 0.075,
594
+ input: 0.15,
595
+ output: 0.6,
596
+ },
585
597
  type: 'chat',
586
598
  },
587
599
  {
@@ -596,7 +608,11 @@ describe('transformToChatModelCards', () => {
596
608
  source: 'builtin',
597
609
  id: 'o1-mini',
598
610
  maxOutput: 65536,
599
- pricing: { input: 1.1, output: 4.4 },
611
+ pricing: {
612
+ cachedInput: 0.55,
613
+ input: 1.1,
614
+ output: 4.4,
615
+ },
600
616
  releasedAt: '2024-09-12',
601
617
  type: 'chat',
602
618
  },
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { getUserAuth } from '../auth';
3
+ import { extractBearerToken, getUserAuth } from '../auth';
4
4
 
5
5
  // Mock auth constants
6
6
  let mockEnableClerk = false;
@@ -93,3 +93,47 @@ describe('getUserAuth', () => {
93
93
  });
94
94
  });
95
95
  });
96
+
97
+ describe('extractBearerToken', () => {
98
+ it('should return the token when authHeader is valid', () => {
99
+ const token = 'test-token';
100
+ const authHeader = `Bearer ${token}`;
101
+ expect(extractBearerToken(authHeader)).toBe(token);
102
+ });
103
+
104
+ it('should return null when authHeader is missing', () => {
105
+ expect(extractBearerToken()).toBeNull();
106
+ });
107
+
108
+ it('should return null when authHeader is null', () => {
109
+ expect(extractBearerToken(null)).toBeNull();
110
+ });
111
+
112
+ it('should return null when authHeader does not start with "Bearer "', () => {
113
+ const authHeader = 'Invalid format';
114
+ expect(extractBearerToken(authHeader)).toBeNull();
115
+ });
116
+
117
+ it('should return null when authHeader is only "Bearer"', () => {
118
+ const authHeader = 'Bearer';
119
+ expect(extractBearerToken(authHeader)).toBeNull();
120
+ });
121
+
122
+ it('should return null when authHeader is an empty string', () => {
123
+ const authHeader = '';
124
+ expect(extractBearerToken(authHeader)).toBeNull();
125
+ });
126
+
127
+ it('should handle extra spaces correctly', () => {
128
+ const token = 'test-token-with-spaces';
129
+ const authHeaderWithExtraSpaces = ` Bearer ${token} `;
130
+ const authHeaderLeadingSpace = ` Bearer ${token}`;
131
+ const authHeaderTrailingSpace = `Bearer ${token} `;
132
+ const authHeaderMultipleSpacesBetween = `Bearer ${token}`;
133
+
134
+ expect(extractBearerToken(authHeaderWithExtraSpaces)).toBe(token);
135
+ expect(extractBearerToken(authHeaderLeadingSpace)).toBe(token);
136
+ expect(extractBearerToken(authHeaderTrailingSpace)).toBe(token);
137
+ expect(extractBearerToken(authHeaderMultipleSpacesBetween)).toBe(token);
138
+ });
139
+ });
@@ -1,15 +1,17 @@
1
1
  import { enableClerk, enableNextAuth } from '@/const/auth';
2
- import { ClerkAuth } from '@/libs/clerk-auth';
3
- import NextAuthEdge from '@/libs/next-auth/edge';
4
2
 
5
3
  export const getUserAuth = async () => {
6
4
  if (enableClerk) {
5
+ const { ClerkAuth } = await import('@/libs/clerk-auth');
6
+
7
7
  const clerkAuth = new ClerkAuth();
8
8
 
9
9
  return await clerkAuth.getAuth();
10
10
  }
11
11
 
12
12
  if (enableNextAuth) {
13
+ const { default: NextAuthEdge } = await import('@/libs/next-auth/edge');
14
+
13
15
  const session = await NextAuthEdge.auth();
14
16
 
15
17
  const userId = session?.user.id;
@@ -19,3 +21,25 @@ export const getUserAuth = async () => {
19
21
 
20
22
  throw new Error('Auth method is not enabled');
21
23
  };
24
+
25
+ /**
26
+ * 从授权头中提取 Bearer Token
27
+ * @param authHeader - 授权头 (例如 "Bearer xxx")
28
+ * @returns Bearer Token 或 null(如果授权头无效或不存在)
29
+ */
30
+ export const extractBearerToken = (authHeader?: string | null): string | null => {
31
+ if (!authHeader) return null;
32
+
33
+ const trimmedHeader = authHeader.trim(); // Trim leading/trailing spaces
34
+
35
+ // Check if it starts with 'Bearer ' (case-insensitive check might be desired depending on spec)
36
+ if (!trimmedHeader.toLowerCase().startsWith('bearer ')) {
37
+ return null;
38
+ }
39
+
40
+ // Extract the token part after "Bearer " and trim potential spaces around the token itself
41
+ const token = trimmedHeader.slice(7).trim();
42
+
43
+ // Return the token only if it's not an empty string after trimming
44
+ return token || null;
45
+ };