@lobehub/lobehub 2.0.0-next.16 → 2.0.0-next.18

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 (45) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -45
  3. package/README.zh-CN.md +2 -45
  4. package/changelog/v1.json +18 -0
  5. package/e2e/src/features/discover/smoke.feature +34 -1
  6. package/e2e/src/steps/discover/smoke.steps.ts +116 -4
  7. package/locales/ar/oauth.json +1 -0
  8. package/locales/bg-BG/oauth.json +1 -0
  9. package/locales/de-DE/oauth.json +1 -0
  10. package/locales/en-US/oauth.json +1 -0
  11. package/locales/es-ES/oauth.json +1 -0
  12. package/locales/fa-IR/oauth.json +1 -0
  13. package/locales/fr-FR/oauth.json +1 -0
  14. package/locales/it-IT/oauth.json +1 -0
  15. package/locales/ja-JP/oauth.json +1 -0
  16. package/locales/ko-KR/oauth.json +1 -0
  17. package/locales/nl-NL/oauth.json +1 -0
  18. package/locales/pl-PL/oauth.json +1 -0
  19. package/locales/pt-BR/oauth.json +1 -0
  20. package/locales/ru-RU/oauth.json +1 -0
  21. package/locales/tr-TR/oauth.json +1 -0
  22. package/locales/vi-VN/oauth.json +1 -0
  23. package/locales/zh-CN/oauth.json +1 -0
  24. package/locales/zh-TW/oauth.json +1 -0
  25. package/package.json +1 -1
  26. package/packages/model-runtime/src/utils/googleErrorParser.test.ts +125 -0
  27. package/packages/model-runtime/src/utils/googleErrorParser.ts +103 -77
  28. package/packages/types/src/message/ui/params.ts +98 -4
  29. package/packages/types/src/user/index.ts +13 -1
  30. package/packages/types/src/user/settings/index.ts +22 -0
  31. package/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -0
  32. package/src/app/[variants]/(main)/discover/(list)/features/SortButton/index.tsx +1 -1
  33. package/src/app/[variants]/(main)/discover/(list)/mcp/features/List/Item.tsx +1 -0
  34. package/src/app/[variants]/(main)/discover/(list)/model/features/List/Item.tsx +1 -0
  35. package/src/app/[variants]/(main)/discover/(list)/provider/features/List/Item.tsx +1 -0
  36. package/src/app/[variants]/(main)/discover/components/CategoryMenu.tsx +9 -1
  37. package/src/app/[variants]/oauth/consent/[uid]/Consent/BuiltinConsent.tsx +57 -0
  38. package/src/app/[variants]/oauth/consent/[uid]/{Consent.tsx → Consent/index.tsx} +9 -1
  39. package/src/app/[variants]/oauth/consent/[uid]/Login.tsx +9 -1
  40. package/src/locales/default/oauth.ts +1 -0
  41. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -41
  42. package/src/server/routers/lambda/message.ts +4 -11
  43. package/src/server/routers/lambda/user.ts +24 -25
  44. package/src/services/message/server.ts +0 -4
  45. package/src/services/message/type.ts +0 -2
@@ -86,6 +86,119 @@ describe('googleErrorParser', () => {
86
86
  });
87
87
  expect(result.prefix).toBe('Error');
88
88
  });
