@lobehub/chat 1.110.4 → 1.110.6

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 (34) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/apps/desktop/package.json +1 -1
  3. package/changelog/v1.json +14 -0
  4. package/locales/ar/setting.json +11 -0
  5. package/locales/bg-BG/setting.json +11 -0
  6. package/locales/de-DE/setting.json +11 -0
  7. package/locales/en-US/setting.json +11 -0
  8. package/locales/es-ES/setting.json +11 -0
  9. package/locales/fa-IR/setting.json +11 -0
  10. package/locales/fr-FR/setting.json +11 -0
  11. package/locales/it-IT/setting.json +11 -0
  12. package/locales/ja-JP/setting.json +11 -0
  13. package/locales/ko-KR/setting.json +11 -0
  14. package/locales/nl-NL/setting.json +11 -0
  15. package/locales/pl-PL/setting.json +11 -0
  16. package/locales/pt-BR/setting.json +11 -0
  17. package/locales/ru-RU/setting.json +11 -0
  18. package/locales/tr-TR/setting.json +11 -0
  19. package/locales/vi-VN/setting.json +11 -0
  20. package/locales/zh-CN/setting.json +11 -0
  21. package/locales/zh-TW/setting.json +11 -0
  22. package/package.json +46 -46
  23. package/packages/types/src/user/settings/general.ts +3 -0
  24. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +33 -5
  25. package/src/const/settings/common.ts +1 -0
  26. package/src/layout/GlobalProvider/AppTheme.tsx +4 -4
  27. package/src/libs/model-runtime/google/index.ts +8 -98
  28. package/src/libs/model-runtime/utils/googleErrorParser.test.ts +228 -0
  29. package/src/libs/model-runtime/utils/googleErrorParser.ts +228 -0
  30. package/src/locales/default/setting.ts +11 -0
  31. package/src/server/routers/lambda/agent.ts +2 -2
  32. package/src/server/services/user/index.ts +2 -2
  33. package/src/store/user/slices/settings/selectors/general.test.ts +1 -0
  34. package/src/store/user/slices/settings/selectors/general.ts +2 -0
