@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/connection/buffer.js +5 -4
- package/connection/message-router.js +9 -1
- package/index.js +1 -1
- package/local-runtime/server/handlers/agent-file-terminal.js +23 -5
- package/local-runtime/server/handlers/client-workbench.js +19 -4
- package/local-runtime/server/routes/upload-routes.js +96 -1
- package/local-runtime/server/workbench-preview.js +94 -3
- package/local-runtime/server/ws-agent.js +1 -1
- package/local-runtime/server/ws-client.js +1 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +125 -100
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/workbench/file-ops.js +97 -2
- package/workbench/utils.js +5 -0
- package/workbench.js +1 -1
|
Binary file
|
package/package.json
CHANGED
package/workbench/file-ops.js
CHANGED
|
@@ -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) {
|
package/workbench/utils.js
CHANGED
|
@@ -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
|
|