@lobehub/lobehub 2.0.0-next.226 → 2.0.0-next.227

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.227](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.226...v2.0.0-next.227)
6
+
7
+ <sup>Released on **2026-01-06**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Allow zero-byte files and add business hooks for error handling.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Allow zero-byte files and add business hooks for error handling, closes [#11283](https://github.com/lobehub/lobe-chat/issues/11283) ([38f5b78](https://github.com/lobehub/lobe-chat/commit/38f5b78))
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.226](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.225...v2.0.0-next.226)
6
31
 
7
32
  <sup>Released on **2026-01-06**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Allow zero-byte files and add business hooks for error handling."
6
+ ]
7
+ },
8
+ "date": "2026-01-06",
9
+ "version": "2.0.0-next.227"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
@@ -440,6 +440,17 @@ Solutions:
440
440
 
441
441
  - A straightforward troubleshooting method is to use the `curl` command in the LobeChat container terminal to access your authentication service at `https://auth.example.com/.well-known/openid-configuration`. If JSON format data is returned, it indicates your authentication service is functioning correctly.
442
442
 
443
+ #### OAuth Token Exchange Failures with Reverse Proxy
444
+
445
+ If OAuth authentication fails during the token exchange phase when using Docker behind a reverse proxy, this is typically caused by the default `MIDDLEWARE_REWRITE_THROUGH_LOCAL=1` setting which rewrites URLs to `127.0.0.1:3210`.
446
+
447
+ **Solution**: Set `MIDDLEWARE_REWRITE_THROUGH_LOCAL=0` in your `.env` file and restart Docker containers:
448
+
449
+ ```bash
450
+ docker compose down
451
+ docker compose up -d
452
+ ```
453
+
443
454
  ````markdown
444
455
  ## Extended Configuration
445
456
 
@@ -421,6 +421,17 @@ lobe-chat | [auth][error] TypeError: fetch failed
421
421
 
422
422
  - 一个直接的排查方式,你可以在 LobeChat 容器的终端中,使用 `curl` 命令访问你的鉴权服务 `https://auth.example.com/.well-known/openid-configuration`,如果返回了 JSON 格式的数据,则说明你的鉴权服务正常运行。
423
423
 
424
+ #### 反向代理下 OAuth 令牌交换失败
425
+
426
+ 如果在反向代理后使用 Docker 时 OAuth 认证在令牌交换阶段失败,这通常是由默认的 `MIDDLEWARE_REWRITE_THROUGH_LOCAL=1` 设置引起的,该设置会将 URL 重写为 `127.0.0.1:3210`。
427
+
428
+ **解决方案**: 在 `.env` 文件中设置 `MIDDLEWARE_REWRITE_THROUGH_LOCAL=0` 并重启 Docker 容器:
429
+
430
+ ```bash
431
+ docker compose down
432
+ docker compose up -d
433
+ ```
434
+
424
435
  ## 拓展配置
425
436
 
426
437
  为了完善你的 LobeChat 服务,你可以根据你的需求进行以下拓展配置。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.226",
3
+ "version": "2.0.0-next.227",
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",
@@ -860,6 +860,34 @@ const lobehubChatModels: AIChatModelCard[] = [
860
860
  releasedAt: '2025-08-26',
861
861
  type: 'chat',
862
862
  },
