@lobehub/chat 1.61.5 → 1.62.0
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/docs/self-hosting/advanced/auth/next-auth/casdoor.mdx +2 -1
- package/docs/self-hosting/advanced/auth/next-auth/casdoor.zh-CN.mdx +2 -1
- package/locales/en-US/components.json +3 -3
- package/package.json +2 -2
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -7
- package/src/app/(backend)/api/webhooks/casdoor/validateRequest.ts +7 -4
- package/src/components/ModelSelect/index.tsx +24 -2
- package/src/components/Thinking/index.tsx +7 -2
- package/src/config/aiModels/jina.ts +7 -5
- package/src/config/aiModels/perplexity.ts +8 -0
- package/src/config/llm.ts +8 -0
- package/src/database/client/migrations.json +12 -8
- package/src/database/migrations/0015_add_message_search_metadata.sql +2 -0
- package/src/database/migrations/meta/0015_snapshot.json +3616 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/schemas/message.ts +3 -1
- package/src/database/server/models/message.ts +2 -0
- package/src/features/Conversation/components/ChatItem/index.tsx +10 -1
- package/src/features/Conversation/components/MarkdownElements/Thinking/Render.tsx +5 -1
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkCustomTagPlugin.ts +1 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.test.ts +107 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent.ts +6 -0
- package/src/libs/agent-runtime/perplexity/index.test.ts +156 -12
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +3 -3
- package/src/libs/agent-runtime/utils/streams/bedrock/claude.ts +6 -2
- package/src/libs/agent-runtime/utils/streams/bedrock/llama.ts +3 -3
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +3 -3
- package/src/libs/agent-runtime/utils/streams/ollama.ts +3 -3
- package/src/libs/agent-runtime/utils/streams/openai.ts +26 -8
- package/src/libs/agent-runtime/utils/streams/protocol.ts +33 -8
- package/src/libs/agent-runtime/utils/streams/vertex-ai.ts +3 -3
- package/src/locales/default/components.ts +1 -0
- package/src/server/globalConfig/index.test.ts +81 -0
- package/src/server/routers/lambda/user.test.ts +305 -0
- package/src/server/services/nextAuthUser/index.ts +2 -2
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +17 -6
- package/src/store/chat/slices/message/action.ts +12 -7
- package/src/types/aiModel.ts +5 -0
- package/src/types/message/base.ts +13 -0
- package/src/types/message/chat.ts +3 -2
- package/src/utils/errorResponse.test.ts +37 -1
- package/src/utils/fetch/fetchSSE.ts +17 -1
@@ -2,8 +2,16 @@ import { ChatStreamCallbacks } from '@/libs/agent-runtime';
|
|
2
2
|
|
3
3
|
import { AgentRuntimeErrorType } from '../../error';
|
4
4
|
|
5
|
-
|
5
|
+
/**
|
6
|
+
* context in the stream to save temporarily data
|
7
|
+
*/
|
8
|
+
export interface StreamContext {
|
6
9
|
id: string;
|
10
|
+
/**
|
11
|
+
* As pplx citations is in every chunk, but we only need to return it once
|
12
|
+
* this flag is used to check if the pplx citation is returned,and then not return it again
|
13
|
+
*/
|
14
|
+
returnedPplxCitation?: boolean;
|
7
15
|
tool?: {
|
8
16
|
id: string;
|
9
17
|
index: number;
|
@@ -15,7 +23,20 @@ export interface StreamStack {
|
|
15
23
|
export interface StreamProtocolChunk {
|
16
24
|
data: any;
|
17
25
|
id?: string;
|
18
|
-
type:
|
26
|
+
type: // pure text
|
27
|
+
| 'text'
|
28
|
+
// Tools use
|
29
|
+
| 'tool_calls'
|
30
|
+
// Model Thinking
|
31
|
+
| 'reasoning'
|
32
|
+
// Search or Grounding
|
33
|
+
| 'citations'
|
34
|
+
// stop signal
|
35
|
+
| 'stop'
|
36
|
+
// Error
|
37
|
+
| 'error'
|
38
|
+
// unknown data result
|
39
|
+
| 'data';
|
19
40
|
}
|
20
41
|
|
21
42
|
export interface StreamToolCallChunkData {
|
@@ -85,16 +106,20 @@ export const convertIterableToStream = <T>(stream: AsyncIterable<T>) => {
|
|
85
106
|
* Create a transformer to convert the response into an SSE format
|
86
107
|
*/
|
87
108
|
export const createSSEProtocolTransformer = (
|
88
|
-
transformer: (chunk: any, stack:
|
89
|
-
streamStack?:
|
109
|
+
transformer: (chunk: any, stack: StreamContext) => StreamProtocolChunk | StreamProtocolChunk[],
|
110
|
+
streamStack?: StreamContext,
|
90
111
|
) =>
|
91
112
|
new TransformStream({
|
92
113
|
transform: (chunk, controller) => {
|
93
|
-
const
|
114
|
+
const result = transformer(chunk, streamStack || { id: '' });
|
115
|
+
|
116
|
+
const buffers = Array.isArray(result) ? result : [result];
|
94
117
|
|
95
|
-
|
96
|
-
|
97
|
-
|
118
|
+
buffers.forEach(({ type, id, data }) => {
|
119
|
+
controller.enqueue(`id: ${id}\n`);
|
120
|
+
controller.enqueue(`event: ${type}\n`);
|
121
|
+
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
|
122
|
+
});
|
98
123
|
},
|
99
124
|
});
|
100
125
|
|
@@ -4,8 +4,8 @@ import { nanoid } from '@/utils/uuid';
|
|
4
4
|
|
5
5
|
import { ChatStreamCallbacks } from '../../types';
|
6
6
|
import {
|
7
|
+
StreamContext,
|
7
8
|
StreamProtocolChunk,
|
8
|
-
StreamStack,
|
9
9
|
createCallbacksTransformer,
|
10
10
|
createSSEProtocolTransformer,
|
11
11
|
generateToolCallId,
|
@@ -13,7 +13,7 @@ import {
|
|
13
13
|
|
14
14
|
const transformVertexAIStream = (
|
15
15
|
chunk: GenerateContentResponse,
|
16
|
-
stack:
|
16
|
+
stack: StreamContext,
|
17
17
|
): StreamProtocolChunk => {
|
18
18
|
// maybe need another structure to add support for multiple choices
|
19
19
|
const candidates = chunk.candidates;
|
@@ -67,7 +67,7 @@ export const VertexAIStream = (
|
|
67
67
|
rawStream: ReadableStream<EnhancedGenerateContentResponse>,
|
68
68
|
callbacks?: ChatStreamCallbacks,
|
69
69
|
) => {
|
70
|
-
const streamStack:
|
70
|
+
const streamStack: StreamContext = { id: 'chat_' + nanoid() };
|
71
71
|
|
72
72
|
return rawStream
|
73
73
|
.pipeThrough(createSSEProtocolTransformer(transformVertexAIStream, streamStack))
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { getAppConfig } from '@/config/app';
|
4
|
+
import { knowledgeEnv } from '@/config/knowledge';
|
5
|
+
import { SystemEmbeddingConfig } from '@/types/knowledgeBase';
|
6
|
+
import { FilesConfigItem } from '@/types/user/settings/filesConfig';
|
7
|
+
|
8
|
+
import { getServerDefaultAgentConfig, getServerDefaultFilesConfig } from './index';
|
9
|
+
import { parseAgentConfig } from './parseDefaultAgent';
|
10
|
+
import { parseFilesConfig } from './parseFilesConfig';
|
11
|
+
|
12
|
+
vi.mock('@/config/app', () => ({
|
13
|
+
getAppConfig: vi.fn(),
|
14
|
+
}));
|
15
|
+
|
16
|
+
vi.mock('@/config/knowledge', () => ({
|
17
|
+
knowledgeEnv: {
|
18
|
+
DEFAULT_FILES_CONFIG: 'test_config',
|
19
|
+
},
|
20
|
+
}));
|
21
|
+
|
22
|
+
vi.mock('./parseDefaultAgent', () => ({
|
23
|
+
parseAgentConfig: vi.fn(),
|
24
|
+
}));
|
25
|
+
|
26
|
+
vi.mock('./parseFilesConfig', () => ({
|
27
|
+
parseFilesConfig: vi.fn(),
|
28
|
+
}));
|
29
|
+
|
30
|
+
describe('getServerDefaultAgentConfig', () => {
|
31
|
+
it('should return parsed agent config', () => {
|
32
|
+
const mockConfig = { key: 'value' };
|
33
|
+
vi.mocked(getAppConfig).mockReturnValue({
|
34
|
+
DEFAULT_AGENT_CONFIG: 'test_agent_config',
|
35
|
+
} as any);
|
36
|
+
vi.mocked(parseAgentConfig).mockReturnValue(mockConfig);
|
37
|
+
|
38
|
+
const result = getServerDefaultAgentConfig();
|
39
|
+
|
40
|
+
expect(parseAgentConfig).toHaveBeenCalledWith('test_agent_config');
|
41
|
+
expect(result).toEqual(mockConfig);
|
42
|
+
});
|
43
|
+
|
44
|
+
it('should return empty object if parseAgentConfig returns undefined', () => {
|
45
|
+
vi.mocked(getAppConfig).mockReturnValue({
|
46
|
+
DEFAULT_AGENT_CONFIG: 'test_agent_config',
|
47
|
+
} as any);
|
48
|
+
vi.mocked(parseAgentConfig).mockReturnValue(undefined);
|
49
|
+
|
50
|
+
const result = getServerDefaultAgentConfig();
|
51
|
+
|
52
|
+
expect(result).toEqual({});
|
53
|
+
});
|
54
|
+
});
|
55
|
+
|
56
|
+
describe('getServerDefaultFilesConfig', () => {
|
57
|
+
it('should return parsed files config', () => {
|
58
|
+
const mockEmbeddingModel: FilesConfigItem = {
|
59
|
+
model: 'test-model',
|
60
|
+
provider: 'test-provider',
|
61
|
+
};
|
62
|
+
|
63
|
+
const mockRerankerModel: FilesConfigItem = {
|
64
|
+
model: 'test-reranker',
|
65
|
+
provider: 'test-provider',
|
66
|
+
};
|
67
|
+
|
68
|
+
const mockConfig: SystemEmbeddingConfig = {
|
69
|
+
embeddingModel: mockEmbeddingModel,
|
70
|
+
queryMode: 'hybrid',
|
71
|
+
rerankerModel: mockRerankerModel,
|
72
|
+
};
|
73
|
+
|
74
|
+
vi.mocked(parseFilesConfig).mockReturnValue(mockConfig);
|
75
|
+
|
76
|
+
const result = getServerDefaultFilesConfig();
|
77
|
+
|
78
|
+
expect(parseFilesConfig).toHaveBeenCalledWith('test_config');
|
79
|
+
expect(result).toEqual(mockConfig);
|
80
|
+
});
|
81
|
+
});
|
@@ -0,0 +1,305 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { enableClerk } from '@/const/auth';
|
5
|
+
import { serverDB } from '@/database/server';
|
6
|
+
import { MessageModel } from '@/database/server/models/message';
|
7
|
+
import { SessionModel } from '@/database/server/models/session';
|
8
|
+
import { UserModel, UserNotFoundError } from '@/database/server/models/user';
|
9
|
+
import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
|
10
|
+
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
11
|
+
import { UserService } from '@/server/services/user';
|
12
|
+
|
13
|
+
import { userRouter } from './user';
|
14
|
+
|
15
|
+
// Mock modules
|
16
|
+
vi.mock('@clerk/nextjs/server', () => ({
|
17
|
+
currentUser: vi.fn(),
|
18
|
+
}));
|
19
|
+
|
20
|
+
vi.mock('@/database/server', () => ({
|
21
|
+
serverDB: {},
|
22
|
+
}));
|
23
|
+
|
24
|
+
vi.mock('@/database/server/models/message');
|
25
|
+
vi.mock('@/database/server/models/session');
|
26
|
+
vi.mock('@/database/server/models/user');
|
27
|
+
vi.mock('@/libs/next-auth/adapter');
|
28
|
+
vi.mock('@/server/modules/KeyVaultsEncrypt');
|
29
|
+
vi.mock('@/server/services/user');
|
30
|
+
vi.mock('@/const/auth', () => ({
|
31
|
+
enableClerk: true,
|
32
|
+
}));
|
33
|
+
|
34
|
+
describe('userRouter', () => {
|
35
|
+
const mockUserId = 'test-user-id';
|
36
|
+
const mockCtx = {
|
37
|
+
userId: mockUserId,
|
38
|
+
};
|
39
|
+
|
40
|
+
beforeEach(() => {
|
41
|
+
vi.clearAllMocks();
|
42
|
+
});
|
43
|
+
|
44
|
+
describe('getUserRegistrationDuration', () => {
|
45
|
+
it('should return registration duration', async () => {
|
46
|
+
const mockDuration = { duration: 100, createdAt: '2023-01-01', updatedAt: '2023-01-02' };
|
47
|
+
vi.mocked(UserModel).mockImplementation(
|
48
|
+
() =>
|
49
|
+
({
|
50
|
+
getUserRegistrationDuration: vi.fn().mockResolvedValue(mockDuration),
|
51
|
+
}) as any,
|
52
|
+
);
|
53
|
+
|
54
|
+
const result = await userRouter.createCaller({ ...mockCtx }).getUserRegistrationDuration();
|
55
|
+
|
56
|
+
expect(result).toEqual(mockDuration);
|
57
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
58
|
+
});
|
59
|
+
});
|
60
|
+
|
61
|
+
describe('getUserSSOProviders', () => {
|
62
|
+
it('should return SSO providers', async () => {
|
63
|
+
const mockProviders = [
|
64
|
+
{
|
65
|
+
provider: 'google',
|
66
|
+
providerAccountId: '123',
|
67
|
+
userId: 'user-1',
|
68
|
+
type: 'oauth',
|
69
|
+
},
|
70
|
+
];
|
71
|
+
vi.mocked(UserModel).mockImplementation(
|
72
|
+
() =>
|
73
|
+
({
|
74
|
+
getUserSSOProviders: vi.fn().mockResolvedValue(mockProviders),
|
75
|
+
}) as any,
|
76
|
+
);
|
77
|
+
|
78
|
+
const result = await userRouter.createCaller({ ...mockCtx }).getUserSSOProviders();
|
79
|
+
|
80
|
+
expect(result).toEqual(mockProviders);
|
81
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
82
|
+
});
|
83
|
+
});
|
84
|
+
|
85
|
+
describe('getUserState', () => {
|
86
|
+
it('should return user state', async () => {
|
87
|
+
const mockState = {
|
88
|
+
isOnboarded: true,
|
89
|
+
preference: { telemetry: true },
|
90
|
+
settings: {},
|
91
|
+
userId: mockUserId,
|
92
|
+
};
|
93
|
+
|
94
|
+
vi.mocked(UserModel).mockImplementation(
|
95
|
+
() =>
|
96
|
+
({
|
97
|
+
getUserState: vi.fn().mockResolvedValue(mockState),
|
98
|
+
}) as any,
|
99
|
+
);
|
100
|
+
|
101
|
+
vi.mocked(MessageModel).mockImplementation(
|
102
|
+
() =>
|
103
|
+
({
|
104
|
+
hasMoreThanN: vi.fn().mockResolvedValue(true),
|
105
|
+
}) as any,
|
106
|
+
);
|
107
|
+
|
108
|
+
vi.mocked(SessionModel).mockImplementation(
|
109
|
+
() =>
|
110
|
+
({
|
111
|
+
hasMoreThanN: vi.fn().mockResolvedValue(true),
|
112
|
+
}) as any,
|
113
|
+
);
|
114
|
+
|
115
|
+
const result = await userRouter.createCaller({ ...mockCtx }).getUserState();
|
116
|
+
|
117
|
+
expect(result).toEqual({
|
118
|
+
isOnboard: true,
|
119
|
+
preference: { telemetry: true },
|
120
|
+
settings: {},
|
121
|
+
hasConversation: true,
|
122
|
+
canEnablePWAGuide: true,
|
123
|
+
canEnableTrace: true,
|
124
|
+
userId: mockUserId,
|
125
|
+
});
|
126
|
+
});
|
127
|
+
|
128
|
+
it('should create new user when user not found (clerk enabled)', async () => {
|
129
|
+
const mockClerkUser = {
|
130
|
+
id: mockUserId,
|
131
|
+
createdAt: new Date(),
|
132
|
+
emailAddresses: [{ id: 'email-1', emailAddress: 'test@example.com' }],
|
133
|
+
firstName: 'Test',
|
134
|
+
lastName: 'User',
|
135
|
+
imageUrl: 'avatar.jpg',
|
136
|
+
phoneNumbers: [],
|
137
|
+
primaryEmailAddressId: 'email-1',
|
138
|
+
primaryPhoneNumberId: null,
|
139
|
+
username: 'testuser',
|
140
|
+
};
|
141
|
+
|
142
|
+
const { currentUser } = await import('@clerk/nextjs/server');
|
143
|
+
vi.mocked(currentUser).mockResolvedValue(mockClerkUser as any);
|
144
|
+
|
145
|
+
vi.mocked(UserService).mockImplementation(
|
146
|
+
() =>
|
147
|
+
({
|
148
|
+
createUser: vi.fn().mockResolvedValue({ success: true }),
|
149
|
+
}) as any,
|
150
|
+
);
|
151
|
+
|
152
|
+
vi.mocked(UserModel).mockImplementation(
|
153
|
+
() =>
|
154
|
+
({
|
155
|
+
getUserState: vi
|
156
|
+
.fn()
|
157
|
+
.mockRejectedValueOnce(new UserNotFoundError())
|
158
|
+
.mockResolvedValueOnce({
|
159
|
+
isOnboarded: false,
|
160
|
+
preference: { telemetry: null },
|
161
|
+
settings: {},
|
162
|
+
}),
|
163
|
+
}) as any,
|
164
|
+
);
|
165
|
+
|
166
|
+
vi.mocked(MessageModel).mockImplementation(
|
167
|
+
() =>
|
168
|
+
({
|
169
|
+
hasMoreThanN: vi.fn().mockResolvedValue(false),
|
170
|
+
}) as any,
|
171
|
+
);
|
172
|
+
|
173
|
+
vi.mocked(SessionModel).mockImplementation(
|
174
|
+
() =>
|
175
|
+
({
|
176
|
+
hasMoreThanN: vi.fn().mockResolvedValue(false),
|
177
|
+
}) as any,
|
178
|
+
);
|
179
|
+
|
180
|
+
const result = await userRouter.createCaller({ ...mockCtx } as any).getUserState();
|
181
|
+
|
182
|
+
expect(result).toEqual({
|
183
|
+
isOnboard: true,
|
184
|
+
preference: { telemetry: null },
|
185
|
+
settings: {},
|
186
|
+
hasConversation: false,
|
187
|
+
canEnablePWAGuide: false,
|
188
|
+
canEnableTrace: false,
|
189
|
+
userId: mockUserId,
|
190
|
+
});
|
191
|
+
});
|
192
|
+
});
|
193
|
+
|
194
|
+
describe('makeUserOnboarded', () => {
|
195
|
+
it('should update user onboarded status', async () => {
|
196
|
+
vi.mocked(UserModel).mockImplementation(
|
197
|
+
() =>
|
198
|
+
({
|
199
|
+
updateUser: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
200
|
+
}) as any,
|
201
|
+
);
|
202
|
+
|
203
|
+
await userRouter.createCaller({ ...mockCtx }).makeUserOnboarded();
|
204
|
+
|
205
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
206
|
+
});
|
207
|
+
});
|
208
|
+
|
209
|
+
describe('unlinkSSOProvider', () => {
|
210
|
+
it('should unlink SSO provider successfully', async () => {
|
211
|
+
const mockInput = {
|
212
|
+
provider: 'google',
|
213
|
+
providerAccountId: '123',
|
214
|
+
};
|
215
|
+
|
216
|
+
const mockAccount = {
|
217
|
+
userId: mockUserId,
|
218
|
+
provider: 'google',
|
219
|
+
providerAccountId: '123',
|
220
|
+
type: 'oauth',
|
221
|
+
};
|
222
|
+
|
223
|
+
vi.mocked(LobeNextAuthDbAdapter).mockReturnValue({
|
224
|
+
getAccount: vi.fn().mockResolvedValue(mockAccount),
|
225
|
+
unlinkAccount: vi.fn().mockResolvedValue(undefined),
|
226
|
+
} as any);
|
227
|
+
|
228
|
+
await expect(
|
229
|
+
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
230
|
+
).resolves.not.toThrow();
|
231
|
+
});
|
232
|
+
|
233
|
+
it('should throw error if account does not exist', async () => {
|
234
|
+
const mockInput = {
|
235
|
+
provider: 'google',
|
236
|
+
providerAccountId: '123',
|
237
|
+
};
|
238
|
+
|
239
|
+
vi.mocked(LobeNextAuthDbAdapter).mockReturnValue({
|
240
|
+
getAccount: vi.fn().mockResolvedValue(null),
|
241
|
+
unlinkAccount: vi.fn(),
|
242
|
+
} as any);
|
243
|
+
|
244
|
+
await expect(
|
245
|
+
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
246
|
+
).rejects.toThrow('The account does not exist');
|
247
|
+
});
|
248
|
+
|
249
|
+
it('should throw error if adapter methods are not implemented', async () => {
|
250
|
+
const mockInput = {
|
251
|
+
provider: 'google',
|
252
|
+
providerAccountId: '123',
|
253
|
+
};
|
254
|
+
|
255
|
+
vi.mocked(LobeNextAuthDbAdapter).mockReturnValue({} as any);
|
256
|
+
|
257
|
+
await expect(
|
258
|
+
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
259
|
+
).rejects.toThrow('The method in LobeNextAuthDbAdapter `unlinkAccount` is not implemented');
|
260
|
+
});
|
261
|
+
});
|
262
|
+
|
263
|
+
describe('updateSettings', () => {
|
264
|
+
it('should update settings with encrypted key vaults', async () => {
|
265
|
+
const mockSettings = {
|
266
|
+
keyVaults: { openai: { key: 'test-key' } },
|
267
|
+
general: { language: 'en-US' },
|
268
|
+
};
|
269
|
+
|
270
|
+
const mockEncryptedVaults = 'encrypted-data';
|
271
|
+
const mockGateKeeper = {
|
272
|
+
encrypt: vi.fn().mockResolvedValue(mockEncryptedVaults),
|
273
|
+
};
|
274
|
+
|
275
|
+
vi.mocked(KeyVaultsGateKeeper.initWithEnvKey).mockResolvedValue(mockGateKeeper as any);
|
276
|
+
vi.mocked(UserModel).mockImplementation(
|
277
|
+
() =>
|
278
|
+
({
|
279
|
+
updateSetting: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
280
|
+
}) as any,
|
281
|
+
);
|
282
|
+
|
283
|
+
await userRouter.createCaller({ ...mockCtx }).updateSettings(mockSettings);
|
284
|
+
|
285
|
+
expect(mockGateKeeper.encrypt).toHaveBeenCalledWith(JSON.stringify(mockSettings.keyVaults));
|
286
|
+
});
|
287
|
+
|
288
|
+
it('should update settings without key vaults', async () => {
|
289
|
+
const mockSettings = {
|
290
|
+
general: { language: 'en-US' },
|
291
|
+
};
|
292
|
+
|
293
|
+
vi.mocked(UserModel).mockImplementation(
|
294
|
+
() =>
|
295
|
+
({
|
296
|
+
updateSetting: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
297
|
+
}) as any,
|
298
|
+
);
|
299
|
+
|
300
|
+
await userRouter.createCaller({ ...mockCtx }).updateSettings(mockSettings);
|
301
|
+
|
302
|
+
expect(UserModel).toHaveBeenCalledWith(serverDB, mockUserId);
|
303
|
+
});
|
304
|
+
});
|
305
|
+
});
|
@@ -17,7 +17,7 @@ export class NextAuthUserService {
|
|
17
17
|
{ providerAccountId, provider }: { provider: string; providerAccountId: string },
|
18
18
|
data: Partial<UserItem>,
|
19
19
|
) => {
|
20
|
-
pino.info(
|
20
|
+
pino.info(`updating user "${JSON.stringify({ provider, providerAccountId })}" due to webhook`);
|
21
21
|
// 1. Find User by account
|
22
22
|
// @ts-expect-error: Already impl in `LobeNextauthDbAdapter`
|
23
23
|
const user = await this.adapter.getUserByAccount({
|
@@ -37,7 +37,7 @@ export class NextAuthUserService {
|
|
37
37
|
});
|
38
38
|
} else {
|
39
39
|
pino.warn(
|
40
|
-
`[${provider}]: Webhooks handler user update for "${JSON.stringify(data)}", but no user was found by the providerAccountId.`,
|
40
|
+
`[${provider}]: Webhooks handler user "${JSON.stringify({ provider, providerAccountId })}" update for "${JSON.stringify(data)}", but no user was found by the providerAccountId.`,
|
41
41
|
);
|
42
42
|
}
|
43
43
|
return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
|
@@ -455,7 +455,7 @@ export const generateAIChat: StateCreator<
|
|
455
455
|
await messageService.updateMessageError(messageId, error);
|
456
456
|
await refreshMessages();
|
457
457
|
},
|
458
|
-
onFinish: async (content, { traceId, observationId, toolCalls, reasoning }) => {
|
458
|
+
onFinish: async (content, { traceId, observationId, toolCalls, reasoning, citations }) => {
|
459
459
|
// if there is traceId, update it
|
460
460
|
if (traceId) {
|
461
461
|
msgTraceId = traceId;
|
@@ -470,15 +470,26 @@ export const generateAIChat: StateCreator<
|
|
470
470
|
}
|
471
471
|
|
472
472
|
// update the content after fetch result
|
473
|
-
await internal_updateMessageContent(
|
474
|
-
messageId,
|
475
|
-
content,
|
473
|
+
await internal_updateMessageContent(messageId, content, {
|
476
474
|
toolCalls,
|
477
|
-
!!reasoning ? { content: reasoning, duration } : undefined,
|
478
|
-
|
475
|
+
reasoning: !!reasoning ? { content: reasoning, duration } : undefined,
|
476
|
+
search: !!citations ? { citations } : undefined,
|
477
|
+
});
|
479
478
|
},
|
480
479
|
onMessageHandle: async (chunk) => {
|
481
480
|
switch (chunk.type) {
|
481
|
+
case 'citations': {
|
482
|
+
// if there is no citations, then stop
|
483
|
+
if (!chunk.citations || chunk.citations.length <= 0) return;
|
484
|
+
|
485
|
+
internal_dispatchMessage({
|
486
|
+
id: messageId,
|
487
|
+
type: 'updateMessage',
|
488
|
+
value: { search: { citations: chunk.citations } },
|
489
|
+
});
|
490
|
+
break;
|
491
|
+
}
|
492
|
+
|
482
493
|
case 'text': {
|
483
494
|
output += chunk.text;
|
484
495
|
|
@@ -16,6 +16,7 @@ import {
|
|
16
16
|
ChatMessage,
|
17
17
|
ChatMessageError,
|
18
18
|
CreateMessageParams,
|
19
|
+
GroundingSearch,
|
19
20
|
MessageToolCall,
|
20
21
|
ModelReasoning,
|
21
22
|
} from '@/types/message';
|
@@ -73,8 +74,11 @@ export interface ChatMessageAction {
|
|
73
74
|
internal_updateMessageContent: (
|
74
75
|
id: string,
|
75
76
|
content: string,
|
76
|
-
|
77
|
-
|
77
|
+
extra?: {
|
78
|
+
toolCalls?: MessageToolCall[];
|
79
|
+
reasoning?: ModelReasoning;
|
80
|
+
search?: GroundingSearch;
|
81
|
+
},
|
78
82
|
) => Promise<void>;
|
79
83
|
/**
|
80
84
|
* update the message error with optimistic update
|
@@ -272,17 +276,17 @@ export const chatMessage: StateCreator<
|
|
272
276
|
await messageService.updateMessage(id, { error });
|
273
277
|
await get().refreshMessages();
|
274
278
|
},
|
275
|
-
internal_updateMessageContent: async (id, content,
|
279
|
+
internal_updateMessageContent: async (id, content, extra) => {
|
276
280
|
const { internal_dispatchMessage, refreshMessages, internal_transformToolCalls } = get();
|
277
281
|
|
278
282
|
// Due to the async update method and refresh need about 100ms
|
279
283
|
// we need to update the message content at the frontend to avoid the update flick
|
280
284
|
// refs: https://medium.com/@kyledeguzmanx/what-are-optimistic-updates-483662c3e171
|
281
|
-
if (toolCalls) {
|
285
|
+
if (extra?.toolCalls) {
|
282
286
|
internal_dispatchMessage({
|
283
287
|
id,
|
284
288
|
type: 'updateMessage',
|
285
|
-
value: { tools: internal_transformToolCalls(toolCalls) },
|
289
|
+
value: { tools: internal_transformToolCalls(extra?.toolCalls) },
|
286
290
|
});
|
287
291
|
} else {
|
288
292
|
internal_dispatchMessage({ id, type: 'updateMessage', value: { content } });
|
@@ -290,8 +294,9 @@ export const chatMessage: StateCreator<
|
|
290
294
|
|
291
295
|
await messageService.updateMessage(id, {
|
292
296
|
content,
|
293
|
-
tools: toolCalls ? internal_transformToolCalls(toolCalls) : undefined,
|
294
|
-
reasoning,
|
297
|
+
tools: extra?.toolCalls ? internal_transformToolCalls(extra?.toolCalls) : undefined,
|
298
|
+
reasoning: extra?.reasoning,
|
299
|
+
search: extra?.search,
|
295
300
|
});
|
296
301
|
await refreshMessages();
|
297
302
|
},
|
package/src/types/aiModel.ts
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
export interface CitationItem {
|
2
|
+
id?: string;
|
3
|
+
onlyUrl?: boolean;
|
4
|
+
title?: string;
|
5
|
+
url: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export interface GroundingSearch {
|
9
|
+
citations?: CitationItem[];
|
10
|
+
searchQueries?: string[];
|
11
|
+
}
|
12
|
+
|
1
13
|
export interface ModelReasoning {
|
2
14
|
content?: string;
|
3
15
|
duration?: number;
|
@@ -20,6 +32,7 @@ export interface MessageItem {
|
|
20
32
|
quotaId: string | null;
|
21
33
|
reasoning: ModelReasoning | null;
|
22
34
|
role: string;
|
35
|
+
search: GroundingSearch | null;
|
23
36
|
sessionId: string | null;
|
24
37
|
threadId: string | null;
|
25
38
|
// jsonb type
|
@@ -2,7 +2,7 @@ import { IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
2
2
|
|
3
3
|
import { ILobeAgentRuntimeErrorType } from '@/libs/agent-runtime';
|
4
4
|
import { ErrorType } from '@/types/fetch';
|
5
|
-
import { MessageRoleType, ModelReasoning } from '@/types/message/base';
|
5
|
+
import { GroundingSearch, MessageRoleType, ModelReasoning } from '@/types/message/base';
|
6
6
|
import { ChatPluginPayload, ChatToolPayload } from '@/types/message/tools';
|
7
7
|
import { Translate } from '@/types/message/translate';
|
8
8
|
import { MetaData } from '@/types/meta';
|
@@ -100,11 +100,12 @@ export interface ChatMessage {
|
|
100
100
|
ragRawQuery?: string | null;
|
101
101
|
|
102
102
|
reasoning?: ModelReasoning | null;
|
103
|
-
|
104
103
|
/**
|
105
104
|
* message role type
|
106
105
|
*/
|
107
106
|
role: MessageRoleType;
|
107
|
+
|
108
|
+
search?: GroundingSearch | null;
|
108
109
|
sessionId?: string;
|
109
110
|
threadId?: string | null;
|
110
111
|
tool_call_id?: string;
|