@lobehub/chat 1.81.4 → 1.81.6

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.
Files changed (120) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/release.yml +5 -0
  3. package/.github/workflows/test.yml +5 -0
  4. package/CHANGELOG.md +50 -0
  5. package/changelog/v1.json +18 -0
  6. package/locales/ar/auth.json +1 -1
  7. package/locales/ar/hotkey.json +4 -0
  8. package/locales/ar/models.json +3 -0
  9. package/locales/bg-BG/auth.json +1 -1
  10. package/locales/bg-BG/hotkey.json +4 -0
  11. package/locales/bg-BG/models.json +3 -0
  12. package/locales/de-DE/auth.json +1 -1
  13. package/locales/de-DE/hotkey.json +4 -0
  14. package/locales/de-DE/models.json +3 -0
  15. package/locales/en-US/auth.json +1 -1
  16. package/locales/en-US/hotkey.json +4 -0
  17. package/locales/en-US/models.json +3 -0
  18. package/locales/es-ES/auth.json +1 -1
  19. package/locales/es-ES/hotkey.json +4 -0
  20. package/locales/es-ES/models.json +3 -0
  21. package/locales/fa-IR/auth.json +1 -1
  22. package/locales/fa-IR/hotkey.json +4 -0
  23. package/locales/fa-IR/models.json +3 -0
  24. package/locales/fr-FR/auth.json +1 -1
  25. package/locales/fr-FR/hotkey.json +4 -0
  26. package/locales/fr-FR/models.json +3 -0
  27. package/locales/it-IT/auth.json +1 -1
  28. package/locales/it-IT/hotkey.json +4 -0
  29. package/locales/it-IT/models.json +3 -0
  30. package/locales/ja-JP/auth.json +1 -1
  31. package/locales/ja-JP/hotkey.json +4 -0
  32. package/locales/ja-JP/models.json +3 -0
  33. package/locales/ko-KR/auth.json +1 -1
  34. package/locales/ko-KR/hotkey.json +4 -0
  35. package/locales/ko-KR/models.json +3 -0
  36. package/locales/nl-NL/auth.json +1 -1
  37. package/locales/nl-NL/hotkey.json +4 -0
  38. package/locales/nl-NL/models.json +3 -0
  39. package/locales/pl-PL/auth.json +1 -1
  40. package/locales/pl-PL/hotkey.json +4 -0
  41. package/locales/pl-PL/models.json +3 -0
  42. package/locales/pt-BR/auth.json +1 -1
  43. package/locales/pt-BR/hotkey.json +4 -0
  44. package/locales/pt-BR/models.json +3 -0
  45. package/locales/ru-RU/auth.json +1 -1
  46. package/locales/ru-RU/hotkey.json +4 -0
  47. package/locales/ru-RU/models.json +3 -0
  48. package/locales/tr-TR/auth.json +1 -1
  49. package/locales/tr-TR/hotkey.json +4 -0
  50. package/locales/tr-TR/models.json +3 -0
  51. package/locales/vi-VN/auth.json +1 -1
  52. package/locales/vi-VN/hotkey.json +4 -0
  53. package/locales/vi-VN/models.json +3 -0
  54. package/locales/zh-CN/auth.json +1 -1
  55. package/locales/zh-CN/changelog.json +1 -1
  56. package/locales/zh-CN/clerk.json +1 -1
  57. package/locales/zh-CN/discover.json +1 -1
  58. package/locales/zh-CN/file.json +1 -1
  59. package/locales/zh-CN/hotkey.json +4 -0
  60. package/locales/zh-CN/knowledgeBase.json +1 -1
  61. package/locales/zh-CN/metadata.json +1 -1
  62. package/locales/zh-CN/migration.json +1 -1
  63. package/locales/zh-CN/models.json +3 -0
  64. package/locales/zh-CN/ragEval.json +1 -1
  65. package/locales/zh-CN/thread.json +1 -1
  66. package/locales/zh-CN/welcome.json +1 -1
  67. package/locales/zh-TW/auth.json +1 -1
  68. package/locales/zh-TW/hotkey.json +4 -0
  69. package/locales/zh-TW/models.json +3 -0
  70. package/package.json +6 -4
  71. package/packages/file-loaders/README.md +63 -0
  72. package/packages/file-loaders/package.json +42 -0
  73. package/packages/file-loaders/src/index.ts +2 -0
  74. package/packages/file-loaders/src/loadFile.ts +206 -0
  75. package/packages/file-loaders/src/loaders/docx/__snapshots__/index.test.ts.snap +74 -0
  76. package/packages/file-loaders/src/loaders/docx/fixtures/test.docx +0 -0
  77. package/packages/file-loaders/src/loaders/docx/index.test.ts +41 -0
  78. package/packages/file-loaders/src/loaders/docx/index.ts +73 -0
  79. package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +58 -0
  80. package/packages/file-loaders/src/loaders/excel/fixtures/test.xlsx +0 -0
  81. package/packages/file-loaders/src/loaders/excel/index.test.ts +47 -0
  82. package/packages/file-loaders/src/loaders/excel/index.ts +121 -0
  83. package/packages/file-loaders/src/loaders/index.ts +19 -0
  84. package/packages/file-loaders/src/loaders/pdf/__snapshots__/index.test.ts.snap +98 -0
  85. package/packages/file-loaders/src/loaders/pdf/index.test.ts +49 -0
  86. package/packages/file-loaders/src/loaders/pdf/index.ts +133 -0
  87. package/packages/file-loaders/src/loaders/pptx/__snapshots__/index.test.ts.snap +40 -0
  88. package/packages/file-loaders/src/loaders/pptx/fixtures/test.pptx +0 -0
  89. package/packages/file-loaders/src/loaders/pptx/index.test.ts +47 -0
  90. package/packages/file-loaders/src/loaders/pptx/index.ts +186 -0
  91. package/packages/file-loaders/src/loaders/text/__snapshots__/index.test.ts.snap +15 -0
  92. package/packages/file-loaders/src/loaders/text/fixtures/test.txt +2 -0
  93. package/packages/file-loaders/src/loaders/text/index.test.ts +38 -0
  94. package/packages/file-loaders/src/loaders/text/index.ts +53 -0
  95. package/packages/file-loaders/src/types.ts +200 -0
  96. package/packages/file-loaders/src/utils/isTextReadableFile.ts +68 -0
  97. package/packages/file-loaders/src/utils/parser-utils.ts +112 -0
  98. package/packages/file-loaders/test/__snapshots__/loaders.test.ts.snap +93 -0
  99. package/packages/file-loaders/test/fixtures/test.csv +4 -0
  100. package/packages/file-loaders/test/fixtures/test.docx +0 -0
  101. package/packages/file-loaders/test/fixtures/test.epub +0 -0
  102. package/packages/file-loaders/test/fixtures/test.md +3 -0
  103. package/packages/file-loaders/test/fixtures/test.pptx +0 -0
  104. package/packages/file-loaders/test/fixtures/test.txt +3 -0
  105. package/packages/file-loaders/test/loaders.test.ts +39 -0
  106. package/src/config/aiModels/github.ts +2 -4
  107. package/src/config/aiModels/google.ts +3 -4
  108. package/src/config/aiModels/sensenova.ts +4 -5
  109. package/src/const/hotkeys.ts +6 -0
  110. package/src/features/ChatInput/ActionBar/Clear.tsx +18 -8
  111. package/src/hooks/useHotkeys/chatScope.ts +7 -0
  112. package/src/libs/agent-runtime/google/index.ts +1 -1
  113. package/src/libs/agent-runtime/sensenova/index.ts +20 -27
  114. package/src/libs/agent-runtime/utils/sensenovaHelpers.test.ts +24 -33
  115. package/src/libs/agent-runtime/utils/sensenovaHelpers.ts +2 -3
  116. package/src/locales/default/hotkey.ts +4 -0
  117. package/src/server/modules/MCPClient/__tests__/__snapshots__/index.test.ts.snap +113 -0
  118. package/src/server/modules/MCPClient/__tests__/index.test.ts +81 -0
  119. package/src/server/modules/MCPClient/index.ts +80 -0
  120. package/src/types/hotkey.ts +1 -0