863
+ {
864
+ abilities: {
865
+ functionCall: true,
866
+ reasoning: true,
867
+ search: true,
868
+ vision: true,
869
+ },
870
+ contextWindowTokens: 256_000,
871
+ description:
872
+ 'Our newest and strongest flagship model, excelling in NLP, math, and reasoning—an ideal all-rounder.',
873
+ displayName: 'Grok 4',
874
+ enabled: true,
875
+ id: 'grok-4',
876
+ pricing: {
877
+ units: [
878
+ { name: 'textInput_cacheRead', rate: 0.75, strategy: 'fixed', unit: 'millionTokens' },
879
+ { name: 'textInput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
880
+ { name: 'textOutput', rate: 15, strategy: 'fixed', unit: 'millionTokens' },
881
+ ],
882
+ },
883
+ releasedAt: '2025-07-09',
884
+ settings: {
885
+ // reasoning_effort is not supported by grok-4. Specifying reasoning_effort parameter will get an error response.
886
+ // extendParams: ['reasoningEffort'],
887
+ searchImpl: 'params',
888
+ },
889
+ type: 'chat',
890
+ },
863
891
  {
864
892
  abilities: {
865
893
  imageOutput: true,
@@ -0,0 +1,9 @@
1
+ import type { ErrorType } from '@lobechat/types';
2
+ import type { AlertProps } from '@lobehub/ui';
3
+
4
+ export default function useBusinessErrorAlertConfig(
5
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
6
+ errorType?: ErrorType,
7
+ ): AlertProps | undefined {
8
+ return undefined;
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { ErrorType } from '@lobechat/types';
2
+
3
+ export interface BusinessErrorContentResult {
4
+ errorType?: string;
5
+ hideMessage?: boolean;
6
+ }
7
+
8
+ export default function useBusinessErrorContent(
9
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
10
+ errorType?: ErrorType | string,
11
+ ): BusinessErrorContentResult {
12
+ return {};
13
+ }
@@ -0,0 +1,12 @@
1
+ export interface BusinessFileUploadCheckParams {
2
+ actualSize: number;
3
+ clientIp?: string;
4
+ inputSize: number;
5
+ url: string;
6
+ userId: string;
7
+ }
8
+
9
+ export async function businessFileUploadCheck(
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ params: BusinessFileUploadCheckParams,
12
+ ): Promise<void> {}
@@ -7,6 +7,8 @@ import dynamic from 'next/dynamic';
7
7
  import { memo, useMemo } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
10
+ import useBusinessErrorAlertConfig from '@/business/client/hooks/useBusinessErrorAlertConfig';
11
+ import useBusinessErrorContent from '@/business/client/hooks/useBusinessErrorContent';
10
12
  import useRenderBusinessChatErrorMessageExtra from '@/business/client/hooks/useRenderBusinessChatErrorMessageExtra';
11
13
  import ErrorContent from '@/features/Conversation/ChatItem/components/ErrorContent';
12
14
  import { useProviderName } from '@/hooks/useProviderName';
@@ -85,18 +87,26 @@ const getErrorAlertConfig = (
85
87
  export const useErrorContent = (error: any) => {
86
88
  const { t } = useTranslation('error');
87
89
  const providerName = useProviderName(error?.body?.provider || '');
90
+ const businessAlertConfig = useBusinessErrorAlertConfig(error?.type);
91
+ const { errorType: businessErrorType, hideMessage } = useBusinessErrorContent(error?.type);
88
92
 
89
93
  return useMemo<AlertProps | undefined>(() => {
90
94
  if (!error) return;
91
95
  const messageError = error;
92
96
 
93
- const alertConfig = getErrorAlertConfig(messageError.type);
97
+ // Use business alert config if provided, otherwise fall back to default
98
+ const alertConfig = businessAlertConfig ?? getErrorAlertConfig(messageError.type);
99
+
100
+ // Use business error type if provided, otherwise use original
101
+ const finalErrorType = businessErrorType ?? messageError.type;
94
102
 
95
103
  return {
96
- message: t(`response.${messageError.type}` as any, { provider: providerName }),
104
+ message: hideMessage
105
+ ? undefined
106
+ : t(`response.${finalErrorType}` as any, { provider: providerName }),
97
107
  ...alertConfig,
98
108
  };
99
- }, [error]);
109
+ }, [businessAlertConfig, businessErrorType, error, hideMessage, providerName, t]);
100
110
  };
101
111
 
102
112
  interface ErrorExtraProps {
@@ -273,7 +273,7 @@ describe('fileRouter', () => {
273
273
  );
274
274
  });
275
275
 
276
- it('should throw error when getFileMetadata fails and input size is less than 1', async () => {
276
+ it('should throw error when getFileMetadata fails and input size is negative', async () => {
277
277
  mockFileModelCheckHash.mockResolvedValue({ isExist: false });
278
278
  mockFileServiceGetFileMetadata.mockRejectedValue(new Error('File not found in S3'));
279
279
 
@@ -282,11 +282,11 @@ describe('fileRouter', () => {
282
282
  hash: 'test-hash',
283
283
  fileType: 'text',
284
284
  name: 'test.txt',
285
- size: 0,
285
+ size: -1,
286
286
  url: 'files/non-existent.txt',
287
287
  metadata: {},
288
288
  }),
289
- ).rejects.toThrow('File size must be at least 1 byte');
289
+ ).rejects.toThrow('File size cannot be negative');
290
290
  });
291
291
 
292
292
  it('should use input size when getFileMetadata returns contentLength less than 1', async () => {
@@ -315,10 +315,10 @@ describe('fileRouter', () => {
315
315
  );
316
316
  });
317
317
 
318
- it('should throw error when both getFileMetadata contentLength and input size are less than 1', async () => {
318
+ it('should throw error when both getFileMetadata contentLength and input size are negative', async () => {
319
319
  mockFileModelCheckHash.mockResolvedValue({ isExist: false });
320
320
  mockFileServiceGetFileMetadata.mockResolvedValue({
321
- contentLength: 0,
321
+ contentLength: -1,
322
322
  contentType: 'text/plain',
323
323
  });
324
324
 
@@ -327,11 +327,11 @@ describe('fileRouter', () => {
327
327
  hash: 'test-hash',
328
328
  fileType: 'text',
329
329
  name: 'test.txt',
330
- size: 0,
330
+ size: -1,
331
331
  url: 'files/test.txt',
332
332
  metadata: {},
333
333
  }),
334
- ).rejects.toThrow('File size must be at least 1 byte');
334
+ ).rejects.toThrow('File size cannot be negative');
335
335
  });
336
336
  });
337
337
 
@@ -1,6 +1,7 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { z } from 'zod';
3
3
 
4
+ import { businessFileUploadCheck } from '@/business/server/lambda-routers/file';
4
5
  import { checkFileStorageUsage } from '@/business/server/trpc-middlewares/lambda';
5
6
  import { serverDBEnv } from '@/config/db';
6
7
  import { AsyncTaskModel } from '@/database/models/asyncTask';
@@ -74,8 +75,16 @@ export const fileRouter = router({
74
75
  // If metadata fetch fails, use original size from input
75
76
  }
76
77
 
77
- if (actualSize < 1) {
78
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'File size must be at least 1 byte' });
78
+ await businessFileUploadCheck({
79
+ actualSize,
80
+ clientIp: ctx.clientIp ?? undefined,
81
+ inputSize: input.size,
82
+ url: input.url,
83
+ userId: ctx.userId,
84
+ });
85
+
86
+ if (actualSize < 0) {
87
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'File size cannot be negative' });
79
88
  }
80
89
 
81
90
  const { id } = await ctx.fileModel.create(
@@ -367,7 +376,7 @@ export const fileRouter = router({
367
376
 
368
377
  if (!file) return;
369
378
 
370
- // delele the file from remove from S3 if it is not used by other files
379
+ // delete the file from S3 if it is not used by other files
371
380
  await ctx.fileService.deleteFile(file.url!);
372
381
  }),
373
382