@lobehub/chat 1.26.16 → 1.26.18
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/package.json +7 -7
- package/src/app/(backend)/webapi/chat/[provider]/route.ts +8 -2
- package/src/config/file.ts +2 -0
- package/src/database/server/models/message.ts +6 -4
- package/src/libs/agent-runtime/error.ts +2 -0
- package/src/libs/next-auth/sso-providers/azure-ad.ts +2 -2
- package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +25 -0
- package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +4 -0
- package/src/server/modules/S3/index.ts +11 -0
- package/src/server/routers/lambda/file.ts +12 -8
- package/src/server/routers/lambda/ragEval.ts +1 -1
- package/src/server/utils/files.test.ts +8 -7
- package/src/server/utils/files.ts +8 -1
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.26.18](https://github.com/lobehub/lobe-chat/compare/v1.26.17...v1.26.18)
|
6
|
+
|
7
|
+
<sup>Released on **2024-11-03**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Fix MS Entra ID and Azure AD authorization.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's fixed
|
19
|
+
|
20
|
+
- **misc**: Fix MS Entra ID and Azure AD authorization, closes [#4579](https://github.com/lobehub/lobe-chat/issues/4579) ([ced8a08](https://github.com/lobehub/lobe-chat/commit/ced8a08))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.26.17](https://github.com/lobehub/lobe-chat/compare/v1.26.16...v1.26.17)
|
31
|
+
|
32
|
+
<sup>Released on **2024-10-31**</sup>
|
33
|
+
|
34
|
+
#### ♻ Code Refactoring
|
35
|
+
|
36
|
+
- **misc**: Improve server log on chat api.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### Code refactoring
|
44
|
+
|
45
|
+
- **misc**: Improve server log on chat api, closes [#4559](https://github.com/lobehub/lobe-chat/issues/4559) ([cd8a134](https://github.com/lobehub/lobe-chat/commit/cd8a134))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.26.16](https://github.com/lobehub/lobe-chat/compare/v1.26.15...v1.26.16)
|
6
56
|
|
7
57
|
<sup>Released on **2024-10-31**</sup>
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.26.
|
3
|
+
"version": "1.26.18",
|
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",
|
@@ -102,7 +102,7 @@
|
|
102
102
|
"dependencies": {
|
103
103
|
"@ant-design/icons": "^5.5.1",
|
104
104
|
"@ant-design/pro-components": "^2.7.18",
|
105
|
-
"@anthropic-ai/sdk": "^0.
|
105
|
+
"@anthropic-ai/sdk": "^0.31.0",
|
106
106
|
"@auth/core": "^0.37.0",
|
107
107
|
"@aws-sdk/client-bedrock-runtime": "^3.675.0",
|
108
108
|
"@aws-sdk/client-s3": "^3.675.0",
|
@@ -150,7 +150,7 @@
|
|
150
150
|
"debug": "^4.3.7",
|
151
151
|
"dexie": "^3.2.7",
|
152
152
|
"diff": "^5.2.0",
|
153
|
-
"drizzle-orm": "^0.
|
153
|
+
"drizzle-orm": "^0.36.0",
|
154
154
|
"drizzle-zod": "^0.5.1",
|
155
155
|
"fast-deep-equal": "^3.1.3",
|
156
156
|
"file-type": "^19.6.0",
|
@@ -224,7 +224,7 @@
|
|
224
224
|
"url-join": "^5.0.0",
|
225
225
|
"use-merge-value": "^1.2.0",
|
226
226
|
"utility-types": "^3.11.0",
|
227
|
-
"uuid": "^
|
227
|
+
"uuid": "^11.0.0",
|
228
228
|
"ws": "^8.18.0",
|
229
229
|
"y-protocols": "^1.0.6",
|
230
230
|
"y-webrtc": "^10.3.0",
|
@@ -248,7 +248,7 @@
|
|
248
248
|
"@testing-library/react": "^16.0.1",
|
249
249
|
"@types/chroma-js": "^2.4.4",
|
250
250
|
"@types/debug": "^4.1.12",
|
251
|
-
"@types/diff": "^
|
251
|
+
"@types/diff": "^6.0.0",
|
252
252
|
"@types/ip": "^1.1.3",
|
253
253
|
"@types/json-schema": "^7.0.15",
|
254
254
|
"@types/lodash": "^4.17.12",
|
@@ -271,7 +271,7 @@
|
|
271
271
|
"consola": "^3.2.3",
|
272
272
|
"dotenv": "^16.4.5",
|
273
273
|
"dpdm-fast": "^1.0.4",
|
274
|
-
"drizzle-kit": "^0.
|
274
|
+
"drizzle-kit": "^0.27.0",
|
275
275
|
"eslint": "^8.57.1",
|
276
276
|
"eslint-plugin-mdx": "^2.3.4",
|
277
277
|
"eslint-plugin-unused-imports": "4.0.1",
|
@@ -302,7 +302,7 @@
|
|
302
302
|
"vitest": "~1.2.2",
|
303
303
|
"vitest-canvas-mock": "^0.3.3"
|
304
304
|
},
|
305
|
-
"packageManager": "pnpm@9.12.
|
305
|
+
"packageManager": "pnpm@9.12.3",
|
306
306
|
"publishConfig": {
|
307
307
|
"access": "public",
|
308
308
|
"registry": "https://registry.npmjs.org"
|
@@ -1,5 +1,9 @@
|
|
1
1
|
import { checkAuth } from '@/app/(backend)/middleware/auth';
|
2
|
-
import {
|
2
|
+
import {
|
3
|
+
AGENT_RUNTIME_ERROR_SET,
|
4
|
+
AgentRuntime,
|
5
|
+
ChatCompletionErrorPayload,
|
6
|
+
} from '@/libs/agent-runtime';
|
3
7
|
import { createTraceOptions, initAgentRuntimeWithUserPayload } from '@/server/modules/AgentRuntime';
|
4
8
|
import { ChatErrorType } from '@/types/fetch';
|
5
9
|
import { ChatStreamPayload } from '@/types/openai/chat';
|
@@ -44,8 +48,10 @@ export const POST = checkAuth(async (req: Request, { params, jwtPayload, createR
|
|
44
48
|
} = e as ChatCompletionErrorPayload;
|
45
49
|
|
46
50
|
const error = errorContent || e;
|
51
|
+
|
52
|
+
const logMethod = AGENT_RUNTIME_ERROR_SET.has(errorType as string) ? 'warn' : 'error';
|
47
53
|
// track the error at server side
|
48
|
-
console
|
54
|
+
console[logMethod](`Route: [${provider}] ${errorType}:`, error);
|
49
55
|
|
50
56
|
return createErrorResponse(errorType, { error, ...res, provider });
|
51
57
|
}
|
package/src/config/file.ts
CHANGED
@@ -31,6 +31,7 @@ export const getFileConfig = () => {
|
|
31
31
|
S3_BUCKET: process.env.S3_BUCKET,
|
32
32
|
S3_ENABLE_PATH_STYLE: process.env.S3_ENABLE_PATH_STYLE === '1',
|
33
33
|
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
34
|
+
S3_PREVIEW_URL_EXPIRE_IN: parseInt(process.env.S3_PREVIEW_URL_EXPIRE_IN || '7200'),
|
34
35
|
S3_PUBLIC_DOMAIN,
|
35
36
|
S3_REGION: process.env.S3_REGION,
|
36
37
|
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
|
@@ -46,6 +47,7 @@ export const getFileConfig = () => {
|
|
46
47
|
S3_ENABLE_PATH_STYLE: z.boolean(),
|
47
48
|
|
48
49
|
S3_ENDPOINT: z.string().url().optional(),
|
50
|
+
S3_PREVIEW_URL_EXPIRE_IN: z.number(),
|
49
51
|
S3_PUBLIC_DOMAIN: z.string().url().optional(),
|
50
52
|
S3_REGION: z.string().optional(),
|
51
53
|
S3_SECRET_ACCESS_KEY: z.string().optional(),
|
@@ -127,10 +127,12 @@ export class MessageModel {
|
|
127
127
|
.leftJoin(files, eq(files.id, messagesFiles.fileId))
|
128
128
|
.where(inArray(messagesFiles.messageId, messageIds));
|
129
129
|
|
130
|
-
const relatedFileList =
|
131
|
-
|
132
|
-
|
133
|
-
|
130
|
+
const relatedFileList = await Promise.all(
|
131
|
+
rawRelatedFileList.map(async (file) => ({
|
132
|
+
...file,
|
133
|
+
url: await getFullFileUrl(file.url),
|
134
|
+
})),
|
135
|
+
);
|
134
136
|
|
135
137
|
const imageList = relatedFileList.filter((i) => (i.fileType || '').startsWith('image'));
|
136
138
|
const fileList = relatedFileList.filter((i) => !(i.fileType || '').startsWith('image'));
|
@@ -27,5 +27,7 @@ export const AgentRuntimeErrorType = {
|
|
27
27
|
OpenAIBizError: 'OpenAIBizError',
|
28
28
|
} as const;
|
29
29
|
|
30
|
+
export const AGENT_RUNTIME_ERROR_SET = new Set<string>(Object.values(AgentRuntimeErrorType));
|
31
|
+
|
30
32
|
export type ILobeAgentRuntimeErrorType =
|
31
33
|
(typeof AgentRuntimeErrorType)[keyof typeof AgentRuntimeErrorType];
|
@@ -2,6 +2,7 @@ import AzureAD from 'next-auth/providers/azure-ad';
|
|
2
2
|
|
3
3
|
import { authEnv } from '@/config/auth';
|
4
4
|
|
5
|
+
import { getMicrosoftEntraIdIssuer } from './microsoft-entra-id-helper';
|
5
6
|
import { CommonProviderConfig } from './sso.config';
|
6
7
|
|
7
8
|
const provider = {
|
@@ -14,8 +15,7 @@ const provider = {
|
|
14
15
|
// TODO(NextAuth ENVs Migration): Remove once nextauth envs migration time end
|
15
16
|
clientId: authEnv.AZURE_AD_CLIENT_ID ?? process.env.AUTH_AZURE_AD_ID,
|
16
17
|
clientSecret: authEnv.AZURE_AD_CLIENT_SECRET ?? process.env.AUTH_AZURE_AD_SECRET,
|
17
|
-
|
18
|
-
tenantId: authEnv.AZURE_AD_TENANT_ID ?? process.env.AUTH_AZURE_AD_TENANT_ID,
|
18
|
+
issuer: getMicrosoftEntraIdIssuer(),
|
19
19
|
// Remove end
|
20
20
|
// TODO(NextAuth): map unique user id to `providerAccountId` field
|
21
21
|
// profile(profile) {
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { authEnv } from '@/config/auth';
|
2
|
+
|
3
|
+
function getTenantId() {
|
4
|
+
return (
|
5
|
+
process.env.MICROSOFT_ENTRA_ID_TENANT_ID ??
|
6
|
+
process.env.AUTH_AZURE_AD_TENANT_ID ??
|
7
|
+
authEnv.AZURE_AD_TENANT_ID
|
8
|
+
);
|
9
|
+
}
|
10
|
+
|
11
|
+
function getIssuer() {
|
12
|
+
const issuer = process.env.MICROSOFT_ENTRA_ID_ISSUER;
|
13
|
+
if (issuer) {
|
14
|
+
return issuer;
|
15
|
+
}
|
16
|
+
const tenantId = getTenantId();
|
17
|
+
if (tenantId) {
|
18
|
+
// refs: https://github.com/nextauthjs/next-auth/discussions/9154#discussioncomment-10583104
|
19
|
+
return `https://login.microsoftonline.com/${tenantId}/v2.0`;
|
20
|
+
} else {
|
21
|
+
return undefined;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
export { getIssuer as getMicrosoftEntraIdIssuer, getTenantId as getMicrosoftEntraIdTenantId };
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import MicrosoftEntraID from 'next-auth/providers/microsoft-entra-id';
|
2
2
|
|
3
|
+
import { getMicrosoftEntraIdIssuer } from './microsoft-entra-id-helper';
|
3
4
|
import { CommonProviderConfig } from './sso.config';
|
4
5
|
|
5
6
|
const provider = {
|
@@ -9,6 +10,9 @@ const provider = {
|
|
9
10
|
// Specify auth scope, at least include 'openid email'
|
10
11
|
// all scopes in Azure AD ref: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc#openid-connect-scopes
|
11
12
|
authorization: { params: { scope: 'openid email profile' } },
|
13
|
+
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID ?? process.env.AUTH_AZURE_AD_ID,
|
14
|
+
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET ?? process.env.AUTH_AZURE_AD_SECRET,
|
15
|
+
issuer: getMicrosoftEntraIdIssuer(),
|
12
16
|
}),
|
13
17
|
};
|
14
18
|
|
@@ -105,6 +105,17 @@ export class S3 {
|
|
105
105
|
return getSignedUrl(this.client, command, { expiresIn: 3600 });
|
106
106
|
}
|
107
107
|
|
108
|
+
public async createPreSignedUrlForPreview(key: string, expiresIn?: number): Promise<string> {
|
109
|
+
const command = new GetObjectCommand({
|
110
|
+
Bucket: this.bucket,
|
111
|
+
Key: key,
|
112
|
+
});
|
113
|
+
|
114
|
+
return getSignedUrl(this.client, command, {
|
115
|
+
expiresIn: expiresIn ?? fileEnv.S3_PREVIEW_URL_EXPIRE_IN,
|
116
|
+
});
|
117
|
+
}
|
118
|
+
|
108
119
|
public async uploadContent(path: string, content: string) {
|
109
120
|
const command = new PutObjectCommand({
|
110
121
|
ACL: this.setAcl ? 'public-read' : undefined,
|
@@ -57,7 +57,7 @@ export const fileRouter = router({
|
|
57
57
|
url: input.url,
|
58
58
|
});
|
59
59
|
|
60
|
-
return { id, url: getFullFileUrl(input.url) };
|
60
|
+
return { id, url: await getFullFileUrl(input.url) };
|
61
61
|
}),
|
62
62
|
findById: fileProcedure
|
63
63
|
.input(
|
@@ -69,7 +69,7 @@ export const fileRouter = router({
|
|
69
69
|
const item = await ctx.fileModel.findById(input.id);
|
70
70
|
if (!item) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
|
71
71
|
|
72
|
-
return { ...item, url: getFullFileUrl(item?.url) };
|
72
|
+
return { ...item, url: await getFullFileUrl(item?.url) };
|
73
73
|
}),
|
74
74
|
|
75
75
|
getFileItemById: fileProcedure
|
@@ -102,7 +102,7 @@ export const fileRouter = router({
|
|
102
102
|
embeddingError: embeddingTask?.error,
|
103
103
|
embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
|
104
104
|
finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
|
105
|
-
url: getFullFileUrl(item.url!),
|
105
|
+
url: await getFullFileUrl(item.url!),
|
106
106
|
};
|
107
107
|
}),
|
108
108
|
|
@@ -124,13 +124,14 @@ export const fileRouter = router({
|
|
124
124
|
AsyncTaskType.Embedding,
|
125
125
|
);
|
126
126
|
|
127
|
-
|
127
|
+
const resultFiles = [] as any[];
|
128
|
+
for (const { chunkTaskId, embeddingTaskId, ...item } of fileList as any[]) {
|
128
129
|
const chunkTask = chunkTaskId ? chunkTasks.find((task) => task.id === chunkTaskId) : null;
|
129
130
|
const embeddingTask = embeddingTaskId
|
130
131
|
? embeddingTasks.find((task) => task.id === embeddingTaskId)
|
131
132
|
: null;
|
132
133
|
|
133
|
-
|
134
|
+
const fileItem = {
|
134
135
|
...item,
|
135
136
|
chunkCount: chunks.find((chunk) => chunk.id === item.id)?.count ?? null,
|
136
137
|
chunkingError: chunkTask?.error ?? null,
|
@@ -138,9 +139,12 @@ export const fileRouter = router({
|
|
138
139
|
embeddingError: embeddingTask?.error ?? null,
|
139
140
|
embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
|
140
141
|
finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
|
141
|
-
url: getFullFileUrl(item.url!),
|
142
|
-
};
|
143
|
-
|
142
|
+
url: await getFullFileUrl(item.url!),
|
143
|
+
} as FileListItem;
|
144
|
+
resultFiles.push(fileItem);
|
145
|
+
}
|
146
|
+
|
147
|
+
return resultFiles;
|
144
148
|
}),
|
145
149
|
|
146
150
|
removeAllFiles: fileProcedure.mutation(async ({ ctx }) => {
|
@@ -8,6 +8,7 @@ const config = {
|
|
8
8
|
S3_ENABLE_PATH_STYLE: false,
|
9
9
|
S3_PUBLIC_DOMAIN: 'https://example.com',
|
10
10
|
S3_BUCKET: 'my-bucket',
|
11
|
+
S3_SET_ACL: true,
|
11
12
|
};
|
12
13
|
|
13
14
|
vi.mock('@/config/file', () => ({
|
@@ -17,20 +18,20 @@ vi.mock('@/config/file', () => ({
|
|
17
18
|
}));
|
18
19
|
|
19
20
|
describe('getFullFileUrl', () => {
|
20
|
-
it('should return empty string for null or undefined input', () => {
|
21
|
-
expect(getFullFileUrl(null)).toBe('');
|
22
|
-
expect(getFullFileUrl(undefined)).toBe('');
|
21
|
+
it('should return empty string for null or undefined input', async () => {
|
22
|
+
expect(await getFullFileUrl(null)).toBe('');
|
23
|
+
expect(await getFullFileUrl(undefined)).toBe('');
|
23
24
|
});
|
24
25
|
|
25
|
-
it('should return correct URL when S3_ENABLE_PATH_STYLE is false', () => {
|
26
|
+
it('should return correct URL when S3_ENABLE_PATH_STYLE is false', async () => {
|
26
27
|
const url = 'path/to/file.jpg';
|
27
|
-
expect(getFullFileUrl(url)).toBe('https://example.com/path/to/file.jpg');
|
28
|
+
expect(await getFullFileUrl(url)).toBe('https://example.com/path/to/file.jpg');
|
28
29
|
});
|
29
30
|
|
30
|
-
it('should return correct URL when S3_ENABLE_PATH_STYLE is true', () => {
|
31
|
+
it('should return correct URL when S3_ENABLE_PATH_STYLE is true', async () => {
|
31
32
|
config.S3_ENABLE_PATH_STYLE = true;
|
32
33
|
const url = 'path/to/file.jpg';
|
33
|
-
expect(getFullFileUrl(url)).toBe('https://example.com/my-bucket/path/to/file.jpg');
|
34
|
+
expect(await getFullFileUrl(url)).toBe('https://example.com/my-bucket/path/to/file.jpg');
|
34
35
|
config.S3_ENABLE_PATH_STYLE = false;
|
35
36
|
});
|
36
37
|
});
|
@@ -1,10 +1,17 @@
|
|
1
1
|
import urlJoin from 'url-join';
|
2
2
|
|
3
3
|
import { fileEnv } from '@/config/file';
|
4
|
+
import { S3 } from '@/server/modules/S3';
|
4
5
|
|
5
|
-
export const getFullFileUrl = (url?: string | null) => {
|
6
|
+
export const getFullFileUrl = async (url?: string | null, expiresIn?: number) => {
|
6
7
|
if (!url) return '';
|
7
8
|
|
9
|
+
// If bucket is not set public read, the preview address needs to be regenerated each time
|
10
|
+
if (!fileEnv.S3_SET_ACL) {
|
11
|
+
const s3 = new S3();
|
12
|
+
return await s3.createPreSignedUrlForPreview(url, expiresIn);
|
13
|
+
}
|
14
|
+
|
8
15
|
if (fileEnv.S3_ENABLE_PATH_STYLE) {
|
9
16
|
return urlJoin(fileEnv.S3_PUBLIC_DOMAIN!, fileEnv.S3_BUCKET!, url);
|
10
17
|
}
|