@lobehub/chat 1.11.4 → 1.11.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 (67) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/ar/error.json +3 -1
  3. package/locales/bg-BG/error.json +3 -1
  4. package/locales/de-DE/error.json +3 -1
  5. package/locales/en-US/error.json +3 -1
  6. package/locales/es-ES/error.json +3 -1
  7. package/locales/fr-FR/error.json +3 -1
  8. package/locales/it-IT/error.json +3 -1
  9. package/locales/ja-JP/error.json +3 -1
  10. package/locales/ko-KR/error.json +3 -1
  11. package/locales/nl-NL/error.json +3 -1
  12. package/locales/pl-PL/error.json +3 -1
  13. package/locales/pt-BR/error.json +3 -1
  14. package/locales/ru-RU/error.json +3 -1
  15. package/locales/tr-TR/error.json +3 -1
  16. package/locales/vi-VN/error.json +3 -1
  17. package/locales/zh-CN/error.json +2 -0
  18. package/locales/zh-TW/error.json +3 -1
  19. package/package.json +2 -3
  20. package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/ArtifactList/index.tsx +1 -1
  21. package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/index.tsx +2 -2
  22. package/src/app/(main)/chat/(workspace)/@portal/Home/index.tsx +13 -0
  23. package/src/app/(main)/chat/(workspace)/@portal/_layout/Desktop.tsx +1 -1
  24. package/src/app/(main)/chat/(workspace)/@portal/components/SkeletonLoading.tsx +14 -0
  25. package/src/app/(main)/chat/(workspace)/@portal/default.tsx +6 -6
  26. package/src/app/(main)/chat/(workspace)/@portal/error.tsx +5 -0
  27. package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +1 -0
  28. package/src/app/(main)/chat/(workspace)/@portal/loading.tsx +3 -0
  29. package/src/app/(main)/chat/(workspace)/@portal/router.tsx +19 -0
  30. package/src/app/metadata.ts +2 -3
  31. package/src/config/app.ts +2 -2
  32. package/src/const/message.ts +2 -0
  33. package/src/features/Conversation/Messages/Assistant/index.tsx +2 -2
  34. package/src/features/Conversation/Messages/Default.tsx +8 -3
  35. package/src/libs/agent-runtime/error.ts +1 -0
  36. package/src/libs/agent-runtime/utils/streams/openai.test.ts +42 -0
  37. package/src/libs/agent-runtime/utils/streams/openai.ts +63 -40
  38. package/src/libs/agent-runtime/utils/streams/protocol.ts +1 -1
  39. package/src/libs/trpc/client/edge.ts +1 -0
  40. package/src/libs/trpc/middleware/userAuth.ts +1 -0
  41. package/src/locales/default/error.ts +6 -1
  42. package/src/server/ld.ts +8 -10
  43. package/src/services/__tests__/chat.test.ts +0 -1
  44. package/src/store/agent/slices/chat/action.ts +2 -1
  45. package/src/store/chat/slices/message/action.test.ts +4 -4
  46. package/src/store/chat/slices/message/action.ts +2 -2
  47. package/src/store/session/slices/session/action.ts +2 -1
  48. package/src/store/tool/slices/plugin/action.ts +2 -1
  49. package/src/store/user/slices/settings/action.ts +3 -1
  50. package/src/types/fetch.ts +1 -0
  51. package/src/utils/downloadFile.ts +18 -0
  52. package/src/utils/{fetch.test.ts → fetch/__tests__/fetchSSE.test.ts} +168 -258
  53. package/src/utils/fetch/__tests__/parseError.test.ts +89 -0
  54. package/src/utils/fetch/__tests__/parseToolCalls.test.ts +123 -0
  55. package/src/utils/fetch/fetchEventSource/index.ts +110 -0
  56. package/src/utils/fetch/fetchEventSource/parse.ts +182 -0
  57. package/src/utils/{fetch.ts → fetch/fetchSSE.ts} +100 -118
  58. package/src/utils/fetch/index.ts +2 -0
  59. package/src/utils/fetch/parseError.ts +26 -0
  60. package/src/utils/fetch/parseToolCalls.ts +25 -0
  61. package/vitest.config.ts +1 -0
  62. package/src/app/(main)/chat/(workspace)/@portal/index.tsx +0 -24
  63. /package/src/app/(main)/chat/(workspace)/@portal/{features/ArtifactUI → Artifacts}/Footer.tsx +0 -0
  64. /package/src/app/(main)/chat/(workspace)/@portal/{features/ArtifactUI → Artifacts}/ToolRender.tsx +0 -0
  65. /package/src/app/(main)/chat/(workspace)/@portal/{features/ArtifactUI → Artifacts}/index.tsx +0 -0
  66. /package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/ArtifactList/Item/index.tsx +0 -0
  67. /package/src/app/(main)/chat/(workspace)/@portal/{features → Home}/Artifacts/ArtifactList/Item/style.ts +0 -0