@@ -0,0 +1,228 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AgentRuntimeErrorType } from '../error';
4
+
5
+ import {
6
+ cleanErrorMessage,
7
+ extractStatusCodeFromError,
8
+ parseGoogleErrorMessage,
9
+ } from './googleErrorParser';
10
+
11
+ describe('googleErrorParser', () => {
12
+ describe('cleanErrorMessage', () => {
13
+ it('should remove leading asterisk and spaces', () => {
14
+ const input = '* API key not valid. Please check your credentials.';
15
+ const expected = 'API key not valid. Please check your credentials.';
16
+ expect(cleanErrorMessage(input)).toBe(expected);
17
+ });
18
+
19
+ it('should convert escaped newlines to actual newlines', () => {
20
+ const input = 'Error occurred\\nPlease try again';
21
+ const expected = 'Error occurred Please try again';
22
+ expect(cleanErrorMessage(input)).toBe(expected);
23
+ });
24
+
25
+ it('should replace multiple newlines with single space', () => {
26
+ const input = 'Line 1\n\n\nLine 2\nLine 3';
27
+ const expected = 'Line 1 Line 2 Line 3';
28
+ expect(cleanErrorMessage(input)).toBe(expected);
29
+ });
30
+
31
+ it('should trim whitespace', () => {
32
+ const input = ' \t Error message \t ';
33
+ const expected = 'Error message';
34
+ expect(cleanErrorMessage(input)).toBe(expected);
35
+ });
36
+
37
+ it('should handle combined formatting issues', () => {
38
+ const input = '* API key not valid.\\nPlease check your credentials.\\n\\nContact support if needed. ';
39
+ const expected = 'API key not valid. Please check your credentials. Contact support if needed.';
40
+ expect(cleanErrorMessage(input)).toBe(expected);
41
+ });
42
+ });
43
+
44
+ describe('extractStatusCodeFromError', () => {
45
+ it('should extract status code and message correctly', () => {
46
+ const input = 'Connection failed [503 Service Unavailable] Please try again later';
47
+ const result = extractStatusCodeFromError(input);
48
+
49
+ expect(result.errorDetails).toEqual({
50
+ message: 'Please try again later',
51
+ statusCode: 503,
52
+ statusCodeText: '[503 Service Unavailable]',
53
+ });
54
+ expect(result.prefix).toBe('Connection failed');
55
+ });
56
+
57
+ it('should handle different status codes', () => {
58
+ const input = 'Request failed [401 Unauthorized] Invalid credentials';
59
+ const result = extractStatusCodeFromError(input);
60
+
61
+ expect(result.errorDetails).toEqual({
62
+ message: 'Invalid credentials',
63
+ statusCode: 401,
64
+ statusCodeText: '[401 Unauthorized]',
65
+ });
66
+ expect(result.prefix).toBe('Request failed');
67
+ });
68
+
69
+ it('should return null for messages without status codes', () => {
70
+ const input = 'Simple error message without status code';
71
+ const result = extractStatusCodeFromError(input);
72
+
73
+ expect(result.errorDetails).toBeNull();
74
+ expect(result.prefix).toBe('Simple error message without status code');
75
+ });
76
+
77
+ it('should handle empty message after status code', () => {
78
+ const input = 'Error [404 Not Found]';
79
+ const result = extractStatusCodeFromError(input);
80
+
81
+ expect(result.errorDetails).toEqual({
82
+ message: '',
83
+ statusCode: 404,
84
+ statusCodeText: '[404 Not Found]',
85
+ });
86
+ expect(result.prefix).toBe('Error');
87
+ });
88
+ });
89
+
90
+ describe('parseGoogleErrorMessage', () => {
91
+ it('should handle location not supported error', () => {
92
+ const input = 'This location is not supported for Google AI services';
93
+ const result = parseGoogleErrorMessage(input);
94
+
95
+ expect(result.errorType).toBe(AgentRuntimeErrorType.LocationNotSupportError);
96
+ expect(result.error.message).toBe(input);
97
+ });
98
+
99
+ it('should handle status JSON format', () => {
100
+ const input = 'got status: UNAVAILABLE. {"error":{"code":503,"message":"Service temporarily unavailable","status":"UNAVAILABLE"}}';
101
+ const result = parseGoogleErrorMessage(input);
102
+
103
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
104
+ expect(result.error).toEqual({
105
+ code: 503,
106
+ message: 'Service temporarily unavailable',
107
+ status: 'UNAVAILABLE',
108
+ });
109
+ });
110
+
111
+ it('should handle direct JSON parsing', () => {
112
+ const input = '{"error":{"code":400,"message":"* API key not valid. Please pass a valid API key.","status":"INVALID_ARGUMENT"}}';
113
+ const result = parseGoogleErrorMessage(input);
114
+
115
+ expect(result.errorType).toBe(AgentRuntimeErrorType.InvalidProviderAPIKey);
116
+ expect(result.error).toEqual({
117
+ code: 400,
118
+ message: 'API key not valid. Please pass a valid API key.',
119
+ status: 'INVALID_ARGUMENT',
120
+ });
121
+ });
122
+
123
+ it('should handle quota limit error', () => {
124
+ const input = '{"error":{"code":429,"message":"Quota limit reached","status":"RESOURCE_EXHAUSTED"}}';
125
+ const result = parseGoogleErrorMessage(input);
126
+
127
+ expect(result.errorType).toBe(AgentRuntimeErrorType.QuotaLimitReached);
128
+ expect(result.error).toEqual({
129
+ code: 429,
130
+ message: 'Quota limit reached',
131
+ status: 'RESOURCE_EXHAUSTED',
132
+ });
133
+ });
134
+
135
+ it('should handle nested JSON format', () => {
136
+ const input = '{"error":{"message":"{\\"error\\":{\\"code\\":400,\\"message\\":\\"Invalid request\\"}}"}}';
137
+ const result = parseGoogleErrorMessage(input);
138
+
139
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
140
+ expect(result.error).toEqual({
141
+ code: 400,
142
+ message: 'Invalid request',
143
+ status: '',
144
+ });
145
+ });
146
+
147
+ it('should handle array format with API_KEY_INVALID', () => {
148
+ const input = 'Request failed [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "API_KEY_INVALID", "domain": "googleapis.com"}]';
149
+ const result = parseGoogleErrorMessage(input);
150
+
151
+ expect(result.errorType).toBe(AgentRuntimeErrorType.InvalidProviderAPIKey);
152
+ });
153
+
154
+ it('should handle array format with other errors', () => {
155
+ const input = 'Request failed [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "QUOTA_EXCEEDED", "domain": "googleapis.com"}]';
156
+ const result = parseGoogleErrorMessage(input);
157
+
158
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
159
+ expect(result.error).toEqual([{
160
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
161
+ "reason": "QUOTA_EXCEEDED",
162
+ "domain": "googleapis.com"
163
+ }]);
164
+ });
165
+
166
+ it('should handle status code extraction fallback', () => {
167
+ const input = 'Connection failed [503 Service Unavailable] Please try again later';
168
+ const result = parseGoogleErrorMessage(input);
169
+
170
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
171
+ expect(result.error).toEqual({
172
+ message: 'Please try again later',
173
+ statusCode: 503,
174
+ statusCodeText: '[503 Service Unavailable]',
175
+ });
176
+ });
177
+
178
+ it('should handle complex nested JSON with message cleaning', () => {
179
+ const input = '{"error":{"code":400,"message":"* Request contains invalid parameters\\nPlease check the documentation\\n\\nContact support for help"}}';
180
+ const result = parseGoogleErrorMessage(input);
181
+
182
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
183
+ expect(result.error.message).toBe('Request contains invalid parameters Please check the documentation Contact support for help');
184
+ });
185
+
186
+ it('should return default error for unparseable messages', () => {
187
+ const input = 'Some random error message that cannot be parsed';
188
+ const result = parseGoogleErrorMessage(input);
189
+
190
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
191
+ expect(result.error.message).toBe(input);
192
+ });
193
+
194
+ it('should handle malformed JSON gracefully', () => {
195
+ const input = '{"error":{"code":400,"message":"Invalid JSON{incomplete';
196
+ const result = parseGoogleErrorMessage(input);
197
+
198
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
199
+ expect(result.error.message).toBe(input);
200
+ });
201
+
202
+ it('should handle empty error object in JSON', () => {
203
+ const input = '{"error":{}}';
204
+ const result = parseGoogleErrorMessage(input);
205
+
206
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
207
+ expect(result.error).toEqual({
208
+ code: null,
209
+ message: input,
210
+ status: '',
211
+ });
212
+ });
213
+
214
+ it('should handle recursion depth limit', () => {
215
+ // Create a deeply nested JSON that exceeds the recursion limit
216
+ let deeplyNested = '{"error":{"code":400,"message":"Deep error"}}';
217
+ for (let i = 0; i < 6; i++) {
218
+ deeplyNested = `{"error":{"message":"${deeplyNested.replaceAll('"', '\\"')}"}}`;
219
+ }
220
+
221
+ const result = parseGoogleErrorMessage(deeplyNested);
222
+
223
+ // Should still return a valid result, but might not reach the deepest level
224
+ expect(result.errorType).toBe(AgentRuntimeErrorType.ProviderBizError);
225
+ expect(result.error).toBeDefined();
226
+ });
227
+ });
228
+ });
@@ -0,0 +1,228 @@
1
+ import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../error';
2
+
3
+ export interface ParsedError {
4
+ error: any;
5
+ errorType: ILobeAgentRuntimeErrorType;
6
+ }
7
+
8
+ export interface GoogleChatError {
9
+ '@type': string;
10
+ 'domain': string;
11
+ 'metadata': {
12
+ service: string;
13
+ };
14
+ 'reason': string;
15
+ }
16
+
17
+ export type GoogleChatErrors = GoogleChatError[];
18
+
19
+ /**
20
+ * 清理错误消息,移除格式化字符和多余的空格
21
+ * @param message - 原始错误消息
22
+ * @returns 清理后的错误消息
23
+ */
24
+ export function cleanErrorMessage(message: string): string {
25
+ return message
26
+ .replaceAll(/^\*\s*/g, '') // 移除开头的星号和空格
27
+ .replaceAll('\\n', '\n') // 转换转义的换行符
28
+ .replaceAll(/\n+/g, ' ') // 将多个换行符替换为单个空格
29
+ .trim(); // 去除首尾空格
30
+ }
31
+
32
+ /**
33
+ * 从错误消息中提取状态码信息
34
+ * @param message - 错误消息
35
+ * @returns 提取的错误详情和前缀
36
+ */
37
+ export function extractStatusCodeFromError(message: string): {
38
+ errorDetails: any;
39
+ prefix: string;
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
+ };
60
+
61
+ return {
62
+ errorDetails: resultJson,
63
+ prefix: prefix,
64
+ };
65
+ }
66
+
67
+ // 如果无法匹配,返回原始消息
68
+ return {
69
+ errorDetails: null,
70
+ prefix: message,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * 解析Google AI API返回的错误消息
76
+ * @param message - 原始错误消息
77
+ * @returns 解析后的错误对象和错误类型
78
+ */
79
+ export function parseGoogleErrorMessage(message: string): ParsedError {
80
+ const defaultError = {
81
+ error: { message },
82
+ errorType: AgentRuntimeErrorType.ProviderBizError,
83
+ };
84
+
85
+ // 快速识别特殊错误
86
+ if (message.includes('location is not supported')) {
87
+ return { error: { message }, errorType: AgentRuntimeErrorType.LocationNotSupportError };
88
+ }
89
+
90
+ // 统一的错误类型判断函数
91
+ const getErrorType = (code: number | null, message: string): ILobeAgentRuntimeErrorType => {
92
+ if (code === 400 && message.includes('API key not valid')) {
93
+ return AgentRuntimeErrorType.InvalidProviderAPIKey;
94
+ } else if (code === 429) {
95
+ return AgentRuntimeErrorType.QuotaLimitReached;
96
+ }
97
+ return AgentRuntimeErrorType.ProviderBizError;
98
+ };
99
+
100
+ // 递归解析JSON,处理嵌套的JSON字符串
101
+ const parseJsonRecursively = (str: string, maxDepth: number = 5): any => {
102
+ if (maxDepth <= 0) return null;
103
+
104
+ try {
105
+ const parsed = JSON.parse(str);
106
+
107
+ // 如果解析出的对象包含error字段
108
+ if (parsed && typeof parsed === 'object' && parsed.error) {
109
+ const errorInfo = parsed.error;
110
+
111
+ // 清理错误消息
112
+ if (typeof errorInfo.message === 'string') {
113
+ errorInfo.message = cleanErrorMessage(errorInfo.message);
114
+
115
+ // 如果error.message还是一个JSON字符串,继续递归解析
116
+ try {
117
+ const nestedResult = parseJsonRecursively(errorInfo.message, maxDepth - 1);
118
+ // 只有当深层结果包含带有 code 的 error 对象时,才优先返回深层结果
119
+ if (nestedResult && nestedResult.error && nestedResult.error.code) {
120
+ return nestedResult;
121
+ }
122
+ } catch {
123
+ // 如果嵌套解析失败,使用当前层的信息
124
+ }
125
+ }
126
+
127
+ return parsed;
128
+ }
129
+
130
+ return parsed;
131
+ } catch {
132
+ return null;
133
+ }
134
+ };
135
+
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
+ };
157
+ }
158
+ }
159
+
160
+ // 2. 尝试直接解析整个消息作为JSON
161
+ const directParsed = parseJsonRecursively(message);
162
+ if (directParsed && directParsed.error) {
163
+ const errorInfo = directParsed.error;
164
+ const finalMessage = errorInfo.message || message;
165
+ const finalCode = errorInfo.code || null;
166
+ const finalStatus = errorInfo.status || '';
167
+
168
+ return {
169
+ error: {
170
+ code: finalCode,
171
+ message: finalMessage,
172
+ status: finalStatus,
173
+ },
174
+ errorType: getErrorType(finalCode, finalMessage),
175
+ };
176
+ }
177
+
178
+ // 3. 处理嵌套JSON格式,特别是message字段包含JSON的情况
179
+ try {
180
+ const firstLevelParsed = JSON.parse(message);
181
+ if (firstLevelParsed && firstLevelParsed.error && firstLevelParsed.error.message) {
182
+ const nestedParsed = parseJsonRecursively(firstLevelParsed.error.message);
183
+ if (nestedParsed && nestedParsed.error) {
184
+ const errorInfo = nestedParsed.error;
185
+ const finalMessage = errorInfo.message || message;
186
+ const finalCode = errorInfo.code || null;
187
+ const finalStatus = errorInfo.status || '';
188
+
189
+ return {
190
+ error: {
191
+ code: finalCode,
192
+ message: finalMessage,
193
+ status: finalStatus,
194
+ },
195
+ errorType: getErrorType(finalCode, finalMessage),
196
+ };
197
+ }
198
+ }
199
+ } catch {
200
+ // 继续其他解析方式
201
+ }
202
+
203
+ // 4. 原有的数组格式解析逻辑
204
+ const startIndex = message.lastIndexOf('[');
205
+ if (startIndex !== -1) {
206
+ try {
207
+ const jsonString = message.slice(startIndex);
208
+ const json: GoogleChatErrors = JSON.parse(jsonString);
209
+ const bizError = json[0];
210
+
211
+ if (bizError?.reason === 'API_KEY_INVALID') {
212
+ return { ...defaultError, errorType: AgentRuntimeErrorType.InvalidProviderAPIKey };
213
+ }
214
+
215
+ return { error: json, errorType: AgentRuntimeErrorType.ProviderBizError };
216
+ } catch {
217
+ // 忽略解析错误
218
+ }
219
+ }
220
+
221
+ // 5. 使用状态码提取逻辑作为最后的后备方案
222
+ const errorObj = extractStatusCodeFromError(message);
223
+ if (errorObj.errorDetails) {
224
+ return { error: errorObj.errorDetails, errorType: AgentRuntimeErrorType.ProviderBizError };
225
+ }
226
+
227
+ return defaultError;
228
+ }
@@ -186,10 +186,21 @@ export default {
186
186
  },
