@yeaft/webchat-agent 0.0.233 → 0.0.234

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 (4) hide show
  1. package/connection.js +14 -777
  2. package/package.json +1 -1
  3. package/service.js +23 -624
  4. package/workbench.js +15 -938
package/workbench.js CHANGED
@@ -1,943 +1,20 @@
1
- import { existsSync, writeFileSync, mkdirSync } from 'fs';
2
- import { readFile, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
3
- import { join, basename, dirname, extname, resolve, isAbsolute, relative } from 'path';
4
- import { platform } from 'os';
5
- import { exec } from 'child_process';
6
- import { promisify } from 'util';
7
- import ctx from './context.js';
1
+ /**
2
+ * workbench.js re-export entry point.
3
+ * Actual implementations live in workbench/ sub-modules.
4
+ */
8
5
 
9
- const execAsync = promisify(exec);
6
+ export { resolveAndValidatePath, getGitRoot, validateGitPath } from './workbench/utils.js';
10
7
 
11
- // 路径安全校验 - 确保路径格式正确
12
- export function resolveAndValidatePath(filePath, workDir) {
13
- const resolved = isAbsolute(filePath) ? resolve(filePath) : resolve(workDir, filePath);
14
- return resolved;
15
- }
8
+ export {
9
+ handleReadFile, handleWriteFile, handleListDirectory,
10
+ handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir
11
+ } from './workbench/file-ops.js';
16
12
 
17
- // Helper: resolve git root for a working directory
18
- export async function getGitRoot(workDir) {
19
- try {
20
- const { stdout } = await execAsync('git rev-parse --show-toplevel', {
21
- cwd: workDir, timeout: 5000, windowsHide: true
22
- });
23
- return stdout.trim();
24
- } catch {
25
- return workDir;
26
- }
27
- }
13
+ export {
14
+ handleGitStatus, handleGitDiff, handleGitAdd, handleGitReset,
15
+ handleGitRestore, handleGitCommit, handleGitPush
16
+ } from './workbench/git-ops.js';
28
17
 
29
- // Helper: validate file path for shell safety
30
- export function validateGitPath(filePath) {
31
- return filePath && !/[`$;|&><!\n\r]/.test(filePath);
32
- }
18
+ export { handleFileSearch } from './workbench/file-search.js';
33
19
 
34
- // Binary file extensions MIME type mapping
35
- const BINARY_EXTENSIONS = {
36
- '.pdf': 'application/pdf',
37
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
38
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
39
- '.xls': 'application/vnd.ms-excel',
40
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
41
- '.ppt': 'application/vnd.ms-powerpoint',
42
- '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
43
- '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
44
- '.ico': 'image/x-icon'
45
- };
46
-
47
- export async function handleReadFile(msg) {
48
- const { conversationId, filePath, _requestUserId } = msg;
49
- console.log('[Agent] handleReadFile received:', { filePath, conversationId, workDir: msg.workDir });
50
- const conv = ctx.conversations.get(conversationId);
51
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
52
-
53
- try {
54
- const resolved = resolveAndValidatePath(filePath, workDir);
55
- const ext = extname(resolved).toLowerCase();
56
- const mimeType = BINARY_EXTENSIONS[ext];
57
-
58
- if (mimeType) {
59
- // Binary file: read as Buffer, send base64
60
- const buffer = await readFile(resolved);
61
- console.log('[Agent] Sending binary file_content:', { filePath: resolved, size: buffer.length, mimeType, conversationId });
62
- ctx.sendToServer({
63
- type: 'file_content',
64
- conversationId,
65
- _requestUserId,
66
- filePath: resolved,
67
- content: buffer.toString('base64'),
68
- binary: true,
69
- mimeType
70
- });
71
- } else {
72
- // Text file: read as utf-8
73
- const content = await readFile(resolved, 'utf-8');
74
-
75
- // 检测语言
76
- const langMap = {
77
- '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
78
- '.ts': 'javascript', '.tsx': 'javascript', '.jsx': 'javascript',
79
- '.py': 'python', '.pyw': 'python',
80
- '.html': 'htmlmixed', '.htm': 'htmlmixed',
81
- '.css': 'css', '.scss': 'css', '.less': 'css',
82
- '.json': 'javascript',
83
- '.md': 'markdown',
84
- '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
85
- '.cs': 'text/x-csharp', '.java': 'text/x-java',
86
- '.cpp': 'text/x-c++src', '.c': 'text/x-csrc', '.h': 'text/x-csrc',
87
- '.xml': 'xml', '.svg': 'xml',
88
- '.yaml': 'yaml', '.yml': 'yaml',
89
- '.sql': 'sql',
90
- '.go': 'go', '.rs': 'rust', '.rb': 'ruby',
91
- '.php': 'php', '.swift': 'swift'
92
- };
93
-
94
- console.log('[Agent] Sending file_content:', { filePath: resolved, contentLen: content.length, conversationId });
95
- ctx.sendToServer({
96
- type: 'file_content',
97
- conversationId,
98
- _requestUserId,
99
- filePath: resolved,
100
- content,
101
- language: langMap[ext] || null
102
- });
103
- }
104
- } catch (e) {
105
- ctx.sendToServer({
106
- type: 'file_content',
107
- conversationId,
108
- _requestUserId,
109
- filePath,
110
- content: '',
111
- error: e.message
112
- });
113
- }
114
- }
115
-
116
- export async function handleWriteFile(msg) {
117
- const { conversationId, filePath, content, _requestUserId } = msg;
118
- const conv = ctx.conversations.get(conversationId);
119
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
120
-
121
- try {
122
- const resolved = resolveAndValidatePath(filePath, workDir);
123
- await writeFile(resolved, content, 'utf-8');
124
-
125
- ctx.sendToServer({
126
- type: 'file_saved',
127
- conversationId,
128
- _requestUserId,
129
- filePath: resolved,
130
- success: true
131
- });
132
- } catch (e) {
133
- ctx.sendToServer({
134
- type: 'file_saved',
135
- conversationId,
136
- _requestUserId,
137
- filePath,
138
- success: false,
139
- error: e.message
140
- });
141
- }
142
- }
143
-
144
- export async function handleListDirectory(msg) {
145
- const { conversationId, dirPath, _requestUserId, _requestClientId } = msg;
146
- const conv = ctx.conversations.get(conversationId);
147
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
148
-
149
- // 空路径:列出驱动器(Windows)或根目录(Unix)
150
- if (!dirPath || dirPath === '') {
151
- try {
152
- if (platform() === 'win32') {
153
- const drives = [];
154
- for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('')) {
155
- const drivePath = letter + ':\\';
156
- if (existsSync(drivePath)) {
157
- drives.push({ name: letter + ':', type: 'directory', size: 0 });
158
- }
159
- }
160
- ctx.sendToServer({
161
- type: 'directory_listing',
162
- conversationId,
163
- _requestUserId,
164
- _requestClientId,
165
- dirPath: '',
166
- entries: drives
167
- });
168
- } else {
169
- // Unix: 列出根目录
170
- const entries = await readdir('/', { withFileTypes: true });
171
- const result = entries
172
- .filter(e => !e.name.startsWith('.'))
173
- .map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', size: 0 }))
174
- .sort((a, b) => a.name.localeCompare(b.name));
175
- ctx.sendToServer({
176
- type: 'directory_listing',
177
- conversationId,
178
- _requestUserId,
179
- _requestClientId,
180
- dirPath: '/',
181
- entries: result
182
- });
183
- }
184
- } catch (e) {
185
- ctx.sendToServer({
186
- type: 'directory_listing',
187
- conversationId,
188
- _requestUserId,
189
- _requestClientId,
190
- dirPath: '',
191
- entries: [],
192
- error: e.message
193
- });
194
- }
195
- return;
196
- }
197
-
198
- try {
199
- const resolved = resolveAndValidatePath(dirPath, workDir);
200
- const entries = await readdir(resolved, { withFileTypes: true });
201
- const result = [];
202
-
203
- for (const entry of entries) {
204
- // 跳过隐藏文件和 node_modules
205
- if (entry.name.startsWith('.') && entry.name !== '..') continue;
206
- if (entry.name === 'node_modules') continue;
207
-
208
- try {
209
- const fullPath = join(resolved, entry.name);
210
- const s = await stat(fullPath);
211
- result.push({
212
- name: entry.name,
213
- type: entry.isDirectory() ? 'directory' : 'file',
214
- size: s.size
215
- });
216
- } catch {
217
- result.push({
218
- name: entry.name,
219
- type: entry.isDirectory() ? 'directory' : 'file',
220
- size: 0
221
- });
222
- }
223
- }
224
-
225
- // 排序:目录在前,文件在后,各自按名称排序
226
- result.sort((a, b) => {
227
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
228
- return a.name.localeCompare(b.name);
229
- });
230
-
231
- ctx.sendToServer({
232
- type: 'directory_listing',
233
- conversationId,
234
- _requestUserId,
235
- _requestClientId,
236
- dirPath: resolved,
237
- entries: result
238
- });
239
- } catch (e) {
240
- ctx.sendToServer({
241
- type: 'directory_listing',
242
- conversationId,
243
- _requestUserId,
244
- _requestClientId,
245
- dirPath: dirPath || workDir,
246
- entries: [],
247
- error: e.message
248
- });
249
- }
250
- }
251
-
252
- export async function handleGitStatus(msg) {
253
- const { conversationId, _requestUserId } = msg;
254
- const conv = ctx.conversations.get(conversationId);
255
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
256
-
257
- try {
258
- // Get git repo root to ensure paths are consistent
259
- let gitRoot = workDir;
260
- try {
261
- const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
262
- cwd: workDir,
263
- timeout: 5000,
264
- windowsHide: true
265
- });
266
- gitRoot = rootOut.trim();
267
- } catch {}
268
-
269
- const { stdout: statusOut } = await execAsync('git status --porcelain', {
270
- cwd: gitRoot,
271
- timeout: 10000,
272
- windowsHide: true
273
- });
274
-
275
- let branch = '';
276
- try {
277
- const { stdout: branchOut } = await execAsync('git branch --show-current', {
278
- cwd: gitRoot,
279
- timeout: 5000,
280
- windowsHide: true
281
- });
282
- branch = branchOut.trim();
283
- } catch {}
284
-
285
- // Get ahead/behind counts relative to upstream
286
- let ahead = 0, behind = 0;
287
- try {
288
- const { stdout: abOut } = await execAsync('git rev-list --left-right --count HEAD...@{upstream}', {
289
- cwd: gitRoot, timeout: 5000, windowsHide: true
290
- });
291
- const parts = abOut.trim().split(/\s+/);
292
- ahead = parseInt(parts[0]) || 0;
293
- behind = parseInt(parts[1]) || 0;
294
- } catch {}
295
-
296
- const files = [];
297
- for (const line of statusOut.split('\n')) {
298
- if (!line || line.length < 4) continue;
299
- const indexStatus = line[0];
300
- const workTreeStatus = line[1];
301
- const path = line.slice(3);
302
- // Handle renamed files: "R old -> new"
303
- const displayPath = path.includes(' -> ') ? path.split(' -> ')[1] : path;
304
- files.push({ path: displayPath, indexStatus, workTreeStatus });
305
- }
306
-
307
- ctx.sendToServer({
308
- type: 'git_status_result',
309
- conversationId,
310
- _requestUserId,
311
- branch,
312
- files,
313
- ahead,
314
- behind,
315
- workDir,
316
- gitRoot
317
- });
318
- } catch (e) {
319
- ctx.sendToServer({
320
- type: 'git_status_result',
321
- conversationId,
322
- _requestUserId,
323
- error: e.message,
324
- isGitRepo: !e.message.includes('not a git repository') && !e.message.includes('ENOENT')
325
- });
326
- }
327
- }
328
-
329
- export async function handleGitDiff(msg) {
330
- const { conversationId, filePath, staged, untracked, fullFile, _requestUserId } = msg;
331
- const conv = ctx.conversations.get(conversationId);
332
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
333
-
334
- try {
335
- // 安全检查:验证 filePath 不包含 shell 注入字符
336
- if (!filePath || /[`$;|&><!\n\r]/.test(filePath)) {
337
- ctx.sendToServer({
338
- type: 'git_diff_result',
339
- conversationId,
340
- _requestUserId,
341
- filePath,
342
- error: 'Invalid file path'
343
- });
344
- return;
345
- }
346
-
347
- // Get git repo root — git status paths are relative to this, not workDir
348
- let gitRoot = workDir;
349
- try {
350
- const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
351
- cwd: workDir,
352
- timeout: 5000,
353
- windowsHide: true
354
- });
355
- gitRoot = rootOut.trim();
356
- } catch {}
357
-
358
- if (untracked) {
359
- // Untracked files: resolve path relative to git root
360
- const fullPath = resolve(gitRoot, filePath);
361
- const resolved = resolveAndValidatePath(fullPath, gitRoot);
362
- const content = await readFile(resolved, 'utf-8');
363
- ctx.sendToServer({
364
- type: 'git_diff_result',
365
- conversationId,
366
- _requestUserId,
367
- filePath,
368
- diff: null,
369
- newFileContent: content
370
- });
371
- return;
372
- }
373
-
374
- // 使用 -- 分隔选项和路径, execAsync 的参数已被验证无注入字符
375
- const contextFlag = fullFile ? '-U99999' : '';
376
- const cmd = staged
377
- ? `git diff --cached ${contextFlag} -- "${filePath}"`
378
- : `git diff ${contextFlag} -- "${filePath}"`;
379
-
380
- const { stdout } = await execAsync(cmd, {
381
- cwd: gitRoot,
382
- timeout: 15000,
383
- maxBuffer: 5 * 1024 * 1024,
384
- windowsHide: true
385
- });
386
-
387
- // 如果 git diff 返回空,尝试用 --cached 或反之
388
- if (!stdout.trim() && !staged) {
389
- const cachedCmd = `git diff --cached ${contextFlag} -- "${filePath}"`;
390
- const { stdout: cachedOut } = await execAsync(cachedCmd, {
391
- cwd: gitRoot,
392
- timeout: 15000,
393
- maxBuffer: 5 * 1024 * 1024,
394
- windowsHide: true
395
- });
396
- if (cachedOut.trim()) {
397
- ctx.sendToServer({
398
- type: 'git_diff_result',
399
- conversationId,
400
- _requestUserId,
401
- filePath,
402
- staged: true,
403
- diff: cachedOut
404
- });
405
- return;
406
- }
407
- } else if (!stdout.trim() && staged) {
408
- const wtCmd = `git diff ${contextFlag} -- "${filePath}"`;
409
- const { stdout: wtOut } = await execAsync(wtCmd, {
410
- cwd: gitRoot,
411
- timeout: 15000,
412
- maxBuffer: 5 * 1024 * 1024,
413
- windowsHide: true
414
- });
415
- if (wtOut.trim()) {
416
- ctx.sendToServer({
417
- type: 'git_diff_result',
418
- conversationId,
419
- _requestUserId,
420
- filePath,
421
- staged: false,
422
- diff: wtOut
423
- });
424
- return;
425
- }
426
- }
427
-
428
- ctx.sendToServer({
429
- type: 'git_diff_result',
430
- conversationId,
431
- _requestUserId,
432
- filePath,
433
- staged: !!staged,
434
- diff: stdout
435
- });
436
- } catch (e) {
437
- ctx.sendToServer({
438
- type: 'git_diff_result',
439
- conversationId,
440
- _requestUserId,
441
- filePath,
442
- error: e.message
443
- });
444
- }
445
- }
446
-
447
- export async function handleGitAdd(msg) {
448
- const { conversationId, filePath, addAll, _requestUserId } = msg;
449
- const conv = ctx.conversations.get(conversationId);
450
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
451
-
452
- try {
453
- const gitRoot = await getGitRoot(workDir);
454
-
455
- if (addAll) {
456
- await execAsync('git add -A', { cwd: gitRoot, timeout: 10000, windowsHide: true });
457
- } else {
458
- if (!validateGitPath(filePath)) {
459
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: 'Invalid file path' });
460
- return;
461
- }
462
- await execAsync(`git add -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
463
- }
464
-
465
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: true, message: addAll ? 'All files staged' : `Staged: ${filePath}` });
466
- } catch (e) {
467
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'add', success: false, error: e.message });
468
- }
469
- }
470
-
471
- export async function handleGitReset(msg) {
472
- const { conversationId, filePath, resetAll, _requestUserId } = msg;
473
- const conv = ctx.conversations.get(conversationId);
474
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
475
-
476
- try {
477
- const gitRoot = await getGitRoot(workDir);
478
-
479
- if (resetAll) {
480
- await execAsync('git reset HEAD', { cwd: gitRoot, timeout: 10000, windowsHide: true });
481
- } else {
482
- if (!validateGitPath(filePath)) {
483
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: 'Invalid file path' });
484
- return;
485
- }
486
- await execAsync(`git reset HEAD -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
487
- }
488
-
489
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: true, message: resetAll ? 'All files unstaged' : `Unstaged: ${filePath}` });
490
- } catch (e) {
491
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'reset', success: false, error: e.message });
492
- }
493
- }
494
-
495
- export async function handleGitRestore(msg) {
496
- const { conversationId, filePath, _requestUserId } = msg;
497
- const conv = ctx.conversations.get(conversationId);
498
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
499
-
500
- try {
501
- if (!validateGitPath(filePath)) {
502
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: 'Invalid file path' });
503
- return;
504
- }
505
-
506
- const gitRoot = await getGitRoot(workDir);
507
- await execAsync(`git restore -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
508
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: true, message: `Restored: ${filePath}` });
509
- } catch (e) {
510
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: false, error: e.message });
511
- }
512
- }
513
-
514
- export async function handleGitCommit(msg) {
515
- const { conversationId, commitMessage, _requestUserId } = msg;
516
- const conv = ctx.conversations.get(conversationId);
517
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
518
-
519
- try {
520
- if (!commitMessage || !commitMessage.trim()) {
521
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: 'Commit message is required' });
522
- return;
523
- }
524
-
525
- const gitRoot = await getGitRoot(workDir);
526
-
527
- // Write commit message to temp file to avoid shell injection
528
- const tmpFile = join(gitRoot, '.git', 'WEBCHAT_COMMIT_MSG');
529
- await writeFile(tmpFile, commitMessage.trim(), 'utf8');
530
-
531
- try {
532
- const { stdout } = await execAsync(`git commit -F "${tmpFile}"`, {
533
- cwd: gitRoot, timeout: 30000, windowsHide: true
534
- });
535
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: true, message: stdout.trim() });
536
- } finally {
537
- // Clean up temp file
538
- try { await writeFile(tmpFile, '', 'utf8'); } catch {}
539
- }
540
- } catch (e) {
541
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'commit', success: false, error: e.stderr?.trim() || e.message });
542
- }
543
- }
544
-
545
- export async function handleGitPush(msg) {
546
- const { conversationId, _requestUserId } = msg;
547
- const conv = ctx.conversations.get(conversationId);
548
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
549
-
550
- try {
551
- const gitRoot = await getGitRoot(workDir);
552
- const { stdout, stderr } = await execAsync('git push', {
553
- cwd: gitRoot, timeout: 60000, windowsHide: true
554
- });
555
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: true, message: (stdout + '\n' + stderr).trim() || 'Push complete' });
556
- } catch (e) {
557
- ctx.sendToServer({ type: 'git_op_result', conversationId, _requestUserId, operation: 'push', success: false, error: e.stderr?.trim() || e.message });
558
- }
559
- }
560
-
561
- export async function handleFileSearch(msg) {
562
- const { conversationId, query, _requestUserId } = msg;
563
- const conv = ctx.conversations.get(conversationId);
564
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
565
- const searchRoot = msg.dirPath || workDir;
566
-
567
- try {
568
- if (!query || query.trim().length === 0) {
569
- ctx.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [] });
570
- return;
571
- }
572
-
573
- const resolved = resolve(searchRoot);
574
- const results = [];
575
- const MAX_RESULTS = 100;
576
- const lowerQuery = query.toLowerCase();
577
- const skipDirs = new Set(['.git', 'node_modules', '__pycache__', '.next', '.nuxt', 'dist', 'build', '.cache', 'bin', 'obj']);
578
-
579
- async function walk(dir, depth) {
580
- if (depth > 10 || results.length >= MAX_RESULTS) return;
581
- try {
582
- const entries = await readdir(dir, { withFileTypes: true });
583
- for (const entry of entries) {
584
- if (results.length >= MAX_RESULTS) return;
585
- if (entry.name.startsWith('.') && depth > 0) continue;
586
- if (skipDirs.has(entry.name) && entry.isDirectory()) continue;
587
-
588
- const fullPath = join(dir, entry.name);
589
-
590
- if (entry.name.toLowerCase().includes(lowerQuery)) {
591
- let size = 0;
592
- try { const s = await stat(fullPath); size = s.size; } catch {}
593
- results.push({
594
- name: entry.name,
595
- path: relative(resolved, fullPath).replace(/\\/g, '/'),
596
- fullPath: fullPath.replace(/\\/g, '/'),
597
- type: entry.isDirectory() ? 'directory' : 'file',
598
- size
599
- });
600
- }
601
-
602
- if (entry.isDirectory()) {
603
- await walk(fullPath, depth + 1);
604
- }
605
- }
606
- } catch {}
607
- }
608
-
609
- await walk(resolved, 0);
610
-
611
- ctx.sendToServer({
612
- type: 'file_search_result',
613
- conversationId,
614
- _requestUserId,
615
- query,
616
- results,
617
- truncated: results.length >= MAX_RESULTS
618
- });
619
- } catch (e) {
620
- ctx.sendToServer({ type: 'file_search_result', conversationId, _requestUserId, query, results: [], error: e.message });
621
- }
622
- }
623
-
624
- export async function handleCreateFile(msg) {
625
- const { conversationId, filePath, isDirectory, _requestUserId } = msg;
626
- const conv = ctx.conversations.get(conversationId);
627
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
628
-
629
- try {
630
- const resolved = resolveAndValidatePath(filePath, workDir);
631
- if (isDirectory) {
632
- await mkdir(resolved, { recursive: true });
633
- } else {
634
- // Ensure parent directory exists
635
- const parentDir = dirname(resolved);
636
- await mkdir(parentDir, { recursive: true });
637
- // Create file only if it doesn't exist
638
- if (existsSync(resolved)) {
639
- throw new Error('File already exists: ' + resolved);
640
- }
641
- await writeFile(resolved, '', 'utf-8');
642
- }
643
- ctx.sendToServer({
644
- type: 'file_op_result', conversationId, _requestUserId,
645
- operation: 'create', success: true,
646
- message: (isDirectory ? 'Directory' : 'File') + ' created: ' + basename(resolved)
647
- });
648
- } catch (e) {
649
- ctx.sendToServer({
650
- type: 'file_op_result', conversationId, _requestUserId,
651
- operation: 'create', success: false, error: e.message
652
- });
653
- }
654
- }
655
-
656
- export async function handleDeleteFiles(msg) {
657
- const { conversationId, paths, _requestUserId } = msg;
658
- const conv = ctx.conversations.get(conversationId);
659
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
660
-
661
- try {
662
- if (!paths || paths.length === 0) throw new Error('No paths specified');
663
- const deleted = [];
664
- const errors = [];
665
-
666
- for (const p of paths) {
667
- try {
668
- const resolved = resolveAndValidatePath(p, workDir);
669
- const s = await stat(resolved);
670
- if (s.isDirectory()) {
671
- await rm(resolved, { recursive: true, force: true });
672
- } else {
673
- await unlink(resolved);
674
- }
675
- deleted.push(basename(resolved));
676
- } catch (e) {
677
- errors.push(basename(p) + ': ' + e.message);
678
- }
679
- }
680
-
681
- const message = deleted.length > 0
682
- ? 'Deleted: ' + deleted.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
683
- : 'Failed: ' + errors.join(', ');
684
-
685
- ctx.sendToServer({
686
- type: 'file_op_result', conversationId, _requestUserId,
687
- operation: 'delete', success: deleted.length > 0,
688
- message, deletedCount: deleted.length, errorCount: errors.length
689
- });
690
- } catch (e) {
691
- ctx.sendToServer({
692
- type: 'file_op_result', conversationId, _requestUserId,
693
- operation: 'delete', success: false, error: e.message
694
- });
695
- }
696
- }
697
-
698
- export async function handleMoveFiles(msg) {
699
- const { conversationId, paths, destination, newName, _requestUserId } = msg;
700
- const conv = ctx.conversations.get(conversationId);
701
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
702
-
703
- try {
704
- if (!paths || paths.length === 0) throw new Error('No paths specified');
705
- if (!destination) throw new Error('No destination specified');
706
-
707
- const destResolved = resolveAndValidatePath(destination, workDir);
708
- // Ensure destination directory exists
709
- await mkdir(destResolved, { recursive: true });
710
-
711
- const moved = [];
712
- const errors = [];
713
-
714
- for (const p of paths) {
715
- try {
716
- const srcResolved = resolveAndValidatePath(p, workDir);
717
- const name = (newName && paths.length === 1) ? newName : basename(srcResolved);
718
- const destPath = join(destResolved, name);
719
- if (existsSync(destPath)) {
720
- throw new Error('Target already exists: ' + name);
721
- }
722
- await rename(srcResolved, destPath);
723
- moved.push(name);
724
- } catch (e) {
725
- errors.push(basename(p) + ': ' + e.message);
726
- }
727
- }
728
-
729
- const message = moved.length > 0
730
- ? 'Moved: ' + moved.join(', ') + ' → ' + basename(destResolved) + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
731
- : 'Failed: ' + errors.join(', ');
732
-
733
- ctx.sendToServer({
734
- type: 'file_op_result', conversationId, _requestUserId,
735
- operation: 'move', success: moved.length > 0,
736
- message, movedCount: moved.length, errorCount: errors.length
737
- });
738
- } catch (e) {
739
- ctx.sendToServer({
740
- type: 'file_op_result', conversationId, _requestUserId,
741
- operation: 'move', success: false, error: e.message
742
- });
743
- }
744
- }
745
-
746
- export async function handleCopyFiles(msg) {
747
- const { conversationId, paths, destination, _requestUserId } = msg;
748
- const conv = ctx.conversations.get(conversationId);
749
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
750
-
751
- try {
752
- if (!paths || paths.length === 0) throw new Error('No paths specified');
753
- if (!destination) throw new Error('No destination specified');
754
-
755
- const destResolved = resolveAndValidatePath(destination, workDir);
756
- await mkdir(destResolved, { recursive: true });
757
-
758
- const copied = [];
759
- const errors = [];
760
-
761
- for (const p of paths) {
762
- try {
763
- const srcResolved = resolveAndValidatePath(p, workDir);
764
- const name = basename(srcResolved);
765
- let destPath = join(destResolved, name);
766
-
767
- // If copying to same directory, generate a unique name
768
- if (destPath === srcResolved) {
769
- const ext = extname(name);
770
- const base = basename(name, ext);
771
- let counter = 1;
772
- do {
773
- destPath = join(destResolved, `${base} (copy${counter > 1 ? ' ' + counter : ''})${ext}`);
774
- counter++;
775
- } while (existsSync(destPath));
776
- }
777
-
778
- const srcStat = await stat(srcResolved);
779
- if (srcStat.isDirectory()) {
780
- await cp(srcResolved, destPath, { recursive: true });
781
- } else {
782
- await copyFile(srcResolved, destPath);
783
- }
784
- copied.push(basename(destPath));
785
- } catch (e) {
786
- errors.push(basename(p) + ': ' + e.message);
787
- }
788
- }
789
-
790
- const message = copied.length > 0
791
- ? 'Copied: ' + copied.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
792
- : 'Failed: ' + errors.join(', ');
793
-
794
- ctx.sendToServer({
795
- type: 'file_op_result', conversationId, _requestUserId,
796
- operation: 'copy', success: copied.length > 0,
797
- message, copiedCount: copied.length, errorCount: errors.length
798
- });
799
- } catch (e) {
800
- ctx.sendToServer({
801
- type: 'file_op_result', conversationId, _requestUserId,
802
- operation: 'copy', success: false, error: e.message
803
- });
804
- }
805
- }
806
-
807
- export async function handleUploadToDir(msg) {
808
- const { conversationId, files, dirPath, _requestUserId } = msg;
809
- const conv = ctx.conversations.get(conversationId);
810
- const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
811
-
812
- try {
813
- if (!files || files.length === 0) throw new Error('No files specified');
814
-
815
- const targetDir = resolveAndValidatePath(dirPath || workDir, workDir);
816
- await mkdir(targetDir, { recursive: true });
817
-
818
- const saved = [];
819
- const errors = [];
820
-
821
- for (const file of files) {
822
- try {
823
- const dest = join(targetDir, file.name);
824
- const buffer = Buffer.from(file.data, 'base64');
825
- await writeFile(dest, buffer);
826
- saved.push(file.name);
827
- } catch (e) {
828
- errors.push(file.name + ': ' + e.message);
829
- }
830
- }
831
-
832
- const message = saved.length > 0
833
- ? 'Uploaded: ' + saved.join(', ') + (errors.length > 0 ? '; Errors: ' + errors.join(', ') : '')
834
- : 'Failed: ' + errors.join(', ');
835
-
836
- ctx.sendToServer({
837
- type: 'file_op_result', conversationId, _requestUserId,
838
- operation: 'upload', success: saved.length > 0,
839
- message, uploadedCount: saved.length, errorCount: errors.length
840
- });
841
- } catch (e) {
842
- ctx.sendToServer({
843
- type: 'file_op_result', conversationId, _requestUserId,
844
- operation: 'upload', success: false, error: e.message
845
- });
846
- }
847
- }
848
-
849
- // 临时文件目录名 (不易冲突)
850
- const TEMP_UPLOAD_DIR = '.claude-tmp-attachments';
851
-
852
- export async function handleTransferFiles(msg) {
853
- const { conversationId, files, prompt, workDir, claudeSessionId } = msg;
854
- const { startClaudeQuery } = await import('./claude.js');
855
-
856
- let state = ctx.conversations.get(conversationId);
857
- const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
858
-
859
- // 创建临时目录
860
- const uploadDir = join(effectiveWorkDir, TEMP_UPLOAD_DIR);
861
- if (!existsSync(uploadDir)) {
862
- mkdirSync(uploadDir, { recursive: true });
863
- }
864
-
865
- const savedFiles = [];
866
- const imageFiles = [];
867
-
868
- for (const file of files) {
869
- try {
870
- const timestamp = Date.now();
871
- const ext = extname(file.name);
872
- const baseName = basename(file.name, ext);
873
- const uniqueName = `${baseName}_${timestamp}${ext}`;
874
- const filePath = join(uploadDir, uniqueName);
875
- const relativePath = join(TEMP_UPLOAD_DIR, uniqueName);
876
-
877
- const buffer = Buffer.from(file.data, 'base64');
878
- writeFileSync(filePath, buffer);
879
-
880
- const isImage = file.mimeType.startsWith('image/');
881
- savedFiles.push({
882
- name: file.name,
883
- path: relativePath,
884
- mimeType: file.mimeType,
885
- isImage
886
- });
887
-
888
- if (isImage) {
889
- imageFiles.push({
890
- mimeType: file.mimeType,
891
- data: file.data
892
- });
893
- }
894
-
895
- console.log(`Saved file: ${relativePath}`);
896
- } catch (e) {
897
- console.error(`Error saving file ${file.name}:`, e.message);
898
- }
899
- }
900
-
901
- // 如果没有活跃的查询,启动新的
902
- if (!state || !state.query || !state.inputStream) {
903
- const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
904
- console.log(`[SDK] Starting Claude for ${conversationId} (files), resume: ${resumeSessionId || 'none'}`);
905
- state = await startClaudeQuery(conversationId, effectiveWorkDir, resumeSessionId);
906
- }
907
-
908
- // 构造带附件的消息
909
- const fileListText = savedFiles.map(f =>
910
- `- ${f.path} (${f.isImage ? '图片' : f.mimeType})`
911
- ).join('\n');
912
-
913
- const fullPrompt = `用户上传了以下文件:\n${fileListText}\n\n用户说:${prompt}`;
914
-
915
- // 构造 content 数组
916
- const content = [];
917
-
918
- for (const img of imageFiles) {
919
- content.push({
920
- type: 'image',
921
- source: {
922
- type: 'base64',
923
- media_type: img.mimeType,
924
- data: img.data
925
- }
926
- });
927
- }
928
-
929
- content.push({
930
- type: 'text',
931
- text: fullPrompt
932
- });
933
-
934
- // 发送用户消息到输入流
935
- const userMessage = {
936
- type: 'user',
937
- message: { role: 'user', content }
938
- };
939
-
940
- console.log(`[${conversationId}] Sending with ${savedFiles.length} files, ${imageFiles.length} images`);
941
- state.turnActive = true;
942
- state.inputStream.enqueue(userMessage);
943
- }
20
+ export { handleTransferFiles } from './workbench/transfer.js';