@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
@@ -68,7 +68,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
|
|
68
68
|
) => OpenAI.ChatCompletionCreateParamsStreaming;
|
69
69
|
handleStream?: (
|
70
70
|
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
71
|
-
callbacks?: ChatStreamCallbacks,
|
71
|
+
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number },
|
72
72
|
) => ReadableStream;
|
73
73
|
handleStreamBizErrorType?: (error: {
|
74
74
|
message: string;
|
@@ -256,7 +256,10 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
256
256
|
|
257
257
|
return StreamingResponse(
|
258
258
|
chatCompletion?.handleStream
|
259
|
-
? chatCompletion.handleStream(prod,
|
259
|
+
? chatCompletion.handleStream(prod, {
|
260
|
+
callbacks: streamOptions.callbacks,
|
261
|
+
inputStartAt,
|
262
|
+
})
|
260
263
|
: OpenAIStream(prod, { ...streamOptions, inputStartAt }),
|
261
264
|
{
|
262
265
|
headers: options?.headers,
|
@@ -276,7 +279,10 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
276
279
|
|
277
280
|
return StreamingResponse(
|
278
281
|
chatCompletion?.handleStream
|
279
|
-
? chatCompletion.handleStream(stream,
|
282
|
+
? chatCompletion.handleStream(stream, {
|
283
|
+
callbacks: streamOptions.callbacks,
|
284
|
+
inputStartAt,
|
285
|
+
})
|
280
286
|
: OpenAIStream(stream, { ...streamOptions, inputStartAt }),
|
281
287
|
{
|
282
288
|
headers: options?.headers,
|
@@ -46,9 +46,11 @@ describe('QwenAIStream', () => {
|
|
46
46
|
const onCompletionMock = vi.fn();
|
47
47
|
|
48
48
|
const protocolStream = QwenAIStream(mockOpenAIStream, {
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
callbacks: {
|
50
|
+
onStart: onStartMock,
|
51
|
+
onText: onTextMock,
|
52
|
+
onCompletion: onCompletionMock,
|
53
|
+
},
|
52
54
|
});
|
53
55
|
|
54
56
|
const decoder = new TextDecoder();
|
@@ -111,7 +113,9 @@ describe('QwenAIStream', () => {
|
|
111
113
|
const onToolCallMock = vi.fn();
|
112
114
|
|
113
115
|
const protocolStream = QwenAIStream(mockOpenAIStream, {
|
114
|
-
|
116
|
+
callbacks: {
|
117
|
+
onToolsCalling: onToolCallMock,
|
118
|
+
},
|
115
119
|
});
|
116
120
|
|
117
121
|
const decoder = new TextDecoder();
|
@@ -92,7 +92,9 @@ export const transformQwenStream = (chunk: OpenAI.ChatCompletionChunk): StreamPr
|
|
92
92
|
|
93
93
|
export const QwenAIStream = (
|
94
94
|
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
95
|
-
|
95
|
+
// TODO: preserve for RFC 097
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
97
|
+
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
|
96
98
|
) => {
|
97
99
|
const readableStream =
|
98
100
|
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
@@ -96,7 +96,9 @@ describe('SparkAIStream', () => {
|
|
96
96
|
const onToolCallMock = vi.fn();
|
97
97
|
|
98
98
|
const protocolStream = SparkAIStream(mockStream, {
|
99
|
-
|
99
|
+
callbacks: {
|
100
|
+
onToolsCalling: onToolCallMock,
|
101
|
+
},
|
100
102
|
});
|
101
103
|
|
102
104
|
const decoder = new TextDecoder();
|
@@ -156,7 +158,9 @@ describe('SparkAIStream', () => {
|
|
156
158
|
const onTextMock = vi.fn();
|
157
159
|
|
158
160
|
const protocolStream = SparkAIStream(mockStream, {
|
159
|
-
|
161
|
+
callbacks: {
|
162
|
+
onText: onTextMock,
|
163
|
+
},
|
160
164
|
});
|
161
165
|
|
162
166
|
const decoder = new TextDecoder();
|
@@ -123,7 +123,9 @@ export const transformSparkStream = (chunk: OpenAI.ChatCompletionChunk): StreamP
|
|
123
123
|
|
124
124
|
export const SparkAIStream = (
|
125
125
|
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
126
|
-
|
126
|
+
// TODO: preserve for RFC 097
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
128
|
+
{ callbacks, inputStartAt }: { callbacks?: ChatStreamCallbacks; inputStartAt?: number } = {},
|
127
129
|
) => {
|
128
130
|
const readableStream =
|
129
131
|
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
@@ -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 });
|
@@ -0,0 +1,211 @@
|
|
1
|
+
import { LocalFileItem, LocalMoveFilesResultItem } from '@lobechat/electron-client-ipc';
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { localFileService } from '@/services/electron/localFileService';
|
5
|
+
import { ChatStore } from '@/store/chat/store';
|
6
|
+
|
7
|
+
import { localFileSlice } from '../localFile';
|
8
|
+
|
9
|
+
vi.mock('@/services/electron/localFileService', () => ({
|
10
|
+
localFileService: {
|
11
|
+
listLocalFiles: vi.fn(),
|
12
|
+
moveLocalFiles: vi.fn(),
|
13
|
+
readLocalFile: vi.fn(),
|
14
|
+
readLocalFiles: vi.fn(),
|
15
|
+
renameLocalFile: vi.fn(),
|
16
|
+
searchLocalFiles: vi.fn(),
|
17
|
+
writeFile: vi.fn(),
|
18
|
+
},
|
19
|
+
}));
|
20
|
+
|
21
|
+
const mockSet = vi.fn();
|
22
|
+
|
23
|
+
const mockStore = {
|
24
|
+
internal_triggerLocalFileToolCalling: vi.fn(),
|
25
|
+
internal_updateMessageContent: vi.fn(),
|
26
|
+
internal_updateMessagePluginError: vi.fn(),
|
27
|
+
set: mockSet,
|
28
|
+
toggleLocalFileLoading: vi.fn(),
|
29
|
+
updatePluginArguments: vi.fn(),
|
30
|
+
updatePluginState: vi.fn(),
|
31
|
+
} as unknown as ChatStore;
|
32
|
+
|
33
|
+
const createStore = () => {
|
34
|
+
return localFileSlice(
|
35
|
+
(set) => ({
|
36
|
+
...mockStore,
|
37
|
+
set,
|
38
|
+
}),
|
39
|
+
() => mockStore,
|
40
|
+
{} as any,
|
41
|
+
);
|
42
|
+
};
|
43
|
+
|
44
|
+
describe('localFileSlice', () => {
|
45
|
+
const store = createStore();
|
46
|
+
|
47
|
+
beforeEach(() => {
|
48
|
+
vi.clearAllMocks();
|
49
|
+
});
|
50
|
+
|
51
|
+
describe('internal_triggerLocalFileToolCalling', () => {
|
52
|
+
it('should handle successful calling', async () => {
|
53
|
+
const mockContent = { foo: 'bar' };
|
54
|
+
const mockState = { state: 'test' };
|
55
|
+
const mockService = vi.fn().mockResolvedValue({ content: mockContent, state: mockState });
|
56
|
+
|
57
|
+
await store.internal_triggerLocalFileToolCalling('test-id', mockService);
|
58
|
+
|
59
|
+
expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
|
60
|
+
expect(mockStore.updatePluginState).toBeCalledWith('test-id', mockState);
|
61
|
+
expect(mockStore.internal_updateMessageContent).toBeCalledWith(
|
62
|
+
'test-id',
|
63
|
+
JSON.stringify(mockContent),
|
64
|
+
);
|
65
|
+
expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', false);
|
66
|
+
});
|
67
|
+
|
68
|
+
it('should handle error', async () => {
|
69
|
+
const mockError = new Error('test error');
|
70
|
+
const mockService = vi.fn().mockRejectedValue(mockError);
|
71
|
+
|
72
|
+
await store.internal_triggerLocalFileToolCalling('test-id', mockService);
|
73
|
+
|
74
|
+
expect(mockStore.internal_updateMessagePluginError).toBeCalledWith('test-id', {
|
75
|
+
body: mockError,
|
76
|
+
message: 'test error',
|
77
|
+
type: 'PluginServerError',
|
78
|
+
});
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('listLocalFiles', () => {
|
83
|
+
it('should call listLocalFiles service and update state', async () => {
|
84
|
+
const mockResult: LocalFileItem[] = [
|
85
|
+
{
|
86
|
+
name: 'test.txt',
|
87
|
+
path: '/test.txt',
|
88
|
+
isDirectory: false,
|
89
|
+
createdTime: new Date(),
|
90
|
+
lastAccessTime: new Date(),
|
91
|
+
modifiedTime: new Date(),
|
92
|
+
size: 100,
|
93
|
+
type: 'file',
|
94
|
+
},
|
95
|
+
];
|
96
|
+
vi.mocked(localFileService.listLocalFiles).mockResolvedValue(mockResult);
|
97
|
+
|
98
|
+
await store.listLocalFiles('test-id', { path: '/test' });
|
99
|
+
|
100
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
101
|
+
});
|
102
|
+
});
|
103
|
+
|
104
|
+
describe('moveLocalFiles', () => {
|
105
|
+
it('should handle successful move', async () => {
|
106
|
+
const mockResults = [
|
107
|
+
{
|
108
|
+
sourcePath: '/test.txt',
|
109
|
+
destinationPath: '/target/test.txt',
|
110
|
+
success: true,
|
111
|
+
},
|
112
|
+
] as unknown as LocalMoveFilesResultItem[];
|
113
|
+
|
114
|
+
vi.mocked(localFileService.moveLocalFiles).mockResolvedValue(mockResults);
|
115
|
+
|
116
|
+
await store.moveLocalFiles('test-id', {
|
117
|
+
sourcePaths: ['/test.txt'],
|
118
|
+
destinationDir: '/target',
|
119
|
+
} as any);
|
120
|
+
|
121
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
describe('writeLocalFile', () => {
|
126
|
+
it('should handle successful write', async () => {
|
127
|
+
vi.mocked(localFileService.writeFile).mockResolvedValue({
|
128
|
+
success: true,
|
129
|
+
newPath: '/test.txt',
|
130
|
+
});
|
131
|
+
|
132
|
+
await store.writeLocalFile('test-id', { path: '/test.txt', content: 'test' });
|
133
|
+
|
134
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
135
|
+
});
|
136
|
+
|
137
|
+
it('should handle write error', async () => {
|
138
|
+
vi.mocked(localFileService.writeFile).mockResolvedValue({
|
139
|
+
success: false,
|
140
|
+
error: 'Write failed',
|
141
|
+
newPath: '/test.txt',
|
142
|
+
});
|
143
|
+
|
144
|
+
await store.writeLocalFile('test-id', { path: '/test.txt', content: 'test' });
|
145
|
+
|
146
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
147
|
+
});
|
148
|
+
});
|
149
|
+
|
150
|
+
describe('renameLocalFile', () => {
|
151
|
+
it('should handle successful rename', async () => {
|
152
|
+
vi.mocked(localFileService.renameLocalFile).mockResolvedValue({
|
153
|
+
success: true,
|
154
|
+
newPath: '/new.txt',
|
155
|
+
});
|
156
|
+
|
157
|
+
await store.renameLocalFile('test-id', { path: '/test.txt', newName: 'new.txt' });
|
158
|
+
|
159
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
160
|
+
});
|
161
|
+
|
162
|
+
it('should handle rename error', async () => {
|
163
|
+
vi.mocked(localFileService.renameLocalFile).mockResolvedValue({
|
164
|
+
success: false,
|
165
|
+
error: 'Rename failed',
|
166
|
+
newPath: '/test.txt',
|
167
|
+
});
|
168
|
+
|
169
|
+
await store.renameLocalFile('test-id', { path: '/test.txt', newName: 'new.txt' });
|
170
|
+
|
171
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalled();
|
172
|
+
});
|
173
|
+
|
174
|
+
it('should validate new filename', async () => {
|
175
|
+
vi.mocked(localFileService.renameLocalFile).mockRejectedValue(
|
176
|
+
new Error('Invalid new name provided'),
|
177
|
+
);
|
178
|
+
|
179
|
+
await store.renameLocalFile('test-id', {
|
180
|
+
path: '/test.txt',
|
181
|
+
newName: '../invalid.txt',
|
182
|
+
});
|
183
|
+
|
184
|
+
expect(mockStore.internal_triggerLocalFileToolCalling).toBeCalledWith(
|
185
|
+
'test-id',
|
186
|
+
expect.any(Function),
|
187
|
+
);
|
188
|
+
});
|
189
|
+
});
|
190
|
+
|
191
|
+
describe('toggleLocalFileLoading', () => {
|
192
|
+
it('should toggle loading state', () => {
|
193
|
+
const mockSetFn = vi.fn();
|
194
|
+
const testStore = localFileSlice(mockSetFn, () => mockStore, {} as any);
|
195
|
+
|
196
|
+
testStore.toggleLocalFileLoading('test-id', true);
|
197
|
+
expect(mockSetFn).toHaveBeenCalledWith(
|
198
|
+
expect.any(Function),
|
199
|
+
false,
|
200
|
+
'toggleLocalFileLoading/start',
|
201
|
+
);
|
202
|
+
|
203
|
+
testStore.toggleLocalFileLoading('test-id', false);
|
204
|
+
expect(mockSetFn).toHaveBeenCalledWith(
|
205
|
+
expect.any(Function),
|
206
|
+
false,
|
207
|
+
'toggleLocalFileLoading/end',
|
208
|
+
);
|
209
|
+
});
|
210
|
+
});
|
211
|
+
});
|
@@ -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
|
});
|
@@ -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}
|
@@ -2,10 +2,11 @@ import { RenameLocalFileParams } from '@lobechat/electron-client-ipc';
|
|
2
2
|
import { Icon } from '@lobehub/ui';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import { ArrowRightIcon } from 'lucide-react';
|
5
|
+
import path from 'path-browserify-esm';
|
5
6
|
import React, { memo } from 'react';
|
6
7
|
import { Flexbox } from 'react-layout-kit';
|
7
8
|
|
8
|
-
import
|
9
|
+
import { LocalFile } from '@/features/LocalFile';
|
9
10
|
import { LocalReadFileState } from '@/tools/local-files/type';
|
10
11
|
import { ChatMessagePluginError } from '@/types/message';
|
11
12
|
|
@@ -28,17 +29,15 @@ interface RenameLocalFileProps {
|
|
28
29
|
const RenameLocalFile = memo<RenameLocalFileProps>(({ args }) => {
|
29
30
|
const { styles } = useStyles();
|
30
31
|
|
31
|
-
const oldFileName =
|
32
|
+
const { base: oldFileName, dir } = path.parse(args.path);
|
33
|
+
|
32
34
|
return (
|
33
35
|
<Flexbox align={'center'} className={styles.container} gap={8} horizontal paddingInline={12}>
|
34
36
|
<Flexbox>{oldFileName}</Flexbox>
|
35
37
|
<Flexbox>
|
36
38
|
<Icon icon={ArrowRightIcon} />
|
37
39
|
</Flexbox>
|
38
|
-
<
|
39
|
-
<FileIcon fileName={args.newName} size={20} variant={'raw'} />
|
40
|
-
{args.newName}
|
41
|
-
</Flexbox>
|
40
|
+
<LocalFile name={args.newName} path={path.join(dir, args.newName)} />
|
42
41
|
</Flexbox>
|
43
42
|
);
|
44
43
|
});
|
@@ -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;
|
@@ -8,12 +8,14 @@ import ListFiles from './ListFiles';
|
|
8
8
|
import ReadLocalFile from './ReadLocalFile';
|
9
9
|
import RenameLocalFile from './RenameLocalFile';
|
10
10
|
import SearchFiles from './SearchFiles';
|
11
|
+
import WriteFile from './WriteFile';
|
11
12
|
|
12
13
|
const RenderMap = {
|
13
14
|
[LocalFilesApiName.searchLocalFiles]: SearchFiles,
|
14
15
|
[LocalFilesApiName.listLocalFiles]: ListFiles,
|
15
16
|
[LocalFilesApiName.readLocalFile]: ReadLocalFile,
|
16
17
|
[LocalFilesApiName.renameLocalFile]: RenameLocalFile,
|
18
|
+
[LocalFilesApiName.writeLocalFile]: WriteFile,
|
17
19
|
};
|
18
20
|
|
19
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: {
|