@lobehub/lobehub 2.0.0-next.29 → 2.0.0-next.30
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/message.test.ts +0 -129
- package/packages/database/src/models/message.ts +0 -49
- package/packages/utils/src/server/auth.ts +6 -6
- package/packages/utils/src/server/geo.ts +9 -9
- package/packages/utils/src/server/xor.ts +7 -7
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +783 -2
- package/src/server/routers/lambda/message.ts +17 -84
- package/src/server/services/message/__tests__/index.test.ts +348 -0
- package/src/server/services/message/index.ts +159 -0
- package/src/services/message/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.30](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.29...v2.0.0-next.30)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-11-05**</sup>
|
|
8
|
+
|
|
9
|
+
#### ♻ Code Refactoring
|
|
10
|
+
|
|
11
|
+
- **misc**: Enhance message router with service layer and comprehensive tests.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Code refactoring
|
|
19
|
+
|
|
20
|
+
- **misc**: Enhance message router with service layer and comprehensive tests, closes [#10056](https://github.com/lobehub/lobe-chat/issues/10056) ([62110e0](https://github.com/lobehub/lobe-chat/commit/62110e0))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.29](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.28...v2.0.0-next.29)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-11-04**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.30",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -932,135 +932,6 @@ describe('MessageModel', () => {
|
|
|
932
932
|
});
|
|
933
933
|
});
|
|
934
934
|
|
|
935
|
-
describe('createNewMessage', () => {
|
|
936
|
-
it('should create message and return id with messages list', async () => {
|
|
937
|
-
// Call createNewMessage method
|
|
938
|
-
const result = await messageModel.createNewMessage({
|
|
939
|
-
role: 'user',
|
|
940
|
-
content: 'test message',
|
|
941
|
-
sessionId: '1',
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
// Assert return structure
|
|
945
|
-
expect(result).toHaveProperty('id');
|
|
946
|
-
expect(result).toHaveProperty('messages');
|
|
947
|
-
expect(result.id).toBeDefined();
|
|
948
|
-
expect(result.messages).toBeInstanceOf(Array);
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
it('should return newly created message in messages list', async () => {
|
|
952
|
-
const content = 'new test message ' + Date.now();
|
|
953
|
-
|
|
954
|
-
const result = await messageModel.createNewMessage({
|
|
955
|
-
role: 'user',
|
|
956
|
-
content,
|
|
957
|
-
sessionId: '1',
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
// Verify newly created message is in the list
|
|
961
|
-
const createdMessage = result.messages.find((m) => m.id === result.id);
|
|
962
|
-
expect(createdMessage).toBeDefined();
|
|
963
|
-
expect(createdMessage?.content).toBe(content);
|
|
964
|
-
expect(createdMessage?.role).toBe('user');
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
it('should return all messages for the session', async () => {
|
|
968
|
-
// Create multiple messages
|
|
969
|
-
await messageModel.create({ role: 'user', content: 'message 1', sessionId: '1' });
|
|
970
|
-
await messageModel.create({ role: 'assistant', content: 'message 2', sessionId: '1' });
|
|
971
|
-
|
|
972
|
-
// Create third message and get complete list
|
|
973
|
-
const result = await messageModel.createNewMessage({
|
|
974
|
-
role: 'user',
|
|
975
|
-
content: 'message 3',
|
|
976
|
-
sessionId: '1',
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
// Verify all messages are returned
|
|
980
|
-
expect(result.messages.length).toBeGreaterThanOrEqual(3);
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
it('should apply groupAssistantMessages transformation', async () => {
|
|
984
|
-
// Create an assistant message with tools
|
|
985
|
-
const assistantMsg = await messageModel.create({
|
|
986
|
-
role: 'assistant',
|
|
987
|
-
content: 'Checking weather',
|
|
988
|
-
tools: [
|
|
989
|
-
{
|
|
990
|
-
id: 'tool-1',
|
|
991
|
-
identifier: 'weather',
|
|
992
|
-
apiName: 'getWeather',
|
|
993
|
-
arguments: '{"city":"Beijing"}',
|
|
994
|
-
type: 'default',
|
|
995
|
-
},
|
|
996
|
-
],
|
|
997
|
-
sessionId: '1',
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
// Create corresponding tool message
|
|
1001
|
-
const toolMsg = await messageModel.create({
|
|
1002
|
-
role: 'tool',
|
|
1003
|
-
content: 'Beijing: Sunny, 25°C',
|
|
1004
|
-
tool_call_id: 'tool-1',
|
|
1005
|
-
sessionId: '1',
|
|
1006
|
-
plugin: {
|
|
1007
|
-
identifier: 'weather',
|
|
1008
|
-
apiName: 'getWeather',
|
|
1009
|
-
arguments: '{"city":"Beijing"}',
|
|
1010
|
-
type: 'default',
|
|
1011
|
-
},
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
// Create new message and get list
|
|
1015
|
-
const result = await messageModel.createNewMessage({
|
|
1016
|
-
role: 'user',
|
|
1017
|
-
content: 'Thanks!',
|
|
1018
|
-
sessionId: '1',
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
// Verify assistant message is grouped
|
|
1022
|
-
const groupMessage = result.messages.find((m) => m.id === assistantMsg.id);
|
|
1023
|
-
expect(groupMessage?.role).toBe('group');
|
|
1024
|
-
expect(groupMessage?.children).toBeDefined();
|
|
1025
|
-
expect(groupMessage?.children).toHaveLength(1);
|
|
1026
|
-
|
|
1027
|
-
// Verify tool results are merged into children
|
|
1028
|
-
const childBlock = groupMessage?.children?.[0];
|
|
1029
|
-
expect(childBlock?.tools).toBeDefined();
|
|
1030
|
-
expect(childBlock?.tools).toHaveLength(1);
|
|
1031
|
-
|
|
1032
|
-
// Verify tool contains execution result
|
|
1033
|
-
const tool = childBlock?.tools?.[0];
|
|
1034
|
-
expect(tool?.id).toBe('tool-1');
|
|
1035
|
-
expect(tool?.identifier).toBe('weather');
|
|
1036
|
-
expect(tool?.result).toBeDefined();
|
|
1037
|
-
expect(tool?.result?.content).toBe('Beijing: Sunny, 25°C');
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
it('should filter messages by topicId if provided', async () => {
|
|
1041
|
-
const topicId = 'topic-1';
|
|
1042
|
-
await serverDB.insert(topics).values({ id: topicId, sessionId: '1', userId });
|
|
1043
|
-
|
|
1044
|
-
// Create messages with different topics
|
|
1045
|
-
await messageModel.create({ role: 'user', content: 'topic 1 msg', sessionId: '1', topicId });
|
|
1046
|
-
await messageModel.create({ role: 'user', content: 'no topic msg', sessionId: '1' });
|
|
1047
|
-
|
|
1048
|
-
// Create new message and specify topicId
|
|
1049
|
-
const result = await messageModel.createNewMessage({
|
|
1050
|
-
role: 'user',
|
|
1051
|
-
content: 'new topic msg',
|
|
1052
|
-
sessionId: '1',
|
|
1053
|
-
topicId,
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
// Verify only messages from that topic are returned
|
|
1057
|
-
expect(result.messages.every((m) => m.topicId === topicId || m.topicId === undefined)).toBe(
|
|
1058
|
-
true,
|
|
1059
|
-
);
|
|
1060
|
-
expect(result.messages.find((m) => m.content === 'no topic msg')).toBeUndefined();
|
|
1061
|
-
});
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
935
|
describe('updateMessage', () => {
|
|
1065
936
|
it('should update message content', async () => {
|
|
1066
937
|
// Create test data
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ChatTranslate,
|
|
8
8
|
ChatVideoItem,
|
|
9
9
|
CreateMessageParams,
|
|
10
|
-
CreateMessageResult,
|
|
11
10
|
DBMessageItem,
|
|
12
11
|
ModelRankItem,
|
|
13
12
|
NewMessageQueryParams,
|
|
@@ -532,54 +531,6 @@ export class MessageModel {
|
|
|
532
531
|
});
|
|
533
532
|
};
|
|
534
533
|
|
|
535
|
-
/**
|
|
536
|
-
* Create a new message and return the complete message list
|
|
537
|
-
*
|
|
538
|
-
* This method combines message creation and querying into a single operation,
|
|
539
|
-
* reducing the need for separate refresh calls and improving performance.
|
|
540
|
-
*
|
|
541
|
-
* @param params - Message creation parameters
|
|
542
|
-
* @param options - Query options for post-processing
|
|
543
|
-
* @returns Object containing the created message ID and full message list
|
|
544
|
-
*
|
|
545
|
-
* @example
|
|
546
|
-
* const { id, messages } = await messageModel.createNewMessage({
|
|
547
|
-
* role: 'assistant',
|
|
548
|
-
* content: 'Hello',
|
|
549
|
-
* tools: [...],
|
|
550
|
-
* sessionId: 'session-1',
|
|
551
|
-
* });
|
|
552
|
-
* // messages already contains grouped structure, no need to refresh
|
|
553
|
-
*/
|
|
554
|
-
createNewMessage = async (
|
|
555
|
-
params: CreateMessageParams,
|
|
556
|
-
options: {
|
|
557
|
-
postProcessUrl?: (path: string | null, file: { fileType: string }) => Promise<string>;
|
|
558
|
-
} = {},
|
|
559
|
-
): Promise<CreateMessageResult> => {
|
|
560
|
-
// 1. Create the message (reuse existing create method)
|
|
561
|
-
const item = await this.create(params);
|
|
562
|
-
|
|
563
|
-
// 2. Query all messages for this session/topic
|
|
564
|
-
// query() method internally applies groupAssistantMessages transformation
|
|
565
|
-
const messages = await this.query(
|
|
566
|
-
{
|
|
567
|
-
current: 0,
|
|
568
|
-
groupId: params.groupId,
|
|
569
|
-
pageSize: 9999,
|
|
570
|
-
sessionId: params.sessionId,
|
|
571
|
-
topicId: params.topicId, // Get all messages
|
|
572
|
-
},
|
|
573
|
-
{ ...options, groupAssistantMessages: true },
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
// 3. Return the result
|
|
577
|
-
return {
|
|
578
|
-
id: item.id,
|
|
579
|
-
messages,
|
|
580
|
-
};
|
|
581
|
-
};
|
|
582
|
-
|
|
583
534
|
batchCreate = async (newMessages: DBMessageItem[]) => {
|
|
584
535
|
const messagesToInsert = newMessages.map((m) => {
|
|
585
536
|
// TODO: need a better way to handle this
|
|
@@ -29,9 +29,9 @@ export const getUserAuth = async () => {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @param authHeader -
|
|
34
|
-
* @returns Bearer Token
|
|
32
|
+
* Extract Bearer Token from authorization header
|
|
33
|
+
* @param authHeader - Authorization header (e.g. "Bearer xxx")
|
|
34
|
+
* @returns Bearer Token or null (if authorization header is invalid or does not exist)
|
|
35
35
|
*/
|
|
36
36
|
export const extractBearerToken = (authHeader?: string | null): string | null => {
|
|
37
37
|
if (!authHeader) return null;
|
|
@@ -51,9 +51,9 @@ export const extractBearerToken = (authHeader?: string | null): string | null =>
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @param authHeader - Oidc-Auth header
|
|
56
|
-
* @returns JWT token
|
|
54
|
+
* Extract JWT token from Oidc-Auth header
|
|
55
|
+
* @param authHeader - Oidc-Auth header value (e.g. "Oidc-Auth xxx")
|
|
56
|
+
* @returns JWT token or null (if authorization header is invalid or does not exist)
|
|
57
57
|
*/
|
|
58
58
|
export const extractOidcAuthToken = (authHeader?: string | null): string | null => {
|
|
59
59
|
if (!authHeader) return null;
|
|
@@ -13,19 +13,19 @@ const getLocalTime = (timeZone: string) => {
|
|
|
13
13
|
const isValidTimeZone = (timeZone: string) => {
|
|
14
14
|
try {
|
|
15
15
|
getLocalTime(timeZone);
|
|
16
|
-
return true; //
|
|
16
|
+
return true; // If no exception is thrown, the timezone is valid
|
|
17
17
|
} catch (e) {
|
|
18
|
-
//
|
|
18
|
+
// If a RangeError is caught, the timezone is invalid
|
|
19
19
|
if (e instanceof RangeError) {
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
|
-
//
|
|
22
|
+
// If it's another error, better to re-throw it
|
|
23
23
|
throw e;
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export const parseDefaultThemeFromCountry = (request: NextRequest) => {
|
|
28
|
-
// 1.
|
|
28
|
+
// 1. Get country code from request headers
|
|
29
29
|
const geo = geolocation(request);
|
|
30
30
|
|
|
31
31
|
const countryCode =
|
|
@@ -35,22 +35,22 @@ export const parseDefaultThemeFromCountry = (request: NextRequest) => {
|
|
|
35
35
|
request.headers.get('x-zeabur-ip-country') || // Zeabur
|
|
36
36
|
request.headers.get('x-country-code'); // Netlify
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// If no country code is obtained, return light theme directly
|
|
39
39
|
if (!countryCode) return 'light';
|
|
40
40
|
|
|
41
|
-
// 2.
|
|
41
|
+
// 2. Get timezone information for the country
|
|
42
42
|
const country = getCountry(countryCode);
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// If country information is not found or the country has no timezone information, return light theme
|
|
45
45
|
if (!country?.timezones?.length) return 'light';
|
|
46
46
|
|
|
47
47
|
const timeZone = country.timezones.find((tz) => isValidTimeZone(tz));
|
|
48
48
|
if (!timeZone) return 'light';
|
|
49
49
|
|
|
50
|
-
// 3.
|
|
50
|
+
// 3. Get the current time in the country's first timezone
|
|
51
51
|
const localTime = getLocalTime(timeZone);
|
|
52
52
|
|
|
53
|
-
// 4.
|
|
53
|
+
// 4. Parse the hour and determine the theme
|
|
54
54
|
const localHour = parseInt(localTime);
|
|
55
55
|
// console.log(
|
|
56
56
|
// `[theme] Country: ${countryCode}, Timezone: ${country.timezones[0]}, LocalHour: ${localHour}`,
|
|
@@ -3,15 +3,15 @@ import { ClientSecretPayload } from '@lobechat/types';
|
|
|
3
3
|
import { SECRET_XOR_KEY } from '@/const/auth';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Convert Base64 string to Uint8Array
|
|
7
7
|
*/
|
|
8
8
|
const base64ToUint8Array = (base64: string): Uint8Array => {
|
|
9
|
-
// Node.js
|
|
9
|
+
// Use Buffer directly in Node.js environment
|
|
10
10
|
return Buffer.from(base64, 'base64');
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Perform XOR operation on Uint8Array (same as the client-side xorProcess function)
|
|
15
15
|
*/
|
|
16
16
|
const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
|
|
17
17
|
const result = new Uint8Array(data.length);
|
|
@@ -22,7 +22,7 @@ const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Convert Uint8Array to string (UTF-8 decoding)
|
|
26
26
|
*/
|
|
27
27
|
const uint8ArrayToString = (arr: Uint8Array): string => {
|
|
28
28
|
return new TextDecoder().decode(arr);
|
|
@@ -31,13 +31,13 @@ const uint8ArrayToString = (arr: Uint8Array): string => {
|
|
|
31
31
|
export const getXorPayload = (token: string): ClientSecretPayload => {
|
|
32
32
|
const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY);
|
|
33
33
|
|
|
34
|
-
// 1. Base64
|
|
34
|
+
// 1. Base64 decoding
|
|
35
35
|
const base64DecodedBytes = base64ToUint8Array(token);
|
|
36
36
|
|
|
37
|
-
// 2. XOR
|
|
37
|
+
// 2. XOR deobfuscation
|
|
38
38
|
const xorDecryptedBytes = xorProcess(base64DecodedBytes, keyBytes);
|
|
39
39
|
|
|
40
|
-
// 3.
|
|
40
|
+
// 3. Convert to string and parse JSON
|
|
41
41
|
const decodedJsonString = uint8ArrayToString(xorDecryptedBytes);
|
|
42
42
|
|
|
43
43
|
return JSON.parse(decodedJsonString) as ClientSecretPayload;
|