89
+
90
+ it('should handle multiple spaces between status code and text', () => {
91
+ const input = 'Error [500 Internal Server Error] Something went wrong';
92
+ const result = extractStatusCodeFromError(input);
93
+
94
+ expect(result.errorDetails).toEqual({
95
+ message: 'Something went wrong',
96
+ statusCode: 500,
97
+ statusCodeText: '[500 Internal Server Error]',
98
+ });
99
+ expect(result.prefix).toBe('Error');
100
+ });
101
+
102
+ it('should handle status code at the beginning of message', () => {
103
+ const input = '[429 Too Many Requests] Rate limit exceeded';
104
+ const result = extractStatusCodeFromError(input);
105
+
106
+ expect(result.errorDetails).toEqual({
107
+ message: 'Rate limit exceeded',
108
+ statusCode: 429,
109
+ statusCodeText: '[429 Too Many Requests]',
110
+ });
111
+ expect(result.prefix).toBe('');
112
+ });
113
+
114
+ it('should handle status code at the end of message', () => {
115
+ const input = 'Request failed [503 Service Unavailable]';
116
+ const result = extractStatusCodeFromError(input);
117
+
118
+ expect(result.errorDetails).toEqual({
119
+ message: '',
120
+ statusCode: 503,
121
+ statusCodeText: '[503 Service Unavailable]',
122
+ });
123
+ expect(result.prefix).toBe('Request failed');
124
+ });
125
+
126
+ it('should not be vulnerable to ReDoS attacks', () => {
127
+ // Test with a potentially malicious input that could cause catastrophic backtracking
128
+ const maliciousInput = 'Error ' + 'a'.repeat(10000) + ' [not matching]';
129
+ const startTime = Date.now();
130
+ const result = extractStatusCodeFromError(maliciousInput);
131
+ const endTime = Date.now();
132
+
133
+ // Should complete quickly (under 100ms) even with large input
134
+ expect(endTime - startTime).toBeLessThan(100);
135
+ expect(result.errorDetails).toBeNull();
136
+ });
137
+
138
+ it('should not be vulnerable to ReDoS with many spaces', () => {
139
+ // Test the specific case mentioned in the security warning: '[9 ' + many spaces
140
+ const maliciousInput = '[9 ' + ' '.repeat(10000) + ']';
141
+ const startTime = Date.now();
142
+ const result = extractStatusCodeFromError(maliciousInput);
143
+ const endTime = Date.now();
144
+
145
+ // Should complete quickly (under 100ms) even with many spaces
146
+ expect(endTime - startTime).toBeLessThan(100);
147
+ expect(result.errorDetails).toBeDefined();
148
+ });
149
+
150
+ it('should handle very long error messages efficiently', () => {
151
+ const longMessage = 'a'.repeat(50000);
152
+ const input = `Prefix [400 Bad Request] ${longMessage}`;
153
+ const startTime = Date.now();
154
+ const result = extractStatusCodeFromError(input);
155
+ const endTime = Date.now();
156
+
157
+ // Should complete quickly even with very long input
158
+ expect(endTime - startTime).toBeLessThan(100);
159
+ expect(result.errorDetails?.statusCode).toBe(400);
160
+ });
161
+
162
+ it('should safely handle edge cases with match.index', () => {
163
+ // Test various edge cases where match.index might be 0 or at different positions
164
+ const testCases = [
165
+ { input: '[400 Bad Request]', expectedPrefix: '' },
166
+ { input: '[500 Error] Message', expectedPrefix: '' },
167
+ { input: 'Some text [404 Not Found] More text', expectedPrefix: 'Some text' },
168
+ ];
169
+
170
+ testCases.forEach(({ input, expectedPrefix }) => {
171
+ const result = extractStatusCodeFromError(input);
172
+ expect(result.errorDetails).toBeDefined();
173
+ expect(result.prefix).toBe(expectedPrefix);
174
+ });
175
+ });
176
+
177
+ it('should find status code when there are multiple brackets', () => {
178
+ // Real-world case from Google AI API errors with JSON arrays in the message
179
+ const input =
180
+ '[GoogleGenerativeAI Error]: Error fetching: [400 Bad Request] API key not valid. [{"@type":"type.googleapis.com"}]';
181
+ const result = extractStatusCodeFromError(input);
182
+
183
+ expect(result.errorDetails).toEqual({
184
+ message: 'API key not valid. [{"@type":"type.googleapis.com"}]',
185
+ statusCode: 400,
186
+ statusCodeText: '[400 Bad Request]',
187
+ });
188
+ expect(result.prefix).toBe('[GoogleGenerativeAI Error]: Error fetching:');
189
+ });
190
+
191
+ it('should skip brackets that do not contain valid status codes', () => {
192
+ const input = 'Error [not a code] happened [500 Internal Server Error] please retry';
193
+ const result = extractStatusCodeFromError(input);
194
+
195
+ expect(result.errorDetails).toEqual({
196
+ message: 'please retry',
197
+ statusCode: 500,
198
+ statusCodeText: '[500 Internal Server Error]',
199
+ });
200
+ expect(result.prefix).toBe('Error [not a code] happened');
201
+ });
89
202
  });
