@lobehub/chat 1.84.15 → 1.84.17
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 +50 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +121 -20
- package/apps/desktop/src/main/core/Browser.ts +6 -1
- package/apps/desktop/src/main/core/BrowserManager.ts +2 -2
- package/changelog/v1.json +18 -0
- package/package.json +4 -2
- package/packages/electron-client-ipc/src/events/localFile.ts +2 -0
- package/packages/electron-client-ipc/src/types/localFile.ts +16 -0
- package/src/app/[variants]/(main)/settings/provider/(detail)/ollama/CheckError.tsx +1 -1
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +2 -1
- package/src/features/{Conversation/components/MarkdownElements/LocalFile/Render → LocalFile}/LocalFile.tsx +8 -10
- package/src/features/LocalFile/LocalFolder.tsx +65 -0
- package/src/features/LocalFile/index.tsx +2 -0
- package/src/features/OllamaSetupGuide/Desktop.tsx +36 -40
- package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +1 -3
- package/src/services/electron/localFileService.ts +5 -0
- package/src/store/chat/slices/builtinTool/actions/localFile.ts +27 -3
- package/src/store/electron/selectors/desktopState.ts +7 -0
- package/src/store/electron/selectors/index.ts +1 -0
- package/src/store/tool/slices/store/action.ts +1 -1
- package/src/tools/local-files/Render/ListFiles/index.tsx +2 -45
- package/src/tools/local-files/Render/RenameLocalFile/index.tsx +45 -0
- package/src/tools/local-files/Render/RunCommand/index.tsx +35 -0
- package/src/tools/local-files/Render/WriteFile/index.tsx +32 -0
- package/src/tools/local-files/Render/index.tsx +4 -0
- package/src/tools/local-files/index.ts +20 -21
- package/src/tools/local-files/systemRole.ts +7 -13
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.84.17](https://github.com/lobehub/lobe-chat/compare/v1.84.16...v1.84.17)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-03**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Add write file tool to local-file plugin.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Add write file tool to local-file plugin, closes [#7684](https://github.com/lobehub/lobe-chat/issues/7684) ([e22e932](https://github.com/lobehub/lobe-chat/commit/e22e932))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.84.16](https://github.com/lobehub/lobe-chat/compare/v1.84.15...v1.84.16)
|
31
|
+
|
32
|
+
<sup>Released on **2025-05-02**</sup>
|
33
|
+
|
34
|
+
#### 🐛 Bug Fixes
|
35
|
+
|
36
|
+
- **misc**: Fix desktop quiting with reopen window.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### What's fixed
|
44
|
+
|
45
|
+
- **misc**: Fix desktop quiting with reopen window, closes [#7675](https://github.com/lobehub/lobe-chat/issues/7675) ([edeabcf](https://github.com/lobehub/lobe-chat/commit/edeabcf))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.84.15](https://github.com/lobehub/lobe-chat/compare/v1.84.14...v1.84.15)
|
6
56
|
|
7
57
|
<sup>Released on **2025-05-01**</sup>
|
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
OpenLocalFileParams,
|
10
10
|
OpenLocalFolderParams,
|
11
11
|
RenameLocalFileResult,
|
12
|
+
WriteLocalFileParams,
|
12
13
|
} from '@lobechat/electron-client-ipc';
|
13
14
|
import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
|
14
15
|
import { shell } from 'electron';
|
@@ -20,13 +21,18 @@ import { promisify } from 'node:util';
|
|
20
21
|
import FileSearchService from '@/services/fileSearchSrv';
|
21
22
|
import { FileResult, SearchOptions } from '@/types/fileSearch';
|
22
23
|
import { makeSureDirExist } from '@/utils/file-system';
|
24
|
+
import { createLogger } from '@/utils/logger';
|
23
25
|
|
24
26
|
import { ControllerModule, ipcClientEvent } from './index';
|
25
27
|
|
28
|
+
// 创建日志记录器
|
29
|
+
const logger = createLogger('controllers:LocalFileCtr');
|
30
|
+
|
26
31
|
const statPromise = promisify(fs.stat);
|
27
32
|
const readdirPromise = promisify(fs.readdir);
|
28
33
|
const renamePromiseFs = promisify(fs.rename);
|
29
34
|
const accessPromise = promisify(fs.access);
|
35
|
+
const writeFilePromise = promisify(fs.writeFile);
|
30
36
|
|
31
37
|
export default class LocalFileCtr extends ControllerModule {
|
32
38
|
private get searchService() {
|
@@ -38,11 +44,20 @@ export default class LocalFileCtr extends ControllerModule {
|
|
38
44
|
*/
|
39
45
|
@ipcClientEvent('searchLocalFiles')
|
40
46
|
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
|
47
|
+
logger.debug('Received file search request:', { keywords: params.keywords });
|
48
|
+
|
41
49
|
const options: Omit<SearchOptions, 'keywords'> = {
|
42
50
|
limit: 30,
|
43
51
|
};
|
44
52
|
|
45
|
-
|
53
|
+
try {
|
54
|
+
const results = await this.searchService.search(params.keywords, options);
|
55
|
+
logger.debug('File search completed', { count: results.length });
|
56
|
+
return results;
|
57
|
+
} catch (error) {
|
58
|
+
logger.error('File search failed:', error);
|
59
|
+
return [];
|
60
|
+
}
|
46
61
|
}
|
47
62
|
|
48
63
|
@ipcClientEvent('openLocalFile')
|
@@ -50,11 +65,14 @@ export default class LocalFileCtr extends ControllerModule {
|
|
50
65
|
error?: string;
|
51
66
|
success: boolean;
|
52
67
|
}> {
|
68
|
+
logger.debug('Attempting to open file:', { filePath });
|
69
|
+
|
53
70
|
try {
|
54
71
|
await shell.openPath(filePath);
|
72
|
+
logger.debug('File opened successfully:', { filePath });
|
55
73
|
return { success: true };
|
56
74
|
} catch (error) {
|
57
|
-
|
75
|
+
logger.error(`Failed to open file ${filePath}:`, error);
|
58
76
|
return { error: (error as Error).message, success: false };
|
59
77
|
}
|
60
78
|
}
|
@@ -64,35 +82,42 @@ export default class LocalFileCtr extends ControllerModule {
|
|
64
82
|
error?: string;
|
65
83
|
success: boolean;
|
66
84
|
}> {
|
85
|
+
const folderPath = isDirectory ? targetPath : path.dirname(targetPath);
|
86
|
+
logger.debug('Attempting to open folder:', { folderPath, isDirectory, targetPath });
|
87
|
+
|
67
88
|
try {
|
68
|
-
const folderPath = isDirectory ? targetPath : path.dirname(targetPath);
|
69
89
|
await shell.openPath(folderPath);
|
90
|
+
logger.debug('Folder opened successfully:', { folderPath });
|
70
91
|
return { success: true };
|
71
92
|
} catch (error) {
|
72
|
-
|
93
|
+
logger.error(`Failed to open folder ${folderPath}:`, error);
|
73
94
|
return { error: (error as Error).message, success: false };
|
74
95
|
}
|
75
96
|
}
|
76
97
|
|
77
98
|
@ipcClientEvent('readLocalFiles')
|
78
99
|
async readFiles({ paths }: LocalReadFilesParams): Promise<LocalReadFileResult[]> {
|
100
|
+
logger.debug('Starting batch file reading:', { count: paths.length });
|
101
|
+
|
79
102
|
const results: LocalReadFileResult[] = [];
|
80
103
|
|
81
104
|
for (const filePath of paths) {
|
82
105
|
// 初始化结果对象
|
106
|
+
logger.debug('Reading single file:', { filePath });
|
83
107
|
const result = await this.readFile({ path: filePath });
|
84
|
-
|
85
108
|
results.push(result);
|
86
109
|
}
|
87
110
|
|
111
|
+
logger.debug('Batch file reading completed', { count: results.length });
|
88
112
|
return results;
|
89
113
|
}
|
90
114
|
|
91
115
|
@ipcClientEvent('readLocalFile')
|
92
116
|
async readFile({ path: filePath, loc }: LocalReadFileParams): Promise<LocalReadFileResult> {
|
93
|
-
|
94
|
-
|
117
|
+
const effectiveLoc = loc ?? [0, 200];
|
118
|
+
logger.debug('Starting to read file:', { filePath, loc: effectiveLoc });
|
95
119
|
|
120
|
+
try {
|
96
121
|
const fileDocument = await loadFile(filePath);
|
97
122
|
|
98
123
|
const [startLine, endLine] = effectiveLoc;
|
@@ -106,6 +131,13 @@ export default class LocalFileCtr extends ControllerModule {
|
|
106
131
|
const charCount = content.length;
|
107
132
|
const lineCount = selectedLines.length;
|
108
133
|
|
134
|
+
logger.debug('File read successfully:', {
|
135
|
+
filePath,
|
136
|
+
selectedLineCount: lineCount,
|
137
|
+
totalCharCount,
|
138
|
+
totalLineCount,
|
139
|
+
});
|
140
|
+
|
109
141
|
const result: LocalReadFileResult = {
|
110
142
|
// Char count for the selected range
|
111
143
|
charCount,
|
@@ -128,6 +160,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
128
160
|
try {
|
129
161
|
const stats = await statPromise(filePath);
|
130
162
|
if (stats.isDirectory()) {
|
163
|
+
logger.warn('Attempted to read directory content:', { filePath });
|
131
164
|
result.content = 'This is a directory and cannot be read as plain text.';
|
132
165
|
result.charCount = 0;
|
133
166
|
result.lineCount = 0;
|
@@ -136,12 +169,12 @@ export default class LocalFileCtr extends ControllerModule {
|
|
136
169
|
result.totalLineCount = 0;
|
137
170
|
}
|
138
171
|
} catch (statError) {
|
139
|
-
|
172
|
+
logger.error(`Failed to get file status ${filePath}:`, statError);
|
140
173
|
}
|
141
174
|
|
142
175
|
return result;
|
143
176
|
} catch (error) {
|
144
|
-
|
177
|
+
logger.error(`Failed to read file ${filePath}:`, error);
|
145
178
|
const errorMessage = (error as Error).message;
|
146
179
|
return {
|
147
180
|
charCount: 0,
|
@@ -160,13 +193,20 @@ export default class LocalFileCtr extends ControllerModule {
|
|
160
193
|
|
161
194
|
@ipcClientEvent('listLocalFiles')
|
162
195
|
async listLocalFiles({ path: dirPath }: ListLocalFileParams): Promise<FileResult[]> {
|
196
|
+
logger.debug('Listing directory contents:', { dirPath });
|
197
|
+
|
163
198
|
const results: FileResult[] = [];
|
164
199
|
try {
|
165
200
|
const entries = await readdirPromise(dirPath);
|
201
|
+
logger.debug('Directory entries retrieved successfully:', {
|
202
|
+
dirPath,
|
203
|
+
entriesCount: entries.length,
|
204
|
+
});
|
166
205
|
|
167
206
|
for (const entry of entries) {
|
168
207
|
// Skip specific system files based on the ignore list
|
169
208
|
if (SYSTEM_FILES_TO_IGNORE.includes(entry)) {
|
209
|
+
logger.debug('Ignoring system file:', { fileName: entry });
|
170
210
|
continue;
|
171
211
|
}
|
172
212
|
|
@@ -186,7 +226,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
186
226
|
});
|
187
227
|
} catch (statError) {
|
188
228
|
// Silently ignore files we can't stat (e.g. permissions)
|
189
|
-
|
229
|
+
logger.error(`Failed to get file status ${fullPath}:`, statError);
|
190
230
|
}
|
191
231
|
}
|
192
232
|
|
@@ -199,9 +239,10 @@ export default class LocalFileCtr extends ControllerModule {
|
|
199
239
|
return (a.name || '').localeCompare(b.name || ''); // Then sort by name
|
200
240
|
});
|
201
241
|
|
242
|
+
logger.debug('Directory listing successful', { dirPath, resultCount: results.length });
|
202
243
|
return results;
|
203
244
|
} catch (error) {
|
204
|
-
|
245
|
+
logger.error(`Failed to list directory ${dirPath}:`, error);
|
205
246
|
// Rethrow or return an empty array/error object depending on desired behavior
|
206
247
|
// For now, returning empty array on error listing directory itself
|
207
248
|
return [];
|
@@ -210,16 +251,21 @@ export default class LocalFileCtr extends ControllerModule {
|
|
210
251
|
|
211
252
|
@ipcClientEvent('moveLocalFiles')
|
212
253
|
async handleMoveFiles({ items }: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
|
254
|
+
logger.debug('Starting batch file move:', { itemsCount: items?.length });
|
255
|
+
|
213
256
|
const results: LocalMoveFilesResultItem[] = [];
|
214
257
|
|
215
258
|
if (!items || items.length === 0) {
|
216
|
-
|
259
|
+
logger.warn('moveLocalFiles called with empty parameters');
|
217
260
|
return [];
|
218
261
|
}
|
219
262
|
|
220
263
|
// 逐个处理移动请求
|
221
264
|
for (const item of items) {
|
222
265
|
const { oldPath: sourcePath, newPath } = item;
|
266
|
+
const logPrefix = `[Moving file ${sourcePath} -> ${newPath}]`;
|
267
|
+
logger.debug(`${logPrefix} Starting process`);
|
268
|
+
|
223
269
|
const resultItem: LocalMoveFilesResultItem = {
|
224
270
|
newPath: undefined,
|
225
271
|
sourcePath,
|
@@ -228,6 +274,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
228
274
|
|
229
275
|
// 基本验证
|
230
276
|
if (!sourcePath || !newPath) {
|
277
|
+
logger.error(`${logPrefix} Parameter validation failed: source or target path is empty`);
|
231
278
|
resultItem.error = 'Both oldPath and newPath are required for each item.';
|
232
279
|
results.push(resultItem);
|
233
280
|
continue;
|
@@ -237,10 +284,13 @@ export default class LocalFileCtr extends ControllerModule {
|
|
237
284
|
// 检查源是否存在
|
238
285
|
try {
|
239
286
|
await accessPromise(sourcePath, fs.constants.F_OK);
|
287
|
+
logger.debug(`${logPrefix} Source file exists`);
|
240
288
|
} catch (accessError: any) {
|
241
289
|
if (accessError.code === 'ENOENT') {
|
290
|
+
logger.error(`${logPrefix} Source file does not exist`);
|
242
291
|
throw new Error(`Source path not found: ${sourcePath}`);
|
243
292
|
} else {
|
293
|
+
logger.error(`${logPrefix} Permission error accessing source file:`, accessError);
|
244
294
|
throw new Error(
|
245
295
|
`Permission denied accessing source path: ${sourcePath}. ${accessError.message}`,
|
246
296
|
);
|
@@ -249,7 +299,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
249
299
|
|
250
300
|
// 检查目标路径是否与源路径相同
|
251
301
|
if (path.normalize(sourcePath) === path.normalize(newPath)) {
|
252
|
-
|
302
|
+
logger.info(`${logPrefix} Source and target paths are identical, skipping move`);
|
253
303
|
resultItem.success = true;
|
254
304
|
resultItem.newPath = newPath; // 即使未移动,也报告目标路径
|
255
305
|
results.push(resultItem);
|
@@ -259,14 +309,15 @@ export default class LocalFileCtr extends ControllerModule {
|
|
259
309
|
// LBYL: 确保目标目录存在
|
260
310
|
const targetDir = path.dirname(newPath);
|
261
311
|
makeSureDirExist(targetDir);
|
312
|
+
logger.debug(`${logPrefix} Ensured target directory exists: ${targetDir}`);
|
262
313
|
|
263
314
|
// 执行移动 (rename)
|
264
315
|
await renamePromiseFs(sourcePath, newPath);
|
265
316
|
resultItem.success = true;
|
266
317
|
resultItem.newPath = newPath;
|
267
|
-
|
318
|
+
logger.info(`${logPrefix} Move successful`);
|
268
319
|
} catch (error) {
|
269
|
-
|
320
|
+
logger.error(`${logPrefix} Move failed:`, error);
|
270
321
|
// 使用与 handleMoveFile 类似的错误处理逻辑
|
271
322
|
let errorMessage = (error as Error).message;
|
272
323
|
if ((error as any).code === 'ENOENT')
|
@@ -296,6 +347,10 @@ export default class LocalFileCtr extends ControllerModule {
|
|
296
347
|
results.push(resultItem);
|
297
348
|
}
|
298
349
|
|
350
|
+
logger.debug('Batch file move completed', {
|
351
|
+
successCount: results.filter((r) => r.success).length,
|
352
|
+
totalCount: results.length,
|
353
|
+
});
|
299
354
|
return results;
|
300
355
|
}
|
301
356
|
|
@@ -307,8 +362,12 @@ export default class LocalFileCtr extends ControllerModule {
|
|
307
362
|
newName: string;
|
308
363
|
path: string;
|
309
364
|
}): Promise<RenameLocalFileResult> {
|
365
|
+
const logPrefix = `[Renaming ${currentPath} -> ${newName}]`;
|
366
|
+
logger.debug(`${logPrefix} Starting rename request`);
|
367
|
+
|
310
368
|
// Basic validation (can also be done in frontend action)
|
311
369
|
if (!currentPath || !newName) {
|
370
|
+
logger.error(`${logPrefix} Parameter validation failed: path or new name is empty`);
|
312
371
|
return { error: 'Both path and newName are required.', newPath: '', success: false };
|
313
372
|
}
|
314
373
|
// Prevent path traversal or using invalid characters/names
|
@@ -319,6 +378,7 @@ export default class LocalFileCtr extends ControllerModule {
|
|
319
378
|
newName === '..' ||
|
320
379
|
/["*/:<>?\\|]/.test(newName) // Check for typical invalid filename characters
|
321
380
|
) {
|
381
|
+
logger.error(`${logPrefix} New filename contains illegal characters: ${newName}`);
|
322
382
|
return {
|
323
383
|
error:
|
324
384
|
'Invalid new name. It cannot contain path separators (/, \\), be "." or "..", or include characters like < > : " / \\ | ? *.',
|
@@ -331,18 +391,19 @@ export default class LocalFileCtr extends ControllerModule {
|
|
331
391
|
try {
|
332
392
|
const dir = path.dirname(currentPath);
|
333
393
|
newPath = path.join(dir, newName);
|
394
|
+
logger.debug(`${logPrefix} Calculated new path: ${newPath}`);
|
334
395
|
|
335
396
|
// Check if paths are identical after calculation
|
336
397
|
if (path.normalize(currentPath) === path.normalize(newPath)) {
|
337
|
-
|
338
|
-
|
398
|
+
logger.info(
|
399
|
+
`${logPrefix} Source path and calculated target path are identical, skipping rename`,
|
339
400
|
);
|
340
401
|
// Consider success as no change is needed, but maybe inform the user?
|
341
402
|
// Return success for now.
|
342
403
|
return { newPath, success: true };
|
343
404
|
}
|
344
405
|
} catch (error) {
|
345
|
-
|
406
|
+
logger.error(`${logPrefix} Failed to calculate new path:`, error);
|
346
407
|
return {
|
347
408
|
error: `Internal error calculating the new path: ${(error as Error).message}`,
|
348
409
|
newPath: '',
|
@@ -353,12 +414,12 @@ export default class LocalFileCtr extends ControllerModule {
|
|
353
414
|
// Perform the rename operation using fs.promises.rename directly
|
354
415
|
try {
|
355
416
|
await renamePromise(currentPath, newPath);
|
356
|
-
|
417
|
+
logger.info(`${logPrefix} Rename successful: ${currentPath} -> ${newPath}`);
|
357
418
|
// Optionally return the newPath if frontend needs it
|
358
419
|
// return { success: true, newPath: newPath };
|
359
420
|
return { newPath, success: true };
|
360
421
|
} catch (error) {
|
361
|
-
|
422
|
+
logger.error(`${logPrefix} Rename failed:`, error);
|
362
423
|
let errorMessage = (error as Error).message;
|
363
424
|
// Provide more specific error messages based on common codes
|
364
425
|
if ((error as any).code === 'ENOENT') {
|
@@ -377,4 +438,44 @@ export default class LocalFileCtr extends ControllerModule {
|
|
377
438
|
return { error: errorMessage, newPath: '', success: false };
|
378
439
|
}
|
379
440
|
}
|
441
|
+
|
442
|
+
@ipcClientEvent('writeLocalFile')
|
443
|
+
async handleWriteFile({ path: filePath, content }: WriteLocalFileParams) {
|
444
|
+
const logPrefix = `[Writing file ${filePath}]`;
|
445
|
+
logger.debug(`${logPrefix} Starting to write file`, { contentLength: content?.length });
|
446
|
+
|
447
|
+
// 验证参数
|
448
|
+
if (!filePath) {
|
449
|
+
logger.error(`${logPrefix} Parameter validation failed: path is empty`);
|
450
|
+
return { error: 'Path cannot be empty', success: false };
|
451
|
+
}
|
452
|
+
|
453
|
+
if (content === undefined) {
|
454
|
+
logger.error(`${logPrefix} Parameter validation failed: content is empty`);
|
455
|
+
return { error: 'Content cannot be empty', success: false };
|
456
|
+
}
|
457
|
+
|
458
|
+
try {
|
459
|
+
// 确保目标目录存在
|
460
|
+
const dirname = path.dirname(filePath);
|
461
|
+
logger.debug(`${logPrefix} Creating directory: ${dirname}`);
|
462
|
+
fs.mkdirSync(dirname, { recursive: true });
|
463
|
+
|
464
|
+
// 写入文件内容
|
465
|
+
logger.debug(`${logPrefix} Starting to write content to file`);
|
466
|
+
await writeFilePromise(filePath, content, 'utf8');
|
467
|
+
logger.info(`${logPrefix} File written successfully`, {
|
468
|
+
path: filePath,
|
469
|
+
size: content.length,
|
470
|
+
});
|
471
|
+
|
472
|
+
return { success: true };
|
473
|
+
} catch (error) {
|
474
|
+
logger.error(`${logPrefix} Failed to write file:`, error);
|
475
|
+
return {
|
476
|
+
error: `Failed to write file: ${(error as Error).message}`,
|
477
|
+
success: false,
|
478
|
+
};
|
479
|
+
}
|
480
|
+
}
|
380
481
|
}
|
@@ -55,6 +55,12 @@ export default class Browser {
|
|
55
55
|
return this.retrieveOrInitialize();
|
56
56
|
}
|
57
57
|
|
58
|
+
get webContents() {
|
59
|
+
if (this._browserWindow.isDestroyed()) return null;
|
60
|
+
|
61
|
+
return this._browserWindow.webContents;
|
62
|
+
}
|
63
|
+
|
58
64
|
/**
|
59
65
|
* Method to construct BrowserWindows object
|
60
66
|
* @param options
|
@@ -210,7 +216,6 @@ export default class Browser {
|
|
210
216
|
session: browserWindow.webContents.session,
|
211
217
|
});
|
212
218
|
|
213
|
-
console.log('platform:',process.platform);
|
214
219
|
// Windows 11 can use this new API
|
215
220
|
if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
|
216
221
|
logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
|
@@ -157,8 +157,8 @@ export default class BrowserManager {
|
|
157
157
|
this.webContentsMap.set(browser.browserWindow.webContents, identifier);
|
158
158
|
|
159
159
|
// 当窗口关闭时清理映射
|
160
|
-
browser.browserWindow.on('
|
161
|
-
this.webContentsMap.delete(browser.
|
160
|
+
browser.browserWindow.on('close', () => {
|
161
|
+
if (browser.webContents) this.webContentsMap.delete(browser.webContents);
|
162
162
|
});
|
163
163
|
|
164
164
|
return browser;
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Add write file tool to local-file plugin."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-05-03",
|
9
|
+
"version": "1.84.17"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"fixes": [
|
14
|
+
"Fix desktop quiting with reopen window."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-05-02",
|
18
|
+
"version": "1.84.16"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"fixes": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.84.
|
3
|
+
"version": "1.84.17",
|
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",
|
@@ -147,7 +147,7 @@
|
|
147
147
|
"@lobehub/icons": "^2.0.0",
|
148
148
|
"@lobehub/tts": "^2.0.0",
|
149
149
|
"@lobehub/ui": "^2.0.10",
|
150
|
-
"@modelcontextprotocol/sdk": "^1.
|
150
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
151
151
|
"@neondatabase/serverless": "^1.0.0",
|
152
152
|
"@next/third-parties": "^15.3.0",
|
153
153
|
"@react-spring/web": "^9.7.5",
|
@@ -163,6 +163,7 @@
|
|
163
163
|
"@vercel/edge-config": "^1.4.0",
|
164
164
|
"@vercel/functions": "^2.0.0",
|
165
165
|
"@vercel/speed-insights": "^1.2.0",
|
166
|
+
"@xterm/xterm": "^5.5.0",
|
166
167
|
"ahooks": "^3.8.4",
|
167
168
|
"ai": "^3.4.33",
|
168
169
|
"antd": "^5.24.6",
|
@@ -212,6 +213,7 @@
|
|
212
213
|
"openai": "^4.91.1",
|
213
214
|
"openapi-fetch": "^0.9.8",
|
214
215
|
"partial-json": "^0.1.7",
|
216
|
+
"path-browserify-esm": "^1.0.6",
|
215
217
|
"pdf-parse": "^1.1.1",
|
216
218
|
"pdfjs-dist": "4.8.69",
|
217
219
|
"pg": "^8.14.1",
|
@@ -11,6 +11,7 @@ import {
|
|
11
11
|
OpenLocalFolderParams,
|
12
12
|
RenameLocalFileParams,
|
13
13
|
RenameLocalFileResult,
|
14
|
+
WriteLocalFileParams,
|
14
15
|
} from '../types';
|
15
16
|
|
16
17
|
export interface LocalFilesDispatchEvents {
|
@@ -25,4 +26,5 @@ export interface LocalFilesDispatchEvents {
|
|
25
26
|
|
26
27
|
renameLocalFile: (params: RenameLocalFileParams) => RenameLocalFileResult;
|
27
28
|
searchLocalFiles: (params: LocalSearchFilesParams) => LocalFileItem[];
|
29
|
+
writeLocalFile: (params: WriteLocalFileParams) => RenameLocalFileResult;
|
28
30
|
}
|
@@ -55,6 +55,22 @@ export interface LocalReadFilesParams {
|
|
55
55
|
paths: string[];
|
56
56
|
}
|
57
57
|
|
58
|
+
export interface WriteLocalFileParams {
|
59
|
+
/**
|
60
|
+
* 要写入的内容
|
61
|
+
*/
|
62
|
+
content: string;
|
63
|
+
|
64
|
+
/**
|
65
|
+
* 要写入的文件路径
|
66
|
+
*/
|
67
|
+
path: string;
|
68
|
+
}
|
69
|
+
|
70
|
+
export interface RunCommandParams {
|
71
|
+
command: string;
|
72
|
+
}
|
73
|
+
|
58
74
|
export interface LocalReadFileResult {
|
59
75
|
/**
|
60
76
|
* Character count of the content within the specified `loc` range.
|
@@ -44,7 +44,7 @@ const CheckError = ({
|
|
44
44
|
|
45
45
|
const errorMessage = errorBody.error?.message;
|
46
46
|
|
47
|
-
if (error?.type === 'OllamaServiceUnavailable') return <OllamaSetupGuide
|
47
|
+
if (error?.type === 'OllamaServiceUnavailable') return <OllamaSetupGuide />;
|
48
48
|
|
49
49
|
// error of not pull the model
|
50
50
|
const unresolvedModel = errorMessage?.match(UNRESOLVED_MODEL_REGEXP)?.[1];
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import isEqual from 'fast-deep-equal';
|
2
2
|
import React, { memo } from 'react';
|
3
3
|
|
4
|
+
import { LocalFile } from '@/features/LocalFile';
|
5
|
+
|
4
6
|
import { MarkdownElementProps } from '../../type';
|
5
|
-
import LocalFile from './LocalFile';
|
6
7
|
|
7
8
|
interface LocalFileProps {
|
8
9
|
isDirectory: boolean;
|
@@ -31,17 +31,17 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
31
31
|
`,
|
32
32
|
}));
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
path,
|
37
|
-
isDirectory,
|
38
|
-
}: {
|
39
|
-
isDirectory: boolean;
|
34
|
+
interface LocalFileProps {
|
35
|
+
isDirectory?: boolean;
|
40
36
|
name: string;
|
41
|
-
path
|
42
|
-
}
|
37
|
+
path?: string;
|
38
|
+
}
|
39
|
+
|
40
|
+
export const LocalFile = ({ name, path, isDirectory = false }: LocalFileProps) => {
|
43
41
|
const { styles } = useStyles();
|
44
42
|
const handleClick = () => {
|
43
|
+
if (!path) return;
|
44
|
+
|
45
45
|
localFileService.openLocalFileOrFolder(path, isDirectory);
|
46
46
|
};
|
47
47
|
|
@@ -61,5 +61,3 @@ const LocalFile = ({
|
|
61
61
|
</Flexbox>
|
62
62
|
);
|
63
63
|
};
|
64
|
-
|
65
|
-
export default LocalFile;
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import { createStyles } from 'antd-style';
|
2
|
+
import path from 'path-browserify-esm';
|
3
|
+
import React from 'react';
|
4
|
+
import { Flexbox } from 'react-layout-kit';
|
5
|
+
|
6
|
+
import FileIcon from '@/components/FileIcon';
|
7
|
+
import { localFileService } from '@/services/electron/localFileService';
|
8
|
+
|
9
|
+
const useStyles = createStyles(({ css, token }) => ({
|
10
|
+
container: css`
|
11
|
+
cursor: pointer;
|
12
|
+
|
13
|
+
padding-block: 2px;
|
14
|
+
padding-inline: 4px 8px;
|
15
|
+
border-radius: 4px;
|
16
|
+
|
17
|
+
color: ${token.colorTextSecondary};
|
18
|
+
|
19
|
+
:hover {
|
20
|
+
color: ${token.colorText};
|
21
|
+
background: ${token.colorFillTertiary};
|
22
|
+
}
|
23
|
+
`,
|
24
|
+
title: css`
|
25
|
+
overflow: hidden;
|
26
|
+
display: block;
|
27
|
+
|
28
|
+
line-height: 20px;
|
29
|
+
color: inherit;
|
30
|
+
text-overflow: ellipsis;
|
31
|
+
white-space: nowrap;
|
32
|
+
`,
|
33
|
+
}));
|
34
|
+
|
35
|
+
interface LocalFolderProps {
|
36
|
+
path: string;
|
37
|
+
size?: number;
|
38
|
+
}
|
39
|
+
|
40
|
+
export const LocalFolder = ({ path: pathname, size = 22 }: LocalFolderProps) => {
|
41
|
+
const { styles } = useStyles();
|
42
|
+
const handleClick = () => {
|
43
|
+
if (!path) return;
|
44
|
+
|
45
|
+
localFileService.openLocalFolder({ isDirectory: true, path: pathname });
|
46
|
+
};
|
47
|
+
|
48
|
+
const { base } = path.parse(pathname);
|
49
|
+
|
50
|
+
return (
|
51
|
+
<Flexbox
|
52
|
+
align={'center'}
|
53
|
+
className={styles.container}
|
54
|
+
gap={4}
|
55
|
+
horizontal
|
56
|
+
onClick={handleClick}
|
57
|
+
style={{ display: 'inline-flex', verticalAlign: 'middle' }}
|
58
|
+
>
|
59
|
+
<FileIcon fileName={base} isDirectory size={size} variant={'raw'} />
|
60
|
+
<Flexbox align={'baseline'} gap={4} horizontal style={{ overflow: 'hidden', width: '100%' }}>
|
61
|
+
<div className={styles.title}>{base}</div>
|
62
|
+
</Flexbox>
|
63
|
+
</Flexbox>
|
64
|
+
);
|
65
|
+
};
|
@@ -9,8 +9,6 @@ import { Center } from 'react-layout-kit';
|
|
9
9
|
import FormAction from '@/components/FormAction';
|
10
10
|
import { useChatStore } from '@/store/chat';
|
11
11
|
|
12
|
-
import { ErrorActionContainer } from '../Conversation/Error/style';
|
13
|
-
|
14
12
|
// TODO: 优化 Ollama setup 的流程,isDesktop 模式下可以直接做到端到端检测
|
15
13
|
const OllamaDesktopSetupGuide = memo<{ id?: string }>(({ id }) => {
|
16
14
|
const theme = useTheme();
|
@@ -22,44 +20,42 @@ const OllamaDesktopSetupGuide = memo<{ id?: string }>(({ id }) => {
|
|
22
20
|
]);
|
23
21
|
|
24
22
|
return (
|
25
|
-
<
|
26
|
-
<
|
27
|
-
<
|
28
|
-
|
29
|
-
|
30
|
-
<
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
</Center>
|
62
|
-
</ErrorActionContainer>
|
23
|
+
<Center gap={16} paddingBlock={32} style={{ maxWidth: 300, width: '100%' }}>
|
24
|
+
<FormAction
|
25
|
+
avatar={<Ollama color={theme.colorPrimary} size={64} />}
|
26
|
+
description={
|
27
|
+
<span>
|
28
|
+
<Trans i18nKey={'OllamaSetupGuide.install.description'} ns={'components'}>
|
29
|
+
请确认你已经开启 Ollama ,如果没有安装 Ollama ,请前往官网
|
30
|
+
<Link href={'https://ollama.com/download'}>下载</Link>
|
31
|
+
</Trans>
|
32
|
+
</span>
|
33
|
+
}
|
34
|
+
title={t('OllamaSetupGuide.install.title')}
|
35
|
+
/>
|
36
|
+
{id && (
|
37
|
+
<>
|
38
|
+
<Button
|
39
|
+
block
|
40
|
+
onClick={() => {
|
41
|
+
delAndRegenerateMessage(id);
|
42
|
+
}}
|
43
|
+
style={{ marginTop: 8 }}
|
44
|
+
type={'primary'}
|
45
|
+
>
|
46
|
+
{t('OllamaSetupGuide.action.start')}
|
47
|
+
</Button>
|
48
|
+
<Button
|
49
|
+
block
|
50
|
+
onClick={() => {
|
51
|
+
deleteMessage(id);
|
52
|
+
}}
|
53
|
+
>
|
54
|
+
{t('OllamaSetupGuide.action.close')}
|
55
|
+
</Button>
|
56
|
+
</>
|
57
|
+
)}
|
58
|
+
</Center>
|
63
59
|
);
|
64
60
|
});
|
65
61
|
|
@@ -21,11 +21,9 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
|
|
21
21
|
"name": "echo",
|
22
22
|
},
|
23
23
|
{
|
24
|
+
"annotations": {},
|
24
25
|
"description": "Lists all available tools and methods",
|
25
26
|
"inputSchema": {
|
26
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
27
|
-
"additionalProperties": false,
|
28
|
-
"properties": {},
|
29
27
|
"type": "object",
|
30
28
|
},
|
31
29
|
"name": "debug",
|
@@ -10,6 +10,7 @@ import {
|
|
10
10
|
OpenLocalFileParams,
|
11
11
|
OpenLocalFolderParams,
|
12
12
|
RenameLocalFileParams,
|
13
|
+
WriteLocalFileParams,
|
13
14
|
dispatch,
|
14
15
|
} from '@lobechat/electron-client-ipc';
|
15
16
|
|
@@ -46,6 +47,10 @@ class LocalFileService {
|
|
46
47
|
return dispatch('renameLocalFile', params);
|
47
48
|
}
|
48
49
|
|
50
|
+
async writeFile(params: WriteLocalFileParams) {
|
51
|
+
return dispatch('writeLocalFile', params);
|
52
|
+
}
|
53
|
+
|
49
54
|
async openLocalFileOrFolder(path: string, isDirectory: boolean) {
|
50
55
|
if (isDirectory) {
|
51
56
|
return this.openLocalFolder({ isDirectory, path });
|
@@ -6,6 +6,7 @@ import {
|
|
6
6
|
LocalSearchFilesParams,
|
7
7
|
MoveLocalFilesParams,
|
8
8
|
RenameLocalFileParams,
|
9
|
+
WriteLocalFileParams,
|
9
10
|
} from '@lobechat/electron-client-ipc';
|
10
11
|
import { StateCreator } from 'zustand/vanilla';
|
11
12
|
|
@@ -23,7 +24,7 @@ import {
|
|
23
24
|
export interface LocalFileAction {
|
24
25
|
internal_triggerLocalFileToolCalling: <T = any>(
|
25
26
|
id: string,
|
26
|
-
callingService: () => Promise<{ content: any; state
|
27
|
+
callingService: () => Promise<{ content: any; state?: T }>,
|
27
28
|
) => Promise<boolean>;
|
28
29
|
|
29
30
|
listLocalFiles: (id: string, params: ListLocalFileParams) => Promise<boolean>;
|
@@ -34,8 +35,9 @@ export interface LocalFileAction {
|
|
34
35
|
renameLocalFile: (id: string, params: RenameLocalFileParams) => Promise<boolean>;
|
35
36
|
// Added rename action
|
36
37
|
searchLocalFiles: (id: string, params: LocalSearchFilesParams) => Promise<boolean>;
|
37
|
-
|
38
38
|
toggleLocalFileLoading: (id: string, loading: boolean) => void;
|
39
|
+
|
40
|
+
writeLocalFile: (id: string, params: WriteLocalFileParams) => Promise<boolean>;
|
39
41
|
}
|
40
42
|
|
41
43
|
export const localFileSlice: StateCreator<
|
@@ -48,7 +50,9 @@ export const localFileSlice: StateCreator<
|
|
48
50
|
get().toggleLocalFileLoading(id, true);
|
49
51
|
try {
|
50
52
|
const { state, content } = await callingService();
|
51
|
-
|
53
|
+
if (state) {
|
54
|
+
await get().updatePluginState(id, state as any);
|
55
|
+
}
|
52
56
|
await get().internal_updateMessageContent(id, JSON.stringify(content));
|
53
57
|
} catch (error) {
|
54
58
|
await get().internal_updateMessagePluginError(id, {
|
@@ -183,4 +187,24 @@ export const localFileSlice: StateCreator<
|
|
183
187
|
`toggleLocalFileLoading/${loading ? 'start' : 'end'}`,
|
184
188
|
);
|
185
189
|
},
|
190
|
+
|
191
|
+
writeLocalFile: async (id, params) => {
|
192
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
193
|
+
const result = await localFileService.writeFile(params);
|
194
|
+
|
195
|
+
let content: { message: string; success: boolean };
|
196
|
+
|
197
|
+
if (result.success) {
|
198
|
+
content = {
|
199
|
+
message: `成功写入文件 ${params.path}`,
|
200
|
+
success: true,
|
201
|
+
};
|
202
|
+
} else {
|
203
|
+
const errorMessage = result.error;
|
204
|
+
|
205
|
+
content = { message: errorMessage || '写入文件失败', success: false };
|
206
|
+
}
|
207
|
+
return { content };
|
208
|
+
});
|
209
|
+
},
|
186
210
|
});
|
@@ -70,7 +70,7 @@ export const createPluginStoreSlice: StateCreator<
|
|
70
70
|
loadPluginStore: async () => {
|
71
71
|
const pluginMarketIndex = await toolService.getToolList();
|
72
72
|
|
73
|
-
set({ pluginStoreList: pluginMarketIndex }, false, n('loadPluginList'));
|
73
|
+
set({ pluginStoreList: pluginMarketIndex || [] }, false, n('loadPluginList'));
|
74
74
|
|
75
75
|
return pluginMarketIndex;
|
76
76
|
},
|
@@ -1,42 +1,12 @@
|
|
1
1
|
import { ListLocalFileParams } from '@lobechat/electron-client-ipc';
|
2
|
-
import { Typography } from 'antd';
|
3
|
-
import { createStyles } from 'antd-style';
|
4
2
|
import React, { memo } from 'react';
|
5
|
-
import { Flexbox } from 'react-layout-kit';
|
6
3
|
|
7
|
-
import
|
8
|
-
import { localFileService } from '@/services/electron/localFileService';
|
4
|
+
import { LocalFolder } from '@/features/LocalFile';
|
9
5
|
import { LocalFileListState } from '@/tools/local-files/type';
|
10
6
|
import { ChatMessagePluginError } from '@/types/message';
|
11
7
|
|
12
8
|
import SearchResult from './Result';
|
13
9
|
|
14
|
-
const useStyles = createStyles(({ css, token, cx }) => ({
|
15
|
-
actions: cx(css`
|
16
|
-
cursor: pointer;
|
17
|
-
color: ${token.colorTextTertiary};
|
18
|
-
opacity: 1;
|
19
|
-
transition: opacity 0.2s ${token.motionEaseInOut};
|
20
|
-
`),
|
21
|
-
container: css`
|
22
|
-
cursor: pointer;
|
23
|
-
|
24
|
-
padding-block: 2px;
|
25
|
-
padding-inline: 4px;
|
26
|
-
border-radius: 4px;
|
27
|
-
|
28
|
-
color: ${token.colorTextSecondary};
|
29
|
-
|
30
|
-
:hover {
|
31
|
-
color: ${token.colorText};
|
32
|
-
background: ${token.colorFillTertiary};
|
33
|
-
}
|
34
|
-
`,
|
35
|
-
path: css`
|
36
|
-
color: ${token.colorTextSecondary};
|
37
|
-
`,
|
38
|
-
}));
|
39
|
-
|
40
10
|
interface ListFilesProps {
|
41
11
|
args: ListLocalFileParams;
|
42
12
|
messageId: string;
|
@@ -45,22 +15,9 @@ interface ListFilesProps {
|
|
45
15
|
}
|
46
16
|
|
47
17
|
const ListFiles = memo<ListFilesProps>(({ messageId, pluginError, args, pluginState }) => {
|
48
|
-
const { styles } = useStyles();
|
49
18
|
return (
|
50
19
|
<>
|
51
|
-
<
|
52
|
-
className={styles.container}
|
53
|
-
gap={8}
|
54
|
-
horizontal
|
55
|
-
onClick={() => {
|
56
|
-
localFileService.openLocalFolder({ isDirectory: true, path: args.path });
|
57
|
-
}}
|
58
|
-
>
|
59
|
-
<FileIcon fileName={args.path} isDirectory size={22} variant={'raw'} />
|
60
|
-
<Typography.Text className={styles.path} ellipsis>
|
61
|
-
{args.path}
|
62
|
-
</Typography.Text>
|
63
|
-
</Flexbox>
|
20
|
+
<LocalFolder path={args.path} />
|
64
21
|
<SearchResult
|
65
22
|
listResults={pluginState?.listResults}
|
66
23
|
messageId={messageId}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { RenameLocalFileParams } from '@lobechat/electron-client-ipc';
|
2
|
+
import { Icon } from '@lobehub/ui';
|
3
|
+
import { createStyles } from 'antd-style';
|
4
|
+
import { ArrowRightIcon } from 'lucide-react';
|
5
|
+
import path from 'path-browserify-esm';
|
6
|
+
import React, { memo } from 'react';
|
7
|
+
import { Flexbox } from 'react-layout-kit';
|
8
|
+
|
9
|
+
import { LocalFile } from '@/features/LocalFile';
|
10
|
+
import { LocalReadFileState } from '@/tools/local-files/type';
|
11
|
+
import { ChatMessagePluginError } from '@/types/message';
|
12
|
+
|
13
|
+
const useStyles = createStyles(({ css, token }) => ({
|
14
|
+
container: css`
|
15
|
+
color: ${token.colorTextQuaternary};
|
16
|
+
`,
|
17
|
+
new: css`
|
18
|
+
color: ${token.colorTextSecondary};
|
19
|
+
`,
|
20
|
+
}));
|
21
|
+
|
22
|
+
interface RenameLocalFileProps {
|
23
|
+
args: RenameLocalFileParams;
|
24
|
+
messageId: string;
|
25
|
+
pluginError: ChatMessagePluginError;
|
26
|
+
pluginState: LocalReadFileState;
|
27
|
+
}
|
28
|
+
|
29
|
+
const RenameLocalFile = memo<RenameLocalFileProps>(({ args }) => {
|
30
|
+
const { styles } = useStyles();
|
31
|
+
|
32
|
+
const { base: oldFileName, dir } = path.parse(args.path);
|
33
|
+
|
34
|
+
return (
|
35
|
+
<Flexbox align={'center'} className={styles.container} gap={8} horizontal paddingInline={12}>
|
36
|
+
<Flexbox>{oldFileName}</Flexbox>
|
37
|
+
<Flexbox>
|
38
|
+
<Icon icon={ArrowRightIcon} />
|
39
|
+
</Flexbox>
|
40
|
+
<LocalFile name={args.newName} path={path.join(dir, args.newName)} />
|
41
|
+
</Flexbox>
|
42
|
+
);
|
43
|
+
});
|
44
|
+
|
45
|
+
export default RenameLocalFile;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { RunCommandParams } from '@lobechat/electron-client-ipc';
|
2
|
+
import { Terminal } from '@xterm/xterm';
|
3
|
+
import '@xterm/xterm/css/xterm.css';
|
4
|
+
import { memo, useEffect, useRef } from 'react';
|
5
|
+
|
6
|
+
import { LocalReadFileState } from '@/tools/local-files/type';
|
7
|
+
import { ChatMessagePluginError } from '@/types/message';
|
8
|
+
|
9
|
+
interface RunCommandProps {
|
10
|
+
args: RunCommandParams;
|
11
|
+
messageId: string;
|
12
|
+
pluginError: ChatMessagePluginError;
|
13
|
+
pluginState: LocalReadFileState;
|
14
|
+
}
|
15
|
+
|
16
|
+
const RunCommand = memo<RunCommandProps>(({ args }) => {
|
17
|
+
const terminalRef = useRef(null);
|
18
|
+
|
19
|
+
useEffect(() => {
|
20
|
+
if (!terminalRef.current) return;
|
21
|
+
|
22
|
+
const term = new Terminal({ cols: 80, cursorBlink: true, rows: 30 });
|
23
|
+
|
24
|
+
term.open(terminalRef.current);
|
25
|
+
term.write(args.command);
|
26
|
+
|
27
|
+
return () => {
|
28
|
+
term.dispose();
|
29
|
+
};
|
30
|
+
}, []);
|
31
|
+
|
32
|
+
return <div ref={terminalRef} />;
|
33
|
+
});
|
34
|
+
|
35
|
+
export default RunCommand;
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { WriteLocalFileParams } from '@lobechat/electron-client-ipc';
|
2
|
+
import { Icon } from '@lobehub/ui';
|
3
|
+
import { Skeleton } from 'antd';
|
4
|
+
import { ChevronRight } from 'lucide-react';
|
5
|
+
import path from 'path-browserify-esm';
|
6
|
+
import { memo } from 'react';
|
7
|
+
import { Flexbox } from 'react-layout-kit';
|
8
|
+
|
9
|
+
import { LocalFile, LocalFolder } from '@/features/LocalFile';
|
10
|
+
import { ChatMessagePluginError } from '@/types/message';
|
11
|
+
|
12
|
+
interface WriteFileProps {
|
13
|
+
args: WriteLocalFileParams;
|
14
|
+
messageId: string;
|
15
|
+
pluginError: ChatMessagePluginError;
|
16
|
+
}
|
17
|
+
|
18
|
+
const WriteFile = memo<WriteFileProps>(({ args }) => {
|
19
|
+
if (!args) return <Skeleton active />;
|
20
|
+
|
21
|
+
const { base, dir } = path.parse(args.path);
|
22
|
+
|
23
|
+
return (
|
24
|
+
<Flexbox horizontal>
|
25
|
+
<LocalFolder path={dir} />
|
26
|
+
<Icon icon={ChevronRight} />
|
27
|
+
<LocalFile name={base} path={args.path} />
|
28
|
+
</Flexbox>
|
29
|
+
);
|
30
|
+
});
|
31
|
+
|
32
|
+
export default WriteFile;
|
@@ -6,12 +6,16 @@ import { BuiltinRenderProps } from '@/types/tool';
|
|
6
6
|
|
7
7
|
import ListFiles from './ListFiles';
|
8
8
|
import ReadLocalFile from './ReadLocalFile';
|
9
|
+
import RenameLocalFile from './RenameLocalFile';
|
9
10
|
import SearchFiles from './SearchFiles';
|
11
|
+
import WriteFile from './WriteFile';
|
10
12
|
|
11
13
|
const RenderMap = {
|
12
14
|
[LocalFilesApiName.searchLocalFiles]: SearchFiles,
|
13
15
|
[LocalFilesApiName.listLocalFiles]: ListFiles,
|
14
16
|
[LocalFilesApiName.readLocalFile]: ReadLocalFile,
|
17
|
+
[LocalFilesApiName.renameLocalFile]: RenameLocalFile,
|
18
|
+
[LocalFilesApiName.writeLocalFile]: WriteFile,
|
15
19
|
};
|
16
20
|
|
17
21
|
const LocalFilesRender = memo<BuiltinRenderProps<LocalFileItem[]>>(
|
@@ -8,7 +8,7 @@ export const LocalFilesApiName = {
|
|
8
8
|
readLocalFile: 'readLocalFile',
|
9
9
|
renameLocalFile: 'renameLocalFile',
|
10
10
|
searchLocalFiles: 'searchLocalFiles',
|
11
|
-
|
11
|
+
writeLocalFile: 'writeLocalFile',
|
12
12
|
};
|
13
13
|
|
14
14
|
export const LocalFilesManifest: BuiltinToolManifest = {
|
@@ -176,26 +176,25 @@ export const LocalFilesManifest: BuiltinToolManifest = {
|
|
176
176
|
type: 'object',
|
177
177
|
},
|
178
178
|
},
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
// },
|
179
|
+
{
|
180
|
+
description:
|
181
|
+
'Write content to a specific file. Input should be the file path and content. Overwrites existing file or creates a new one.',
|
182
|
+
name: LocalFilesApiName.writeLocalFile,
|
183
|
+
parameters: {
|
184
|
+
properties: {
|
185
|
+
content: {
|
186
|
+
description: 'The content to write',
|
187
|
+
type: 'string',
|
188
|
+
},
|
189
|
+
path: {
|
190
|
+
description: 'The file path to write to',
|
191
|
+
type: 'string',
|
192
|
+
},
|
193
|
+
},
|
194
|
+
required: ['path', 'content'],
|
195
|
+
type: 'object',
|
196
|
+
},
|
197
|
+
},
|
199
198
|
],
|
200
199
|
identifier: 'lobe-local-files',
|
201
200
|
meta: {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
export const systemPrompt = `You have a Local
|
1
|
+
export const systemPrompt = `You have a Local System tool with capabilities to interact with the user's local file system. You can list directories, read file contents, search for files, move, and rename files/directories.
|
2
2
|
|
3
3
|
<user_context>
|
4
4
|
Here are some known locations and system details on the user's system. User is using the Operating System: {{platform}}({{arch}}). Use these paths when the user refers to these common locations by name (e.g., "my desktop", "downloads folder").
|
@@ -12,26 +12,20 @@ Here are some known locations and system details on the user's system. User is u
|
|
12
12
|
- App Data: {{userDataPath}} (Use this primarily for plugin-related data or configurations if needed, less for general user files)
|
13
13
|
</user_context>
|
14
14
|
|
15
|
+
<core_capabilities>
|
15
16
|
You have access to a set of tools to interact with the user's local file system:
|
16
17
|
|
17
18
|
1. **listLocalFiles**: Lists files and directories in a specified path.
|
18
19
|
2. **readLocalFile**: Reads the content of a specified file, optionally within a line range.
|
19
|
-
3. **
|
20
|
-
4. **
|
21
|
-
5. **
|
22
|
-
|
23
|
-
<core_capabilities>
|
24
|
-
1. List files and folders in a directory (listFiles)
|
25
|
-
2. Read the content of a specific file (readFile)
|
26
|
-
3. Search for files based on a query and various filter options (searchFiles)
|
27
|
-
4. Rename a file or folder within its current directory (renameFile)
|
28
|
-
5. Move a file or folder to a new location, potentially renaming it (moveFile)
|
29
|
-
6. Write content to a specific file (writeFile) - // TODO: Implement later
|
20
|
+
3. **writeFile**: Write content to a specific file, only support plain text file like \`.text\` or \`.md\`
|
21
|
+
4. **searchLocalFiles**: Searches for files based on keywords and other criteria. Use this tool to find files if the user is unsure about the exact path.
|
22
|
+
5. **renameLocalFile**: Renames a single file or directory in its current location.
|
23
|
+
6. **moveLocalFiles**: Moves multiple files or directories. Can be used for renaming during the move.
|
30
24
|
</core_capabilities>
|
31
25
|
|
32
26
|
<workflow>
|
33
27
|
1. Understand the user's request regarding local files (listing, reading, searching, renaming, moving, writing).
|
34
|
-
2. Select the appropriate tool (listFiles, readFile, searchFiles, renameFile,
|
28
|
+
2. Select the appropriate tool (listFiles, readFile, searchFiles, renameFile, moveLocalFiles, writeFile).
|
35
29
|
3. Execute the file operation. **If the user mentions a common location (like Desktop, Documents, Downloads, etc.) without providing a full path, use the corresponding path from the <user_context> section.**
|
36
30
|
4. Present the results (directory listing, file content, search results) or confirmation of the rename or move operation.
|
37
31
|
</workflow>
|