@yeaft/webchat-agent 1.0.647 → 1.0.649

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.647",
3
+ "version": "1.0.649",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,6 +12,26 @@ export const MAX_WORKBENCH_PREVIEW_BYTES = 20 * 1024 * 1024;
12
12
  export const WORKBENCH_FILE_CHUNK_BYTES = 1024 * 1024;
13
13
  export const WORKBENCH_VIDEO_CHUNK_BYTES = 1024 * 1024;
14
14
 
15
+ // Unknown extensions may be binary. Never open lossy UTF-8 or control bytes in
16
+ // an editable buffer: saving that buffer could irreversibly corrupt the file.
17
+ export function isBinaryFileContent(buffer) {
18
+ if (buffer.some(byte => byte === 0 || (byte < 32 && ![9, 10, 12, 13].includes(byte)))) return true;
19
+ try {
20
+ new TextDecoder('utf-8', { fatal: true }).decode(buffer);
21
+ return false;
22
+ } catch {
23
+ return true;
24
+ }
25
+ }
26
+
27
+ function assertPreviewSize(size) {
28
+ if (size <= MAX_WORKBENCH_PREVIEW_BYTES) return;
29
+ const error = new Error(`File is too large to transfer (${(size / 1024 / 1024).toFixed(1)} MB). The file limit is 20 MB.`);
30
+ error.code = 'FILE_PREVIEW_TOO_LARGE';
31
+ error.details = { sizeBytes: size, limitBytes: MAX_WORKBENCH_PREVIEW_BYTES };
32
+ throw error;
33
+ }
34
+
15
35
  async function canonicalWorkspaceReadPath(filePath, workDir, errorMessage = 'File is outside the active workspace.') {
16
36
  const canonicalRoot = await realpath(resolve(workDir));
17
37
  const canonicalFile = await realpath(resolveAndValidatePath(filePath, canonicalRoot));
@@ -199,20 +219,33 @@ export async function handleReadFile(msg) {
199
219
  error.code = 'VIDEO_STREAM_REQUIRED';
200
220
  throw error;
201
221
  }
202
- const mimeType = BINARY_EXTENSIONS[ext];
203
-
222
+ let mimeType = BINARY_EXTENSIONS[ext];
223
+ const fileStat = await stat(resolved);
224
+ if (!fileStat.isFile()) throw new Error('Path is not a regular file.');
225
+ if (mimeType) assertPreviewSize(fileStat.size);
226
+ // Keep the existing text-file size behavior. Unknown binary files are
227
+ // classified from a bounded prefix before allocating their full contents.
228
+ if (!mimeType) {
229
+ const handle = await open(resolved, 'r');
230
+ try {
231
+ const sample = Buffer.alloc(Math.min(fileStat.size, 8192));
232
+ const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
233
+ // Do not mistake a UTF-8 character split at the sample boundary for
234
+ // binary. Streaming decoding still rejects invalid bytes within it.
235
+ const prefix = sample.subarray(0, bytesRead);
236
+ let invalidUtf8 = false;
237
+ try { new TextDecoder('utf-8', { fatal: true }).decode(prefix, { stream: bytesRead < fileStat.size }); }
238
+ catch { invalidUtf8 = true; }
239
+ if (invalidUtf8 || prefix.some(byte => byte === 0 || (byte < 32 && ![9, 10, 12, 13].includes(byte)))) {
240
+ mimeType = 'application/octet-stream';
241
+ assertPreviewSize(fileStat.size);
242
+ }
243
+ } finally { await handle.close(); }
244
+ }
245
+ const buffer = await readFile(resolved);
246
+ if (!mimeType && isBinaryFileContent(buffer)) mimeType = 'application/octet-stream';
204
247
  if (mimeType) {
205
- const fileStat = await stat(resolved);
206
- if (fileStat.size > MAX_WORKBENCH_PREVIEW_BYTES) {
207
- const error = new Error(`File is too large to transfer (${(fileStat.size / 1024 / 1024).toFixed(1)} MB). The file limit is 20 MB.`);
208
- error.code = 'FILE_PREVIEW_TOO_LARGE';
209
- error.details = {
210
- sizeBytes: fileStat.size,
211
- limitBytes: MAX_WORKBENCH_PREVIEW_BYTES,
212
- };
213
- throw error;
214
- }
215
- const buffer = await readFile(resolved);
248
+ assertPreviewSize(buffer.length);
216
249
  console.log('[Agent] Sending binary file_content:', { filePath: resolved, size: buffer.length, mimeType, conversationId });
217
250
  await sendBinaryFile(msg, {
218
251
  conversationId,
@@ -223,8 +256,7 @@ export async function handleReadFile(msg) {
223
256
  requestedFilePath: filePath,
224
257
  }, buffer, mimeType);
225
258
  } else {
226
- // Text file: read as utf-8
227
- const content = await readFile(resolved, 'utf-8');
259
+ const content = buffer.toString('utf-8');
228
260
 
229
261
  // 检测语言
230
262
  const langMap = {
@@ -30,6 +30,14 @@ export function validateGitPath(filePath) {
30
30
  // Binary file extensions → MIME type mapping
31
31
  export const BINARY_EXTENSIONS = {
32
32
  '.pdf': 'application/pdf',
33
+ '.doc': 'application/msword',
34
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.m4a': 'audio/mp4',
35
+ '.flac': 'audio/flac', '.aac': 'audio/aac',
36
+ // Unsupported formats are downloads, never decoded into the text editor.
37
+ ...Object.fromEntries(['.zip', '.gz', '.bz2', '.xz', '.7z', '.rar', '.tar',
38
+ '.exe', '.dll', '.so', '.dylib', '.wasm', '.bin', '.sqlite', '.sqlite3', '.db',
39
+ '.woff', '.woff2', '.ttf', '.otf', '.avif', '.heic', '.tif', '.tiff']
40
+ .map(ext => [ext, 'application/octet-stream'])),
33
41
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
34
42
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
35
43
  '.xls': 'application/vnd.ms-excel',