@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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +2 -1
- package/src/app/(backend)/middleware/auth/index.test.ts +1 -1
- package/src/app/(backend)/middleware/auth/index.ts +3 -2
- package/src/app/(backend)/webapi/assistant/[id]/route.ts +5 -1
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +6 -6
- package/src/app/(backend)/webapi/chat/[provider]/route.ts +1 -1
- package/src/app/(backend)/webapi/chat/anthropic/route.test.ts +3 -1
- package/src/app/(backend)/webapi/chat/anthropic/route.ts +1 -1
- package/src/app/(backend)/webapi/chat/google/route.test.ts +3 -1
- package/src/app/(backend)/webapi/chat/google/route.ts +2 -1
- package/src/app/(backend)/webapi/chat/minimax/route.test.ts +3 -1
- package/src/app/(backend)/webapi/chat/minimax/route.ts +2 -1
- package/src/app/(backend)/webapi/chat/models/[provider]/route.ts +1 -1
- package/src/app/(backend)/webapi/chat/openai/route.test.ts +3 -1
- package/src/app/(backend)/webapi/chat/openai/route.ts +2 -1
- package/src/app/(backend)/webapi/chat/wenxin/route.test.ts +1 -1
- package/src/app/(backend)/webapi/chat/wenxin/route.ts +1 -1
- package/src/app/(backend)/webapi/text-to-image/[provider]/route.ts +1 -1
- package/src/components/Analytics/ReactScan.tsx +11 -0
- package/src/components/Analytics/index.tsx +4 -0
- package/src/config/analytics.ts +6 -0
- package/src/libs/agent-runtime/types/chat.ts +7 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
- package/src/utils/clientIP.test.ts +54 -0
- 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
|
+
[](#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
|
+
[](#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.
|
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
|
-
|
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
|
-
|
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, {
|
26
|
+
expect(UniverseRoute).toHaveBeenCalledWith(mockRequest, {
|
27
|
+
params: Promise.resolve({ provider: 'anthropic' }),
|
28
|
+
});
|
27
29
|
});
|
28
30
|
});
|
@@ -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, {
|
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) =>
|
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, {
|
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) =>
|
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, {
|
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) =>
|
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
|
});
|
@@ -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
|
};
|
package/src/config/analytics.ts
CHANGED
@@ -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
|
+
};
|