@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.
- package/.eslintrc.js +1 -0
- package/.github/workflows/release.yml +5 -0
- package/.github/workflows/test.yml +5 -0
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/auth.json +1 -1
- package/locales/ar/hotkey.json +4 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/auth.json +1 -1
- package/locales/bg-BG/hotkey.json +4 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/auth.json +1 -1
- package/locales/de-DE/hotkey.json +4 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/auth.json +1 -1
- package/locales/en-US/hotkey.json +4 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/auth.json +1 -1
- package/locales/es-ES/hotkey.json +4 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/auth.json +1 -1
- package/locales/fa-IR/hotkey.json +4 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/auth.json +1 -1
- package/locales/fr-FR/hotkey.json +4 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/auth.json +1 -1
- package/locales/it-IT/hotkey.json +4 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/auth.json +1 -1
- package/locales/ja-JP/hotkey.json +4 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/auth.json +1 -1
- package/locales/ko-KR/hotkey.json +4 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/auth.json +1 -1
- package/locales/nl-NL/hotkey.json +4 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/auth.json +1 -1
- package/locales/pl-PL/hotkey.json +4 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/auth.json +1 -1
- package/locales/pt-BR/hotkey.json +4 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/auth.json +1 -1
- package/locales/ru-RU/hotkey.json +4 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/auth.json +1 -1
- package/locales/tr-TR/hotkey.json +4 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/auth.json +1 -1
- package/locales/vi-VN/hotkey.json +4 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/auth.json +1 -1
- package/locales/zh-CN/changelog.json +1 -1
- package/locales/zh-CN/clerk.json +1 -1
- package/locales/zh-CN/discover.json +1 -1
- package/locales/zh-CN/file.json +1 -1
- package/locales/zh-CN/hotkey.json +4 -0
- package/locales/zh-CN/knowledgeBase.json +1 -1
- package/locales/zh-CN/metadata.json +1 -1
- package/locales/zh-CN/migration.json +1 -1
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-CN/ragEval.json +1 -1
- package/locales/zh-CN/thread.json +1 -1
- package/locales/zh-CN/welcome.json +1 -1
- package/locales/zh-TW/auth.json +1 -1
- package/locales/zh-TW/hotkey.json +4 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +6 -4
- package/packages/file-loaders/README.md +63 -0
- package/packages/file-loaders/package.json +42 -0
- package/packages/file-loaders/src/index.ts +2 -0
- package/packages/file-loaders/src/loadFile.ts +206 -0
- package/packages/file-loaders/src/loaders/docx/__snapshots__/index.test.ts.snap +74 -0
- package/packages/file-loaders/src/loaders/docx/fixtures/test.docx +0 -0
- package/packages/file-loaders/src/loaders/docx/index.test.ts +41 -0
- package/packages/file-loaders/src/loaders/docx/index.ts +73 -0
- package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +58 -0
- package/packages/file-loaders/src/loaders/excel/fixtures/test.xlsx +0 -0
- package/packages/file-loaders/src/loaders/excel/index.test.ts +47 -0
- package/packages/file-loaders/src/loaders/excel/index.ts +121 -0
- package/packages/file-loaders/src/loaders/index.ts +19 -0
- package/packages/file-loaders/src/loaders/pdf/__snapshots__/index.test.ts.snap +98 -0
- package/packages/file-loaders/src/loaders/pdf/index.test.ts +49 -0
- package/packages/file-loaders/src/loaders/pdf/index.ts +133 -0
- package/packages/file-loaders/src/loaders/pptx/__snapshots__/index.test.ts.snap +40 -0
- package/packages/file-loaders/src/loaders/pptx/fixtures/test.pptx +0 -0
- package/packages/file-loaders/src/loaders/pptx/index.test.ts +47 -0
- package/packages/file-loaders/src/loaders/pptx/index.ts +186 -0
- package/packages/file-loaders/src/loaders/text/__snapshots__/index.test.ts.snap +15 -0
- package/packages/file-loaders/src/loaders/text/fixtures/test.txt +2 -0
- package/packages/file-loaders/src/loaders/text/index.test.ts +38 -0
- package/packages/file-loaders/src/loaders/text/index.ts +53 -0
- package/packages/file-loaders/src/types.ts +200 -0
- package/packages/file-loaders/src/utils/isTextReadableFile.ts +68 -0
- package/packages/file-loaders/src/utils/parser-utils.ts +112 -0
- package/packages/file-loaders/test/__snapshots__/loaders.test.ts.snap +93 -0
- package/packages/file-loaders/test/fixtures/test.csv +4 -0
- package/packages/file-loaders/test/fixtures/test.docx +0 -0
- package/packages/file-loaders/test/fixtures/test.epub +0 -0
- package/packages/file-loaders/test/fixtures/test.md +3 -0
- package/packages/file-loaders/test/fixtures/test.pptx +0 -0
- package/packages/file-loaders/test/fixtures/test.txt +3 -0
- package/packages/file-loaders/test/loaders.test.ts +39 -0
- package/src/config/aiModels/github.ts +2 -4
- package/src/config/aiModels/google.ts +3 -4
- package/src/config/aiModels/sensenova.ts +4 -5
- package/src/const/hotkeys.ts +6 -0
- package/src/features/ChatInput/ActionBar/Clear.tsx +18 -8
- package/src/hooks/useHotkeys/chatScope.ts +7 -0
- package/src/libs/agent-runtime/google/index.ts +1 -1
- package/src/libs/agent-runtime/sensenova/index.ts +20 -27
- package/src/libs/agent-runtime/utils/sensenovaHelpers.test.ts +24 -33
- package/src/libs/agent-runtime/utils/sensenovaHelpers.ts +2 -3
- package/src/locales/default/hotkey.ts +4 -0
- package/src/server/modules/MCPClient/__tests__/__snapshots__/index.test.ts.snap +113 -0
- package/src/server/modules/MCPClient/__tests__/index.test.ts +81 -0
- package/src/server/modules/MCPClient/index.ts +80 -0
- 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
|
+
`;
|
Binary file
|
Binary file
|
Binary 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
|
-
|
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:
|
116
|
+
description:
|
117
|
+
'最新版本模型 (V5.5),支持多图的输入,全面实现模型基础能力优化,在对象属性识别、空间关系、动作事件识别、场景理解、情感识别、逻辑常识推理和文本理解生成上都实现了较大提升。',
|
119
118
|
displayName: 'SenseChat 5.5 Vision',
|
120
119
|
id: 'SenseChat-Vision',
|
121
120
|
pricing: {
|
package/src/const/hotkeys.ts
CHANGED
@@ -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
|
14
|
-
|
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={
|
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(
|
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
|
-
|
75
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
84
|
-
|
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:
|
77
|
-
role:
|
67
|
+
content: 'Hi',
|
68
|
+
role: 'user',
|
78
69
|
},
|
79
70
|
{
|
80
71
|
image_url: {
|
81
|
-
detail:
|
82
|
-
url:
|
72
|
+
detail: 'auto',
|
73
|
+
url: 'data:image/jpeg;base64,ABCDEF123456',
|
83
74
|
},
|
84
|
-
type:
|
85
|
-
}
|
75
|
+
type: 'image_url',
|
76
|
+
},
|
86
77
|
],
|
87
|
-
role:
|
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:
|
100
|
-
{ type: 'image_url', image_url: { url:
|
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:
|
105
|
-
{ type: 'image_base64', image_base64:
|
95
|
+
{ type: 'text', text: 'Hi' },
|
96
|
+
{ type: 'image_base64', image_base64: 'ABCDEF123456' },
|
106
97
|
]);
|
107
98
|
});
|
108
99
|
});
|