@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.
- package/CHANGELOG.md +50 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +121 -20
- package/changelog/v1.json +18 -0
- package/package.json +2 -1
- package/packages/electron-client-ipc/src/events/localFile.ts +2 -0
- package/packages/electron-client-ipc/src/types/localFile.ts +16 -0
- 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/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +9 -3
- package/src/libs/agent-runtime/utils/streams/qwen.test.ts +8 -4
- package/src/libs/agent-runtime/utils/streams/qwen.ts +3 -1
- package/src/libs/agent-runtime/utils/streams/spark.test.ts +6 -2
- package/src/libs/agent-runtime/utils/streams/spark.ts +3 -1
- package/src/services/electron/localFileService.ts +5 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localFile.test.ts +211 -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/tools/local-files/Render/ListFiles/index.tsx +2 -45
- package/src/tools/local-files/Render/RenameLocalFile/index.tsx +5 -6
- 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 +2 -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.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
|
+
[](#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
|
+
[](#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>**
|
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>**
|
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
|
-
|
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
|
}
|
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.
|
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
|
-
|
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
|
+
};
|