187
187
 
188
188
  settingAppearance: {
189
+ animationMode: {
190
+ agile: '敏捷',
191
+ desc: '选择应用程序的操作响应的动画速度',
192
+ disabled: '关闭',
193
+ elegant: '优雅',
194
+ title: '响应动画',
195
+ },
189
196
  neutralColor: {
190
197
  desc: '不同色彩倾向的灰阶自定义',
191
198
  title: '中性色',
192
199
  },
200
+ noAnimation: {
201
+ desc: '禁用应用程序中的所有动画效果',
202
+ title: '无动画模式',
203
+ },
193
204
  preview: {
194
205
  title: '调色盘',
195
206
  },
@@ -93,8 +93,8 @@ export const agentRouter = router({
93
93
  const user = await UserModel.findById(ctx.serverDB, ctx.userId);
94
94
  if (!user) return DEFAULT_AGENT_CONFIG;
95
95
 
96
- const res = await ctx.agentService.createInbox();
97
- pino.info('create inbox session', res);
96
+ await ctx.agentService.createInbox();
97
+ pino.info('create inbox session');
98
98
  }
99
99
  }
100
100
 
@@ -123,9 +123,9 @@ export class UserService {
123
123
  if (!file) {
124
124
  return null;
125
125
  }
126
- const fileBuffer = Buffer.from(file);
127
- return fileBuffer;
126
+ return Buffer.from(file);
128
127
  } catch (error) {
128
+ // @ts-expect-error 这里很奇怪,升级了 pino 就报错,我怀疑是 pino 的问题
129
129
  pino.error('Failed to get user avatar:', error);
130
130
  }
131
131
  };
@@ -16,6 +16,7 @@ describe('settingsSelectors', () => {
16
16
  const result = userGeneralSettingsSelectors.config(s as UserStore);
17
17
 
18
18
  expect(result).toEqual({
19
+ animationMode: 'agile',
19
20
  fontSize: 12,
20
21
  highlighterTheme: 'lobe-theme',
21
22
  mermaidTheme: 'lobe-theme',
@@ -9,8 +9,10 @@ const fontSize = (s: UserStore) => generalConfig(s).fontSize;
9
9
  const highlighterTheme = (s: UserStore) => generalConfig(s).highlighterTheme;
10
10
  const mermaidTheme = (s: UserStore) => generalConfig(s).mermaidTheme;
11
11
  const transitionMode = (s: UserStore) => generalConfig(s).transitionMode;
12
+ const animationMode = (s: UserStore) => generalConfig(s).animationMode;
12
13
 
13
14
  export const userGeneralSettingsSelectors = {
15
+ animationMode,
14
16
  config: generalConfig,
15
17
  fontSize,
16
18
  highlighterTheme,