@yeaft/webchat-agent 0.0.233 → 0.0.235

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.
@@ -0,0 +1,436 @@
1
+ import { readFile, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join, basename, dirname, extname } from 'path';
4
+ import { platform } from 'os';
5
+ import ctx from '../context.js';
6
+ import { resolveAndValidatePath, BINARY_EXTENSIONS } from './utils.js';
7
+
8
+ export async function handleReadFile(msg) {
9
+ const { conversationId, filePath, _requestUserId } = msg;
10
+ console.log('[Agent] handleReadFile received:', { filePath, conversationId, workDir: msg.workDir });
11
+ const conv = ctx.conversations.get(conversationId);
12
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
13
+
14
+ try {
15
+ const resolved = resolveAndValidatePath(filePath, workDir);
16
+ const ext = extname(resolved).toLowerCase();
17
+ const mimeType = BINARY_EXTENSIONS[ext];
18
+
19
+ if (mimeType) {
20
+ // Binary file: read as Buffer, send base64
21
+ const buffer = await readFile(resolved);
22
+ console.log('[Agent] Sending binary file_content:', { filePath: resolved, size: buffer.length, mimeType, conversationId });
23
+ ctx.sendToServer({
24
+ type: 'file_content',
25
+ conversationId,
26
+ _requestUserId,
27
+ filePath: resolved,
28
+ content: buffer.toString('base64'),
29
+ binary: true,
30
+ mimeType
31
+ });
32
+ } else {
33
+ // Text file: read as utf-8
34
+ const content = await readFile(resolved, 'utf-8');
35
+
36
+ // 检测语言
37
+ const langMap = {
38
+ '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
39
+ '.ts': 'javascript', '.tsx': 'javascript', '.jsx': 'javascript',
40
+ '.py': 'python', '.pyw': 'python',
41
+ '.html': 'htmlmixed', '.htm': 'htmlmixed',
42
+ '.css': 'css', '.scss': 'css', '.less': 'css',
43
+ '.json': 'javascript',
44
+ '.md': 'markdown',
45
+ '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
46
+ '.cs': 'text/x-csharp', '.java': 'text/x-java',
47
+ '.cpp': 'text/x-c++src', '.c': 'text/x-csrc', '.h': 'text/x-csrc',
48
+ '.xml': 'xml', '.svg': 'xml',
49
+ '.yaml': 'yaml', '.yml': 'yaml',
50
+ '.sql': 'sql',
51
+ '.go': 'go', '.rs': 'rust', '.rb': 'ruby',
52
+ '.php': 'php', '.swift': 'swift'
53
+ };
54
+
55
+ console.log('[Agent] Sending file_content:', { filePath: resolved, contentLen: content.length, conversationId });
56
+ ctx.sendToServer({
57
+ type: 'file_content',
58
+ conversationId,
59
+ _requestUserId,
60
+ filePath: resolved,
61
+ content,
62
+ language: langMap[ext] || null
63
+ });
64
+ }
65
+ } catch (e) {
66
+ ctx.sendToServer({
67
+ type: 'file_content',
68
+ conversationId,
69
+ _requestUserId,
70
+ filePath,
71
+ content: '',
72
+ error: e.message
73
+ });
74
+ }
75
+ }
76
+
77
+ export async function handleWriteFile(msg) {
78
+ const { conversationId, filePath, content, _requestUserId } = msg;
79
+ const conv = ctx.conversations.get(conversationId);
80
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
81
+
82
+ try {
83
+ const resolved = resolveAndValidatePath(filePath, workDir);
84
+ await writeFile(resolved, content, 'utf-8');
85
+
86
+ ctx.sendToServer({
87
+ type: 'file_saved',
88
+ conversationId,
89
+ _requestUserId,
90
+ filePath: resolved,
91
+ success: true
92
+ });
93
+ } catch (e) {
94
+ ctx.sendToServer({
95
+ type: 'file_saved',
96
+ conversationId,
97
+ _requestUserId,
98
+ filePath,
99
+ success: false,
100
+ error: e.message
101
+ });
102
+ }
103
+ }
104
+
105
+ export async function handleListDirectory(msg) {
106
+ const { conversationId, dirPath, _requestUserId, _requestClientId } = msg;
107
+ const conv = ctx.conversations.get(conversationId);
108
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
109
+
110
+ // 空路径:列出驱动器(Windows)或根目录(Unix)
111
+ if (!dirPath || dirPath === '') {
112
+ try {
113
+ if (platform() === 'win32') {
114
+ const drives = [];
115
+ for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('')) {
116
+ const drivePath = letter + ':\\';
117
+ if (existsSync(drivePath)) {
118
+ drives.push({ name: letter + ':', type: 'directory', size: 0 });
119
+ }
120
+ }
121
+ ctx.sendToServer({
122
+ type: 'directory_listing',
123
+ conversationId,
124
+ _requestUserId,
125
+ _requestClientId,
126
+ dirPath: '',
127
+ entries: drives
128
+ });
129
+ } else {
130
+ // Unix: 列出根目录
131
+ const entries = await readdir('/', { withFileTypes: true });
132
+ const result = entries
133
+ .filter(e => !e.name.startsWith('.'))
134
+ .map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', size: 0 }))
135
+ .sort((a, b) => a.name.localeCompare(b.name));
136
+ ctx.sendToServer({
137
+ type: 'directory_listing',
138
+ conversationId,
139
+ _requestUserId,
140
+ _requestClientId,
141
+ dirPath: '/',
142
+ entries: result
143
+ });
144
+ }
145
+ } catch (e) {
146
+ ctx.sendToServer({
147
+ type: 'directory_listing',
148
+ conversationId,
149
+ _requestUserId,
150
+ _requestClientId,
151
+ dirPath: '',
152
+ entries: [],
153
+ error: e.message
154
+ });
155
+ }
156
+ return;
157
+ }
158
+
159
+ try {
160
+ const resolved = resolveAndValidatePath(dirPath, workDir);
161
+ const entries = await readdir(resolved, { withFileTypes: true });
162
+ const result = [];
163
+
164
+ for (const entry of entries) {
165
+ // 跳过隐藏文件和 node_modules
166
+ if (entry.name.startsWith('.') && entry.name !== '..') continue;
167
+ if (entry.name === 'node_modules') continue;
168
+
169
+ try {
170
+ const fullPath = join(resolved, entry.name);
171
+ const s = await stat(fullPath);
172
+ result.push({
173
+ name: entry.name,
174
+ type: entry.isDirectory() ? 'directory' : 'file',
175
+ size: s.size
176
+ });
177
+ } catch {
178
+ result.push({
179
+ name: entry.name,
180
+ type: entry.isDirectory() ? 'directory' : 'file',
181
+ size: 0
182
+ });
183
+ }
184
+ }
185
+
186
+ // 排序:目录在前,文件在后,各自按名称排序
187
+ result.sort((a, b) => {
188
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
189
+ return a.name.localeCompare(b.name);
190
+ });
191
+
192
+ ctx.sendToServer({
193
+ type: 'directory_listing',
194
+ conversationId,
195
+ _requestUserId,
196
+ _requestClientId,
197
+ dirPath: resolved,
198
+ entries: result
199
+ });
200
+ } catch (e) {
201
+ ctx.sendToServer({
202
+ type: 'directory_listing',
203
+ conversationId,
204
+ _requestUserId,
205
+ _requestClientId,
206
+ dirPath: dirPath || workDir,
207
+ entries: [],
208
+ error: e.message
209
+ });
210
+ }
211
+ }
212
+
213
+ export async function handleCreateFile(msg) {
214
+ const { conversationId, filePath, isDirectory, _requestUserId } = msg;
215
+ const conv = ctx.conversations.get(conversationId);
216
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
217
+
218
+ try {
219
+ const resolved = resolveAndValidatePath(filePath, workDir);
220
+ if (isDirectory) {
221
+ await mkdir(resolved, { recursive: true });
222
+ } else {
223
+ // Ensure parent directory exists
224
+ const parentDir = dirname(resolved);
225
+ await mkdir(parentDir, { recursive: true });
226
+ // Create file only if it doesn't exist
227
+ if (existsSync(resolved)) {
228
+ throw new Error('File already exists: ' + resolved);
229
+ }
230
+ await writeFile(resolved, '', 'utf-8');
231
+ }
232
+ ctx.sendToServer({
233
+ type: 'file_op_result', conversationId, _requestUserId,
234
+ operation: 'create', success: true,
235
+ message: (isDirectory ? 'Directory' : 'File') + ' created: ' + basename(resolved)
236
+ });
237
+ } catch (e) {
238
+ ctx.sendToServer({
239
+ type: 'file_op_result', conversationId, _requestUserId,
240
+ operation: 'create', success: false, error: e.message
241
+ });
242
+ }
243
+ }
244
+
245
+ export async function handleDeleteFiles(msg) {
246
+ const { conversationId, paths, _requestUserId } = msg;
247
+ const conv = ctx.conversations.get(conversationId);
248
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
249
+
250
+ try {
251
+ if (!paths || paths.length === 0) throw new Error('No paths specified');
252
+ const deleted = [];
253
+ const errors = [];
254
+
255
+ for (const p of paths) {
256
+ try {
257
+ const resolved = resolveAndValidatePath(p, workDir);
258
+ const s = await stat(resolved);
259
+ if (s.isDirectory()) {
260
+ await rm(resolved, { recursive: true, force: true });
261
+ } else {
262
+ await unlink(resolved);
263
+ }
264
+ deleted.push(basename(resolved));
265
+ } catch (e) {
266
+ errors.push(basename(p) + ': ' + e.message);
267
+ }
268
+ }
269
+
270
+ const message = deleted.length > 0
271
+ ? 'Deleted: ' + deleted.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
272
+ : 'Failed: ' + errors.join(', ');
273
+
274
+ ctx.sendToServer({
275
+ type: 'file_op_result', conversationId, _requestUserId,
276
+ operation: 'delete', success: deleted.length > 0,
277
+ message, deletedCount: deleted.length, errorCount: errors.length
278
+ });
279
+ } catch (e) {
280
+ ctx.sendToServer({
281
+ type: 'file_op_result', conversationId, _requestUserId,
282
+ operation: 'delete', success: false, error: e.message
283
+ });
284
+ }
285
+ }
286
+
287
+ export async function handleMoveFiles(msg) {
288
+ const { conversationId, paths, destination, newName, _requestUserId } = msg;
289
+ const conv = ctx.conversations.get(conversationId);
290
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
291
+
292
+ try {
293
+ if (!paths || paths.length === 0) throw new Error('No paths specified');
294
+ if (!destination) throw new Error('No destination specified');
295
+
296
+ const destResolved = resolveAndValidatePath(destination, workDir);
297
+ // Ensure destination directory exists
298
+ await mkdir(destResolved, { recursive: true });
299
+
300
+ const moved = [];
301
+ const errors = [];
302
+
303
+ for (const p of paths) {
304
+ try {
305
+ const srcResolved = resolveAndValidatePath(p, workDir);
306
+ const name = (newName && paths.length === 1) ? newName : basename(srcResolved);
307
+ const destPath = join(destResolved, name);
308
+ if (existsSync(destPath)) {
309
+ throw new Error('Target already exists: ' + name);
310
+ }
311
+ await rename(srcResolved, destPath);
312
+ moved.push(name);
313
+ } catch (e) {
314
+ errors.push(basename(p) + ': ' + e.message);
315
+ }
316
+ }
317
+
318
+ const message = moved.length > 0
319
+ ? 'Moved: ' + moved.join(', ') + ' → ' + basename(destResolved) + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
320
+ : 'Failed: ' + errors.join(', ');
321
+
322
+ ctx.sendToServer({
323
+ type: 'file_op_result', conversationId, _requestUserId,
324
+ operation: 'move', success: moved.length > 0,
325
+ message, movedCount: moved.length, errorCount: errors.length
326
+ });
327
+ } catch (e) {
328
+ ctx.sendToServer({
329
+ type: 'file_op_result', conversationId, _requestUserId,
330
+ operation: 'move', success: false, error: e.message
331
+ });
332
+ }
333
+ }
334
+
335
+ export async function handleCopyFiles(msg) {
336
+ const { conversationId, paths, destination, _requestUserId } = msg;
337
+ const conv = ctx.conversations.get(conversationId);
338
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
339
+
340
+ try {
341
+ if (!paths || paths.length === 0) throw new Error('No paths specified');
342
+ if (!destination) throw new Error('No destination specified');
343
+
344
+ const destResolved = resolveAndValidatePath(destination, workDir);
345
+ await mkdir(destResolved, { recursive: true });
346
+
347
+ const copied = [];
348
+ const errors = [];
349
+
350
+ for (const p of paths) {
351
+ try {
352
+ const srcResolved = resolveAndValidatePath(p, workDir);
353
+ const name = basename(srcResolved);
354
+ let destPath = join(destResolved, name);
355
+
356
+ // If copying to same directory, generate a unique name
357
+ if (destPath === srcResolved) {
358
+ const ext = extname(name);
359
+ const base = basename(name, ext);
360
+ let counter = 1;
361
+ do {
362
+ destPath = join(destResolved, `${base} (copy${counter > 1 ? ' ' + counter : ''})${ext}`);
363
+ counter++;
364
+ } while (existsSync(destPath));
365
+ }
366
+
367
+ const srcStat = await stat(srcResolved);
368
+ if (srcStat.isDirectory()) {
369
+ await cp(srcResolved, destPath, { recursive: true });
370
+ } else {
371
+ await copyFile(srcResolved, destPath);
372
+ }
373
+ copied.push(basename(destPath));
374
+ } catch (e) {
375
+ errors.push(basename(p) + ': ' + e.message);
376
+ }
377
+ }
378
+
379
+ const message = copied.length > 0
380
+ ? 'Copied: ' + copied.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
381
+ : 'Failed: ' + errors.join(', ');
382
+
383
+ ctx.sendToServer({
384
+ type: 'file_op_result', conversationId, _requestUserId,
385
+ operation: 'copy', success: copied.length > 0,
386
+ message, copiedCount: copied.length, errorCount: errors.length
387
+ });
388
+ } catch (e) {
389
+ ctx.sendToServer({
390
+ type: 'file_op_result', conversationId, _requestUserId,
391
+ operation: 'copy', success: false, error: e.message
392
+ });
393
+ }
394
+ }
395
+
396
+ export async function handleUploadToDir(msg) {
397
+ const { conversationId, files, dirPath, _requestUserId } = msg;
398
+ const conv = ctx.conversations.get(conversationId);
399
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
400
+
401
+ try {
402
+ if (!files || files.length === 0) throw new Error('No files specified');
403
+
404
+ const targetDir = resolveAndValidatePath(dirPath || workDir, workDir);
405
+ await mkdir(targetDir, { recursive: true });
406
+
407
+ const saved = [];
408
+ const errors = [];
409
+
410
+ for (const file of files) {
411
+ try {
412
+ const dest = join(targetDir, file.name);
413
+ const buffer = Buffer.from(file.data, 'base64');
414
+ await writeFile(dest, buffer);
415
+ saved.push(file.name);
416
+ } catch (e) {
417
+ errors.push(file.name + ': ' + e.message);
418
+ }
419
+ }
420
+
421
+ const message = saved.length > 0
422
+ ? 'Uploaded: ' + saved.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
423
+ : 'Failed: ' + errors.join(', ');
424
+
425
+ ctx.sendToServer({
426
+ type: 'file_op_result', conversationId, _requestUserId,
427
+ operation: 'upload', success: saved.length > 0,
428
+ message, uploadedCount: saved.length, errorCount: errors.length
429
+ });
430
+ } catch (e) {
431
+ ctx.sendToServer({
432
+ type: 'file_op_result', conversationId, _requestUserId,
433
+ operation: 'upload', success: false, error: e.message
434
+ });
435
+ }
436
+ }
@@ -0,0 +1,66 @@
1
+ import { readdir, stat } from 'fs/promises';
2
+ import { join, relative, resolve } from 'path';
3
+ import ctx from '../context.js';
4
+
5
+ export async function handleFileSearch(msg) {
6
+ const { conversationId, query, _requestUserId } = msg;
7
+ const conv = ctx.conversations.get(conversationId);
8
+ const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
9
+ const searchRoot = msg.dirPath || workDir;
10
+
11
+ try {
12
+ if (!query || query.trim().length === 0) {
13
+ ctx.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [] });
14
+ return;
15
+ }
16
+
17
+ const resolved = resolve(searchRoot);
18
+ const results = [];
19
+ const MAX_RESULTS = 100;
20
+ const lowerQuery = query.toLowerCase();
21
+ const skipDirs = new Set(['.git', 'node_modules', '__pycache__', '.next', '.nuxt', 'dist', 'build', '.cache', 'bin', 'obj']);
22
+
23
+ async function walk(dir, depth) {
24
+ if (depth > 10 || results.length >= MAX_RESULTS) return;
25
+ try {
26
+ const entries = await readdir(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (results.length >= MAX_RESULTS) return;
29
+ if (entry.name.startsWith('.') && depth > 0) continue;
30
+ if (skipDirs.has(entry.name) && entry.isDirectory()) continue;
31
+
32
+ const fullPath = join(dir, entry.name);
33
+
34
+ if (entry.name.toLowerCase().includes(lowerQuery)) {
35
+ let size = 0;
36
+ try { const s = await stat(fullPath); size = s.size; } catch {}
37
+ results.push({
38
+ name: entry.name,
39
+ path: relative(resolved, fullPath).replace(/\\/g, '/'),
40
+ fullPath: fullPath.replace(/\\/g, '/'),
41
+ type: entry.isDirectory() ? 'directory' : 'file',
42
+ size
43
+ });
44
+ }
45
+
46
+ if (entry.isDirectory()) {
47
+ await walk(fullPath, depth + 1);
48
+ }
49
+ }
50
+ } catch {}
51
+ }
52
+
53
+ await walk(resolved, 0);
54
+
55
+ ctx.sendToServer({
56
+ type: 'file_search_result',
57
+ conversationId,
58
+ _requestUserId,
59
+ query,
60
+ results,
61
+ truncated: results.length >= MAX_RESULTS
62
+ });
63
+ } catch (e) {
64
+ ctx.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [], error: e.message });
65
+ }
66
+ }