@lobehub/chat 1.36.8 → 1.36.10
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 +58 -0
- package/changelog/v1.json +21 -0
- package/locales/ar/models.json +81 -3
- package/locales/ar/providers.json +3 -0
- package/locales/bg-BG/models.json +81 -3
- package/locales/bg-BG/providers.json +3 -0
- package/locales/de-DE/models.json +81 -3
- package/locales/de-DE/providers.json +3 -0
- package/locales/en-US/models.json +81 -3
- package/locales/en-US/providers.json +3 -0
- package/locales/es-ES/models.json +81 -3
- package/locales/es-ES/providers.json +3 -0
- package/locales/fa-IR/models.json +81 -3
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fr-FR/models.json +81 -3
- package/locales/fr-FR/providers.json +3 -0
- package/locales/it-IT/models.json +81 -3
- package/locales/it-IT/providers.json +3 -0
- package/locales/ja-JP/models.json +81 -3
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ko-KR/models.json +81 -3
- package/locales/ko-KR/providers.json +3 -0
- package/locales/nl-NL/models.json +81 -3
- package/locales/nl-NL/providers.json +3 -0
- package/locales/pl-PL/modelProvider.json +9 -9
- package/locales/pl-PL/models.json +81 -3
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pt-BR/models.json +81 -3
- package/locales/pt-BR/providers.json +3 -0
- package/locales/ru-RU/models.json +81 -3
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/models.json +81 -3
- package/locales/tr-TR/providers.json +3 -0
- package/locales/vi-VN/models.json +81 -3
- package/locales/vi-VN/providers.json +3 -0
- package/locales/zh-CN/models.json +91 -13
- package/locales/zh-CN/providers.json +3 -0
- package/locales/zh-TW/models.json +81 -3
- package/locales/zh-TW/providers.json +3 -0
- package/package.json +1 -1
- package/src/app/(backend)/api/webhooks/clerk/route.ts +18 -3
- package/src/config/modelProviders/azure.ts +0 -8
- package/src/config/modelProviders/higress.ts +0 -23
- package/src/config/modelProviders/openai.ts +0 -23
- package/src/database/server/models/__tests__/nextauth.test.ts +33 -0
- package/src/libs/agent-runtime/openai/__snapshots__/index.test.ts.snap +3 -13
- package/src/libs/next-auth/adapter/index.ts +8 -2
- package/src/server/routers/edge/config/__snapshots__/index.test.ts.snap +2 -9
- package/src/server/routers/edge/config/index.test.ts +1 -1
- package/src/server/services/user/index.test.ts +200 -0
- package/src/server/services/user/index.ts +24 -32
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -10
- package/vitest.config.ts +1 -1
@@ -20,14 +20,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
20
20
|
"id": "gpt-3.5-turbo-16k-0613",
|
21
21
|
},
|
22
22
|
{
|
23
|
-
"description": "最新的 GPT-4 Turbo 模型具备视觉功能。现在,视觉请求可以使用 JSON 模式和函数调用。 GPT-4 Turbo 是一个增强版本,为多模态任务提供成本效益高的支持。它在准确性和效率之间找到平衡,适合需要进行实时交互的应用程序场景。",
|
24
23
|
"id": "gpt-4-1106-vision-preview",
|
25
|
-
"pricing": {
|
26
|
-
"input": 10,
|
27
|
-
"output": 30,
|
28
|
-
},
|
29
|
-
"tokens": 128000,
|
30
|
-
"vision": true,
|
31
24
|
},
|
32
25
|
{
|
33
26
|
"id": "gpt-3.5-turbo-instruct-0914",
|
@@ -93,13 +86,10 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
93
86
|
"tokens": 128000,
|
94
87
|
},
|
95
88
|
{
|
96
|
-
"
|
97
|
-
"
|
89
|
+
"deploymentName": "gpt-4-vision",
|
90
|
+
"description": "GPT-4 视觉预览版,专为图像分析和处理任务设计。",
|
91
|
+
"displayName": "GPT 4 Turbo with Vision Preview",
|
98
92
|
"id": "gpt-4-vision-preview",
|
99
|
-
"pricing": {
|
100
|
-
"input": 10,
|
101
|
-
"output": 30,
|
102
|
-
},
|
103
93
|
"tokens": 128000,
|
104
94
|
"vision": true,
|
105
95
|
},
|
@@ -53,7 +53,10 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
|
|
53
53
|
async createUser(user): Promise<AdapterUser> {
|
54
54
|
const { id, name, email, emailVerified, image, providerAccountId } = user;
|
55
55
|
// return the user if it already exists
|
56
|
-
let existingUser =
|
56
|
+
let existingUser =
|
57
|
+
email && typeof email === 'string' && email.trim()
|
58
|
+
? await UserModel.findByEmail(serverDB, email)
|
59
|
+
: undefined;
|
57
60
|
// If the user is not found by email, try to find by providerAccountId
|
58
61
|
if (!existingUser && providerAccountId) {
|
59
62
|
existingUser = await UserModel.findById(serverDB, providerAccountId);
|
@@ -169,7 +172,10 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
|
|
169
172
|
},
|
170
173
|
|
171
174
|
async getUserByEmail(email): Promise<AdapterUser | null> {
|
172
|
-
const lobeUser =
|
175
|
+
const lobeUser =
|
176
|
+
email && typeof email === 'string' && email.trim()
|
177
|
+
? await UserModel.findByEmail(serverDB, email)
|
178
|
+
: undefined;
|
173
179
|
return lobeUser ? mapLobeUserToAdapterUser(lobeUser) : null;
|
174
180
|
},
|
175
181
|
|
@@ -98,16 +98,9 @@ exports[`configRouter > getGlobalConfig > Model Provider env > OPENAI_MODEL_LIST
|
|
98
98
|
"tokens": 128000,
|
99
99
|
},
|
100
100
|
{
|
101
|
-
"
|
102
|
-
"displayName": "GPT-4 Turbo Vision Preview",
|
101
|
+
"displayName": "gpt-4-vision",
|
103
102
|
"enabled": true,
|
104
|
-
"id": "gpt-4-vision
|
105
|
-
"pricing": {
|
106
|
-
"input": 10,
|
107
|
-
"output": 30,
|
108
|
-
},
|
109
|
-
"tokens": 128000,
|
110
|
-
"vision": true,
|
103
|
+
"id": "gpt-4-vision",
|
111
104
|
},
|
112
105
|
]
|
113
106
|
`;
|
@@ -45,7 +45,7 @@ describe('configRouter', () => {
|
|
45
45
|
|
46
46
|
it('should work correct with gpt-4', async () => {
|
47
47
|
process.env.OPENAI_MODEL_LIST =
|
48
|
-
'-all,+gpt-3.5-turbo-1106,+gpt-3.5-turbo,+gpt-4,+gpt-4-32k,+gpt-4-1106-preview,+gpt-4-vision
|
48
|
+
'-all,+gpt-3.5-turbo-1106,+gpt-3.5-turbo,+gpt-4,+gpt-4-32k,+gpt-4-1106-preview,+gpt-4-vision';
|
49
49
|
|
50
50
|
const response = await router.getGlobalConfig();
|
51
51
|
|
@@ -0,0 +1,200 @@
|
|
1
|
+
import { UserJSON } from '@clerk/backend';
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { UserItem } from '@/database/schemas';
|
5
|
+
import { UserModel } from '@/database/server/models/user';
|
6
|
+
import { pino } from '@/libs/logger';
|
7
|
+
|
8
|
+
import { UserService } from './index';
|
9
|
+
|
10
|
+
// Mock dependencies
|
11
|
+
vi.mock('@/database/server/models/user', () => {
|
12
|
+
const MockUserModel = vi.fn();
|
13
|
+
// @ts-ignore
|
14
|
+
MockUserModel.findById = vi.fn();
|
15
|
+
// @ts-ignore
|
16
|
+
MockUserModel.createUser = vi.fn();
|
17
|
+
// @ts-ignore
|
18
|
+
MockUserModel.deleteUser = vi.fn();
|
19
|
+
|
20
|
+
// Mock instance methods
|
21
|
+
MockUserModel.prototype.updateUser = vi.fn();
|
22
|
+
|
23
|
+
return { UserModel: MockUserModel };
|
24
|
+
});
|
25
|
+
|
26
|
+
vi.mock('@/libs/logger', () => ({
|
27
|
+
pino: {
|
28
|
+
info: vi.fn(),
|
29
|
+
},
|
30
|
+
}));
|
31
|
+
|
32
|
+
let service: UserService;
|
33
|
+
const mockUserId = 'test-user-id';
|
34
|
+
|
35
|
+
// Mock user data
|
36
|
+
const mockUserJSON: UserJSON = {
|
37
|
+
id: mockUserId,
|
38
|
+
email_addresses: [{ id: 'email-1', email_address: 'test@example.com' }],
|
39
|
+
phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
|
40
|
+
primary_email_address_id: 'email-1',
|
41
|
+
primary_phone_number_id: 'phone-1',
|
42
|
+
image_url: 'https://example.com/avatar.jpg',
|
43
|
+
first_name: 'Test',
|
44
|
+
last_name: 'User',
|
45
|
+
username: 'testuser',
|
46
|
+
created_at: '2023-01-01T00:00:00Z',
|
47
|
+
} as unknown as UserJSON;
|
48
|
+
|
49
|
+
beforeEach(() => {
|
50
|
+
service = new UserService();
|
51
|
+
vi.clearAllMocks();
|
52
|
+
});
|
53
|
+
|
54
|
+
describe('UserService', () => {
|
55
|
+
describe('createUser', () => {
|
56
|
+
it('should create a new user when user does not exist', async () => {
|
57
|
+
// Mock user not found
|
58
|
+
vi.mocked(UserModel.findById).mockResolvedValue(null as any);
|
59
|
+
|
60
|
+
await service.createUser(mockUserId, mockUserJSON);
|
61
|
+
|
62
|
+
expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
63
|
+
expect(UserModel.createUser).toHaveBeenCalledWith(
|
64
|
+
expect.anything(),
|
65
|
+
expect.objectContaining({
|
66
|
+
id: mockUserId,
|
67
|
+
email: 'test@example.com',
|
68
|
+
phone: '+1234567890',
|
69
|
+
firstName: 'Test',
|
70
|
+
lastName: 'User',
|
71
|
+
username: 'testuser',
|
72
|
+
avatar: 'https://example.com/avatar.jpg',
|
73
|
+
clerkCreatedAt: new Date('2023-01-01T00:00:00Z'),
|
74
|
+
}),
|
75
|
+
);
|
76
|
+
});
|
77
|
+
|
78
|
+
it('should not create user if already exists', async () => {
|
79
|
+
// Mock user found
|
80
|
+
vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
|
81
|
+
|
82
|
+
const result = await service.createUser(mockUserId, mockUserJSON);
|
83
|
+
|
84
|
+
expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
85
|
+
expect(UserModel.createUser).not.toHaveBeenCalled();
|
86
|
+
expect(result).toEqual({
|
87
|
+
message: 'user not created due to user already existing in the database',
|
88
|
+
success: false,
|
89
|
+
});
|
90
|
+
});
|
91
|
+
|
92
|
+
it('should handle user without primary phone number', async () => {
|
93
|
+
vi.mocked(UserModel.findById).mockResolvedValue(null as any);
|
94
|
+
|
95
|
+
const userWithoutPrimaryPhone = {
|
96
|
+
...mockUserJSON,
|
97
|
+
primary_phone_number_id: null,
|
98
|
+
phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
|
99
|
+
} as UserJSON;
|
100
|
+
|
101
|
+
await service.createUser(mockUserId, userWithoutPrimaryPhone);
|
102
|
+
|
103
|
+
expect(UserModel.createUser).toHaveBeenCalledWith(
|
104
|
+
expect.anything(),
|
105
|
+
expect.objectContaining({
|
106
|
+
phone: '+1234567890', // Should use first phone number
|
107
|
+
}),
|
108
|
+
);
|
109
|
+
});
|
110
|
+
});
|
111
|
+
|
112
|
+
describe('deleteUser', () => {
|
113
|
+
it('should delete user', async () => {
|
114
|
+
await service.deleteUser(mockUserId);
|
115
|
+
|
116
|
+
expect(UserModel.deleteUser).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
117
|
+
});
|
118
|
+
|
119
|
+
it('should throw error if deletion fails', async () => {
|
120
|
+
const error = new Error('Deletion failed');
|
121
|
+
vi.mocked(UserModel.deleteUser).mockRejectedValue(error);
|
122
|
+
|
123
|
+
await expect(service.deleteUser(mockUserId)).rejects.toThrow('Deletion failed');
|
124
|
+
});
|
125
|
+
});
|
126
|
+
|
127
|
+
describe('updateUser', () => {
|
128
|
+
it('should update user when user exists', async () => {
|
129
|
+
// Mock user found
|
130
|
+
vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
|
131
|
+
const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
|
132
|
+
|
133
|
+
const result = await service.updateUser(mockUserId, mockUserJSON);
|
134
|
+
|
135
|
+
expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
136
|
+
expect(pino.info).toHaveBeenCalledWith('updating user due to clerk webhook');
|
137
|
+
expect(mockUpdateUser).toHaveBeenCalledWith(
|
138
|
+
expect.objectContaining({
|
139
|
+
id: mockUserId,
|
140
|
+
email: 'test@example.com',
|
141
|
+
phone: '+1234567890',
|
142
|
+
firstName: 'Test',
|
143
|
+
lastName: 'User',
|
144
|
+
username: 'testuser',
|
145
|
+
avatar: 'https://example.com/avatar.jpg',
|
146
|
+
}),
|
147
|
+
);
|
148
|
+
expect(result).toEqual({
|
149
|
+
message: 'user updated',
|
150
|
+
success: true,
|
151
|
+
});
|
152
|
+
});
|
153
|
+
|
154
|
+
it('should not update user when user does not exist', async () => {
|
155
|
+
// Mock user not found
|
156
|
+
vi.mocked(UserModel.findById).mockResolvedValue(null as any);
|
157
|
+
const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
|
158
|
+
|
159
|
+
const result = await service.updateUser(mockUserId, mockUserJSON);
|
160
|
+
|
161
|
+
expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
162
|
+
expect(mockUpdateUser).not.toHaveBeenCalled();
|
163
|
+
expect(result).toEqual({
|
164
|
+
message: "user not updated due to the user don't existing in the database",
|
165
|
+
success: false,
|
166
|
+
});
|
167
|
+
});
|
168
|
+
|
169
|
+
it('should handle user without primary email and phone', async () => {
|
170
|
+
vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
|
171
|
+
const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
|
172
|
+
|
173
|
+
const userWithoutPrimaryContacts = {
|
174
|
+
...mockUserJSON,
|
175
|
+
primary_email_address_id: null,
|
176
|
+
primary_phone_number_id: null,
|
177
|
+
email_addresses: [{ id: 'email-1', email_address: 'test@example.com' }],
|
178
|
+
phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
|
179
|
+
} as UserJSON;
|
180
|
+
|
181
|
+
await service.updateUser(mockUserId, userWithoutPrimaryContacts);
|
182
|
+
|
183
|
+
// Verify that the first email and phone are used when primary is not specified
|
184
|
+
expect(mockUpdateUser).toHaveBeenCalledWith(
|
185
|
+
expect.objectContaining({
|
186
|
+
phone: '+1234567890',
|
187
|
+
}),
|
188
|
+
);
|
189
|
+
});
|
190
|
+
|
191
|
+
it('should handle update failure', async () => {
|
192
|
+
vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
|
193
|
+
const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
|
194
|
+
const error = new Error('Update failed');
|
195
|
+
mockUpdateUser.mockRejectedValue(error);
|
196
|
+
|
197
|
+
await expect(service.updateUser(mockUserId, mockUserJSON)).rejects.toThrow('Update failed');
|
198
|
+
});
|
199
|
+
});
|
200
|
+
});
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { UserJSON } from '@clerk/backend';
|
2
|
-
import { NextResponse } from 'next/server';
|
3
2
|
|
4
3
|
import { serverDB } from '@/database/server';
|
5
4
|
import { UserModel } from '@/database/server/models/user';
|
@@ -12,16 +11,18 @@ export class UserService {
|
|
12
11
|
|
13
12
|
// If user already exists, skip creating a new user
|
14
13
|
if (res)
|
15
|
-
return
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
},
|
20
|
-
{ status: 200 },
|
21
|
-
);
|
14
|
+
return {
|
15
|
+
message: 'user not created due to user already existing in the database',
|
16
|
+
success: false,
|
17
|
+
};
|
22
18
|
|
23
19
|
const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
|
24
|
-
|
20
|
+
|
21
|
+
const phone = params.phone_numbers.find((e, index) => {
|
22
|
+
if (!!params.primary_phone_number_id) return e.id === params.primary_phone_number_id;
|
23
|
+
|
24
|
+
return index === 0;
|
25
|
+
});
|
25
26
|
|
26
27
|
/* ↓ cloud slot ↓ */
|
27
28
|
|
@@ -43,25 +44,14 @@ export class UserService {
|
|
43
44
|
|
44
45
|
/* ↑ cloud slot ↑ */
|
45
46
|
|
46
|
-
return
|
47
|
+
return { message: 'user created', success: true };
|
47
48
|
};
|
48
49
|
|
49
|
-
deleteUser = async (id
|
50
|
-
|
51
|
-
pino.info('delete user due to clerk webhook');
|
52
|
-
|
53
|
-
await UserModel.deleteUser(serverDB, id);
|
54
|
-
|
55
|
-
return NextResponse.json({ message: 'user deleted' }, { status: 200 });
|
56
|
-
} else {
|
57
|
-
pino.warn('clerk sent a delete user request, but no user ID was included in the payload');
|
58
|
-
return NextResponse.json({ message: 'ok' }, { status: 200 });
|
59
|
-
}
|
50
|
+
deleteUser = async (id: string) => {
|
51
|
+
await UserModel.deleteUser(serverDB, id);
|
60
52
|
};
|
61
53
|
|
62
54
|
updateUser = async (id: string, params: UserJSON) => {
|
63
|
-
pino.info('updating user due to clerk webhook');
|
64
|
-
|
65
55
|
const userModel = new UserModel(serverDB, id);
|
66
56
|
|
67
57
|
// Check if user already exists
|
@@ -69,16 +59,18 @@ export class UserService {
|
|
69
59
|
|
70
60
|
// If user not exists, skip update the user
|
71
61
|
if (!res)
|
72
|
-
return
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
);
|
62
|
+
return {
|
63
|
+
message: "user not updated due to the user don't existing in the database",
|
64
|
+
success: false,
|
65
|
+
};
|
66
|
+
|
67
|
+
pino.info('updating user due to clerk webhook');
|
79
68
|
|
80
69
|
const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
|
81
|
-
const phone = params.phone_numbers.find((e) =>
|
70
|
+
const phone = params.phone_numbers.find((e, index) => {
|
71
|
+
if (params.primary_phone_number_id) return e.id === params.primary_phone_number_id;
|
72
|
+
return index === 0;
|
73
|
+
});
|
82
74
|
|
83
75
|
await userModel.updateUser({
|
84
76
|
avatar: params.image_url,
|
@@ -90,6 +82,6 @@ export class UserService {
|
|
90
82
|
username: params.username,
|
91
83
|
});
|
92
84
|
|
93
|
-
return
|
85
|
+
return { message: 'user updated', success: true };
|
94
86
|
};
|
95
87
|
}
|
@@ -411,16 +411,6 @@ export const generateAIChat: StateCreator<
|
|
411
411
|
? agentConfig.params.max_tokens
|
412
412
|
: undefined;
|
413
413
|
|
414
|
-
// 5. handle config for the vision model
|
415
|
-
// Due to the gpt-4-vision-preview model's default max_tokens is very small
|
416
|
-
// we need to set the max_tokens a larger one.
|
417
|
-
if (agentConfig.model === 'gpt-4-vision-preview') {
|
418
|
-
/* eslint-disable unicorn/no-lonely-if */
|
419
|
-
if (!agentConfig.params.max_tokens)
|
420
|
-
// refs: https://github.com/lobehub/lobe-chat/issues/837
|
421
|
-
agentConfig.params.max_tokens = 2048;
|
422
|
-
}
|
423
|
-
|
424
414
|
let isFunctionCall = false;
|
425
415
|
let msgTraceId: string | undefined;
|
426
416
|
let output = '';
|