@lobehub/chat 1.77.4 → 1.77.5

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,23 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.77.5](https://github.com/lobehub/lobe-chat/compare/v1.77.4...v1.77.5)
6
+
7
+ <sup>Released on **2025-04-01**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
5
22
  ### [Version 1.77.4](https://github.com/lobehub/lobe-chat/compare/v1.77.3...v1.77.4)
6
23
 
7
24
  <sup>Released on **2025-03-31**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2025-04-01",
5
+ "version": "1.77.5"
6
+ },
2
7
  {
3
8
  "children": {
4
9
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.77.4",
3
+ "version": "1.77.5",
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",
@@ -11,9 +11,10 @@ interface Params {
11
11
  id: string;
12
12
  }
13
13
 
14
- type Props = { params: Params };
14
+ type Props = { params: Promise<Params> };
15
15
 
16
- const MenuPage = async ({ params }: Props) => {
16
+ const MenuPage = async (props: Props) => {
17
+ const params = await props.params;
17
18
  const id = params.id;
18
19
  const item = await KnowledgeBaseModel.findById(serverDB, params.id);
19
20
 
@@ -1,4 +1,4 @@
1
- import { BrandLoading, LobeChatText } from '@lobehub/ui/brand';
1
+ import { BrandLoading, LobeHubText } from '@lobehub/ui/brand';
2
2
  import { Center } from 'react-layout-kit';
3
3
 
4
4
  import { isCustomBranding } from '@/const/version';
@@ -10,7 +10,7 @@ export default () => {
10
10
 
11
11
  return (
12
12
  <Center height={'100%'} width={'100%'}>
13
- <BrandLoading size={40} style={{ opacity: 0.6 }} text={LobeChatText} />
13
+ <BrandLoading size={40} style={{ opacity: 0.6 }} text={LobeHubText} />
14
14
  </Center>
15
15
  );
16
16
  };
@@ -128,50 +128,49 @@ export class FileModel {
128
128
  };
129
129
 
130
130
  deleteMany = async (ids: string[], removeGlobalFile: boolean = true) => {
131
- const fileList = await this.findByIds(ids);
132
- const hashList = fileList.map((file) => file.fileHash!);
131
+ if (ids.length === 0) return [];
133
132
 
134
133
  return await this.db.transaction(async (trx) => {
135
- // 1. 删除相关的 chunks
134
+ // 1. 先获取文件列表,以便返回删除的文件
135
+ const fileList = await trx.query.files.findMany({
136
+ where: and(inArray(files.id, ids), eq(files.userId, this.userId)),
137
+ });
138
+
139
+ if (fileList.length === 0) return [];
140
+
141
+ // 提取需要检查的文件哈希值
142
+ const hashList = fileList.map((file) => file.fileHash!).filter(Boolean);
143
+
144
+ // 2. 删除相关的 chunks
136
145
  await this.deleteFileChunks(trx as any, ids);
137
146
 
138
- // delete the files
147
+ // 3. 删除文件记录
139
148
  await trx.delete(files).where(and(inArray(files.id, ids), eq(files.userId, this.userId)));
140
149
 
141
- // count the files by hash
142
- const result = await trx
150
+ // 如果不需要删除全局文件,直接返回
151
+ if (!removeGlobalFile || hashList.length === 0) return fileList;
152
+
153
+ // 4. 找出不再被引用的哈希值
154
+ const remainingFiles = await trx
143
155
  .select({
144
- count: count(),
145
- hashId: files.fileHash,
156
+ fileHash: files.fileHash,
146
157
  })
147
158
  .from(files)
148
- .where(inArray(files.fileHash, hashList))
149
- .groupBy(files.fileHash);
150
-
151
- // Create a Map to store the query result
152
- const countMap = new Map(result.map((item) => [item.hashId, item.count]));
159
+ .where(inArray(files.fileHash, hashList));
153
160
 
154
- // Ensure that all incoming hashes have a result, even if it is 0
155
- const fileHashCounts = hashList.map((hashId) => ({
156
- count: countMap.get(hashId) || 0,
157
- hashId: hashId,
158
- }));
161
+ // 将仍在使用的哈希值放入Set中,便于快速查找
162
+ const usedHashes = new Set(remainingFiles.map((file) => file.fileHash));
159
163
 
160
- const needToDeleteList = fileHashCounts.filter((item) => item.count === 0);
164
+ // 找出需要删除的哈希值(不再被任何文件使用的)
165
+ const hashesToDelete = hashList.filter((hash) => !usedHashes.has(hash));
161
166
 
162
- if (needToDeleteList.length === 0 || !removeGlobalFile) return;
167
+ if (hashesToDelete.length === 0) return fileList;
163
168
 
164
- // delete the file from global file if it is not used by other files
165
- await trx.delete(globalFiles).where(
166
- inArray(
167
- globalFiles.hashId,
168
- needToDeleteList.map((item) => item.hashId!),
169
- ),
170
- );
169
+ // 5. 删除不再被引用的全局文件
170
+ await trx.delete(globalFiles).where(inArray(globalFiles.hashId, hashesToDelete));
171
171
 
172
- return fileList.filter((file) =>
173
- needToDeleteList.some((item) => item.hashId === file.fileHash),
174
- );
172
+ // 返回删除的文件列表
173
+ return fileList;
175
174
  });
176
175
  };
177
176
 
@@ -318,25 +317,58 @@ export class FileModel {
318
317
 
319
318
  // 抽象出通用的删除 chunks 方法
320
319
  private deleteFileChunks = async (trx: PgTransaction<any>, fileIds: string[]) => {
321
- const BATCH_SIZE = 1000; // 每批处理的数量
320
+ if (fileIds.length === 0) return;
322
321
 
323
- // 1. 获取所有关联的 chunk IDs
322
+ // 直接使用 JOIN 优化查询,减少数据传输量
324
323
  const relatedChunks = await trx
325
324
  .select({ chunkId: fileChunks.chunkId })
326
325
  .from(fileChunks)
327
- .where(inArray(fileChunks.fileId, fileIds));
326
+ .where(
327
+ and(
328
+ inArray(fileChunks.fileId, fileIds),
329
+ // 确保只查询有效的 chunkId
330
+ notExists(
331
+ trx
332
+ .select()
333
+ .from(knowledgeBaseFiles)
334
+ .where(eq(knowledgeBaseFiles.fileId, fileChunks.fileId)),
335
+ ),
336
+ ),
337
+ );
328
338
 
329
339
  const chunkIds = relatedChunks.map((c) => c.chunkId).filter(Boolean) as string[];
330
340
 
331
341
  if (chunkIds.length === 0) return;
332
342
 
333
- // 2. 分批处理删除
334
- for (let i = 0; i < chunkIds.length; i += BATCH_SIZE) {
335
- const batchChunkIds = chunkIds.slice(i, i + BATCH_SIZE);
343
+ // 批量处理配置
344
+ const BATCH_SIZE = 1000; // 增加批处理量
345
+ const MAX_CONCURRENT_BATCHES = 3; // 最大并行批次数
346
+
347
+ // 分批并行处理
348
+ for (let i = 0; i < chunkIds.length; i += BATCH_SIZE * MAX_CONCURRENT_BATCHES) {
349
+ const batchPromises = [];
336
350
 
337
- await trx.delete(embeddings).where(inArray(embeddings.chunkId, batchChunkIds));
351
+ // 创建多个并行批次
352
+ for (let j = 0; j < MAX_CONCURRENT_BATCHES; j++) {
353
+ const startIdx = i + j * BATCH_SIZE;
354
+ if (startIdx >= chunkIds.length) break;
355
+
356
+ const batchChunkIds = chunkIds.slice(startIdx, startIdx + BATCH_SIZE);
357
+ if (batchChunkIds.length === 0) continue;
358
+
359
+ // 为每个批次创建一个删除任务
360
+ const batchPromise = (async () => {
361
+ // 先删除嵌入向量
362
+ await trx.delete(embeddings).where(inArray(embeddings.chunkId, batchChunkIds));
363
+ // 再删除块
364
+ await trx.delete(chunks).where(inArray(chunks.id, batchChunkIds));
365
+ })();
366
+
367
+ batchPromises.push(batchPromise);
368
+ }
338
369
 
339
- await trx.delete(chunks).where(inArray(chunks.id, batchChunkIds));
370
+ // 等待当前批次的所有任务完成
371
+ await Promise.all(batchPromises);
340
372
  }
341
373
 
342
374
  return chunkIds;
@@ -1724,7 +1724,7 @@ See benchmarks on the launch announcement [here](https://mistral.ai/news/mixtral
1724
1724
  #moe",
1725
1725
  "displayName": "Mistral: Mixtral 8x22B Instruct",
1726
1726
  "enabled": false,
1727
- "functionCall": false,
1727
+ "functionCall": true,
1728
1728
  "id": "mistralai/mixtral-8x22b-instruct",
1729
1729
  "maxTokens": undefined,
1730
1730
  "pricing": {
@@ -74,7 +74,7 @@ export const fileRouter = router({
74
74
  .query(async ({ ctx, input }): Promise<FileListItem | undefined> => {
75
75
  const item = await ctx.fileModel.findById(input.id);
76
76
 
77
- if (!item) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
77
+ if (!item) throw new TRPCError({ code: 'NOT_FOUND', message: 'File not found' });
78
78
 
79
79
  let embeddingTask = null;
80
80
  if (item.embeddingTaskId) {