@@ -0,0 +1,112 @@
1
+ import { DOMParser } from '@xmldom/xmldom';
2
+ import concat from 'concat-stream';
3
+ import { Buffer } from 'node:buffer';
4
+ import yauzl from 'yauzl';
5
+
6
+ // Define basic error messages
7
+ const ERRORMSG = {
8
+ fileCorrupted: (filepath: string | Buffer) =>
9
+ `[OfficeParser]: Your file ${typeof filepath === 'string' ? filepath : 'Buffer'} seems to be corrupted. If you are sure it is fine, please create a ticket.`,
10
+ invalidInput: `[OfficeParser]: Invalid input type: Expected a Buffer or a valid file path`,
11
+ };
12
+
13
+ /** Returns parsed xml document for a given xml text.
14
+ * @param {string} xml The xml string from the doc file
15
+ * @returns {XMLDocument}
16
+ */
17
+ export const parseString = (xml: string) => {
18
+ const parser = new DOMParser();
19
+ return parser.parseFromString(xml, 'text/xml') as unknown as XMLDocument;
20
+ };
21
+
22
+ export interface ExtractedFile {
23
+ content: string;
24
+ path: string;
25
+ }
26
+
27
+ /** Extract specific files from either a ZIP file buffer or file path based on a filter function.
28
+ * @param {Buffer|string} zipInput ZIP file input, either a Buffer or a file path (string).
29
+ * @param {(fileName: string) => boolean} filterFn A function that receives the entry file name and returns true if the file should be extracted.
30
+ * @returns {Promise<ExtractedFile[]>} Resolves to an array of object containing file path and content.
31
+ */
32
+ export function extractFiles(
33
+ zipInput: Buffer | string,
34
+ filterFn: (fileName: string) => boolean,
35
+ ): Promise<ExtractedFile[]> {
36
+ return new Promise((resolve, reject) => {
37
+ /** Processes zip file and resolves with the path of file and their content.
38
+ * @param {yauzl.ZipFile} zipfile
39
+ */
40
+ const processZipfile = (zipfile: yauzl.ZipFile) => {
41
+ const extractedFiles: ExtractedFile[] = [];
42
+ zipfile.readEntry(); // Start reading entries
43
+
44
+ zipfile.on('entry', (entry: yauzl.Entry) => {
45
+ // Directory entries end with a forward slash
46
+ if (entry.fileName.endsWith('/')) {
47
+ zipfile.readEntry(); // Skip directories
48
+ return;
49
+ }
50
+
51
+ // Use the filter function to determine if the file should be extracted
52
+ if (filterFn(entry.fileName)) {
53
+ zipfile.openReadStream(entry, (err, readStream) => {
54
+ if (err) {
55
+ zipfile.close(); // Ensure zipfile is closed on error
56
+ return reject(err);
57
+ }
58
+ if (!readStream) {
59
+ zipfile.close();
60
+ return reject(new Error(`Could not open read stream for ${entry.fileName}`));
61
+ }
62
+
63
+ // Use concat-stream to collect the data into a single Buffer
64
+ readStream.pipe(
65
+ concat((data) => {
66
+ extractedFiles.push({
67
+ content: data.toString('utf8'),
68
+ path: entry.fileName, // Specify encoding
69
+ });
70
+ zipfile.readEntry(); // Continue reading entries
71
+ }),
72
+ );
73
+ readStream.on('error', (streamErr) => {
74
+ zipfile.close();
75
+ reject(streamErr);
76
+ });
77
+ });
78
+ } else {
79
+ zipfile.readEntry(); // Skip entries that don't match the filter
80
+ }
81
+ });
82
+
83
+ zipfile.on('end', () => {
84
+ resolve(extractedFiles);
85
+ zipfile.close(); // Close the zipfile when done reading entries
86
+ });
87
+
88
+ zipfile.on('error', (err) => {
89
+ zipfile.close();
90
+ reject(err);
91
+ });
92
+ };
93
+
94
+ // Determine whether the input is a buffer or file path
95
+ if (Buffer.isBuffer(zipInput)) {
96
+ // Process ZIP from Buffer
97
+ yauzl.fromBuffer(zipInput, { lazyEntries: true }, (err, zipfile) => {
98
+ if (err || !zipfile) return reject(err || new Error('Failed to open zip from buffer'));
99
+ processZipfile(zipfile);
100
+ });
101
+ } else if (typeof zipInput === 'string') {
102
+ // Process ZIP from File Path
103
+ yauzl.open(zipInput, { lazyEntries: true }, (err, zipfile) => {
104
+ if (err || !zipfile)
105
+ return reject(err || new Error(`Failed to open zip file: ${zipInput}`));
106
+ processZipfile(zipfile);
107
+ });
108
+ } else {
109
+ reject(new Error(ERRORMSG.invalidInput));
110
+ }
111
+ });
112
+ }
@@ -0,0 +1,93 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`loadFile Integration Tests > Text Handling (.txt, .csv, .md, etc.) > should load content from a test.csv file using filePath 1`] = `
4
+ {
5
+ "content": "ID,Name,Value
6
+ 1,Alpha,100
7
+ 2,Beta,200
8
+ 3,Gamma,300
9
+ ",
10
+ "fileType": "csv",
11
+ "filename": "test.csv",
12
+ "metadata": {
13
+ "loaderSpecific": undefined,
14
+ },
15
+ "pages": [
16
+ {
17
+ "charCount": 49,
18
+ "lineCount": 5,
19
+ "metadata": {
20
+ "lineNumberEnd": 5,
21
+ "lineNumberStart": 1,
22
+ },
23
+ "pageContent": "ID,Name,Value
24
+ 1,Alpha,100
25
+ 2,Beta,200
26
+ 3,Gamma,300
27
+ ",
28
+ },
29
+ ],
30
+ "totalCharCount": 49,
31
+ "totalLineCount": 5,
32
+ }
33
+ `;
34
+
35
+ exports[`loadFile Integration Tests > Text Handling (.txt, .csv, .md, etc.) > should load content from a test.md file using filePath 1`] = `
36
+ {
37
+ "content": "# Markdown Test
38
+
39
+ This is a test.
40
+ ",
41
+ "fileType": "md",
42
+ "filename": "test.md",
43
+ "metadata": {
44
+ "loaderSpecific": undefined,
45
+ },
46
+ "pages": [
47
+ {
48
+ "charCount": 33,
49
+ "lineCount": 4,
50
+ "metadata": {
51
+ "lineNumberEnd": 4,
52
+ "lineNumberStart": 1,
53
+ },
54
+ "pageContent": "# Markdown Test
55
+
56
+ This is a test.
57
+ ",
58
+ },
59
+ ],
60
+ "totalCharCount": 33,
61
+ "totalLineCount": 4,
62
+ }
63
+ `;
64
+
65
+ exports[`loadFile Integration Tests > Text Handling (.txt, .csv, .md, etc.) > should load content from a test.txt file using filePath 1`] = `
66
+ {
67
+ "content": "This is line 1.
68
+ This is line 2.
69
+ End of text file.
70
+ ",
71
+ "fileType": "txt",
72
+ "filename": "test.txt",
73
+ "metadata": {
74
+ "loaderSpecific": undefined,
75
+ },
76
+ "pages": [
77
+ {
78
+ "charCount": 50,
79
+ "lineCount": 4,
80
+ "metadata": {
81
+ "lineNumberEnd": 4,
82
+ "lineNumberStart": 1,
83
+ },
84
+ "pageContent": "This is line 1.
85
+ This is line 2.
86
+ End of text file.
87
+ ",
88
+ },
89
+ ],
90
+ "totalCharCount": 50,
91
+ "totalLineCount": 4,
92
+ }
93
+ `;
@@ -0,0 +1,4 @@
1
+ ID,Name,Value
2
+ 1,Alpha,100
3
+ 2,Beta,200
4
+ 3,Gamma,300
@@ -0,0 +1,3 @@
1
+ # Markdown Test
2
+
3
+ This is a test.
@@ -0,0 +1,3 @@
1
+ This is line 1.
2
+ This is line 2.
3
+ End of text file.
@@ -0,0 +1,39 @@
1
+ // @vitest-environment node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { loadFile } from '../src';
7
+
8
+ const getFixturePath = (filename: string) => path.join(__dirname, 'fixtures', filename);
9
+
10
+ const TEXT_FILES = ['test.txt', 'test.csv', 'test.md'];
11
+
12
+ describe('loadFile Integration Tests', () => {
13
+ describe('Text Handling (.txt, .csv, .md, etc.)', () => {
14
+ const testPureTextFile = (fileName: string) => {
15
+ it(`should load content from a ${fileName} file using filePath`, async () => {
16
+ const filePath = getFixturePath(fileName);
17
+ const expectedContent = fs.readFileSync(filePath, 'utf-8');
18
+
19
+ // Pass filePath directly to loadFile
20
+ const docs = await loadFile(filePath);
21
+
22
+ expect(docs.content).toEqual(expectedContent);
23
+ expect(docs.source).toEqual(filePath);
24
+
25
+ // @ts-expect-error
26
+ delete docs.source;
27
+ // @ts-expect-error
28
+ delete docs.createdTime;
29
+ // @ts-expect-error
30
+ delete docs.modifiedTime;
31
+ expect(docs).toMatchSnapshot();
32
+ });
33
+ };
34
+
35
+ TEXT_FILES.forEach((file) => {
36
+ testPureTextFile(file);
37
+ });
38
+ });
39
+ });
@@ -48,8 +48,7 @@ const githubChatModels: AIChatModelCard[] = [
48
48
  vision: true,
49
49
  },
