@lobehub/chat 1.84.16 → 1.84.18

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 (29) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +121 -20
  5. package/changelog/v1.json +18 -0
  6. package/package.json +2 -1
  7. package/packages/electron-client-ipc/src/events/localFile.ts +2 -0
  8. package/packages/electron-client-ipc/src/types/localFile.ts +16 -0
  9. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +2 -1
  10. package/src/features/{Conversation/components/MarkdownElements/LocalFile/Render → LocalFile}/LocalFile.tsx +8 -10
  11. package/src/features/LocalFile/LocalFolder.tsx +65 -0
  12. package/src/features/LocalFile/index.tsx +2 -0
  13. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +9 -3
  14. package/src/libs/agent-runtime/utils/streams/qwen.test.ts +8 -4
  15. package/src/libs/agent-runtime/utils/streams/qwen.ts +3 -1
  16. package/src/libs/agent-runtime/utils/streams/spark.test.ts +6 -2
  17. package/src/libs/agent-runtime/utils/streams/spark.ts +3 -1
  18. package/src/services/electron/localFileService.ts +5 -0
  19. package/src/store/chat/slices/builtinTool/actions/__tests__/localFile.test.ts +211 -0
  20. package/src/store/chat/slices/builtinTool/actions/localFile.ts +27 -3
  21. package/src/store/electron/selectors/desktopState.ts +7 -0
  22. package/src/store/electron/selectors/index.ts +1 -0
  23. package/src/tools/local-files/Render/ListFiles/index.tsx +2 -45
  24. package/src/tools/local-files/Render/RenameLocalFile/index.tsx +5 -6
  25. package/src/tools/local-files/Render/RunCommand/index.tsx +35 -0
  26. package/src/tools/local-files/Render/WriteFile/index.tsx +32 -0
  27. package/src/tools/local-files/Render/index.tsx +2 -0
  28. package/src/tools/local-files/index.ts +20 -21
  29. 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.18](https://github.com/lobehub/lobe-chat/compare/v1.84.17...v1.84.18)
6
+
7
+ <sup>Released on **2025-05-03**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Add perf stat support for openai factory.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Add perf stat support for openai factory, closes [#7677](https://github.com/lobehub/lobe-chat/issues/7677) ([40464d1](https://github.com/lobehub/lobe-chat/commit/40464d1))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.84.17](https://github.com/lobehub/lobe-chat/compare/v1.84.16...v1.84.17)
31
+
32
+ <sup>Released on **2025-05-03**</sup>
33
+
34
+ #### 💄 Styles
35
+
36
+ - **misc**: Add write file tool to local-file plugin.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Styles
44
+
45
+ - **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))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.84.16](https://github.com/lobehub/lobe-chat/compare/v1.84.15...v1.84.16)
6
56
 
7
57
  <sup>Released on **2025-05-02**</sup>
package/README.md CHANGED
@@ -330,10 +330,10 @@ In addition, these plugins are not limited to news aggregation, but can also ext
330
330
  | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
331
331
  | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-03-23**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
332
332
  | [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
333
- | [MintbaseSearch](https://lobechat.com/discover/plugin/mintbasesearch)<br/><sup>By **mintbase** on **2024-12-31**</sup> | Find any NFT data on the NEAR Protocol.<br/>`crypto` `nft` |
334
333
  | [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
334
+ | [Google CSE](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | Searches Google through their official CSE API.<br/>`web` `search` |
335
335
 
336
- > 📊 Total plugins: [<kbd>**45**</kbd>](https://lobechat.com/discover/plugins)
336
+ > 📊 Total plugins: [<kbd>**44**</kbd>](https://lobechat.com/discover/plugins)
337
337
 
338
338
  <!-- PLUGIN LIST -->
339
339
 
package/README.zh-CN.md CHANGED
@@ -323,10 +323,10 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
323
323
  | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
324
324
  | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-03-23**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
325
325
  | [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
326
- | [MintbaseSearch](https://lobechat.com/discover/plugin/mintbasesearch)<br/><sup>By **mintbase** on **2024-12-31**</sup> | 在 NEAR 协议上查找任何 NFT 数据。<br/>`加密货币` `nft` |
327
326
  | [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
327
+ | [谷歌自定义搜索引擎](https://lobechat.com/discover/plugin/google-cse)<br/><sup>By **vsnthdev** on **2024-12-02**</sup> | 通过他们的官方自定义搜索引擎 API 搜索谷歌。<br/>`网络` `搜索` |
328
328
 
329
- > 📊 Total plugins: [<kbd>**45**</kbd>](https://lobechat.com/discover/plugins)
329
+ > 📊 Total plugins: [<kbd>**44**</kbd>](https://lobechat.com/discover/plugins)
330
330
 
331
331
  <!-- PLUGIN LIST -->
332
332
 
@@ -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
- return this.searchService.search(params.keywords, options);
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
- console.error(`Failed to open file ${filePath}:`, error);
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
- console.error(`Failed to open folder for path ${targetPath}:`, error);
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
- try {
94
- const effectiveLoc = loc ?? [0, 200];
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
- console.error(`Stat failed for ${filePath} after loadFile:`, statError);
172
+ logger.error(`Failed to get file status ${filePath}:`, statError);
140
173
  }
141
174
 
142
175
  return result;
143
176
  } catch (error) {
144
- console.error(`Error processing file ${filePath}:`, error);
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
- console.error(`Failed to stat ${fullPath}:`, statError);
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
- console.error(`Failed to list directory ${dirPath}:`, error);
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
- console.warn('moveLocalFiles called with empty items array.');
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
- console.log(`Skipping move: source and target path are identical: ${sourcePath}`);
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
- console.log(`Successfully moved ${sourcePath} to ${newPath}`);
318
+ logger.info(`${logPrefix} Move successful`);
268
319
  } catch (error) {
269
- console.error(`Error moving ${sourcePath} to ${newPath}:`, error);
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
- console.log(
338
- `Skipping rename: oldPath and calculated newPath are identical: ${currentPath}`,
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
- console.error(`Error calculating new path for rename ${currentPath} to ${newName}:`, error);
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
- console.log(`Successfully renamed ${currentPath} to ${newPath}`);
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
- console.error(`Error renaming ${currentPath} to ${newPath}:`, error);
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
  }
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add perf stat support for openai factory."
6
+ ]
7
+ },
8
+ "date": "2025-05-03",
9
+ "version": "1.84.18"
10
+ },
11
+ {
12
+ "children": {
13
+ "improvements": [
14
+ "Add write file tool to local-file plugin."
15
+ ]
16
+ },
17
+ "date": "2025-05-03",
18
+ "version": "1.84.17"
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.16",
3
+ "version": "1.84.18",
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",
@@ -213,6 +213,7 @@
213
213
  "openai": "^4.91.1",
214
214
  "openapi-fetch": "^0.9.8",
215
215
  "partial-json": "^0.1.7",
216
+ "path-browserify-esm": "^1.0.6",
216
217
  "pdf-parse": "^1.1.1",
217
218
  "pdfjs-dist": "4.8.69",
218
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.
@@ -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
- const LocalFile = ({
35
- name,
36
- path,
37
- isDirectory,
38
- }: {
39
- isDirectory: boolean;
34
+ interface LocalFileProps {
35
+ isDirectory?: boolean;
40
36
  name: string;
41
- path: string;
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
+ };
@@ -0,0 +1,2 @@
1
+ export { LocalFile } from './LocalFile';
2
+ export { LocalFolder } from './LocalFolder';