@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.
Files changed (28) hide show
  1. package/.github/workflows/desktop-pr-build.yml +7 -3
  2. package/CHANGELOG.md +25 -0
  3. package/apps/desktop/package.json +1 -0
  4. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +55 -11
  5. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +153 -0
  6. package/changelog/v1.json +9 -0
  7. package/locales/en-US/tool.json +12 -1
  8. package/locales/zh-CN/tool.json +12 -1
  9. package/package.json +2 -1
  10. package/packages/electron-client-ipc/src/types/localSystem.ts +4 -0
  11. package/scripts/prebuild.mts +15 -5
  12. package/src/app/[variants]/desktopRouter.config.tsx +0 -17
  13. package/src/app/[variants]/mobileRouter.config.tsx +0 -16
  14. package/src/app/[variants]/page.tsx +5 -4
  15. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +23 -4
  16. package/src/locales/default/tool.ts +11 -0
  17. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +5 -6
  18. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +45 -182
  19. package/src/tools/executionRuntimes.ts +3 -0
  20. package/src/tools/local-system/ExecutionRuntime/index.ts +407 -0
  21. package/src/tools/local-system/Intervention/EditLocalFile/index.tsx +89 -0
  22. package/src/tools/local-system/Intervention/WriteFile/index.tsx +72 -0
  23. package/src/tools/local-system/Intervention/index.ts +4 -0
  24. package/src/tools/local-system/Render/EditLocalFile/index.tsx +67 -0
  25. package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +53 -78
  26. package/src/tools/local-system/Render/index.ts +2 -0
  27. package/src/tools/local-system/index.ts +1 -0
  28. 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 = { foo: 'bar' };
53
+ const mockContent = 'result content';
54
54
  const mockState = { state: 'test' };
55
- const mockService = vi.fn().mockResolvedValue({ content: mockContent, state: mockState });
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: <T = any>(
23
+ internal_triggerLocalFileToolCalling: (
39
24
  id: string,
40
- callingService: () => Promise<{ content: any; state?: T }>,
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<EditLocalFileState>(id, async () => {
76
- const result = await localFileService.editLocalFile(params);
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
- const result = await localFileService.writeFile(params);
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<LocalMoveFilesState>(id, async () => {
109
- const results: LocalMoveFilesResultItem[] = await localFileService.moveLocalFiles(params);
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<LocalRenameFileState>(id, async () => {
135
- const { path: currentPath, newName } = params;
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<GrepContentState>(id, async () => {
180
- const result = await localFileService.grepContent(params);
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<GlobFilesState>(id, async () => {
194
- const result = await localFileService.globFiles(params);
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<LocalFileSearchState>(id, async () => {
206
- const result = await localFileService.searchLocalFiles(params);
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<LocalFileListState>(id, async () => {
214
- const result = await localFileService.listLocalFiles(params);
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<LocalReadFileState>(id, async () => {
230
- const result = await localFileService.readLocalFile(params);
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<LocalReadFilesState>(id, async () => {
238
- const results = await localFileService.readLocalFiles(params);
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<RunCommandState>(id, async () => {
247
- const result = await localFileService.runCommand(params);
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<KillCommandState>(id, async () => {
268
- const result = await localFileService.killCommand(params);
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<GetCommandOutputState>(id, async () => {
281
- const result = await localFileService.getCommandOutput(params);
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
- if (state) {
310
- await get().optimisticUpdatePluginState(id, state as any);
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
  };