@lobehub/chat 1.128.8 → 1.128.9

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.
@@ -13,8 +13,13 @@ jobs:
13
13
  steps:
14
14
  - uses: actions/checkout@v5
15
15
 
16
- - name: Install dbdocs
17
- run: sudo npm install -g dbdocs
16
+ - name: Install bun
17
+ uses: oven-sh/setup-bun@v2
18
+ with:
19
+ bun-version: ${{ secrets.BUN_VERSION }}
20
+
21
+ - name: Install deps
22
+ run: bun i
18
23
 
19
24
  - name: Check dbdocs
20
25
  run: dbdocs
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.128.9](https://github.com/lobehub/lobe-chat/compare/v1.128.8...v1.128.9)
6
+
7
+ <sup>Released on **2025-09-15**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Improve error handle with agent config, support `.doc` file parse.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Improve error handle with agent config, closes [#9263](https://github.com/lobehub/lobe-chat/issues/9263) ([6656217](https://github.com/lobehub/lobe-chat/commit/6656217))
21
+ - **misc**: Support `.doc` file parse, closes [#8182](https://github.com/lobehub/lobe-chat/issues/8182) ([ed42753](https://github.com/lobehub/lobe-chat/commit/ed42753))
22
+
23
+ </details>
24
+
25
+ <div align="right">
26
+
27
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
28
+
29
+ </div>
30
+
5
31
  ### [Version 1.128.8](https://github.com/lobehub/lobe-chat/compare/v1.128.7...v1.128.8)
6
32
 
7
33
  <sup>Released on **2025-09-15**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Improve error handle with agent config, support .doc file parse."
6
+ ]
7
+ },
8
+ "date": "2025-09-15",
9
+ "version": "1.128.9"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.128.8",
3
+ "version": "1.128.9",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -279,6 +279,7 @@
279
279
  "url-join": "^5.0.0",
280
280
  "use-merge-value": "^1.2.0",
281
281
  "uuid": "^11.1.0",
282
+ "word-extractor": "^1.0.4",
282
283
  "ws": "^8.18.3",
283
284
  "yaml": "^2.8.1",
284
285
  "zod": "^3.25.76",
@@ -37,6 +37,7 @@ const getFileType = (filePath: string): SupportedFileType | undefined => {
37
37
  log('File type identified as pdf');
38
38
  return 'pdf';
39
39
  }
40
+ case 'doc':
40
41
  case 'docx': {
41
42
  log('File type identified as docx');
42
43
  return 'docx';
@@ -12,7 +12,12 @@ export class DocxLoader implements FileLoaderInterface {
12
12
  async loadPages(filePath: string): Promise<DocumentPage[]> {
13
13
  log('Loading DOCX file:', filePath);
14
14
  try {
15
- const loader = new LangchainDocxLoader(filePath);
15
+ let loader: LangchainDocxLoader;
16
+ if (filePath.endsWith('.doc')) {
17
+ loader = new LangchainDocxLoader(filePath, { type: 'doc' });
18
+ } else {
19
+ loader = new LangchainDocxLoader(filePath, { type: 'docx' });
20
+ }
16
21
  log('LangChain DocxLoader created');
17
22
  const docs = await loader.load(); // Langchain DocxLoader typically loads the whole doc as one
18
23
  log('DOCX document loaded, parts:', docs.length);
@@ -417,6 +417,78 @@ describe('LobeOpenAICompatibleFactory', () => {
417
417
  'event: text\n',
418
418
  'data: "Hello"\n\n',
419
419
  'id: a\n',
420
+ 'event: usage\n',
421
+ 'data: {"inputTextTokens":5,"outputTextTokens":5,"totalInputTokens":5,"totalOutputTokens":5,"totalTokens":10}\n\n',
422
+ 'id: output_speed\n',
423
+ 'event: speed\n',
424
+ expect.stringMatching(/^data: \{.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft 测试结果不一样
425
+ 'id: a\n',
426
+ 'event: stop\n',
427
+ 'data: "stop"\n\n',
428
+ ]);
429
+
430
+ expect((await reader.read()).done).toBe(true);
431
+ });
432
+
433
+ it('should transform non-streaming response to stream correctly with reasoning content', async () => {
434
+ const mockResponse = {
435
+ id: 'a',
436
+ object: 'chat.completion',
437
+ created: 123,
438
+ model: 'deepseek/deepseek-reasoner',
439
+ choices: [
440
+ {
441
+ index: 0,
442
+ message: {
443
+ role: 'assistant',
444
+ content: 'Hello',
445
+ reasoning_content: 'Thinking content',
446
+ },
447
+ finish_reason: 'stop',
448
+ logprobs: null,
449
+ },
450
+ ],
451
+ usage: {
452
+ prompt_tokens: 5,
453
+ completion_tokens: 5,
454
+ total_tokens: 10,
455
+ },
456
+ } as unknown as OpenAI.ChatCompletion;
457
+ vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
458
+ mockResponse as any,
459
+ );
460
+
461
+ const result = await instance.chat({
462
+ messages: [{ content: 'Hello', role: 'user' }],
463
+ model: 'deepseek/deepseek-reasoner',
464
+ temperature: 0,
465
+ stream: false,
466
+ });
467
+
468
+ const decoder = new TextDecoder();
469
+ const reader = result.body!.getReader();
470
+ const stream: string[] = [];
471
+
472
+ while (true) {
473
+ const { value, done } = await reader.read();
474
+ if (done) break;
475
+ stream.push(decoder.decode(value));
476
+ }
477
+
478
+ expect(stream).toEqual([
479
+ 'id: a\n',
480
+ 'event: reasoning\n',
481
+ 'data: "Thinking content"\n\n',
482
+ 'id: a\n',
483
+ 'event: text\n',
484
+ 'data: "Hello"\n\n',
485
+ 'id: a\n',
486
+ 'event: usage\n',
487
+ 'data: {"inputTextTokens":5,"outputTextTokens":5,"totalInputTokens":5,"totalOutputTokens":5,"totalTokens":10}\n\n',
488
+ 'id: output_speed\n',
489
+ 'event: speed\n',
490
+ expect.stringMatching(/^data: \{.*"tps":.*,"ttft":.*}\n\n$/), // tps ttft 测试结果不一样
491
+ 'id: a\n',
420
492
  'event: stop\n',
421
493
  'data: "stop"\n\n',
422
494
  ]);
@@ -124,6 +124,29 @@ export function transformResponseToStream(data: OpenAI.ChatCompletion) {
124
124
  return new ReadableStream({
125
125
  start(controller) {
126
126
  const choices = data.choices || [];
127
+ const first = choices[0];
128
+ // 兼容:非流式里 DeepSeek 等会把“深度思考”放在 message.reasoning_content
129
+ const message: any = first?.message ?? {};
130
+ const reasoningText =
131
+ typeof message.reasoning_content === 'string' && message.reasoning_content.length > 0
132
+ ? message.reasoning_content
133
+ : null;
134
+ if (reasoningText) {
135
+ controller.enqueue({
136
+ choices: [
137
+ {
138
+ delta: { content: null, reasoning_content: reasoningText, role: 'assistant' },
139
+ finish_reason: null,
140
+ index: first?.index ?? 0,
141
+ logprobs: first?.logprobs ?? null,
142
+ },
143
+ ],
144
+ created: data.created,
145
+ id: data.id,
146
+ model: data.model,
147
+ object: 'chat.completion.chunk',
148
+ } as unknown as OpenAI.ChatCompletionChunk);
149
+ }
127
150
  const chunk: OpenAI.ChatCompletionChunk = {
128
151
  choices: choices.map((choice: OpenAI.ChatCompletion.Choice) => ({
129
152
  delta: {
@@ -149,7 +172,16 @@ export function transformResponseToStream(data: OpenAI.ChatCompletion) {
149
172
  };
150
173
 
151
174
  controller.enqueue(chunk);
152
-
175
+ if (data.usage) {
176
+ controller.enqueue({
177
+ choices: [],
178
+ created: data.created,
179
+ id: data.id,
180
+ model: data.model,
181
+ object: 'chat.completion.chunk',
182
+ usage: data.usage,
183
+ } as unknown as OpenAI.ChatCompletionChunk);
184
+ }
153
185
  controller.enqueue({
154
186
  choices: choices.map((choice: OpenAI.ChatCompletion.Choice) => ({
155
187
  delta: {
@@ -311,7 +343,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
311
343
  callbacks: streamOptions.callbacks,
312
344
  inputStartAt,
313
345
  })
314
- : OpenAIStream(stream, { ...streamOptions, inputStartAt }),
346
+ : OpenAIStream(stream, { ...streamOptions, enableStreaming: false, inputStartAt }),
315
347
  {
316
348
  headers: options?.headers,
317
349
  },
@@ -240,12 +240,13 @@ export const transformAnthropicStream = (
240
240
 
241
241
  export interface AnthropicStreamOptions {
242
242
  callbacks?: ChatStreamCallbacks;
243
+ enableStreaming?: boolean; // 选择 TPS 计算方式(非流式时传 false)
243
244
  inputStartAt?: number;
244
245
  }
245
246
 
246
247
  export const AnthropicStream = (
247
248
  stream: Stream<Anthropic.MessageStreamEvent> | ReadableStream,
248
- { callbacks, inputStartAt }: AnthropicStreamOptions = {},
249
+ { callbacks, inputStartAt, enableStreaming = true }: AnthropicStreamOptions = {},
249
250
  ) => {
250
251
  const streamStack: StreamContext = { id: '' };
251
252
 
@@ -254,7 +255,11 @@ export const AnthropicStream = (
254
255
 
255
256
  return readableStream
256
257
  .pipeThrough(
257
- createTokenSpeedCalculator(transformAnthropicStream, { inputStartAt, streamStack }),
258
+ createTokenSpeedCalculator(transformAnthropicStream, {
259
+ enableStreaming: enableStreaming,
260
+ inputStartAt,
261
+ streamStack,
262
+ }),
258
263
  )
259
264
  .pipeThrough(createSSEProtocolTransformer((c) => c, streamStack))
260
265
  .pipeThrough(createCallbacksTransformer(callbacks));
@@ -213,18 +213,23 @@ const transformGoogleGenerativeAIStream = (
213
213
 
214
214
  export interface GoogleAIStreamOptions {
215
215
  callbacks?: ChatStreamCallbacks;
216
+ enableStreaming?: boolean; // 选择 TPS 计算方式(非流式时传 false)
216
217
  inputStartAt?: number;
217
218
  }
218
219
 
219
220
  export const GoogleGenerativeAIStream = (
220
221
  rawStream: ReadableStream<GenerateContentResponse>,
221
- { callbacks, inputStartAt }: GoogleAIStreamOptions = {},
222
+ { callbacks, inputStartAt, enableStreaming = true }: GoogleAIStreamOptions = {},
222
223
  ) => {
223
224
  const streamStack: StreamContext = { id: 'chat_' + nanoid() };
224
225
 
225
226
  return rawStream
226
227
  .pipeThrough(
227
- createTokenSpeedCalculator(transformGoogleGenerativeAIStream, { inputStartAt, streamStack }),
228
+ createTokenSpeedCalculator(transformGoogleGenerativeAIStream, {
229
+ enableStreaming: enableStreaming,
230
+ inputStartAt,
231
+ streamStack,
232
+ }),
228
233
  )
229
234
  .pipeThrough(
230
235
  createSSEProtocolTransformer((c) => c, streamStack, { requireTerminalEvent: true }),
@@ -417,13 +417,20 @@ export interface OpenAIStreamOptions {
417
417
  name: string;
418
418
  }) => ILobeAgentRuntimeErrorType | undefined;
419
419
  callbacks?: ChatStreamCallbacks;
420
+ enableStreaming?: boolean; // 选择 TPS 计算方式(非流式时传 false)
420
421
  inputStartAt?: number;
421
422
  provider?: string;
422
423
  }
423
424
 
424
425
  export const OpenAIStream = (
425
426
  stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
426
- { callbacks, provider, bizErrorTypeTransformer, inputStartAt }: OpenAIStreamOptions = {},
427
+ {
428
+ callbacks,
429
+ provider,
430
+ bizErrorTypeTransformer,
431
+ inputStartAt,
432
+ enableStreaming = true,
433
+ }: OpenAIStreamOptions = {},
427
434
  ) => {
428
435
  const streamStack: StreamContext = { id: '' };
429
436
 
@@ -439,7 +446,13 @@ export const OpenAIStream = (
439
446
  // provider like huggingface or minimax will return error in the stream,
440
447
  // so in the first Transformer, we need to handle the error
441
448
  .pipeThrough(createFirstErrorHandleTransformer(bizErrorTypeTransformer, provider))
442
- .pipeThrough(createTokenSpeedCalculator(transformWithProvider, { inputStartAt, streamStack }))
449
+ .pipeThrough(
450
+ createTokenSpeedCalculator(transformWithProvider, {
451
+ enableStreaming: enableStreaming,
452
+ inputStartAt,
453
+ streamStack,
454
+ }),
455
+ )
443
456
  .pipeThrough(createSSEProtocolTransformer((c) => c, streamStack))
444
457
  .pipeThrough(createCallbacksTransformer(callbacks))
445
458
  );
@@ -185,7 +185,13 @@ const transformOpenAIStream = (
185
185
 
186
186
  export const OpenAIResponsesStream = (
187
187
  stream: Stream<OpenAI.Responses.ResponseStreamEvent> | ReadableStream,
188
- { callbacks, provider, bizErrorTypeTransformer, inputStartAt }: OpenAIStreamOptions = {},
188
+ {
189
+ callbacks,
190
+ provider,
191
+ bizErrorTypeTransformer,
192
+ inputStartAt,
193
+ enableStreaming = true,
194
+ }: OpenAIStreamOptions = {},
189
195
  ) => {
190
196
  const streamStack: StreamContext = { id: '' };
191
197
 
@@ -198,7 +204,13 @@ export const OpenAIResponsesStream = (
198
204
  // provider like huggingface or minimax will return error in the stream,
199
205
  // so in the first Transformer, we need to handle the error
200
206
  .pipeThrough(createFirstErrorHandleTransformer(bizErrorTypeTransformer, provider))
201
- .pipeThrough(createTokenSpeedCalculator(transformOpenAIStream, { inputStartAt, streamStack }))
207
+ .pipeThrough(
208
+ createTokenSpeedCalculator(transformOpenAIStream, {
209
+ enableStreaming: enableStreaming,
210
+ inputStartAt,
211
+ streamStack,
212
+ }),
213
+ )
202
214
  .pipeThrough(createSSEProtocolTransformer((c) => c, streamStack))
203
215
  .pipeThrough(createCallbacksTransformer(callbacks))
204
216
  );
@@ -360,7 +360,11 @@ export const TOKEN_SPEED_CHUNK_ID = 'output_speed';
360
360
  */
361
361
  export const createTokenSpeedCalculator = (
362
362
  transformer: (chunk: any, stack: StreamContext) => StreamProtocolChunk | StreamProtocolChunk[],
363
- { inputStartAt, streamStack }: { inputStartAt?: number; streamStack?: StreamContext } = {},
363
+ {
364
+ inputStartAt,
365
+ streamStack,
366
+ enableStreaming = true, // 选择 TPS 计算方式(非流式时传 false)
367
+ }: { enableStreaming?: boolean; inputStartAt?: number; streamStack?: StreamContext } = {},
364
368
  ) => {
365
369
  let outputStartAt: number | undefined;
366
370
  let outputThinking: boolean | undefined;
@@ -397,7 +401,9 @@ export const createTokenSpeedCalculator = (
397
401
  : Math.max(0, totalOutputTokens - reasoningTokens);
398
402
  result.push({
399
403
  data: {
400
- tps: (outputTokens / (Date.now() - outputStartAt)) * 1000,
404
+ // 非流式计算 tps 从发出请求开始算
405
+ tps:
406
+ (outputTokens / (Date.now() - (enableStreaming ? outputStartAt : inputStartAt))) * 1000,
401
407
  ttft: outputStartAt - inputStartAt,
402
408
  } as ModelSpeed,
403
409
  id: TOKEN_SPEED_CHUNK_ID,
@@ -116,7 +116,11 @@ export const QwenAIStream = (
116
116
  stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
117
117
  // TODO: preserve for RFC 097
118
118
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
119
- { callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
119
+ {
120
+ callbacks,
121
+ inputStartAt,
122
+ enableStreaming = true,
123
+ }: { callbacks?: ChatStreamCallbacks; enableStreaming?: boolean; inputStartAt?: number } = {},
120
124
  ) => {
121
125
  const streamContext: StreamContext = { id: '' };
122
126
  const readableStream =
@@ -124,7 +128,11 @@ export const QwenAIStream = (
124
128
 
125
129
  return readableStream
126
130
  .pipeThrough(
127
- createTokenSpeedCalculator(transformQwenStream, { inputStartAt, streamStack: streamContext }),
131
+ createTokenSpeedCalculator(transformQwenStream, {
132
+ enableStreaming: enableStreaming,
133
+ inputStartAt,
134
+ streamStack: streamContext,
135
+ }),
128
136
  )
129
137
  .pipeThrough(createSSEProtocolTransformer((c) => c, streamContext))
130
138
  .pipeThrough(createCallbacksTransformer(callbacks));
@@ -114,7 +114,7 @@ describe('SparkAIStream', () => {
114
114
  chunks.push(chunk);
115
115
  }
116
116
 
117
- expect(chunks).toHaveLength(2);
117
+ expect(chunks).toHaveLength(3);
118
118
  expect(chunks[0].choices[0].delta.tool_calls).toEqual([
119
119
  {
120
120
  function: {
@@ -126,7 +126,7 @@ describe('SparkAIStream', () => {
126
126
  type: 'function',
127
127
  },
128
128
  ]);
129
- expect(chunks[1].choices[0].finish_reason).toBeDefined();
129
+ expect(chunks[2].choices[0].finish_reason).toBeDefined();
130
130
  });
131
131
 
132
132
  it('should transform streaming response with tool calls', async () => {
@@ -50,7 +50,16 @@ export function transformSparkResponseToStream(data: OpenAI.ChatCompletion) {
50
50
  };
51
51
 
52
52
  controller.enqueue(chunk);
53
-
53
+ if (data.usage) {
54
+ controller.enqueue({
55
+ choices: [],
56
+ created: data.created,
57
+ id: data.id,
58
+ model: data.model,
59
+ object: 'chat.completion.chunk',
60
+ usage: data.usage,
61
+ } as unknown as OpenAI.ChatCompletionChunk);
62
+ }
54
63
  controller.enqueue({
55
64
  choices: choices.map((choice: OpenAI.ChatCompletion.Choice) => ({
56
65
  delta: {
@@ -143,12 +143,18 @@ const transformVertexAIStream = (
143
143
 
144
144
  export const VertexAIStream = (
145
145
  rawStream: ReadableStream<GenerateContentResponse>,
146
- { callbacks, inputStartAt }: GoogleAIStreamOptions = {},
146
+ { callbacks, inputStartAt, enableStreaming = true }: GoogleAIStreamOptions = {},
147
147
  ) => {
148
148
  const streamStack: StreamContext = { id: 'chat_' + nanoid() };
149
149
 
150
150
  return rawStream
151
- .pipeThrough(createTokenSpeedCalculator(transformVertexAIStream, { inputStartAt, streamStack }))
151
+ .pipeThrough(
152
+ createTokenSpeedCalculator(transformVertexAIStream, {
153
+ enableStreaming: enableStreaming,
154
+ inputStartAt,
155
+ streamStack,
156
+ }),
157
+ )
152
158
  .pipeThrough(createSSEProtocolTransformer((c) => c, streamStack))
153
159
  .pipeThrough(createCallbacksTransformer(callbacks));
154
160
  };
@@ -96,9 +96,12 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
96
96
  });
97
97
  } else {
98
98
  const stream = transformResponseToStream(response as OpenAI.ChatCompletion);
99
- return StreamingResponse(OpenAIStream(stream, { callbacks: options?.callback }), {
100
- headers: options?.headers,
101
- });
99
+ return StreamingResponse(
100
+ OpenAIStream(stream, { callbacks: options?.callback, enableStreaming: false }),
101
+ {
102
+ headers: options?.headers,
103
+ },
104
+ );
102
105
  }
103
106
  } catch (e) {
104
107
  return this.handleError(e, model);
@@ -83,9 +83,12 @@ export class LobeAzureAI implements LobeRuntimeAI {
83
83
 
84
84
  // the azure AI inference response is openai compatible
85
85
  const stream = transformResponseToStream(res.body as OpenAI.ChatCompletion);
86
- return StreamingResponse(OpenAIStream(stream, { callbacks: options?.callback }), {
87
- headers: options?.headers,
88
- });
86
+ return StreamingResponse(
87
+ OpenAIStream(stream, { callbacks: options?.callback, enableStreaming: false }),
88
+ {
89
+ headers: options?.headers,
90
+ },
91
+ );
89
92
  }
90
93
  } catch (e) {
91
94
  let error = e as { [key: string]: any; code: string; message: string };
@@ -195,7 +195,10 @@ describe('ServerService', () => {
195
195
  await service.updateSessionConfig('123', config, signal);
196
196
  expect(lambdaClient.session.updateSessionConfig.mutate).toBeCalledWith(
197
197
  { id: '123', value: config },
198
- { signal },
198
+ {
199
+ signal,
200
+ context: { showNotification: false },
201
+ },
199
202
  );
200
203
  });
201
204
 
@@ -56,7 +56,13 @@ export class ServerService implements ISessionService {
56
56
  };
57
57
 
58
58
  updateSessionConfig: ISessionService['updateSessionConfig'] = (id, config, signal) => {
59
- return lambdaClient.session.updateSessionConfig.mutate({ id, value: config }, { signal });
59
+ return lambdaClient.session.updateSessionConfig.mutate(
60
+ { id, value: config },
61
+ {
62
+ context: { showNotification: false },
63
+ signal,
64
+ },
65
+ );
60
66
  };
61
67
 
62
68
  updateSessionMeta: ISessionService['updateSessionMeta'] = (id, meta, signal) => {