@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 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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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.16",
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.30.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.35.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": "^10.0.0",
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": "^5.2.3",
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.26.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.2",
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 { AgentRuntime, ChatCompletionErrorPayload } from '@/libs/agent-runtime';
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.error(`Route: [${provider}] ${errorType}:`, error);
54
+ console[logMethod](`Route: [${provider}] ${errorType}:`, error);
49
55
 
50
56
  return createErrorResponse(errorType, { error, ...res, provider });
51
57
  }
@@ -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 = rawRelatedFileList.map((file) => ({
131
- ...file,
132
- url: getFullFileUrl(file.url),
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
- // @ts-ignore
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
- return fileList.map(({ chunkTaskId, embeddingTaskId, ...item }): FileListItem => {
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
- return {
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 }) => {
@@ -263,7 +263,7 @@ export const ragEvalRouter = router({
263
263
  // 保存数据
264
264
  await ctx.evaluationModel.update(input.id, {
265
265
  status: EvalEvaluationStatus.Success,
266
- evalRecordsUrl: getFullFileUrl(path),
266
+ evalRecordsUrl: await getFullFileUrl(path),
267
267
  });
268
268
  }
269
269
 
@@ -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
  }