50
50
  contextWindowTokens: 1_047_576,
51
- description:
52
- 'GPT-4.1 是我们用于复杂任务的旗舰模型。它非常适合跨领域解决问题。',
51
+ description: 'GPT-4.1 是我们用于复杂任务的旗舰模型。它非常适合跨领域解决问题。',
53
52
  displayName: 'GPT-4.1',
54
53
  enabled: true,
55
54
  id: 'gpt-4.1',
@@ -88,8 +87,7 @@ const githubChatModels: AIChatModelCard[] = [
88
87
  vision: true,
89
88
  },
90
89
  contextWindowTokens: 1_047_576,
91
- description:
92
- 'GPT-4.1 nano 是最快,最具成本效益的GPT-4.1模型。',
90
+ description: 'GPT-4.1 nano 是最快,最具成本效益的GPT-4.1模型。',
93
91
  displayName: 'GPT-4.1 nano',
94
92
  id: 'gpt-4.1-nano',
95
93
  maxOutput: 32_768,
@@ -1,7 +1,7 @@
1
1
  import { AIChatModelCard } from '@/types/aiModel';
2
2
 
3
3
  const googleChatModels: AIChatModelCard[] = [
4
- {
4
+ {
5
5
  abilities: {
6
6
  functionCall: true,
7
7
  reasoning: true,
@@ -9,8 +9,7 @@ const googleChatModels: AIChatModelCard[] = [
9
9
  vision: true,
10
10
  },
11
11
  contextWindowTokens: 1_048_576 + 65_536,
12
- description:
13
- 'Gemini 2.5 Flash Preview 是 Google 性价比最高的模型,提供全面的功能。',
12
+ description: 'Gemini 2.5 Flash Preview 是 Google 性价比最高的模型,提供全面的功能。',
14
13
  displayName: 'Gemini 2.5 Flash Preview 04-17',
15
14
  enabled: true,
16
15
  id: 'gemini-2.5-flash-preview-04-17',
@@ -85,7 +84,7 @@ const googleChatModels: AIChatModelCard[] = [
85
84
  'Gemini 2.0 Flash Thinking Exp 是 Google 的实验性多模态推理AI模型,能对复杂问题进行推理,拥有新的思维能力。',
86
85
  displayName: 'Gemini 2.0 Flash Thinking Experimental 01-21',
87
86
  enabled: true,
88
- id: 'gemini-2.0-flash-thinking-exp-01-21',
87
+ id: 'gemini-2.0-flash-thinking-exp-01-21',
89
88
  maxOutput: 65_536,
90
89
  pricing: {
91
90
  cachedInput: 0,
@@ -10,8 +10,7 @@ const sensenovaChatModels: AIChatModelCard[] = [
10
10
  vision: true,
11
11
  },
12
12
  contextWindowTokens: 131_072,
13
- description:
14
- '兼顾视觉、语言深度推理,实现慢思考和深度推理,呈现完整的思维链过程。',
13
+ description: '兼顾视觉、语言深度推理,实现慢思考和深度推理,呈现完整的思维链过程。',
15
14
  displayName: 'SenseNova V6 Reasoner',
16
15
  enabled: true,
17
16
  id: 'SenseNova-V6-Reasoner',
@@ -82,8 +81,7 @@ const sensenovaChatModels: AIChatModelCard[] = [
82
81
  functionCall: true,
83
82
  },
84
83
  contextWindowTokens: 32_768,
85
- description:
86
- '是最新的轻量版本模型,达到全量模型90%以上能力,显著降低推理成本。',
84
+ description: '是最新的轻量版本模型,达到全量模型90%以上能力,显著降低推理成本。',
87
85
  displayName: 'SenseChat Turbo 1202',
88
86
  id: 'SenseChat-Turbo-1202',
89
87
  pricing: {
@@ -115,7 +113,8 @@ const sensenovaChatModels: AIChatModelCard[] = [
115
113
  vision: true,
116
114
  },
117
115
  contextWindowTokens: 32_768,
118
- description: '最新版本模型 (V5.5),支持多图的输入,全面实现模型基础能力优化,在对象属性识别、空间关系、动作事件识别、场景理解、情感识别、逻辑常识推理和文本理解生成上都实现了较大提升。',
116
+ description:
117
+ '最新版本模型 (V5.5),支持多图的输入,全面实现模型基础能力优化,在对象属性识别、空间关系、动作事件识别、场景理解、情感识别、逻辑常识推理和文本理解生成上都实现了较大提升。',
119
118
  displayName: 'SenseChat 5.5 Vision',
120
119
  id: 'SenseChat-Vision',
121
120
  pricing: {
@@ -88,4 +88,10 @@ export const HOTKEYS_REGISTRATION: HotkeyRegistration = [
88
88
  nonEditable: true,
89
89
  scopes: [HotkeyScopeEnum.Chat],
90
90
  },
91
+ {
92
+ group: HotkeyGroupEnum.Conversation,
93
+ id: HotkeyEnum.ClearCurrentMessages,
94
+ keys: combineKeys([KeyEnum.Alt, KeyEnum.Backspace]),
95
+ scopes: [HotkeyScopeEnum.Chat],
96
+ },
91
97
  ];
@@ -7,19 +7,28 @@ import { useTranslation } from 'react-i18next';
7
7
  import { useIsMobile } from '@/hooks/useIsMobile';
8
8
  import { useChatStore } from '@/store/chat';
9
9
  import { useFileStore } from '@/store/file';
10
+ import { useUserStore } from '@/store/user';
11
+ import { settingsSelectors } from '@/store/user/selectors';
12
+ import { HotkeyEnum } from '@/types/hotkey';
13
+
14
+ export const useClearCurrentMessages = () => {
15
+ const clearMessage = useChatStore((s) => s.clearMessage);
16
+ const clearImageList = useFileStore((s) => s.clearChatUploadFileList);
17
+
18
+ return useCallback(async () => {
19
+ await clearMessage();
20
+ clearImageList();
21
+ }, [clearImageList, clearMessage]);
22
+ };
10
23
 
11
24
  const Clear = memo(() => {
12
25
  const { t } = useTranslation('setting');
13
- const [clearMessage] = useChatStore((s) => [s.clearMessage]);
14
- const [clearImageList] = useFileStore((s) => [s.clearChatUploadFileList]);
26
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ClearCurrentMessages));
27
+
28
+ const clearCurrentMessages = useClearCurrentMessages();
15
29
  const [confirmOpened, updateConfirmOpened] = useState(false);
16
30
  const mobile = useIsMobile();
17
31
 
18
- const resetConversation = useCallback(async () => {
19
- await clearMessage();
20
- clearImageList();
21
- }, []);
22
-
23
32
  const actionTitle: any = confirmOpened ? void 0 : t('clearCurrentMessages', { ns: 'chat' });
24
33
 
25
34
  const popconfirmPlacement = mobile ? 'top' : 'topRight';
@@ -28,7 +37,7 @@ const Clear = memo(() => {
28
37
  <Popconfirm
29
38
  arrow={false}
30
39
  okButtonProps={{ danger: true, type: 'primary' }}
31
- onConfirm={resetConversation}
40
+ onConfirm={clearCurrentMessages}
32
41
  onOpenChange={updateConfirmOpened}
33
42
  open={confirmOpened}
34
43
  placement={popconfirmPlacement}
@@ -45,6 +54,7 @@ const Clear = memo(() => {
45
54
  root: { maxWidth: 'none' },
46
55
  }}
47
56
  title={actionTitle}
57
+ tooltipHotkey={hotkey}
48
58
  />
49
59
  </Popconfirm>
50
60
  );
@@ -3,6 +3,7 @@ import { parseAsBoolean, useQueryState } from 'nuqs';
3
3
  import { useEffect } from 'react';
4
4
  import { useHotkeysContext } from 'react-hotkeys-hook';
5
5
 
6
+ import { useClearCurrentMessages } from '@/features/ChatInput/ActionBar/Clear';
6
7
  import { useSendMessage } from '@/features/ChatInput/useSend';
7
8
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
8
9
  import { useActionSWR } from '@/libs/swr';
@@ -78,6 +79,11 @@ export const useAddUserMessageHotkey = () => {
78
79
  return useHotkeyById(HotkeyEnum.AddUserMessage, () => send({ onlyAddUserMessage: true }));
79
80
  };
80
81
 
82
+ export const useClearCurrentMessagesHotkey = () => {
83
+ const clearCurrentMessages = useClearCurrentMessages();
84
+ return useHotkeyById(HotkeyEnum.ClearCurrentMessages, () => clearCurrentMessages());
85
+ };
86
+
81
87
  // 注册聚合
82
88
 
83
89
  export const useRegisterChatHotkeys = () => {
@@ -95,6 +101,7 @@ export const useRegisterChatHotkeys = () => {
95
101
  useRegenerateMessageHotkey();
96
102
  useSaveTopicHotkey();
97
103
  useAddUserMessageHotkey();
104
+ useClearCurrentMessagesHotkey();
98
105
 
99
106
  useEffect(() => {
100
107
  enableScope(HotkeyScopeEnum.Chat);
@@ -368,7 +368,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
368
368
  payload?: ChatStreamPayload,
369
369
  ): GoogleFunctionCallTool[] | undefined {
370
370
  // 目前 Tools (例如 googleSearch) 无法与其他 FunctionCall 同时使用
371
- if (payload?.messages?.some(m => m.tool_calls?.length)) {
371
+ if (payload?.messages?.some((m) => m.tool_calls?.length)) {
372
372
  return; // 若历史消息中已有 function calling,则不再注入任何 Tools
373
373
  }
374
374
  if (payload?.enabledSearch) {
@@ -1,10 +1,9 @@
1
+ import type { ChatModelCard } from '@/types/llm';
2
+
1
3
  import { ModelProvider } from '../types';
2
4
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
-
4
5
  import { convertSenseNovaMessage } from '../utils/sensenovaHelpers';
5
6
 
6
- import type { ChatModelCard } from '@/types/llm';
7
-
8
7
  export interface SenseNovaModelCard {
9
8
  id: string;
10
9
  }
@@ -21,10 +20,10 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
21
20
  frequency_penalty !== undefined && frequency_penalty > 0 && frequency_penalty <= 2
22
21
  ? frequency_penalty
23
22
  : undefined,
24
- messages: messages.map((message) =>
23
+ messages: messages.map((message) =>
25
24
  message.role !== 'user' || !/^Sense(Nova-V6|Chat-Vision)/.test(model)
26
25
  ? message
27
- : { ...message, content: convertSenseNovaMessage(message.content) }
26
+ : { ...message, content: convertSenseNovaMessage(message.content) },
28
27
  ) as any[],
29
28
  model,
30
29
  stream: true,
@@ -42,46 +41,40 @@ export const LobeSenseNovaAI = LobeOpenAICompatibleFactory({
42
41
  models: async ({ client }) => {
43
42
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
44
43
 
45
- const functionCallKeywords = [
46
- 'sensechat-5',
47
- ];
44
+ const functionCallKeywords = ['sensechat-5'];
48
45
 
49
- const visionKeywords = [
50
- 'vision',
51
- 'sensenova-v6',
52
- ];
46
+ const visionKeywords = ['vision', 'sensenova-v6'];
53
47
 
54
- const reasoningKeywords = [
55
- 'deepseek-r1',
56
- 'sensenova-v6',
57
- ];
48
+ const reasoningKeywords = ['deepseek-r1', 'sensenova-v6'];
58
49
 
59
50
  client.baseURL = 'https://api.sensenova.cn/v1/llm';
60
51
 
61
- const modelsPage = await client.models.list() as any;
52
+ const modelsPage = (await client.models.list()) as any;
62
53
  const modelList: SenseNovaModelCard[] = modelsPage.data;
63
54
 
64
55
  return modelList
65
56
  .map((model) => {
66
- const knownModel = LOBE_DEFAULT_MODEL_LIST.find((m) => model.id.toLowerCase() === m.id.toLowerCase());
57
+ const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
58
+ (m) => model.id.toLowerCase() === m.id.toLowerCase(),
59
+ );
67
60
 
68
61
  return {
69
62
  contextWindowTokens: knownModel?.contextWindowTokens ?? undefined,
70
63
  displayName: knownModel?.displayName ?? undefined,
71
64
  enabled: knownModel?.enabled || false,
72
65
  functionCall:
73
- functionCallKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
74
- || knownModel?.abilities?.functionCall
75
- || false,
66
+ functionCallKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
67
+ knownModel?.abilities?.functionCall ||
68
+ false,
76
69
  id: model.id,
77
70
  reasoning:
78
- reasoningKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
79
- || knownModel?.abilities?.reasoning
80
- || false,
71
+ reasoningKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
72
+ knownModel?.abilities?.reasoning ||
73
+ false,
81
74
  vision:
82
- visionKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
83
- || knownModel?.abilities?.vision
84
- || false,
75
+ visionKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
76
+ knownModel?.abilities?.vision ||
77
+ false,
85
78
  };
86
79
  })
87
80
  .filter(Boolean) as ChatModelCard[];
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+
2
3
  import { convertSenseNovaMessage } from './sensenovaHelpers';
3
4
 
4
5
  describe('convertSenseNovaMessage', () => {
@@ -10,9 +11,7 @@ describe('convertSenseNovaMessage', () => {
10
11
  });
11
12
 
12
13
  it('should handle array content with text type', () => {
13
- const content = [
14
- { type: 'text', text: 'Hello world' }
15
- ];
14
+ const content = [{ type: 'text', text: 'Hello world' }];
16
15
  const result = convertSenseNovaMessage(content);
17
16
 
18
17
  expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
@@ -20,38 +19,32 @@ describe('convertSenseNovaMessage', () => {
20
19
 
21
20
  it('should convert image_url with base64 format to image_base64', () => {
22
21
  const content = [
23
- { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } }
22
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
24
23
  ];
25
24
  const result = convertSenseNovaMessage(content);
26
25
 
27
- expect(result).toEqual([
28
- { type: 'image_base64', image_base64: 'ABCDEF123456' }
29
- ]);
26
+ expect(result).toEqual([{ type: 'image_base64', image_base64: 'ABCDEF123456' }]);
30
27
  });
31
28
 
32
29
  it('should keep image_url format for non-base64 urls', () => {
33
- const content = [
34
- { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
35
- ];
30
+ const content = [{ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }];
36
31
  const result = convertSenseNovaMessage(content);
37
32
 
38
- expect(result).toEqual([
39
- { type: 'image_url', image_url: 'https://example.com/image.jpg' }
40
- ]);
33
+ expect(result).toEqual([{ type: 'image_url', image_url: 'https://example.com/image.jpg' }]);
41
34
  });
42
35
 
43
36
  it('should handle mixed content types', () => {
44
37
  const content = [
45
38
  { type: 'text', text: 'Hello world' },
46
39
  { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
47
- { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
40
+ { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } },
48
41
  ];
49
42
  const result = convertSenseNovaMessage(content);
50
43
 
51
44
  expect(result).toEqual([
52
45
  { type: 'text', text: 'Hello world' },
53
46
  { type: 'image_base64', image_base64: 'ABCDEF123456' },
54
- { type: 'image_url', image_url: 'https://example.com/image.jpg' }
47
+ { type: 'image_url', image_url: 'https://example.com/image.jpg' },
55
48
  ]);
56
49
  });
57
50
 
@@ -59,13 +52,11 @@ describe('convertSenseNovaMessage', () => {
59
52
  const content = [
60
53
  { type: 'text', text: 'Hello world' },
61
54
  { type: 'unknown', value: 'should be filtered' },
62
- { type: 'image_url', image_url: { notUrl: 'missing url field' } }
55
+ { type: 'image_url', image_url: { notUrl: 'missing url field' } },
63
56
  ];
64
57
  const result = convertSenseNovaMessage(content);
65
58
 
66
- expect(result).toEqual([
67
- { type: 'text', text: 'Hello world' }
68
- ]);
59
+ expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
69
60
  });
70
61
 
71
62
  it('should handle the example input format correctly', () => {
@@ -73,36 +64,36 @@ describe('convertSenseNovaMessage', () => {
73
64
  {
74
65
  content: [
75
66
  {
76
- content: "Hi",
77
- role: "user"
67
+ content: 'Hi',
68
+ role: 'user',
78
69
  },
79
70
  {
80
71
  image_url: {
81
- detail: "auto",
82
- url: "data:image/jpeg;base64,ABCDEF123456"
72
+ detail: 'auto',
73
+ url: 'data:image/jpeg;base64,ABCDEF123456',
83
74
  },
84
- type: "image_url"
85
- }
75
+ type: 'image_url',
76
+ },
86
77
  ],
87
- role: "user"
88
- }
78
+ role: 'user',
79
+ },
89
80
  ];
90
81
 
91
82
  // This is simulating how you might use convertSenseNovaMessage with the example input
92
83
  // Note: The actual function only converts the content part, not the entire messages array
93
84
  const content = messages[0].content;
94
-
85
+
95
86
  // This is how the function would be expected to handle a mixed array like this
96
- // However, the actual test would need to be adjusted based on how your function
87
+ // However, the actual test would need to be adjusted based on how your function
97
88
  // is intended to handle this specific format with nested content objects
98
89
  const result = convertSenseNovaMessage([
99
- { type: 'text', text: "Hi" },
100
- { type: 'image_url', image_url: { url: "data:image/jpeg;base64,ABCDEF123456" } }
90
+ { type: 'text', text: 'Hi' },
91
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,ABCDEF123456' } },
101
92
  ]);
102
93
 
103
94
  expect(result).toEqual([
104
- { type: 'text', text: "Hi" },
105
- { type: 'image_base64', image_base64: "ABCDEF123456" }
95
+ { type: 'text', text: 'Hi' },
96
+ { type: 'image_base64', image_base64: 'ABCDEF123456' },
106
97
  ]);
107
98
  });
108
99
  });