@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 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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Enhance message router with service layer and comprehensive tests."
6
+ ]
7
+ },
8
+ "date": "2025-11-05",
9
+ "version": "2.0.0-next.30"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.29",
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
- * 从授权头中提取 Bearer Token
33
- * @param authHeader - 授权头 (例如 "Bearer xxx")
34
- * @returns Bearer Token null(如果授权头无效或不存在)
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
- * Oidc-Auth header 中提取 JWT token
55
- * @param authHeader - Oidc-Auth header (例如 "Oidc-Auth xxx")
56
- * @returns JWT token null(如果授权头无效或不存在)
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
- // 捕获到 RangeError,说明时区无效
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
- // 如果没有获取到国家代码,直接返回 light 主题
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
- // 如果找不到国家信息或该国家没有时区信息,返回 light 主题
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
- * Base64 字符串转换为 Uint8Array
6
+ * Convert Base64 string to Uint8Array
7
7
  */
8
8
  const base64ToUint8Array = (base64: string): Uint8Array => {
9
- // Node.js 环境下直接使用 Buffer
9
+ // Use Buffer directly in Node.js environment
10
10
  return Buffer.from(base64, 'base64');
11
11
  };
12
12
 
13
13
  /**
14
- * Uint8Array 进行 XOR 运算 (与客户端的 xorProcess 函数相同)
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
- * Uint8Array 转换为字符串 (UTF-8 解码)
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. 转换为字符串并解析 JSON
40
+ // 3. Convert to string and parse JSON
41
41
  const decodedJsonString = uint8ArrayToString(xorDecryptedBytes);
42
42
 
43
43
  return JSON.parse(decodedJsonString) as ClientSecretPayload;