@@ -2,6 +2,8 @@ import { readableFromAsyncIterable } from 'ai';
2
2
  import OpenAI from 'openai';
3
3
  import type { Stream } from 'openai/streaming';
4
4
 
5
+ import { ChatMessageError } from '@/types/message';
6
+
5
7
  import { ChatStreamCallbacks } from '../../types';
6
8
  import {
7
9
  StreamProtocolChunk,
@@ -15,53 +17,74 @@ import {
15
17
  export const transformOpenAIStream = (chunk: OpenAI.ChatCompletionChunk): StreamProtocolChunk => {
16
18
  // maybe need another structure to add support for multiple choices
17
19
 
18
- const item = chunk.choices[0];
19
- if (!item) {
20
- return { data: chunk, id: chunk.id, type: 'data' };
21
- }
20
+ try {
21
+ const item = chunk.choices[0];
22
+ if (!item) {
23
+ return { data: chunk, id: chunk.id, type: 'data' };
24
+ }
22
25
 
23
- if (typeof item.delta?.content === 'string') {
24
- return { data: item.delta.content, id: chunk.id, type: 'text' };
25
- }
26
+ if (typeof item.delta?.content === 'string') {
27
+ return { data: item.delta.content, id: chunk.id, type: 'text' };
28
+ }
29
+
30
+ if (item.delta?.tool_calls) {
31
+ return {
32
+ data: item.delta.tool_calls.map(
33
+ (value, index): StreamToolCallChunkData => ({
34
+ function: value.function,
35
+ id: value.id || generateToolCallId(index, value.function?.name),
36
+
37
+ // mistral's tool calling don't have index and function field, it's data like:
38
+ // [{"id":"xbhnmTtY7","function":{"name":"lobe-image-designer____text2image____builtin","arguments":"{\"prompts\": [\"A photo of a small, fluffy dog with a playful expression and wagging tail.\", \"A watercolor painting of a small, energetic dog with a glossy coat and bright eyes.\", \"A vector illustration of a small, adorable dog with a short snout and perky ears.\", \"A drawing of a small, scruffy dog with a mischievous grin and a wagging tail.\"], \"quality\": \"standard\", \"seeds\": [123456, 654321, 111222, 333444], \"size\": \"1024x1024\", \"style\": \"vivid\"}"}}]
39
+
40
+ // minimax's tool calling don't have index field, it's data like:
41
+ // [{"id":"call_function_4752059746","type":"function","function":{"name":"lobe-image-designer____text2image____builtin","arguments":"{\"prompts\": [\"一个流浪的地球,背景是浩瀚"}}]
42
+
43
+ // so we need to add these default values
44
+ index: typeof value.index !== 'undefined' ? value.index : index,
45
+ type: value.type || 'function',
46
+ }),
47
+ ),
48
+ id: chunk.id,
49
+ type: 'tool_calls',
50
+ } as StreamProtocolToolCallChunk;
51
+ }
26
52
 
27
- if (item.delta?.tool_calls) {
53
+ // 给定结束原因
54
+ if (item.finish_reason) {
55
+ return { data: item.finish_reason, id: chunk.id, type: 'stop' };
56
+ }
57
+
58
+ if (item.delta?.content === null) {
59
+ return { data: item.delta, id: chunk.id, type: 'data' };
60
+ }
61
+
62
+ // 其余情况下,返回 delta 和 index
28
63
  return {
29
- data: item.delta.tool_calls.map(
30
- (value, index): StreamToolCallChunkData => ({
31
- function: value.function,
32
- id: value.id || generateToolCallId(index, value.function?.name),
33
-
34
- // mistral's tool calling don't have index and function field, it's data like:
35
- // [{"id":"xbhnmTtY7","function":{"name":"lobe-image-designer____text2image____builtin","arguments":"{\"prompts\": [\"A photo of a small, fluffy dog with a playful expression and wagging tail.\", \"A watercolor painting of a small, energetic dog with a glossy coat and bright eyes.\", \"A vector illustration of a small, adorable dog with a short snout and perky ears.\", \"A drawing of a small, scruffy dog with a mischievous grin and a wagging tail.\"], \"quality\": \"standard\", \"seeds\": [123456, 654321, 111222, 333444], \"size\": \"1024x1024\", \"style\": \"vivid\"}"}}]
36
-
37
- // minimax's tool calling don't have index field, it's data like:
38
- // [{"id":"call_function_4752059746","type":"function","function":{"name":"lobe-image-designer____text2image____builtin","arguments":"{\"prompts\": [\"一个流浪的地球,背景是浩瀚"}}]
39
-
40
- // so we need to add these default values
41
- index: typeof value.index !== 'undefined' ? value.index : index,
42
- type: value.type || 'function',
43
- }),
44
- ),
64
+ data: { delta: item.delta, id: chunk.id, index: item.index },
45
65
  id: chunk.id,
46
- type: 'tool_calls',
47
- } as StreamProtocolToolCallChunk;
48
- }
66
+ type: 'data',
67
+ };
68
+ } catch (e) {
69
+ const errorName = 'StreamChunkError';
70
+ console.error(`[${errorName}]`, e);
71
+ console.error(`[${errorName}] raw chunk:`, chunk);
49
72
 
50
- // 给定结束原因
51
- if (item.finish_reason) {
52
- return { data: item.finish_reason, id: chunk.id, type: 'stop' };
53
- }
73
+ const err = e as Error;
54
74
 
55
- if (item.delta?.content === null) {
56
- return { data: item.delta, id: chunk.id, type: 'data' };
57
- }
75
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
76
+ const errorData = {
77
+ body: {
78
+ message:
79
+ 'chat response streaming chunk parse error, please contact your API Provider to fix it.',
80
+ context: { error: { message: err.message, name: err.name }, chunk },
81
+ },
82
+ type: 'StreamChunkError',
83
+ } as ChatMessageError;
84
+ /* eslint-enable */
58
85
 
59
- // 其余情况下,返回 delta index
60
- return {
61
- data: { delta: item.delta, id: chunk.id, index: item.index },
62
- id: chunk.id,
63
- type: 'data',
64
- };
86
+ return { data: errorData, id: chunk.id, type: 'error' };
87
+ }
65
88
  };
66
89
 
67
90
  const chatStreamable = async function* (stream: AsyncIterable<OpenAI.ChatCompletionChunk>) {
@@ -13,7 +13,7 @@ export interface StreamStack {
13
13
  export interface StreamProtocolChunk {
14
14
  data: any;
15
15
  id?: string;
16
- type: 'text' | 'tool_calls' | 'data' | 'stop';
16
+ type: 'text' | 'tool_calls' | 'data' | 'stop' | 'error';
17
17
  }
18
18
 
19
19
  export interface StreamToolCallChunkData {
@@ -13,6 +13,7 @@ export const edgeClient = createTRPCClient<EdgeRouter>({
13
13
 
14
14
  return createHeaderWithAuth();
15
15
  },
16
+ maxURLLength: 2083,
16
17
  transformer: superjson,
17
18
  url: withBasePath('/trpc/edge'),
18
19
  }),
@@ -6,6 +6,7 @@ export const userAuth = trpc.middleware(async (opts) => {
6
6
  const { ctx } = opts;
7
7
  // `ctx.user` is nullable
8
8
  if (!ctx.userId) {
9
+ console.log('clerk auth:', ctx.clerkAuth);
9
10
  throw new TRPCError({ code: 'UNAUTHORIZED' });
10
11
  }
11
12
 
@@ -85,10 +85,15 @@ export default {
85
85
  * @deprecated
86
86
  */
87
87
  NoOpenAIAPIKey: 'OpenAI API Key 不正确或为空,请添加自定义 OpenAI API Key',
88
+ /**
89
+ * @deprecated
90
+ */
88
91
  OpenAIBizError: '请求 OpenAI 服务出错,请根据以下信息排查或重试',
89
92
 
90
93
  InvalidBedrockCredentials: 'Bedrock 鉴权未通过,请检查 AccessKeyId/SecretAccessKey 后重试',
91
-
94
+ StreamChunkError:
95
+ '流式请求的消息块解析错误,请检查当前 API 接口是否符合标准规范,或联系你的 API 供应商咨询',
96
+ UnknownChatFetchError: '很抱歉,遇到未知请求错误,请根据以下信息排查或重试',
92
97
  InvalidOllamaArgs: 'Ollama 配置不正确,请检查 Ollama 配置后重试',
93
98
  OllamaBizError: '请求 Ollama 服务出错,请根据以下信息排查或重试',
94
99
  OllamaServiceUnavailable:
package/src/server/ld.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import urlJoin from 'url-join';
2
2
 
3
- import { getAppConfig } from '@/config/app';
4
3
  import { EMAIL_BUSINESS, EMAIL_SUPPORT, OFFICIAL_SITE, OFFICIAL_URL, X } from '@/const/url';
5
4
 
6
5
  import pkg from '../../package.json';
7
6
 
8
- const { SITE_URL = OFFICIAL_URL } = getAppConfig();
9
7
  const LAST_MODIFIED = new Date().toISOString();
10
8
  export const AUTHOR_LIST = {
11
9
  arvinxx: {
@@ -70,7 +68,7 @@ class Ld {
70
68
 
71
69
  genOrganization() {
72
70
  return {
73
- '@id': this.getId(SITE_URL, '#organization'),
71
+ '@id': this.getId(OFFICIAL_URL, '#organization'),
74
72
  '@type': 'Organization',
75
73
  'alternateName': 'LobeChat',
76
74
  'contactPoint': {
@@ -102,7 +100,7 @@ class Ld {
102
100
 
103
101
  getAuthors(ids: string[] = []) {
104
102
  const defaultAuthor = {
105
- '@id': this.getId(SITE_URL, '#organization'),
103
+ '@id': this.getId(OFFICIAL_URL, '#organization'),
106
104
  '@type': 'Organization',
107
105
  };
108
106
  if (!ids || ids.length === 0) return defaultAuthor;
@@ -142,7 +140,7 @@ class Ld {
142
140
  '@id': fixedUrl,
143
141
  '@type': 'WebPage',
144
142
  'about': {
145
- '@id': this.getId(SITE_URL, '#organization'),
143
+ '@id': this.getId(OFFICIAL_URL, '#organization'),
146
144
  },
147
145
  'breadcrumbs': {
148
146
  '@id': this.getId(fixedUrl, '#breadcrumb'),
@@ -155,7 +153,7 @@ class Ld {
155
153
  },
156
154
  'inLanguage': 'en-US',
157
155
  'isPartOf': {
158
- '@id': this.getId(SITE_URL, '#website'),
156
+ '@id': this.getId(OFFICIAL_URL, '#website'),
159
157
  },
160
158
  'name': this.fixTitle(title),
161
159
  'primaryImageOfPage': {
@@ -188,15 +186,15 @@ class Ld {
188
186
 
189
187
  genWebSite() {
190
188
  const baseInfo: any = {
191
- '@id': this.getId(SITE_URL, '#website'),
189
+ '@id': this.getId(OFFICIAL_URL, '#website'),
192
190
  '@type': 'WebSite',
193
191
  'description': pkg.description,
194
192
  'inLanguage': 'en-US',
195
193
  'name': 'LobeChat',
196
194
  'publisher': {
197
- '@id': this.getId(SITE_URL, '#organization'),
195
+ '@id': this.getId(OFFICIAL_URL, '#organization'),
198
196
  },
199
- 'url': SITE_URL,
197
+ 'url': OFFICIAL_URL,
200
198
  };
201
199
 
202
200
  return baseInfo;
@@ -211,7 +209,7 @@ class Ld {
211
209
  }
212
210
 
213
211
  private fixUrl(url: string) {
214
- return urlJoin(SITE_URL, url);
212
+ return urlJoin(OFFICIAL_URL, url);
215
213
  }
216
214
  }
217
215
 
@@ -577,7 +577,6 @@ Get data from users`,
577
577
  body: JSON.stringify(expectedPayload),
578
578
  headers: expect.any(Object),
579
579
  method: 'POST',
580
- signal: expect.any(AbortSignal),
581
580
  });
582
581
  });
583
582
 
@@ -4,6 +4,7 @@ import { SWRResponse, mutate } from 'swr';
4
4
  import { DeepPartial } from 'utility-types';
5
5
  import { StateCreator } from 'zustand/vanilla';
6
6
 
7
+ import { MESSAGE_CANCEL_FLAT } from '@/const/message';
7
8
  import { INBOX_SESSION_ID } from '@/const/session';
8
9
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
9
10
  import { useClientDataSWR, useOnlyFetchOnceSWR } from '@/libs/swr';
@@ -168,7 +169,7 @@ export const createChatSlice: StateCreator<
168
169
 
169
170
  internal_createAbortController: (key) => {
170
171
  const abortController = get()[key] as AbortController;
171
- if (abortController) abortController.abort('canceled');
172
+ if (abortController) abortController.abort(MESSAGE_CANCEL_FLAT);
172
173
  const controller = new AbortController();
173
174
  set({ [key]: controller }, false, 'internal_createAbortController');
174
175
 
@@ -958,10 +958,10 @@ describe('chatMessage actions', () => {
958
958
  it('should not do anything if there is no abortController', async () => {
959
959
  const { result } = renderHook(() => useChatStore());
960
960
 
961
- // 确保没有设置 abortController
962
- useChatStore.setState({ abortController: undefined });
963
-
964
961
  await act(async () => {
962
+ // 确保没有设置 abortController
963
+ useChatStore.setState({ abortController: undefined });
964
+
965
965
  result.current.stopGenerateMessage();
966
966
  });
967
967
 
@@ -1089,7 +1089,7 @@ describe('chatMessage actions', () => {
1089
1089
 
1090
1090
  // Mock fetch to reject with an error
1091
1091
  const errorMessage = 'Error fetching AI response';
1092
- vi.mocked(fetch).mockRejectedValue(new Error(errorMessage));
1092
+ vi.mocked(fetch).mockRejectedValueOnce(new Error(errorMessage));
1093
1093
 
1094
1094
  await act(async () => {
1095
1095
  expect(
@@ -7,7 +7,7 @@ import { template } from 'lodash-es';
7
7
  import { SWRResponse, mutate } from 'swr';
8
8
  import { StateCreator } from 'zustand/vanilla';
9
9
 
10
- import { LOADING_FLAT } from '@/const/message';
10
+ import { LOADING_FLAT, MESSAGE_CANCEL_FLAT } from '@/const/message';
11
11
  import { TraceEventType, TraceNameMap } from '@/const/trace';
12
12
  import { useClientDataSWR } from '@/libs/swr';
13
13
  import { chatService } from '@/services/chat';
@@ -399,7 +399,7 @@ export const chatMessage: StateCreator<
399
399
  const { abortController, internal_toggleChatLoading } = get();
400
400
  if (!abortController) return;
401
401
 
402
- abortController.abort();
402
+ abortController.abort(MESSAGE_CANCEL_FLAT);
403
403
 
404
404
  internal_toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
405
405
  },
@@ -5,6 +5,7 @@ import { DeepPartial } from 'utility-types';
5
5
  import { StateCreator } from 'zustand/vanilla';
6
6
 
7
7
  import { message } from '@/components/AntdStaticMethods';
8
+ import { MESSAGE_CANCEL_FLAT } from '@/const/message';
8
9
  import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
9
10
  import { useClientDataSWR } from '@/libs/swr';
10
11
  import { sessionService } from '@/services/session';
@@ -188,7 +189,7 @@ export const createSessionSlice: StateCreator<
188
189
  const { activeId, refreshSessions } = get();
189
190
 
190
191
  const abortController = get().signalSessionMeta as AbortController;
191
- if (abortController) abortController.abort('canceled');
192
+ if (abortController) abortController.abort(MESSAGE_CANCEL_FLAT);
192
193
  const controller = new AbortController();
193
194
  set({ signalSessionMeta: controller }, false, 'updateSessionMetaSignal');
194
195
 
@@ -2,6 +2,7 @@ import { Schema, ValidationResult } from '@cfworker/json-schema';
2
2
  import useSWR, { SWRResponse } from 'swr';
3
3
  import { StateCreator } from 'zustand/vanilla';
4
4
 
5
+ import { MESSAGE_CANCEL_FLAT } from '@/const/message';
5
6
  import { pluginService } from '@/services/plugin';
6
7
  import { merge } from '@/utils/merge';
7
8
 
@@ -46,7 +47,7 @@ export const createPluginSlice: StateCreator<
46
47
  },
47
48
  updatePluginSettings: async (id, settings) => {
48
49
  const signal = get().updatePluginSettingsSignal;
49
- if (signal) signal.abort('canceled');
50
+ if (signal) signal.abort(MESSAGE_CANCEL_FLAT);
50
51
 
51
52
  const newSignal = new AbortController();
52
53
 
@@ -3,6 +3,7 @@ import isEqual from 'fast-deep-equal';
3
3
  import { DeepPartial } from 'utility-types';
4
4
  import type { StateCreator } from 'zustand/vanilla';
5
5
 
6
+ import { MESSAGE_CANCEL_FLAT } from '@/const/message';
6
7
  import { shareService } from '@/services/share';
7
8
  import { userService } from '@/services/user';
8
9
  import type { UserStore } from '@/store/user';
@@ -63,7 +64,8 @@ export const createSettingsSlice: StateCreator<
63
64
 
64
65
  internal_createSignal: () => {
65
66
  const abortController = get().updateSettingsSignal;
66
- if (abortController && !abortController.signal.aborted) abortController.abort('canceled');
67
+ if (abortController && !abortController.signal.aborted)
68
+ abortController.abort(MESSAGE_CANCEL_FLAT);
67
69
 
68
70
  const newSignal = new AbortController();
69
71
 
@@ -12,6 +12,7 @@ export const ChatErrorType = {
12
12
  NoOpenAIAPIKey: 'NoOpenAIAPIKey',
13
13
  OllamaServiceUnavailable: 'OllamaServiceUnavailable', // 未启动/检测到 Ollama 服务
14
14
  PluginFailToTransformArguments: 'PluginFailToTransformArguments',
15
+ UnknownChatFetchError: 'UnknownChatFetchError',
15
16
 
16
17
  // ******* 客户端错误 ******* //
17
18
  BadRequest: 400,
@@ -0,0 +1,18 @@
1
+ export const downloadFile = async (url: string, fileName: string) => {
2
+ try {
3
+ const res = await fetch(url);
4
+ const blob = await res.blob();
5
+
6
+ const blobUrl = window.URL.createObjectURL(blob);
7
+ const link = document.createElement('a');
8
+ link.href = blobUrl;
9
+ link.download = fileName;
10
+ link.style.display = 'none';
11
+ document.body.append(link);
12
+ link.click();
13
+ link.remove();
14
+ window.URL.revokeObjectURL(blobUrl);
15
+ } catch (error) {
16
+ console.error('Download failed:', error);
17
+ }
18
+ };