@yeaft/webchat-agent 1.0.522 → 1.0.523

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.522",
3
+ "version": "1.0.523",
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",
@@ -1,13 +1,14 @@
1
- import { readFile, realpath, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
1
+ import { open, readFile, realpath, writeFile, readdir, stat, unlink, rename, mkdir, rm, copyFile, cp } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
3
  import { join, basename, dirname, extname, isAbsolute, relative, resolve } from 'path';
4
4
  import { platform } from 'os';
5
5
  import ctx from '../context.js';
6
- import { resolveAndValidatePath, BINARY_EXTENSIONS } from './utils.js';
6
+ import { resolveAndValidatePath, BINARY_EXTENSIONS, VIDEO_EXTENSIONS } from './utils.js';
7
7
  import { sendWorkbenchResult } from './request-routing.js';
8
8
 
9
9
  export const MAX_WORKBENCH_PREVIEW_BYTES = 20 * 1024 * 1024;
10
10
  export const WORKBENCH_FILE_CHUNK_BYTES = 1024 * 1024;
11
+ export const WORKBENCH_VIDEO_CHUNK_BYTES = 1024 * 1024;
11
12
 
12
13
  async function validateResponseImagePath(filePath, workDir) {
13
14
  const canonicalRoot = await realpath(resolve(workDir));
@@ -22,6 +23,95 @@ async function validateResponseImagePath(filePath, workDir) {
22
23
  return canonicalFile;
23
24
  }
24
25
 
26
+ async function canonicalVideoFile(filePath, workDir) {
27
+ const canonicalRoot = await realpath(resolve(workDir));
28
+ const resolved = await realpath(resolveAndValidatePath(filePath, canonicalRoot));
29
+ const relativePath = relative(canonicalRoot, resolved);
30
+ const outside = relativePath === '..'
31
+ || relativePath.startsWith(`..${platform() === 'win32' ? '\\' : '/'}`)
32
+ || isAbsolute(relativePath);
33
+ if (outside) throw new Error('Video is outside the active workspace.');
34
+ const mimeType = VIDEO_EXTENSIONS[extname(resolved).toLowerCase()];
35
+ if (!mimeType) throw new Error('Unsupported video format.');
36
+ return { resolved, mimeType };
37
+ }
38
+
39
+ export async function handleVideoMetadata(msg) {
40
+ const { conversationId, filePath, requestId, _requestUserId, _requestClientId } = msg;
41
+ try {
42
+ const { resolved, mimeType } = await canonicalVideoFile(filePath, msg.workDir || ctx.CONFIG.workDir);
43
+ const handle = await open(resolved, 'r');
44
+ try {
45
+ const fileStat = await handle.stat();
46
+ if (!fileStat.isFile() || !Number.isSafeInteger(fileStat.size)) throw new Error('Video path is not a supported file.');
47
+ await sendWorkbenchResult(ctx, msg, {
48
+ type: 'video_metadata', conversationId, requestId, _requestUserId, _requestClientId,
49
+ filePath: resolved, requestedFilePath: filePath, size: fileStat.size, mimeType,
50
+ mtimeMs: fileStat.mtimeMs,
51
+ });
52
+ } finally {
53
+ await handle.close();
54
+ }
55
+ } catch (error) {
56
+ await sendWorkbenchResult(ctx, msg, {
57
+ type: 'video_metadata', conversationId, requestId, _requestUserId, _requestClientId,
58
+ filePath, requestedFilePath: filePath, error: error.message,
59
+ errorCode: error.code || 'VIDEO_METADATA_FAILED',
60
+ });
61
+ }
62
+ }
63
+
64
+ export async function handleVideoChunk(msg) {
65
+ const { conversationId, filePath, requestId, _requestUserId, _requestClientId } = msg;
66
+ let handle;
67
+ try {
68
+ const { resolved, mimeType } = await canonicalVideoFile(filePath, msg.workDir || ctx.CONFIG.workDir);
69
+ handle = await open(resolved, 'r');
70
+ const fileStat = await handle.stat();
71
+ if (!fileStat.isFile() || !Number.isSafeInteger(fileStat.size)) throw new Error('Video path is not a supported file.');
72
+ const start = Number(msg.start);
73
+ const requestedEnd = Number(msg.end);
74
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(requestedEnd)
75
+ || start < 0 || requestedEnd < start || start >= fileStat.size) {
76
+ const error = new Error('Invalid video byte range.');
77
+ error.code = 'VIDEO_RANGE_INVALID';
78
+ throw error;
79
+ }
80
+ if (Number(msg.expectedSize) !== fileStat.size || Number(msg.expectedMtimeMs) !== fileStat.mtimeMs) {
81
+ const error = new Error('Video changed since the preview was opened.');
82
+ error.code = 'VIDEO_FILE_CHANGED';
83
+ throw error;
84
+ }
85
+ const end = Math.min(requestedEnd, fileStat.size - 1);
86
+ if ((end - start + 1) > WORKBENCH_VIDEO_CHUNK_BYTES) {
87
+ const error = new Error('Video byte range exceeds the chunk limit.');
88
+ error.code = 'VIDEO_RANGE_TOO_LARGE';
89
+ throw error;
90
+ }
91
+ const buffer = Buffer.alloc(end - start + 1);
92
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
93
+ if (bytesRead !== buffer.length) {
94
+ const error = new Error('Video changed while it was being read.');
95
+ error.code = 'VIDEO_FILE_CHANGED';
96
+ throw error;
97
+ }
98
+ const outcome = await sendWorkbenchResult(ctx, msg, {
99
+ type: 'video_chunk', conversationId, requestId, _requestUserId, _requestClientId,
100
+ filePath: resolved, requestedFilePath: filePath, start, end, size: fileStat.size,
101
+ mtimeMs: fileStat.mtimeMs, mimeType, content: buffer.toString('base64'),
102
+ });
103
+ if (outcome === 'dropped') console.warn('[Agent] Video chunk was dropped before delivery');
104
+ } catch (error) {
105
+ await sendWorkbenchResult(ctx, msg, {
106
+ type: 'video_chunk', conversationId, requestId, _requestUserId, _requestClientId,
107
+ filePath, requestedFilePath: filePath, start: msg.start, end: msg.end,
108
+ error: error.message, errorCode: error.code || 'VIDEO_CHUNK_FAILED',
109
+ });
110
+ } finally {
111
+ await handle?.close();
112
+ }
113
+ }
114
+
25
115
  async function reportFileTransferDropped(msg, base) {
26
116
  const errorResult = {
27
117
  ...base,
@@ -81,6 +171,11 @@ export async function handleReadFile(msg) {
81
171
  ? await validateResponseImagePath(filePath, workDir)
82
172
  : resolveAndValidatePath(filePath, workDir);
83
173
  const ext = extname(resolved).toLowerCase();
174
+ if (VIDEO_EXTENSIONS[ext]) {
175
+ const error = new Error('Video files require the Workbench streaming protocol.');
176
+ error.code = 'VIDEO_STREAM_REQUIRED';
177
+ throw error;
178
+ }
84
179
  const mimeType = BINARY_EXTENSIONS[ext];
85
180
 
86
181
  if (mimeType) {
@@ -39,3 +39,8 @@ export const BINARY_EXTENSIONS = {
39
39
  '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
40
40
  '.ico': 'image/x-icon'
41
41
  };
42
+
43
+ export const VIDEO_EXTENSIONS = {
44
+ '.mp4': 'video/mp4', '.m4v': 'video/x-m4v', '.webm': 'video/webm',
45
+ '.ogv': 'video/ogg', '.ogg': 'video/ogg', '.mov': 'video/quicktime'
46
+ };
package/workbench.js CHANGED
@@ -6,7 +6,7 @@
6
6
  export { resolveAndValidatePath, getGitRoot, validateGitPath } from './workbench/utils.js';
7
7
 
8
8
  export {
9
- handleReadFile, handleWriteFile, handleListDirectory,
9
+ handleReadFile, handleVideoMetadata, handleVideoChunk, handleWriteFile, handleListDirectory,
10
10
  handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir
11
11
  } from './workbench/file-ops.js';
12
12