@lobehub/chat 1.36.1 → 1.36.3

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 (27) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +2 -1
  4. package/src/app/(backend)/middleware/auth/index.test.ts +1 -1
  5. package/src/app/(backend)/middleware/auth/index.ts +3 -2
  6. package/src/app/(backend)/webapi/assistant/[id]/route.ts +5 -1
  7. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +6 -6
  8. package/src/app/(backend)/webapi/chat/[provider]/route.ts +1 -1
  9. package/src/app/(backend)/webapi/chat/anthropic/route.test.ts +3 -1
  10. package/src/app/(backend)/webapi/chat/anthropic/route.ts +1 -1
  11. package/src/app/(backend)/webapi/chat/google/route.test.ts +3 -1
  12. package/src/app/(backend)/webapi/chat/google/route.ts +2 -1
  13. package/src/app/(backend)/webapi/chat/minimax/route.test.ts +3 -1
  14. package/src/app/(backend)/webapi/chat/minimax/route.ts +2 -1
  15. package/src/app/(backend)/webapi/chat/models/[provider]/route.ts +1 -1
  16. package/src/app/(backend)/webapi/chat/openai/route.test.ts +3 -1
  17. package/src/app/(backend)/webapi/chat/openai/route.ts +2 -1
  18. package/src/app/(backend)/webapi/chat/wenxin/route.test.ts +1 -1
  19. package/src/app/(backend)/webapi/chat/wenxin/route.ts +1 -1
  20. package/src/app/(backend)/webapi/text-to-image/[provider]/route.ts +1 -1
  21. package/src/components/Analytics/ReactScan.tsx +11 -0
  22. package/src/components/Analytics/index.tsx +4 -0
  23. package/src/config/analytics.ts +6 -0
  24. package/src/libs/agent-runtime/types/chat.ts +7 -0
  25. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
  26. package/src/utils/clientIP.test.ts +54 -0
  27. package/src/utils/clientIP.ts +34 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.36.3](https://github.com/lobehub/lobe-chat/compare/v1.36.2...v1.36.3)
6
+
7
+ <sup>Released on **2024-12-08**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Support request headers for chat.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Support request headers for chat, closes [#4934](https://github.com/lobehub/lobe-chat/issues/4934) ([8cdc062](https://github.com/lobehub/lobe-chat/commit/8cdc062))
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
+
30
+ ### [Version 1.36.2](https://github.com/lobehub/lobe-chat/compare/v1.36.1...v1.36.2)
31
+
32
+ <sup>Released on **2024-12-07**</sup>
33
+
34
+ #### ♻ Code Refactoring
35
+
36
+ - **misc**: Refactor async params route to adapt next15 breaking change.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Code refactoring
44
+
45
+ - **misc**: Refactor async params route to adapt next15 breaking change, closes [#4905](https://github.com/lobehub/lobe-chat/issues/4905) ([5d61950](https://github.com/lobehub/lobe-chat/commit/5d61950))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.36.1](https://github.com/lobehub/lobe-chat/compare/v1.36.0...v1.36.1)
6
56
 
7
57
  <sup>Released on **2024-12-07**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Support request headers for chat."
6
+ ]
7
+ },
8
+ "date": "2024-12-08",
9
+ "version": "1.36.3"
10
+ },
11
+ {
12
+ "children": {
13
+ "improvements": [
14
+ "Refactor async params route to adapt next15 breaking change."
15
+ ]
16
+ },
17
+ "date": "2024-12-07",
18
+ "version": "1.36.2"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.36.1",
3
+ "version": "1.36.3",
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",
@@ -203,6 +203,7 @@
203
203
  "react-layout-kit": "^1.9.0",
204
204
  "react-lazy-load": "^4.0.1",
205
205
  "react-pdf": "^9.1.1",
206
+ "react-scan": "^0.0.40",
206
207
  "react-virtuoso": "^4.12.0",
207
208
  "react-wrap-balancer": "^1.1.1",
208
209
  "remark": "^14.0.3",
@@ -27,7 +27,7 @@ vi.mock('@/utils/server/jwt', () => ({
27
27
  describe('checkAuth', () => {
28
28
  const mockHandler: RequestHandler = vi.fn();
29
29
  const mockRequest = new Request('https://example.com');
30
- const mockOptions = { params: { provider: 'mock' } };
30
+ const mockOptions = { params: Promise.resolve({ provider: 'mock' }) };
31
31
 
32
32
  beforeEach(() => {
33
33
  vi.clearAllMocks();
@@ -11,7 +11,7 @@ import { getJWTPayload } from '@/utils/server/jwt';
11
11
  import { checkAuthMethod } from './utils';
12
12
 
13
13
  type CreateRuntime = (jwtPayload: JWTPayload) => AgentRuntime;
14
- type RequestOptions = { createRuntime?: CreateRuntime; params: { provider: string } };
14
+ type RequestOptions = { createRuntime?: CreateRuntime; params: Promise<{ provider: string }> };
15
15
 
16
16
  export type RequestHandler = (
17
17
  req: Request,
@@ -56,7 +56,8 @@ export const checkAuth =
56
56
 
57
57
  const error = errorContent || e;
58
58
 
59
- return createErrorResponse(errorType, { error, ...res, provider: options.params?.provider });
59
+ const params = await options.params;
60
+ return createErrorResponse(errorType, { error, ...res, provider: params?.provider });
60
61
  }
61
62
 
62
63
  return handler(req, { ...options, jwtPayload });
@@ -3,7 +3,11 @@ import { AssistantStore } from '@/server/modules/AssistantStore';
3
3
 
4
4
  export const runtime = 'edge';
5
5
 
6
- export const GET = async (req: Request, { params }: { params: { id: string } }) => {
6
+ type Params = Promise<{ id: string }>;
7
+
8
+ export const GET = async (req: Request, segmentData: { params: Params }) => {
9
+ const params = await segmentData.params;
10
+
7
11
  const { searchParams } = new URL(req.url);
8
12
 
9
13
  const locale = searchParams.get('locale');
@@ -58,7 +58,7 @@ afterEach(() => {
58
58
  describe('POST handler', () => {
59
59
  describe('init chat model', () => {
60
60
  it('should initialize AgentRuntime correctly with valid authorization', async () => {
61
- const mockParams = { provider: 'test-provider' };
61
+ const mockParams = Promise.resolve({ provider: 'test-provider' });
62
62
 
63
63
  // 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
64
64
  vi.mocked(getJWTPayload).mockResolvedValueOnce({
@@ -83,7 +83,7 @@ describe('POST handler', () => {
83
83
  });
84
84
 
85
85
  it('should return Unauthorized error when LOBE_CHAT_AUTH_HEADER is missing', async () => {
86
- const mockParams = { provider: 'test-provider' };
86
+ const mockParams = Promise.resolve({ provider: 'test-provider' });
87
87
  const requestWithoutAuthHeader = new Request(new URL('https://test.com'), {
88
88
  method: 'POST',
89
89
  body: JSON.stringify({ model: 'test-model' }),
@@ -110,7 +110,7 @@ describe('POST handler', () => {
110
110
  azureApiVersion: 'v1',
111
111
  });
112
112
 
113
- const mockParams = { provider: 'test-provider' };
113
+ const mockParams = Promise.resolve({ provider: 'test-provider' });
114
114
  // 设置 initAgentRuntimeWithUserPayload 的模拟返回值
115
115
  vi.mocked(getAuth).mockReturnValue({} as any);
116
116
  vi.mocked(checkAuthMethod).mockReset();
@@ -141,7 +141,7 @@ describe('POST handler', () => {
141
141
  });
142
142
 
143
143
  it('should return InternalServerError error when throw a unknown error', async () => {
144
- const mockParams = { provider: 'test-provider' };
144
+ const mockParams = Promise.resolve({ provider: 'test-provider' });
145
145
  vi.mocked(getJWTPayload).mockRejectedValueOnce(new Error('unknown error'));
146
146
 
147
147
  const response = await POST(request, { params: mockParams });
@@ -166,7 +166,7 @@ describe('POST handler', () => {
166
166
  userId: 'abc',
167
167
  });
168
168
 
169
- const mockParams = { provider: 'test-provider' };
169
+ const mockParams = Promise.resolve({ provider: 'test-provider' });
170
170
  const mockChatPayload = { message: 'Hello, world!' };
171
171
  request = new Request(new URL('https://test.com'), {
172
172
  headers: { [LOBE_CHAT_AUTH_HEADER]: 'Bearer some-valid-token' },
@@ -192,7 +192,7 @@ describe('POST handler', () => {
192
192
  azureApiVersion: 'v1',
193
193
  });
194
194
 
195
- const mockParams = { provider: 'test-provider' };
195
+ const mockParams = Promise.resolve({ provider: 'test-provider' });
196
196
  const mockChatPayload = { message: 'Hello, world!' };
197
197
  request = new Request(new URL('https://test.com'), {
198
198
  headers: { [LOBE_CHAT_AUTH_HEADER]: 'Bearer some-valid-token' },
@@ -13,7 +13,7 @@ import { getTracePayload } from '@/utils/trace';
13
13
  export const runtime = 'edge';
14
14
 
15
15
  export const POST = checkAuth(async (req: Request, { params, jwtPayload, createRuntime }) => {
16
- const { provider } = params;
16
+ const { provider } = await params;
17
17
 
18
18
  try {
19
19
  // ============ 1. init chat model ============ //
@@ -23,6 +23,8 @@ describe('Anthropic POST function tests', () => {
23
23
  it('should call UniverseRoute with correct parameters', async () => {
24
24
  const mockRequest = new Request('https://example.com', { method: 'POST' });
25
25
  await POST(mockRequest);
26
- expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, { params: { provider: 'anthropic' } });
26
+ expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
27
+ params: Promise.resolve({ provider: 'anthropic' }),
28
+ });
27
29
  });
28
30
  });
@@ -18,4 +18,4 @@ export const preferredRegion = [
18
18
  ];
19
19
 
20
20
  export const POST = async (req: Request) =>
21
- UniverseRoute(req, { params: { provider: 'anthropic' } });
21
+ UniverseRoute(req, { params: Promise.resolve({ provider: 'anthropic' }) });
@@ -28,6 +28,8 @@ describe('Google POST function tests', () => {
28
28
  it('should call UniverseRoute with correct parameters', async () => {
29
29
  const mockRequest = new Request('https://example.com', { method: 'POST' });
30
30
  await POST(mockRequest);
31
- expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, { params: { provider: 'google' } });
31
+ expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
32
+ params: Promise.resolve({ provider: 'google' }),
33
+ });
32
34
  });
33
35
  });
@@ -21,4 +21,5 @@ export const preferredRegion = [
21
21
  'gru1',
22
22
  ];
23
23
 
24
- export const POST = async (req: Request) => UniverseRoute(req, { params: { provider: 'google' } });
24
+ export const POST = async (req: Request) =>
25
+ UniverseRoute(req, { params: Promise.resolve({ provider: 'google' }) });
@@ -19,6 +19,8 @@ describe('Minimax POST function tests', () => {
19
19
  it('should call UniverseRoute with correct parameters', async () => {
20
20
  const mockRequest = new Request('https://example.com', { method: 'POST' });
21
21
  await POST(mockRequest);
22
- expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, { params: { provider: 'minimax' } });
22
+ expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
23
+ params: Promise.resolve({ provider: 'minimax' }),
24
+ });
23
25
  });
24
26
  });
@@ -2,4 +2,5 @@ import { POST as UniverseRoute } from '../[provider]/route';
2
2
 
3
3
  export const runtime = 'nodejs';
4
4
 
5
- export const POST = async (req: Request) => UniverseRoute(req, { params: { provider: 'minimax' } });
5
+ export const POST = async (req: Request) =>
6
+ UniverseRoute(req, { params: Promise.resolve({ provider: 'minimax' }) });
@@ -12,7 +12,7 @@ const noNeedAPIKey = (provider: string) =>
12
12
  [ModelProvider.OpenRouter, ModelProvider.TogetherAI].includes(provider as any);
13
13
 
14
14
  export const GET = checkAuth(async (req, { params, jwtPayload }) => {
15
- const { provider } = params;
15
+ const { provider } = await params;
16
16
 
17
17
  try {
18
18
  const hasDefaultApiKey = jwtPayload.apiKey || 'dont-need-api-key-for-model-list';
@@ -23,6 +23,8 @@ describe('OpenAI POST function tests', () => {
23
23
  it('should call UniverseRoute with correct parameters', async () => {
24
24
  const mockRequest = new Request('https://example.com', { method: 'POST' });
25
25
  await POST(mockRequest);
26
- expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, { params: { provider: 'openai' } });
26
+ expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
27
+ params: Promise.resolve({ provider: 'openai' }),
28
+ });
27
29
  });
28
30
  });
@@ -22,4 +22,5 @@ export const preferredRegion = [
22
22
  'syd1',
23
23
  ];
24
24
 
25
- export const POST = async (req: Request) => UniverseRoute(req, { params: { provider: 'openai' } });
25
+ export const POST = async (req: Request) =>
26
+ UniverseRoute(req, { params: Promise.resolve({ provider: 'openai' }) });
@@ -21,7 +21,7 @@ describe('Wenxin POST function tests', () => {
21
21
  await POST(mockRequest);
22
22
  expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
23
23
  createRuntime: expect.anything(),
24
- params: { provider: 'wenxin' },
24
+ params: Promise.resolve({ provider: 'wenxin' }),
25
25
  });
26
26
  });
27
27
  });
@@ -26,5 +26,5 @@ export const POST = async (req: Request) =>
26
26
 
27
27
  return new AgentRuntime(instance);
28
28
  },
29
- params: { provider: ModelProvider.Wenxin },
29
+ params: Promise.resolve({ provider: ModelProvider.Wenxin }),
30
30
  });
@@ -48,7 +48,7 @@ export const preferredRegion = [
48
48
  // );
49
49
 
50
50
  export const POST = checkAuth(async (req: Request, { params, jwtPayload }) => {
51
- const { provider } = params;
51
+ const { provider } = await params;
52
52
 
53
53
  try {
54
54
  // ============ 1. init chat model ============ //
@@ -0,0 +1,11 @@
1
+ import { Monitoring } from 'react-scan/dist/core/monitor/params/next';
2
+
3
+ interface ReactScanProps {
4
+ apiKey: string;
5
+ }
6
+
7
+ const ReactScan = ({ apiKey }: ReactScanProps) => (
8
+ <Monitoring apiKey={apiKey} url="https://monitoring.react-scan.com/api/v1/ingest" />
9
+ );
10
+
11
+ export default ReactScan;
@@ -9,6 +9,7 @@ const Plausible = dynamic(() => import('./Plausible'));
9
9
  const Posthog = dynamic(() => import('./Posthog'));
10
10
  const Umami = dynamic(() => import('./Umami'));
11
11
  const Clarity = dynamic(() => import('./Clarity'));
12
+ const ReactScan = dynamic(() => import('./ReactScan'));
12
13
 
13
14
  const Analytics = () => {
14
15
  return (
@@ -37,6 +38,9 @@ const Analytics = () => {
37
38
  {analyticsEnv.ENABLED_CLARITY_ANALYTICS && (
38
39
  <Clarity projectId={analyticsEnv.CLARITY_PROJECT_ID} />
39
40
  )}
41
+ {!!analyticsEnv.REACT_SCAN_MONITOR_API_KEY && (
42
+ <ReactScan apiKey={analyticsEnv.REACT_SCAN_MONITOR_API_KEY} />
43
+ )}
40
44
  </>
41
45
  );
42
46
  };
@@ -26,6 +26,8 @@ export const getAnalyticsConfig = () => {
26
26
 
27
27
  ENABLE_GOOGLE_ANALYTICS: z.boolean(),
28
28
  GOOGLE_ANALYTICS_MEASUREMENT_ID: z.string().optional(),
29
+
30
+ REACT_SCAN_MONITOR_API_KEY: z.string().optional(),
29
31
  },
30
32
  runtimeEnv: {
31
33
  // Plausible Analytics
@@ -55,6 +57,10 @@ export const getAnalyticsConfig = () => {
55
57
  // Google Analytics
56
58
  ENABLE_GOOGLE_ANALYTICS: !!process.env.GOOGLE_ANALYTICS_MEASUREMENT_ID,
57
59
  GOOGLE_ANALYTICS_MEASUREMENT_ID: process.env.GOOGLE_ANALYTICS_MEASUREMENT_ID,
60
+
61
+ // React Scan Monitor
62
+ // https://dashboard.react-scan.com
63
+ REACT_SCAN_MONITOR_API_KEY: process.env.REACT_SCAN_MONITOR_API_KEY,
58
64
  },
59
65
  });
60
66
  };
@@ -96,7 +96,14 @@ export interface ChatStreamPayload {
96
96
 
97
97
  export interface ChatCompetitionOptions {
98
98
  callback?: ChatStreamCallbacks;
99
+ /**
100
+ * response headers
101
+ */
99
102
  headers?: Record<string, any>;
103
+ /**
104
+ * send the request to the ai api endpoint
105
+ */
106
+ requestHeaders?: Record<string, any>;
100
107
  signal?: AbortSignal;
101
108
  /**
102
109
  * userId for the chat completion
@@ -211,7 +211,7 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
211
211
  },
212
212
  {
213
213
  // https://github.com/lobehub/lobe-chat/pull/318
214
- headers: { Accept: '*/*' },
214
+ headers: { Accept: '*/*', ...options?.requestHeaders },
215
215
  signal: options?.signal,
216
216
  },
217
217
  );
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { getClientIP } from './clientIP';
4
+
5
+ describe('getClientIP', () => {
6
+ // Helper function to create Headers object
7
+ const createHeaders = (entries: [string, string][]) => {
8
+ return new Headers(entries);
9
+ };
10
+
11
+ it('should return null when no IP headers are present', () => {
12
+ const headers = createHeaders([]);
13
+ expect(getClientIP(headers)).toBe('');
14
+ });
15
+
16
+ it('should handle Cloudflare IP header', () => {
17
+ const headers = createHeaders([['cf-connecting-ip', '1.2.3.4']]);
18
+ expect(getClientIP(headers)).toBe('1.2.3.4');
19
+ });
20
+
21
+ it('should handle x-forwarded-for with single IP', () => {
22
+ const headers = createHeaders([['x-forwarded-for', '5.6.7.8']]);
23
+ expect(getClientIP(headers)).toBe('5.6.7.8');
24
+ });
25
+
26
+ it('should handle x-forwarded-for with multiple IPs and return the first one', () => {
27
+ const headers = createHeaders([['x-forwarded-for', '9.10.11.12, 13.14.15.16, 17.18.19.20']]);
28
+ expect(getClientIP(headers)).toBe('9.10.11.12');
29
+ });
30
+
31
+ it('should handle x-real-ip header', () => {
32
+ const headers = createHeaders([['x-real-ip', '21.22.23.24']]);
33
+ expect(getClientIP(headers)).toBe('21.22.23.24');
34
+ });
35
+
36
+ it('should trim whitespace from IP addresses', () => {
37
+ const headers = createHeaders([['x-client-ip', ' 25.26.27.28 ']]);
38
+ expect(getClientIP(headers)).toBe('25.26.27.28');
39
+ });
40
+
41
+ it('should respect header priority order', () => {
42
+ const headers = createHeaders([
43
+ ['x-forwarded-for', '1.1.1.1'],
44
+ ['cf-connecting-ip', '2.2.2.2'], // Should take precedence
45
+ ['x-real-ip', '3.3.3.3'],
46
+ ]);
47
+ expect(getClientIP(headers)).toBe('2.2.2.2');
48
+ });
49
+
50
+ it('should handle empty x-forwarded-for value', () => {
51
+ const headers = createHeaders([['x-forwarded-for', '']]);
52
+ expect(getClientIP(headers)).toBe('');
53
+ });
54
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 获取客户端 IP
3
+ * @param headers HTTP 请求头
4
+ */
5
+ export const getClientIP = (headers: Headers): string => {
6
+ // 按优先级顺序检查各种 IP 头
7
+ const ipHeaders = [
8
+ 'cf-connecting-ip', // Cloudflare
9
+ 'x-real-ip', // Nginx proxy
10
+ 'x-forwarded-for', // 标准代理头
11
+ 'x-client-ip', // Apache
12
+ 'true-client-ip', // Akamai and Cloudflare
13
+ 'x-cluster-client-ip', // 负载均衡
14
+ 'forwarded', // RFC 7239
15
+ 'fastly-client-ip', // Fastly CDN
16
+ 'x-forwarded', // General forward
17
+ 'x-original-forwarded-for', // Original forwarded
18
+ ];
19
+
20
+ for (const header of ipHeaders) {
21
+ const value = headers.get(header);
22
+ if (!value) continue;
23
+
24
+ // 处理可能包含多个 IP 的情况(比如 x-forwarded-for)
25
+ if (header.toLowerCase() === 'x-forwarded-for') {
26
+ const firstIP = value.split(',')[0].trim();
27
+ if (firstIP) return firstIP;
28
+ }
29
+
30
+ return value.trim();
31
+ }
32
+
33
+ return '';
34
+ };