@lobehub/lobehub 2.0.0-next.73 → 2.0.0-next.74
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/.github/workflows/desktop-pr-build.yml +7 -3
- package/CHANGELOG.md +25 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +55 -11
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +153 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/tool.json +12 -1
- package/locales/zh-CN/tool.json +12 -1
- package/package.json +2 -1
- package/packages/electron-client-ipc/src/types/localSystem.ts +4 -0
- package/scripts/prebuild.mts +15 -5
- package/src/app/[variants]/desktopRouter.config.tsx +0 -17
- package/src/app/[variants]/mobileRouter.config.tsx +0 -16
- package/src/app/[variants]/page.tsx +5 -4
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +23 -4
- package/src/locales/default/tool.ts +11 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +5 -6
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +45 -182
- package/src/tools/executionRuntimes.ts +3 -0
- package/src/tools/local-system/ExecutionRuntime/index.ts +407 -0
- package/src/tools/local-system/Intervention/EditLocalFile/index.tsx +89 -0
- package/src/tools/local-system/Intervention/WriteFile/index.tsx +72 -0
- package/src/tools/local-system/Intervention/index.ts +4 -0
- package/src/tools/local-system/Render/EditLocalFile/index.tsx +67 -0
- package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +53 -78
- package/src/tools/local-system/Render/index.ts +2 -0
- package/src/tools/local-system/index.ts +1 -0
- package/src/tools/local-system/type.ts +4 -3
|
@@ -50,18 +50,17 @@ describe('localFileSlice', () => {
|
|
|
50
50
|
|
|
51
51
|
describe('internal_triggerLocalFileToolCalling', () => {
|
|
52
52
|
it('should handle successful calling', async () => {
|
|
53
|
-
const mockContent =
|
|
53
|
+
const mockContent = 'result content';
|
|
54
54
|
const mockState = { state: 'test' };
|
|
55
|
-
const mockService = vi
|
|
55
|
+
const mockService = vi
|
|
56
|
+
.fn()
|
|
57
|
+
.mockResolvedValue({ content: mockContent, state: mockState, success: true });
|
|
56
58
|
|
|
57
59
|
await store.internal_triggerLocalFileToolCalling('test-id', mockService);
|
|
58
60
|
|
|
59
61
|
expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
|
|
60
62
|
expect(mockStore.optimisticUpdatePluginState).toBeCalledWith('test-id', mockState);
|
|
61
|
-
expect(mockStore.optimisticUpdateMessageContent).toBeCalledWith(
|
|
62
|
-
'test-id',
|
|
63
|
-
JSON.stringify(mockContent),
|
|
64
|
-
);
|
|
63
|
+
expect(mockStore.optimisticUpdateMessageContent).toBeCalledWith('test-id', mockContent);
|
|
65
64
|
expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', false);
|
|
66
65
|
});
|
|
67
66
|
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
GrepContentParams,
|
|
6
6
|
KillCommandParams,
|
|
7
7
|
ListLocalFileParams,
|
|
8
|
-
LocalMoveFilesResultItem,
|
|
9
8
|
LocalReadFileParams,
|
|
10
9
|
LocalReadFilesParams,
|
|
11
10
|
LocalSearchFilesParams,
|
|
@@ -16,28 +15,14 @@ import {
|
|
|
16
15
|
} from '@lobechat/electron-client-ipc';
|
|
17
16
|
import { StateCreator } from 'zustand/vanilla';
|
|
18
17
|
|
|
19
|
-
import { localFileService } from '@/services/electron/localFileService';
|
|
20
18
|
import { ChatStore } from '@/store/chat/store';
|
|
21
|
-
import {
|
|
22
|
-
EditLocalFileState,
|
|
23
|
-
GetCommandOutputState,
|
|
24
|
-
GlobFilesState,
|
|
25
|
-
GrepContentState,
|
|
26
|
-
KillCommandState,
|
|
27
|
-
LocalFileListState,
|
|
28
|
-
LocalFileSearchState,
|
|
29
|
-
LocalMoveFilesState,
|
|
30
|
-
LocalReadFileState,
|
|
31
|
-
LocalReadFilesState,
|
|
32
|
-
LocalRenameFileState,
|
|
33
|
-
RunCommandState,
|
|
34
|
-
} from '@/tools/local-system/type';
|
|
19
|
+
import { LocalSystemExecutionRuntime } from '@/tools/local-system/ExecutionRuntime';
|
|
35
20
|
|
|
36
21
|
/* eslint-disable typescript-sort-keys/interface */
|
|
37
22
|
export interface LocalFileAction {
|
|
38
|
-
internal_triggerLocalFileToolCalling:
|
|
23
|
+
internal_triggerLocalFileToolCalling: (
|
|
39
24
|
id: string,
|
|
40
|
-
callingService: () => Promise<{ content: any; state?:
|
|
25
|
+
callingService: () => Promise<{ content: string; error?: any; state?: any; success: boolean }>,
|
|
41
26
|
) => Promise<boolean>;
|
|
42
27
|
|
|
43
28
|
// File Operations
|
|
@@ -63,6 +48,8 @@ export interface LocalFileAction {
|
|
|
63
48
|
}
|
|
64
49
|
/* eslint-enable typescript-sort-keys/interface */
|
|
65
50
|
|
|
51
|
+
const runtime = new LocalSystemExecutionRuntime();
|
|
52
|
+
|
|
66
53
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
67
54
|
export const localSystemSlice: StateCreator<
|
|
68
55
|
ChatStore,
|
|
@@ -72,148 +59,49 @@ export const localSystemSlice: StateCreator<
|
|
|
72
59
|
> = (set, get) => ({
|
|
73
60
|
// ==================== File Editing ====================
|
|
74
61
|
editLocalFile: async (id, params) => {
|
|
75
|
-
return get().internal_triggerLocalFileToolCalling
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const message = result.success
|
|
79
|
-
? `Successfully replaced ${result.replacements} occurrence(s) in ${params.file_path}`
|
|
80
|
-
: `Edit failed: ${result.error}`;
|
|
81
|
-
|
|
82
|
-
const state: EditLocalFileState = { message, result };
|
|
83
|
-
|
|
84
|
-
return { content: result, state };
|
|
62
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
63
|
+
return await runtime.editLocalFile(params);
|
|
85
64
|
});
|
|
86
65
|
},
|
|
87
66
|
|
|
88
67
|
writeLocalFile: async (id, params) => {
|
|
89
68
|
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let content: { message: string; success: boolean };
|
|
93
|
-
|
|
94
|
-
if (result.success) {
|
|
95
|
-
content = {
|
|
96
|
-
message: `成功写入文件 ${params.path}`,
|
|
97
|
-
success: true,
|
|
98
|
-
};
|
|
99
|
-
} else {
|
|
100
|
-
const errorMessage = result.error;
|
|
101
|
-
|
|
102
|
-
content = { message: errorMessage || '写入文件失败', success: false };
|
|
103
|
-
}
|
|
104
|
-
return { content };
|
|
69
|
+
return await runtime.writeLocalFile(params);
|
|
105
70
|
});
|
|
106
71
|
},
|
|
107
72
|
moveLocalFiles: async (id, params) => {
|
|
108
|
-
return get().internal_triggerLocalFileToolCalling
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// 检查所有文件是否成功移动以更新消息内容
|
|
112
|
-
const allSucceeded = results.every((r) => r.success);
|
|
113
|
-
const someFailed = results.some((r) => !r.success);
|
|
114
|
-
const successCount = results.filter((r) => r.success).length;
|
|
115
|
-
const failedCount = results.length - successCount;
|
|
116
|
-
|
|
117
|
-
let message = '';
|
|
118
|
-
|
|
119
|
-
if (allSucceeded) {
|
|
120
|
-
message = `Successfully moved ${results.length} item(s).`;
|
|
121
|
-
} else if (someFailed) {
|
|
122
|
-
message = `Moved ${successCount} item(s) successfully. Failed to move ${failedCount} item(s).`;
|
|
123
|
-
} else {
|
|
124
|
-
// 所有都失败了?
|
|
125
|
-
message = `Failed to move all ${results.length} item(s).`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const state: LocalMoveFilesState = { results, successCount, totalCount: results.length };
|
|
129
|
-
|
|
130
|
-
return { content: { message, results }, state };
|
|
73
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
74
|
+
return await runtime.moveLocalFiles(params);
|
|
131
75
|
});
|
|
132
76
|
},
|
|
133
77
|
renameLocalFile: async (id, params) => {
|
|
134
|
-
return get().internal_triggerLocalFileToolCalling
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Basic validation for newName (can be done here or backend, maybe better in backend)
|
|
138
|
-
if (
|
|
139
|
-
!newName ||
|
|
140
|
-
newName.includes('/') ||
|
|
141
|
-
newName.includes('\\') ||
|
|
142
|
-
newName === '.' ||
|
|
143
|
-
newName === '..' ||
|
|
144
|
-
/["*/:<>?\\|]/.test(newName)
|
|
145
|
-
) {
|
|
146
|
-
throw new Error(
|
|
147
|
-
'Invalid new name provided. It cannot be empty, contain path separators, or invalid characters.',
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const result = await localFileService.renameLocalFile({ newName, path: currentPath }); // Call the specific service
|
|
152
|
-
|
|
153
|
-
let state: LocalRenameFileState;
|
|
154
|
-
let content: { message: string; success: boolean };
|
|
155
|
-
|
|
156
|
-
if (result.success) {
|
|
157
|
-
state = { newPath: result.newPath!, oldPath: currentPath, success: true };
|
|
158
|
-
// Simplified message
|
|
159
|
-
content = {
|
|
160
|
-
message: `Successfully renamed file ${currentPath} to ${newName}.`,
|
|
161
|
-
success: true,
|
|
162
|
-
};
|
|
163
|
-
} else {
|
|
164
|
-
const errorMessage = result.error;
|
|
165
|
-
state = {
|
|
166
|
-
error: errorMessage,
|
|
167
|
-
newPath: '',
|
|
168
|
-
oldPath: params.path,
|
|
169
|
-
success: false,
|
|
170
|
-
};
|
|
171
|
-
content = { message: errorMessage, success: false };
|
|
172
|
-
}
|
|
173
|
-
return { content, state };
|
|
78
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
79
|
+
return await runtime.renameLocalFile(params);
|
|
174
80
|
});
|
|
175
81
|
},
|
|
176
82
|
|
|
177
83
|
// ==================== Search & Find ====================
|
|
178
84
|
grepContent: async (id, params) => {
|
|
179
|
-
return get().internal_triggerLocalFileToolCalling
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const message = result.success
|
|
183
|
-
? `Found ${result.total_matches} matches in ${result.matches.length} locations`
|
|
184
|
-
: 'Search failed';
|
|
185
|
-
|
|
186
|
-
const state: GrepContentState = { message, result };
|
|
187
|
-
|
|
188
|
-
return { content: result, state };
|
|
85
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
86
|
+
return await runtime.grepContent(params);
|
|
189
87
|
});
|
|
190
88
|
},
|
|
191
89
|
|
|
192
90
|
globLocalFiles: async (id, params) => {
|
|
193
|
-
return get().internal_triggerLocalFileToolCalling
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const message = result.success ? `Found ${result.total_files} files` : 'Glob search failed';
|
|
197
|
-
|
|
198
|
-
const state: GlobFilesState = { message, result };
|
|
199
|
-
|
|
200
|
-
return { content: result, state };
|
|
91
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
92
|
+
return await runtime.globLocalFiles(params);
|
|
201
93
|
});
|
|
202
94
|
},
|
|
203
95
|
|
|
204
96
|
searchLocalFiles: async (id, params) => {
|
|
205
|
-
return get().internal_triggerLocalFileToolCalling
|
|
206
|
-
|
|
207
|
-
const state: LocalFileSearchState = { searchResults: result };
|
|
208
|
-
return { content: result, state };
|
|
97
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
98
|
+
return await runtime.searchLocalFiles(params);
|
|
209
99
|
});
|
|
210
100
|
},
|
|
211
101
|
|
|
212
102
|
listLocalFiles: async (id, params) => {
|
|
213
|
-
return get().internal_triggerLocalFileToolCalling
|
|
214
|
-
|
|
215
|
-
const state: LocalFileListState = { listResults: result };
|
|
216
|
-
return { content: result, state };
|
|
103
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
104
|
+
return await runtime.listLocalFiles(params);
|
|
217
105
|
});
|
|
218
106
|
},
|
|
219
107
|
|
|
@@ -226,67 +114,31 @@ export const localSystemSlice: StateCreator<
|
|
|
226
114
|
},
|
|
227
115
|
|
|
228
116
|
readLocalFile: async (id, params) => {
|
|
229
|
-
return get().internal_triggerLocalFileToolCalling
|
|
230
|
-
|
|
231
|
-
const state: LocalReadFileState = { fileContent: result };
|
|
232
|
-
return { content: result, state };
|
|
117
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
118
|
+
return await runtime.readLocalFile(params);
|
|
233
119
|
});
|
|
234
120
|
},
|
|
235
121
|
|
|
236
122
|
readLocalFiles: async (id, params) => {
|
|
237
|
-
return get().internal_triggerLocalFileToolCalling
|
|
238
|
-
|
|
239
|
-
const state: LocalReadFilesState = { filesContent: results };
|
|
240
|
-
return { content: results, state };
|
|
123
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
124
|
+
return await runtime.readLocalFiles(params);
|
|
241
125
|
});
|
|
242
126
|
},
|
|
243
127
|
|
|
244
128
|
// ==================== Shell Commands ====================
|
|
245
129
|
runCommand: async (id, params) => {
|
|
246
|
-
return get().internal_triggerLocalFileToolCalling
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
let message: string;
|
|
250
|
-
|
|
251
|
-
if (result.success) {
|
|
252
|
-
if (result.shell_id) {
|
|
253
|
-
message = `Command started in background with shell_id: ${result.shell_id}`;
|
|
254
|
-
} else {
|
|
255
|
-
message = `Command completed successfully.`;
|
|
256
|
-
}
|
|
257
|
-
} else {
|
|
258
|
-
message = `Command failed: ${result.error}`;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const state: RunCommandState = { message, result };
|
|
262
|
-
|
|
263
|
-
return { content: result, state };
|
|
130
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
131
|
+
return await runtime.runCommand(params);
|
|
264
132
|
});
|
|
265
133
|
},
|
|
266
134
|
killCommand: async (id, params) => {
|
|
267
|
-
return get().internal_triggerLocalFileToolCalling
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const message = result.success
|
|
271
|
-
? `Successfully killed shell: ${params.shell_id}`
|
|
272
|
-
: `Failed to kill shell: ${result.error}`;
|
|
273
|
-
|
|
274
|
-
const state: KillCommandState = { message, result };
|
|
275
|
-
|
|
276
|
-
return { content: result, state };
|
|
135
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
136
|
+
return await runtime.killCommand(params);
|
|
277
137
|
});
|
|
278
138
|
},
|
|
279
139
|
getCommandOutput: async (id, params) => {
|
|
280
|
-
return get().internal_triggerLocalFileToolCalling
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const message = result.success
|
|
284
|
-
? `Output retrieved. Running: ${result.running}`
|
|
285
|
-
: `Failed: ${result.error}`;
|
|
286
|
-
|
|
287
|
-
const state: GetCommandOutputState = { message, result };
|
|
288
|
-
|
|
289
|
-
return { content: result, state };
|
|
140
|
+
return get().internal_triggerLocalFileToolCalling(id, async () => {
|
|
141
|
+
return await runtime.getCommandOutput(params);
|
|
290
142
|
});
|
|
291
143
|
},
|
|
292
144
|
|
|
@@ -305,11 +157,22 @@ export const localSystemSlice: StateCreator<
|
|
|
305
157
|
internal_triggerLocalFileToolCalling: async (id, callingService) => {
|
|
306
158
|
get().toggleLocalFileLoading(id, true);
|
|
307
159
|
try {
|
|
308
|
-
const { state, content } = await callingService();
|
|
309
|
-
|
|
310
|
-
|
|
160
|
+
const { state, content, success, error } = await callingService();
|
|
161
|
+
|
|
162
|
+
if (success) {
|
|
163
|
+
if (state) {
|
|
164
|
+
await get().optimisticUpdatePluginState(id, state);
|
|
165
|
+
}
|
|
166
|
+
await get().optimisticUpdateMessageContent(id, content);
|
|
167
|
+
} else {
|
|
168
|
+
await get().optimisticUpdateMessagePluginError(id, {
|
|
169
|
+
body: error,
|
|
170
|
+
message: error?.message || 'Operation failed',
|
|
171
|
+
type: 'PluginServerError',
|
|
172
|
+
});
|
|
173
|
+
// Still update content even if failed, to show error message
|
|
174
|
+
await get().optimisticUpdateMessageContent(id, content);
|
|
311
175
|
}
|
|
312
|
-
await get().optimisticUpdateMessageContent(id, JSON.stringify(content));
|
|
313
176
|
} catch (error) {
|
|
314
177
|
await get().optimisticUpdateMessagePluginError(id, {
|
|
315
178
|
body: error,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { LocalSystemManifest } from './local-system';
|
|
2
|
+
import { LocalSystemExecutionRuntime } from './local-system/ExecutionRuntime';
|
|
1
3
|
import { WebBrowsingManifest } from './web-browsing';
|
|
2
4
|
import { WebBrowsingExecutionRuntime } from './web-browsing/ExecutionRuntime';
|
|
3
5
|
|
|
4
6
|
export const BuiltinToolServerRuntimes: Record<string, any> = {
|
|
7
|
+
[LocalSystemManifest.identifier]: LocalSystemExecutionRuntime,
|
|
5
8
|
[WebBrowsingManifest.identifier]: WebBrowsingExecutionRuntime,
|
|
6
9
|
};
|