@lobehub/chat 1.12.13 → 1.12.14

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,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.12.14](https://github.com/lobehub/lobe-chat/compare/v1.12.13...v1.12.14)
6
+
7
+ <sup>Released on **2024-08-24**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix tts file saving in server mode.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix tts file saving in server mode, closes [#3585](https://github.com/lobehub/lobe-chat/issues/3585) ([ab1cb47](https://github.com/lobehub/lobe-chat/commit/ab1cb47))
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
+
5
30
  ### [Version 1.12.13](https://github.com/lobehub/lobe-chat/compare/v1.12.12...v1.12.13)
6
31
 
7
32
  <sup>Released on **2024-08-24**</sup>
package/README.md CHANGED
@@ -268,12 +268,12 @@ Our marketplace is not just a showcase platform but also a collaborative space.
268
268
 
269
269
  | Recent Submits | Description |
270
270
  | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
271
+ | [TypeScript Solution Architect](https://chat-preview.lobehub.com/market?agent=typescript-developer)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-08-24**</sup> | Expert in TypeScript, Node.js, Vue.js 3, Nuxt.js 3, Express.js, React.js, and modern UI libraries.<br/>`type-script` `java-script` `web-development` `coding-standards` `best-practices` |
271
272
  | [Variable Name Conversion Expert](https://chat-preview.lobehub.com/market?agent=variable-name-conversion)<br/><sup>By **[zengyishou](https://github.com/zengyishou)** on **2024-08-21**</sup> | In software development, naming variables is a common yet relatively time-consuming task. This assistant can automatically convert Chinese variable names into English variable names that conform to camelCase, PascalCase, snake_case, kebab-case, and constant naming conventions based on specific naming rules. This not only improves code readability but also alleviates the frustration of variable naming.<br/>`software-development` `variable-naming` `chinese-to-english` `code-standards` `automatic-conversion` |
272
273
  | [Prompt Engineering Expert](https://chat-preview.lobehub.com/market?agent=ai-prompts-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | Specializing in prompt optimization and design<br/>`prompt-engineering` `ai-interaction` `writing` `optimization` `consulting` |
273
274
  | [Commit Message Generator](https://chat-preview.lobehub.com/market?agent=commit-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | Expert at generating precise Git commit messages<br/>`programming` `git` `commit-message` `code-review` |
274
- | [RO-SCIRAW Prompt Word Expert](https://chat-preview.lobehub.com/market?agent=rosciraw)<br/><sup>By **[kirklin](https://github.com/kirklin)** on **2024-08-06**</sup> | The RO-SCIRAW framework, created by Kirk Lin, is a methodology for prompt words that provides a new paradigm for building highly precise and efficient prompt words. Please enter the information for the persona you want to create.<br/>`prompt-word-framework` |
275
275
 
276
- > 📊 Total agents: [<kbd>**315**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
276
+ > 📊 Total agents: [<kbd>**316**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
277
277
 
278
278
  <!-- AGENT LIST -->
279
279
 
package/README.zh-CN.md CHANGED
@@ -256,12 +256,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
256
256
 
257
257
  | 最近新增 | 助手说明 |
258
258
  | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | [TypeScript 解决方案架构师](https://chat-preview.lobehub.com/market?agent=typescript-developer)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-08-24**</sup> | 精通 TypeScript、Node.js、Vue.js 3、Nuxt.js 3、Express.js、React.js 和现代 UI 库。<br/>`类型脚本` `java-script` `网页开发` `编码标准` `最佳实践` |
259
260
  | [开发变量名转换专家](https://chat-preview.lobehub.com/market?agent=variable-name-conversion)<br/><sup>By **[zengyishou](https://github.com/zengyishou)** on **2024-08-21**</sup> | 在软件开发过程中,命名变量是一项常见却相对耗时的任务。本助手能够根据特定的命名规则自动将中文变量名转换为符合小驼峰、大驼峰、下划线、横线以及常量命名规范的英文变量名。这不仅提高了代码的可读性,还解决了变量命名的苦恼。<br/>`软件开发` `变量命名` `中文转英文` `代码规范` `自动转换` |
260
261
  | [提示工程专家](https://chat-preview.lobehub.com/market?agent=ai-prompts-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | 专精 Prompt 优化与设计<br/>`提示工程` `ai交互` `写作` `优化` `咨询` |
261
262
  | [提交信息生成器](https://chat-preview.lobehub.com/market?agent=commit-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | 擅长生成精准的 Git 提交信息<br/>`编程` `git` `提交信息` `代码审查` |
262
- | [RO-SCIRAW 提示词专家](https://chat-preview.lobehub.com/market?agent=rosciraw)<br/><sup>By **[kirklin](https://github.com/kirklin)** on **2024-08-06**</sup> | RO-SCIRAW 框架是由 Kirk Lin 开创的提示词方法论,为构建高度精确和高效的提示词提供了一个全新的范式。请输入你要创建的分身信息。<br/>`提示词框架` |
263
263
 
264
- > 📊 Total agents: [<kbd>**315**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
264
+ > 📊 Total agents: [<kbd>**316**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
265
265
 
266
266
  <!-- AGENT LIST -->
267
267
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.12.13",
3
+ "version": "1.12.14",
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",
@@ -248,9 +248,10 @@ describe('MessageModel', () => {
248
248
 
249
249
  // 断言结果
250
250
  expect(result[0].extra.translate).toEqual({ content: 'translated', from: 'en', to: 'zh' });
251
- // TODO: 确认是否需要包含 tts 字段
252
251
  expect(result[0].extra.tts).toEqual({
253
- // contentMd5: 'md5', file: 'f1', voice: 'voice1'
252
+ contentMd5: 'md5',
253
+ file: 'f1',
254
+ voice: 'voice1',
254
255
  });
255
256
  });
256
257
 
@@ -89,11 +89,9 @@ export class MessageModel {
89
89
  },
90
90
 
91
91
  ttsId: messageTTS.id,
92
-
93
- // TODO: 确认下如何处理 TTS 的读取
94
- // ttsContentMd5: messageTTS.contentMd5,
95
- // ttsFile: messageTTS.fileId,
96
- // ttsVoice: messageTTS.voice,
92
+ ttsContentMd5: messageTTS.contentMd5,
93
+ ttsFile: messageTTS.fileId,
94
+ ttsVoice: messageTTS.voice,
97
95
  /* eslint-enable */
98
96
  })
99
97
  .from(messages)
@@ -113,7 +111,7 @@ export class MessageModel {
113
111
 
114
112
  const messageIds = result.map((message) => message.id as string);
115
113
 
116
- if (messageIds.length === 0) return result;
114
+ if (messageIds.length === 0) return [];
117
115
 
118
116
  // 2. get relative files
119
117
  const rawRelatedFileList = await serverDB
@@ -166,14 +164,7 @@ export class MessageModel {
166
164
  .where(inArray(messageQueries.messageId, messageIds));
167
165
 
168
166
  return result.map(
169
- ({
170
- model,
171
- provider,
172
- translate,
173
- ttsId,
174
- // ttsFile, ttsId, ttsContentMd5, ttsVoice,
175
- ...item
176
- }) => {
167
+ ({ model, provider, translate, ttsId, ttsFile, ttsContentMd5, ttsVoice, ...item }) => {
177
168
  const messageQuery = messageQueriesList.find((relation) => relation.messageId === item.id);
178
169
  return {
179
170
  ...item,
@@ -185,9 +176,9 @@ export class MessageModel {
185
176
  translate,
186
177
  tts: ttsId
187
178
  ? {
188
- // contentMd5: ttsContentMd5,
189
- // file: ttsFile,
190
- // voice: ttsVoice,
179
+ contentMd5: ttsContentMd5,
180
+ file: ttsFile,
181
+ voice: ttsVoice,
191
182
  }
192
183
  : undefined,
193
184
  },
@@ -150,7 +150,7 @@ export const messageRouter = router({
150
150
  value: z
151
151
  .object({
152
152
  contentMd5: z.string().optional(),
153
- fileId: z.string().optional(),
153
+ file: z.string().optional(),
154
154
  voice: z.string().optional(),
155
155
  })
156
156
  .or(z.literal(false)),
@@ -52,38 +52,6 @@ describe('TTSFileAction', () => {
52
52
  expect(fileService.removeFile).toHaveBeenCalledWith(fileId);
53
53
  });
54
54
 
55
- // Test for uploadTTSFile
56
- it('uploadTTSFile should upload the file and return the file id', async () => {
57
- const testFile = new File(['content'], 'test.mp3', { type: 'audio/mp3' });
58
- const uploadedFileData = {
59
- id: 'new-tts-file-id',
60
- createdAt: testFile.lastModified,
61
- data: await testFile.arrayBuffer(),
62
- fileType: testFile.type,
63
- name: testFile.name,
64
- saveMode: 'local',
65
- size: testFile.size,
66
- };
67
-
68
- // Mock the fileService.uploadFile to resolve with uploadedFileData
69
- vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: uploadedFileData.id, url: '' });
70
-
71
- let fileId;
72
- await act(async () => {
73
- fileId = await useStore.getState().uploadTTSFile(testFile);
74
- });
75
-
76
- expect(fileService.createFile).toHaveBeenCalledWith({
77
- createdAt: testFile.lastModified,
78
- data: await testFile.arrayBuffer(),
79
- fileType: testFile.type,
80
- name: testFile.name,
81
- saveMode: 'local',
82
- size: testFile.size,
83
- });
84
- expect(fileId).toBe(uploadedFileData.id);
85
- });
86
-
87
55
  // Test for uploadTTSByArrayBuffers
88
56
  it('uploadTTSByArrayBuffers should create a file and call uploadTTSFile', async () => {
89
57
  const messageId = 'message-id';
@@ -93,8 +61,8 @@ describe('TTSFileAction', () => {
93
61
 
94
62
  // Spy on uploadTTSFile to simulate a successful upload
95
63
  const uploadTTSFileSpy = vi
96
- .spyOn(useStore.getState(), 'uploadTTSFile')
97
- .mockResolvedValue('new-tts-file-id');
64
+ .spyOn(useStore.getState(), 'uploadWithProgress')
65
+ .mockResolvedValue({ id: 'new-tts-file-id', url: '1' });
98
66
 
99
67
  let fileId;
100
68
  await act(async () => {
@@ -1,12 +1,14 @@
1
- import useSWR, { SWRResponse } from 'swr';
1
+ import { SWRResponse } from 'swr';
2
2
  import { StateCreator } from 'zustand/vanilla';
3
3
 
4
+ import { useClientDataSWR } from '@/libs/swr';
4
5
  import { fileService } from '@/services/file';
5
- import { legacyUploadService } from '@/services/upload_legacy';
6
6
  import { FileItem } from '@/types/files';
7
7
 
8
8
  import { FileStore } from '../../store';
9
9
 
10
+ const FETCH_TTS_FILE = 'fetchTTSFile';
11
+
10
12
  export interface TTSFileAction {
11
13
  removeTTSFile: (id: string) => Promise<void>;
12
14
 
@@ -15,8 +17,6 @@ export interface TTSFileAction {
15
17
  arrayBuffers: ArrayBuffer[],
16
18
  ) => Promise<string | undefined>;
17
19
 
18
- uploadTTSFile: (file: File) => Promise<string | undefined>;
19
-
20
20
  useFetchTTSFile: (id: string | null) => SWRResponse<FileItem>;
21
21
  }
22
22
 
@@ -38,26 +38,11 @@ export const createTTSFileSlice: StateCreator<
38
38
  type: fileType,
39
39
  };
40
40
  const file = new File([blob], fileName, fileOptions);
41
- return get().uploadTTSFile(file);
42
- },
43
- uploadTTSFile: async (file) => {
44
- try {
45
- const res = await legacyUploadService.uploadFile({
46
- createdAt: file.lastModified,
47
- data: await file.arrayBuffer(),
48
- fileType: file.type,
49
- name: file.name,
50
- saveMode: 'local',
51
- size: file.size,
52
- });
53
-
54
- const data = await fileService.createFile(res);
55
-
56
- return data.id;
57
- } catch (error) {
58
- // 提示用户上传失败
59
- console.error('upload error:', error);
60
- }
41
+
42
+ const res = await get().uploadWithProgress({ file });
43
+
44
+ return res?.id;
61
45
  },
62
- useFetchTTSFile: (id) => useSWR(id, fileService.getFile),
46
+ useFetchTTSFile: (id) =>
47
+ useClientDataSWR(!!id ? [FETCH_TTS_FILE, id] : null, () => fileService.getFile(id!)),
63
48
  });
@@ -16,7 +16,7 @@ const serverFileService = new ServerService();
16
16
  interface UploadWithProgressParams {
17
17
  file: File;
18
18
  knowledgeBaseId?: string;
19
- onStatusUpdate: (
19
+ onStatusUpdate?: (
20
20
  data:
21
21
  | {
22
22
  id: string;
@@ -53,7 +53,7 @@ export const createFileUploadSlice: StateCreator<
53
53
  > = (set, get) => ({
54
54
  internal_uploadToClientDB: async ({ file, onStatusUpdate }) => {
55
55
  if (!file.type.startsWith('image')) {
56
- onStatusUpdate({ id: file.name, type: 'removeFile' });
56
+ onStatusUpdate?.({ id: file.name, type: 'removeFile' });
57
57
  message.info({
58
58
  content: t('upload.fileOnlySupportInServerMode', {
59
59
  ext: file.name.split('.').pop(),
@@ -73,7 +73,7 @@ export const createFileUploadSlice: StateCreator<
73
73
  file,
74
74
  );
75
75
 
76
- onStatusUpdate({
76
+ onStatusUpdate?.({
77
77
  id: file.name,
78
78
  type: 'updateFile',
79
79
  value: {
@@ -99,7 +99,7 @@ export const createFileUploadSlice: StateCreator<
99
99
  // 2. if file exist, just skip upload
100
100
  if (checkStatus.isExist) {
101
101
  metadata = checkStatus.metadata as FileMetadata;
102
- onStatusUpdate({
102
+ onStatusUpdate?.({
103
103
  id: file.name,
104
104
  type: 'updateFile',
105
105
  value: { status: 'processing', uploadState: { progress: 100, restTime: 0, speed: 0 } },
@@ -107,7 +107,7 @@ export const createFileUploadSlice: StateCreator<
107
107
  } else {
108
108
  // 2. if file don't exist, need upload files
109
109
  metadata = await uploadService.uploadWithProgress(file, (status, upload) => {
110
- onStatusUpdate({
110
+ onStatusUpdate?.({
111
111
  id: file.name,
112
112
  type: 'updateFile',
113
113
  value: { status: status === 'success' ? 'processing' : status, uploadState: upload },
@@ -140,7 +140,7 @@ export const createFileUploadSlice: StateCreator<
140
140
  knowledgeBaseId,
141
141
  );
142
142
 
143
- onStatusUpdate({
143
+ onStatusUpdate?.({
144
144
  id: file.name,
145
145
  type: 'updateFile',
146
146
  value: {
@@ -1,72 +0,0 @@
1
- import { beforeAll, describe, expect, it, vi } from 'vitest';
2
-
3
- import { DB_File } from '@/database/client/schemas/files';
4
- import { edgeClient } from '@/libs/trpc/client';
5
- import { API_ENDPOINTS } from '@/services/_url';
6
- import { serverConfigSelectors } from '@/store/serverConfig/selectors';
7
- import { createServerConfigStore } from '@/store/serverConfig/store';
8
-
9
- import { legacyUploadService as uploadService } from '../upload_legacy';
10
-
11
- vi.mock('@/store/serverConfig/selectors');
12
- vi.mock('@/libs/trpc/client', () => {
13
- return {
14
- edgeClient: {
15
- upload: {
16
- createS3PreSignedUrl: { mutate: vi.fn() },
17
- },
18
- },
19
- };
20
- });
21
-
22
- beforeAll(() => {
23
- createServerConfigStore();
24
- });
25
-
26
- describe('UploadService', () => {
27
- describe('uploadFile', () => {
28
- it('should upload file to server when enableServer is true', async () => {
29
- // Arrange
30
- const file: DB_File = {
31
- data: new ArrayBuffer(10),
32
- fileType: 'text/plain',
33
- metadata: {},
34
- name: 'test.txt',
35
- saveMode: 'local',
36
- size: 10,
37
- };
38
- const mockCreateS3Url = vi.fn().mockResolvedValue('https://example.com');
39
- vi.mocked(edgeClient.upload.createS3PreSignedUrl.mutate).mockImplementation(mockCreateS3Url);
40
- vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(true);
41
- global.fetch = vi.fn().mockResolvedValue({ ok: true } as Response);
42
-
43
- // Act
44
- const result = await uploadService.uploadFile(file);
45
-
46
- // Assert
47
- expect(mockCreateS3Url).toHaveBeenCalledTimes(1);
48
- expect(fetch).toHaveBeenCalledTimes(1);
49
- expect(result.url).toMatch(/\/\d+\/[a-f0-9-]+\.txt/);
50
- expect(result.saveMode).toBe('url');
51
- });
52
-
53
- it('should save file locally when enableServer is false', async () => {
54
- // Arrange
55
- const file: DB_File = {
56
- data: new ArrayBuffer(10),
57
- fileType: 'text/plain',
58
- metadata: {},
59
- name: 'test.txt',
60
- saveMode: 'local',
61
- size: 10,
62
- };
63
- vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(false);
64
-
65
- // Act
66
- const result = await uploadService.uploadFile(file);
67
-
68
- // Assert
69
- expect(result).toEqual(file);
70
- });
71
- });
72
- });
@@ -1,104 +0,0 @@
1
- import { fileEnv } from '@/config/file';
2
- import { DB_File } from '@/database/client/schemas/files';
3
- import { edgeClient } from '@/libs/trpc/client';
4
- import { API_ENDPOINTS } from '@/services/_url';
5
- import { serverConfigSelectors } from '@/store/serverConfig/selectors';
6
- import compressImage from '@/utils/compressImage';
7
- import { uuid } from '@/utils/uuid';
8
-
9
- class UploadService {
10
- async uploadFile(file: DB_File) {
11
- if (this.enableServer) {
12
- const { data, ...params } = file;
13
- const filename = `${uuid()}.${file.name.split('.').at(-1)}`;
14
-
15
- // 精确到以 h 为单位的 path
16
- const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
17
- const dirname = `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}`;
18
- const pathname = `${dirname}/${filename}`;
19
-
20
- const url = await edgeClient.upload.createS3PreSignedUrl.mutate({ pathname });
21
-
22
- const res = await fetch(url, {
23
- body: data,
24
- headers: { 'Content-Type': file.fileType },
25
- method: 'PUT',
26
- });
27
-
28
- if (res.ok) {
29
- return {
30
- ...params,
31
- metadata: { date, dirname: dirname, filename: filename, path: pathname },
32
- name: file.name,
33
- saveMode: 'url',
34
- url: pathname,
35
- } as DB_File;
36
- } else {
37
- throw new Error('Upload Error');
38
- }
39
- }
40
-
41
- // 跳过图片上传测试
42
- const isTestData = file.size === 1;
43
- if (this.isImage(file.fileType) && !isTestData) {
44
- return this.uploadImageFile(file);
45
- }
46
-
47
- // save to local storage
48
- // we may want to save to a remote server later
49
- return file;
50
- }
51
-
52
- /**
53
- * @deprecated
54
- * @param url
55
- * @param file
56
- */
57
- async uploadImageByUrl(url: string, file: Pick<DB_File, 'name' | 'metadata'>) {
58
- const res = await fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' });
59
- const data = await res.arrayBuffer();
60
- const fileType = res.headers.get('content-type') || 'image/webp';
61
-
62
- return this.uploadFile({
63
- data,
64
- fileType,
65
- metadata: file.metadata,
66
- name: file.name,
67
- saveMode: 'local',
68
- size: data.byteLength,
69
- });
70
- }
71
-
72
- private isImage(fileType: string) {
73
- const imageRegex = /^image\//;
74
- return imageRegex.test(fileType);
75
- }
76
-
77
- private async uploadImageFile(file: DB_File) {
78
- // 加载图片
79
- const url = file.url || URL.createObjectURL(new Blob([file.data!]));
80
-
81
- const img = new Image();
82
- img.src = url;
83
- await (() =>
84
- new Promise((resolve) => {
85
- img.addEventListener('load', resolve);
86
- }))();
87
-
88
- // 压缩图片
89
- const base64String = compressImage({ img, type: file.fileType });
90
- const binaryString = atob(base64String.split('base64,')[1]);
91
- const uint8Array = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
92
- file.data = uint8Array.buffer;
93
-
94
- return file;
95
- }
96
-
97
- private get enableServer() {
98
- return serverConfigSelectors.enableUploadFileToServer(
99
- window.global_serverConfigStore.getState(),
100
- );
101
- }
102
- }
103
-
104
- export const legacyUploadService = new UploadService();