90
203
 
91
204
  describe('parseGoogleErrorMessage', () => {
@@ -110,6 +223,18 @@ describe('googleErrorParser', () => {
110
223
  });
111
224
  });
112
225
 
226
+ it('should not be vulnerable to ReDoS in status JSON parsing', () => {
227
+ // Test with malicious input that could cause catastrophic backtracking
228
+ const maliciousInput = 'got status: UNAVAILABLE. ' + 'a'.repeat(10000);
229
+ const startTime = Date.now();
230
+ const result = parseGoogleErrorMessage(maliciousInput);
231
+ const endTime = Date.now();
232
+
233
+ // Should complete quickly (under 100ms) even with large input
234
+ expect(endTime - startTime).toBeLessThan(100);
235
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
236
+ });
237
+
113
238
  it('should handle direct JSON parsing', () => {
114
239
  const input =
115
240
  '{"error":{"code":400,"message":"* API key not valid. Please pass a valid API key.","status":"INVALID_ARGUMENT"}}';
@@ -17,64 +17,81 @@ export interface GoogleChatError {
17
17
  export type GoogleChatErrors = GoogleChatError[];
18
18
 
19
19
  /**
20
- * 清理错误消息,移除格式化字符和多余的空格
21
- * @param message - 原始错误消息
22
- * @returns 清理后的错误消息
20
+ * Clean error message by removing formatting characters and extra spaces
21
+ * @param message - Original error message
22
+ * @returns Cleaned error message
23
23
  */
24
24
  export function cleanErrorMessage(message: string): string {
25
25
  return message
26
- .replaceAll(/^\*\s*/g, '') // 移除开头的星号和空格
27
- .replaceAll('\\n', '\n') // 转换转义的换行符
28
- .replaceAll(/\n+/g, ' ') // 将多个换行符替换为单个空格
29
- .trim(); // 去除首尾空格
26
+ .replaceAll(/^\*\s*/g, '') // Remove leading asterisks and spaces
27
+ .replaceAll('\\n', '\n') // Convert escaped newlines
28
+ .replaceAll(/\n+/g, ' ') // Replace multiple newlines with single space
29
+ .trim(); // Trim leading/trailing spaces
30
30
  }
31
31
 
32
32
  /**
33
- * 从错误消息中提取状态码信息
34
- * @param message - 错误消息
35
- * @returns 提取的错误详情和前缀
33
+ * Extract status code information from error message
34
+ * @param message - Error message
35
+ * @returns Extracted error details and prefix
36
36
  */
37
37
  export function extractStatusCodeFromError(message: string): {
38
38
  errorDetails: any;
39
39
  prefix: string;
40
40
  } {
41
- // 使用正则表达式匹配状态码部分 [数字 描述文本]
42
- const regex = /^(.*?)(\[\d+ [^\]]+])(.*)$/;
43
- const match = message.match(regex);
44
-
45
- if (match) {
46
- const prefix = match[1].trim();
47
- const statusCodeWithBrackets = match[2].trim();
48
- const messageContent = match[3].trim();
49
-
50
- // 提取状态码数字
51
- const statusCodeMatch = statusCodeWithBrackets.match(/\[(\d+)/);
52
- const statusCode = statusCodeMatch ? parseInt(statusCodeMatch[1]) : null;
53
-
54
- // 创建包含状态码和消息的JSON
55
- const resultJson = {
56
- message: messageContent,
57
- statusCode: statusCode,
58
- statusCodeText: statusCodeWithBrackets,
59
- };
41
+ // Match status code pattern [number description text]
42
+ // Use string methods instead of regex to avoid ReDoS attacks
43
+ // We need to find a bracket that contains a status code (3-digit number followed by space and text)
44
+
45
+ let searchStart = 0;
46
+ // eslint-disable-next-line no-constant-condition
47
+ while (true) {
48
+ const openBracketIndex = message.indexOf('[', searchStart);
49
+ if (openBracketIndex === -1) {
50
+ return { errorDetails: null, prefix: message };
51
+ }
60
52
 
61
- return {
62
- errorDetails: resultJson,
63
- prefix: prefix,
64
- };
65
- }
53
+ const closeBracketIndex = message.indexOf(']', openBracketIndex);
54
+ if (closeBracketIndex === -1) {
55
+ return { errorDetails: null, prefix: message };
56
+ }
66
57
 
67
- // 如果无法匹配,返回原始消息
68
- return {
69
- errorDetails: null,
70
- prefix: message,
71
- };
58
+ const bracketContent = message.slice(openBracketIndex + 1, closeBracketIndex).trim();
59
+
60
+ // Find the first space to separate status code from description
61
+ const spaceIndex = bracketContent.indexOf(' ');
62
+ if (spaceIndex !== -1) {
63
+ const statusCodeStr = bracketContent.slice(0, spaceIndex);
64
+ const statusCode = parseInt(statusCodeStr, 10);
65
+
66
+ // Validate that statusCode is a valid HTTP status code (3 digits)
67
+ if (!isNaN(statusCode) && statusCode >= 100 && statusCode < 600) {
68
+ const statusText = bracketContent.slice(spaceIndex + 1).trim();
69
+ const prefix = message.slice(0, openBracketIndex).trim();
70
+ const messageContent = message.slice(closeBracketIndex + 1).trim();
71
+
72
+ // Create JSON containing status code and message
73
+ const resultJson = {
74
+ message: messageContent,
75
+ statusCode: statusCode,
76
+ statusCodeText: `[${statusCode} ${statusText}]`,
77
+ };
78
+
79
+ return {
80
+ errorDetails: resultJson,
81
+ prefix: prefix,
82
+ };
83
+ }
84
+ }
85
+
86
+ // Move to next bracket
87
+ searchStart = openBracketIndex + 1;
88
+ }
72
89
  }
73
90
 
74
91
  /**
75
- * 解析Google AI API返回的错误消息
76
- * @param message - 原始错误消息
77
- * @returns 解析后的错误对象和错误类型
92
+ * Parse error message from Google AI API
93
+ * @param message - Original error message
94
+ * @returns Parsed error object and error type
78
95
  */
79
96
  export function parseGoogleErrorMessage(message: string): ParsedError {
80
97
  const defaultError = {
@@ -82,12 +99,12 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
82
99
  errorType: AgentRuntimeErrorType.ProviderBizError,
83
100
  };
84
101
 
85
- // 快速识别特殊错误
102
+ // Quick identification of special errors
86
103
  if (message.includes('location is not supported')) {
87
104
  return { error: { message }, errorType: AgentRuntimeErrorType.LocationNotSupportError };
88
105
  }
89
106
 
90
- // 统一的错误类型判断函数
107
+ // Unified error type determination function
91
108
  const getErrorType = (code: number | null, message: string): ILobeAgentRuntimeErrorType => {
92
109
  if (code === 400 && message.includes('API key not valid')) {
93
110
  return AgentRuntimeErrorType.InvalidProviderAPIKey;
@@ -97,30 +114,30 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
97
114
  return AgentRuntimeErrorType.ProviderBizError;
98
115
  };
99
116
 
100
- // 递归解析JSON,处理嵌套的JSON字符串
117
+ // Recursively parse JSON, handling nested JSON strings
101
118
  const parseJsonRecursively = (str: string, maxDepth: number = 5): any => {
102
119
  if (maxDepth <= 0) return null;
103
120
 
104
121
  try {
105
122
  const parsed = JSON.parse(str);
106
123
 
107
- // 如果解析出的对象包含error字段
124
+ // If parsed object contains error field
108
125
  if (parsed && typeof parsed === 'object' && parsed.error) {
109
126
  const errorInfo = parsed.error;
110
127
 
111
- // 清理错误消息
128
+ // Clean error message
112
129
  if (typeof errorInfo.message === 'string') {
113
130
  errorInfo.message = cleanErrorMessage(errorInfo.message);
114
131
 
115
- // 如果error.message还是一个JSON字符串,继续递归解析
132
+ // If error.message is still a JSON string, continue recursive parsing
116
133
  try {
117
134
  const nestedResult = parseJsonRecursively(errorInfo.message, maxDepth - 1);
118
- // 只有当深层结果包含带有 code error 对象时,才优先返回深层结果
135
+ // Only return deeper result if it contains an error object with code
119
136
  if (nestedResult && nestedResult.error && nestedResult.error.code) {
120
137
  return nestedResult;
121
138
  }
122
139
  } catch {
123
- // 如果嵌套解析失败,使用当前层的信息
140
+ // If nested parsing fails, use current layer info
124
141
  }
125
142
  }
126
143
 
@@ -133,31 +150,40 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
133
150
  }
134
151
  };
135
152
 
136
- // 1. 处理 "got status: UNAVAILABLE. {JSON}" 格式
137
- const statusJsonMatch = message.match(/got status: (\w+)\.\s*({.*})$/);
138
- if (statusJsonMatch) {
139
- const statusFromMessage = statusJsonMatch[1];
140
- const jsonPart = statusJsonMatch[2];
141
-
142
- const parsedError = parseJsonRecursively(jsonPart);
143
- if (parsedError && parsedError.error) {
144
- const errorInfo = parsedError.error;
145
- const finalMessage = errorInfo.message || message;
146
- const finalCode = errorInfo.code || null;
147
- const finalStatus = errorInfo.status || statusFromMessage;
148
-
149
- return {
150
- error: {
151
- code: finalCode,
152
- message: finalMessage,
153
- status: finalStatus,
154
- },
155
- errorType: getErrorType(finalCode, finalMessage),
156
- };
153
+ // 1. Handle "got status: UNAVAILABLE. {JSON}" format
154
+ const statusPrefix = 'got status: ';
155
+ const statusPrefixIndex = message.indexOf(statusPrefix);
156
+ if (statusPrefixIndex !== -1) {
157
+ const afterPrefix = message.slice(statusPrefixIndex + statusPrefix.length);
158
+ const dotIndex = afterPrefix.indexOf('.');
159
+ if (dotIndex !== -1) {
160
+ const statusFromMessage = afterPrefix.slice(0, dotIndex).trim();
161
+ const afterDot = afterPrefix.slice(dotIndex + 1).trim();
162
+ const braceIndex = afterDot.indexOf('{');
163
+ if (braceIndex !== -1) {
164
+ const jsonPart = afterDot.slice(braceIndex);
165
+
166
+ const parsedError = parseJsonRecursively(jsonPart);
167
+ if (parsedError && parsedError.error) {
168
+ const errorInfo = parsedError.error;
169
+ const finalMessage = errorInfo.message || message;
170
+ const finalCode = errorInfo.code || null;
171
+ const finalStatus = errorInfo.status || statusFromMessage;
172
+
173
+ return {
174
+ error: {
175
+ code: finalCode,
176
+ message: finalMessage,
177
+ status: finalStatus,
178
+ },
179
+ errorType: getErrorType(finalCode, finalMessage),
180
+ };
181
+ }
182
+ }
157
183
  }
158
184
  }
159
185
 
160
- // 2. 尝试直接解析整个消息作为JSON
186
+ // 2. Try to parse entire message as JSON directly
161
187
  const directParsed = parseJsonRecursively(message);
162
188
  if (directParsed && directParsed.error) {
163
189
  const errorInfo = directParsed.error;
@@ -175,7 +201,7 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
175
201
  };
176
202
  }
177
203
 
178
- // 3. 处理嵌套JSON格式,特别是message字段包含JSON的情况
204
+ // 3. Handle nested JSON format, especially when message field contains JSON
179
205
  try {
180
206
  const firstLevelParsed = JSON.parse(message);
181
207
  if (firstLevelParsed && firstLevelParsed.error && firstLevelParsed.error.message) {
@@ -197,10 +223,10 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
197
223
  }
198
224
  }
199
225
  } catch {
200
- // 继续其他解析方式
226
+ // Continue with other parsing methods
201
227
  }
202
228
 
203
- // 4. 原有的数组格式解析逻辑
229
+ // 4. Original array format parsing logic
204
230
  const startIndex = message.lastIndexOf('[');
205
231
  if (startIndex !== -1) {
206
232
  try {
@@ -214,11 +240,11 @@ export function parseGoogleErrorMessage(message: string): ParsedError {
214
240
 
215
241
  return { error: json, errorType: AgentRuntimeErrorType.ProviderBizError };
216
242
  } catch {
217
- // 忽略解析错误
243
+ // Ignore parsing errors
218
244
  }
219
245
  }
220
246
 
221
- // 5. 使用状态码提取逻辑作为最后的后备方案
247
+ // 5. Use status code extraction logic as last fallback
222
248
  const errorObj = extractStatusCodeFromError(message);
223
249
  if (errorObj.errorDetails) {
224
250
  return { error: errorObj.errorDetails, errorType: AgentRuntimeErrorType.ProviderBizError };
@@ -1,9 +1,14 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
2
+ import { z } from 'zod';
3
+
2
4
  import { UploadFileItem } from '../../files';
3
5
  import { MessageSemanticSearchChunk } from '../../rag';
4
- import { ChatMessageError } from '../common/base';
6
+ import { ChatMessageError, ChatMessageErrorSchema } from '../common/base';
5
7
  import { ChatPluginPayload } from '../common/tools';
6
- import { UIChatMessage, UIMessageRoleType } from './chat';
8
+ import { UIChatMessage } from './chat';
9
+ import { SemanticSearchChunkSchema } from './rag';
10
+
11
+ export type CreateMessageRoleType = 'user' | 'assistant' | 'tool' | 'supervisor';
7
12
 
8
13
  export interface CreateMessageParams
9
14
  extends Partial<Omit<UIChatMessage, 'content' | 'role' | 'topicId' | 'chunksList'>> {
@@ -14,7 +19,7 @@ export interface CreateMessageParams
14
19
  fromModel?: string;
15
20
  fromProvider?: string;
16
21
  groupId?: string;
17
- role: UIMessageRoleType;
22
+ role: CreateMessageRoleType;
18
23
  sessionId: string;
19
24
  targetId?: string | null;
20
25
  threadId?: string | null;
@@ -28,7 +33,7 @@ export interface CreateMessageParams
28
33
  */
29
34
  export interface CreateNewMessageParams {
30
35
  // ========== Required fields ==========
31
- role: UIMessageRoleType;
36
+ role: CreateMessageRoleType;
32
37
  content: string;
33
38
  sessionId: string;
34
39
 
@@ -103,3 +108,92 @@ export interface SendGroupMessageParams {
103
108
  */
104
109
  targetMemberId?: string | null;
105
110
  }
111
+
112
+ // ========== Zod Schemas ========== //
113
+
114
+ const UIMessageRoleTypeSchema = z.enum(['user', 'assistant', 'tool', 'supervisor']);
115
+
116
+ const ChatPluginPayloadSchema = z.object({
117
+ apiName: z.string(),
118
+ arguments: z.string(),
119
+ identifier: z.string(),
120
+ type: z.string(),
121
+ });
122
+
123
+ export const CreateMessageParamsSchema = z
124
+ .object({
125
+ content: z.string(),
126
+ role: UIMessageRoleTypeSchema,
127
+ sessionId: z.string().nullable().optional(),
128
+ error: ChatMessageErrorSchema.nullable().optional(),
129
+ fileChunks: z.array(SemanticSearchChunkSchema).optional(),
130
+ files: z.array(z.string()).optional(),
131
+ fromModel: z.string().optional(),
132
+ fromProvider: z.string().optional(),
133
+ groupId: z.string().optional(),
134
+ targetId: z.string().nullable().optional(),
135
+ threadId: z.string().nullable().optional(),
136
+ topicId: z.string().optional(),
137
+ traceId: z.string().optional(),
138
+ // Allow additional fields from UIChatMessage (many can be null)
139
+ agentId: z.string().optional(),
140
+ children: z.any().optional(),
141
+ chunksList: z.any().optional(),
142
+ createdAt: z.number().optional(),
143
+ extra: z.any().optional(),
144
+ favorite: z.boolean().optional(),
145
+ fileList: z.any().optional(),
146
+ id: z.string().optional(),
147
+ imageList: z.any().optional(),
148
+ meta: z.any().optional(),
149
+ metadata: z.any().nullable().optional(),
150
+ model: z.string().nullable().optional(),
151
+ observationId: z.string().optional(),
152
+ parentId: z.string().optional(),
153
+ performance: z.any().optional(),
154
+ plugin: z.any().optional(),
155
+ pluginError: z.any().optional(),
156
+ pluginState: z.any().optional(),
157
+ provider: z.string().nullable().optional(),
158
+ quotaId: z.string().optional(),
159
+ ragQuery: z.string().nullable().optional(),
160
+ ragQueryId: z.string().nullable().optional(),
161
+ reasoning: z.any().optional(),
162
+ search: z.any().optional(),
163
+ tool_call_id: z.string().optional(),
164
+ toolCalls: z.any().optional(),
165
+ tools: z.any().optional(),
166
+ translate: z.any().optional(),
167
+ tts: z.any().optional(),
168
+ updatedAt: z.number().optional(),
169
+ })
170
+ .passthrough();
171
+
172
+ export const CreateNewMessageParamsSchema = z
173
+ .object({
174
+ // Required fields
175
+ role: UIMessageRoleTypeSchema,
176
+ content: z.string(),
177
+ sessionId: z.string().nullable().optional(),
178
+ // Tool related
179
+ tool_call_id: z.string().optional(),
180
+ plugin: ChatPluginPayloadSchema.optional(),
181
+ // Grouping
182
+ parentId: z.string().optional(),
183
+ groupId: z.string().optional(),
184
+ // Context
185
+ topicId: z.string().optional(),
186
+ threadId: z.string().nullable().optional(),
187
+ targetId: z.string().nullable().optional(),
188
+ // Model info
189
+ model: z.string().nullable().optional(),
190
+ provider: z.string().nullable().optional(),
191
+ // Content
192
+ files: z.array(z.string()).optional(),
193
+ // Error handling
194
+ error: ChatMessageErrorSchema.nullable().optional(),
195
+ // Metadata
196
+ traceId: z.string().optional(),
197
+ fileChunks: z.array(SemanticSearchChunkSchema).optional(),
198
+ })
199
+ .passthrough();
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
 
4
4
  import { Plans } from '../subscription';
5
5
  import { TopicDisplayMode } from '../topic';
6
- import { UserSettings } from '../user/settings';
6
+ import { UserSettings } from './settings';
7
7
 
8
8
  export interface LobeUser {
9
9
  avatar?: string;
@@ -74,3 +74,15 @@ export const NextAuthAccountSchame = z.object({
74
74
  provider: z.string(),
75
75
  providerAccountId: z.string(),
76
76
  });
77
+
78
+ export const UserPreferenceSchema = z
79
+ .object({
80
+ disableInputMarkdownRender: z.boolean().optional(),
81
+ enableGroupChat: z.boolean().optional(),
82
+ guide: UserGuideSchema.optional(),
83
+ hideSyncAlert: z.boolean().optional(),
84
+ telemetry: z.boolean().nullable(),
85
+ topicDisplayMode: z.nativeEnum(TopicDisplayMode).optional(),
86
+ useCmdEnterToSend: z.boolean().optional(),
87
+ })
88
+ .partial();
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  import type { LobeAgentSettings } from '../../session';
2
4
  import { UserGeneralConfig } from './general';
3
5
  import { UserHotkeyConfig } from './hotkey';
@@ -18,6 +20,7 @@ export * from './keyVaults';
18
20
  export * from './modelProvider';
19
21
  export * from './sync';
20
22
  export * from './systemAgent';
23
+ export * from './tool';
21
24
  export * from './tts';
22
25
 
23
26
  /**
@@ -34,3 +37,22 @@ export interface UserSettings {
34
37
  tool: UserToolConfig;
35
38
  tts: UserTTSConfig;
36
39
  }
40
+
41
+ /**
42
+ * Zod schema for partial UserSettings updates
43
+ * Uses passthrough to allow any nested settings fields
44
+ */
45
+ export const UserSettingsSchema = z
46
+ .object({
47
+ defaultAgent: z.any().optional(),
48
+ general: z.any().optional(),
49
+ hotkey: z.any().optional(),
50
+ image: z.any().optional(),
51
+ keyVaults: z.any().optional(),
52
+ languageModel: z.any().optional(),
53
+ systemAgent: z.any().optional(),
54
+ tool: z.any().optional(),
55
+ tts: z.any().optional(),
56
+ })
57
+ .passthrough()
58
+ .partial();
@@ -52,6 +52,7 @@ const Pagination = memo<PaginationProps>(({ tab, currentPage, total, pageSize })
52
52
  <Page
53
53
  className={styles.page}
54
54
  current={page ? Number(page) : currentPage}
55
+ data-testid="pagination"
55
56
  onChange={handlePageChange}
56
57
  pageSize={pageSize}
57
58
  showSizeChanger={false}
@@ -174,7 +174,7 @@ const SortButton = memo(() => {
174
174
  }}
175
175
  trigger={['click', 'hover']}
176
176
  >
177
- <Button icon={<Icon icon={ArrowDownWideNarrow} />} type={'text'}>
177
+ <Button data-testid="sort-dropdown" icon={<Icon icon={ArrowDownWideNarrow} />} type={'text'}>
178
178
  {activeItem.label}
179
179
  <Icon icon={ChevronDown} />
180
180
  </Button>
@@ -82,6 +82,7 @@ const McpItem = memo<DiscoverMcpItem>(
82
82
  return (
83
83
  <Block
84
84
  clickable
85
+ data-testid="mcp-item"
85
86
  height={'100%'}
86
87
  onClick={() => {
87
88
  navigate(link);
@@ -61,6 +61,7 @@ const ModelItem = memo<DiscoverModelItem>(
61
61
  return (
62
62
  <Block
63
63
  clickable
64
+ data-testid="model-item"
64
65
  height={'100%'}
65
66
  onClick={() => {
66
67
  navigate(link);
@@ -54,6 +54,7 @@ const ProviderItem = memo<DiscoverProviderItem>(
54
54
  return (
55
55
  <Block
56
56
  clickable
57
+ data-testid="provider-item"
57
58
  height={'100%'}
58
59
  onClick={() => {
59
60
  navigate(link);
@@ -31,7 +31,15 @@ const useStyles = createStyles(({ css, prefixCls }) => {
31
31
  const CategoryMenu = memo<MenuProps>(({ style, ...rest }) => {
32
32
  const { styles } = useStyles();
33
33
 
34
- return <Menu className={styles.menu} mode="inline" style={style} {...rest} />;
34
+ return (
35
+ <Menu
36
+ className={styles.menu}
37
+ data-testid="category-menu"
38
+ mode="inline"
39
+ style={style}
40
+ {...rest}
41
+ />
42
+ );
35
43
  });
36
44
 
37
45
  export default CategoryMenu;