@lightcone-ai/daemon 0.15.49 → 0.15.51
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 +1 -1
- package/src/chat-bridge.js +10 -36
- package/src/publish-job-runner.js +24 -43
- package/src/workspace-file-upload.js +84 -32
package/package.json
CHANGED
package/src/chat-bridge.js
CHANGED
|
@@ -6,11 +6,7 @@ import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync }
|
|
|
6
6
|
import { createHash, randomUUID } from 'crypto';
|
|
7
7
|
import path, { extname } from 'path';
|
|
8
8
|
import { recordUrlNarration } from './_vendor/video/recorder/index.js';
|
|
9
|
-
import {
|
|
10
|
-
VIDEO_EXT,
|
|
11
|
-
VIDEO_MIME,
|
|
12
|
-
writeLocalFileToWorkspace,
|
|
13
|
-
} from './workspace-file-upload.js';
|
|
9
|
+
import { writeLocalFileToWorkspace } from './workspace-file-upload.js';
|
|
14
10
|
import { runRecordUrlNarrationTool } from './record-url-narration-tool.js';
|
|
15
11
|
import { runSubmitToLibraryTool } from './submit-to-library-tool.js';
|
|
16
12
|
import { runRenderTextToImageTool } from './tools/render-text-to-image.js';
|
|
@@ -491,7 +487,7 @@ async function directApi(method, apiPath, body) {
|
|
|
491
487
|
async function directApiVideoUpload(apiPath, {
|
|
492
488
|
localPath,
|
|
493
489
|
filename,
|
|
494
|
-
contentType =
|
|
490
|
+
contentType = 'video/mp4',
|
|
495
491
|
}) {
|
|
496
492
|
const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${apiPath}`;
|
|
497
493
|
const headers = {
|
|
@@ -1090,44 +1086,22 @@ server.tool('write_workspace_file', 'Write a local file directly to the shared w
|
|
|
1090
1086
|
uploadWorkspaceMemory: async ({ workspacePath, workspaceId, content }) => {
|
|
1091
1087
|
await api('PUT', `/workspace-memory?path=${encodeURIComponent(workspacePath)}&workspaceId=${encodeURIComponent(workspaceId)}`, { content });
|
|
1092
1088
|
},
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
mime,
|
|
1099
|
-
}) => {
|
|
1100
|
-
const uploadResult = await runMandatoryLocalTool({
|
|
1101
|
-
toolName: 'write_workspace_file',
|
|
1102
|
-
toolInput: {
|
|
1103
|
-
file_path,
|
|
1104
|
-
path: workspacePath,
|
|
1105
|
-
upload_mode: 'upload_video',
|
|
1106
|
-
},
|
|
1107
|
-
executor: async () => directApiVideoUpload(
|
|
1108
|
-
`/upload-video?workspaceId=${encodeURIComponent(workspaceId)}&filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(workspacePath)}`,
|
|
1109
|
-
{
|
|
1110
|
-
localPath: videoLocalPath,
|
|
1111
|
-
filename,
|
|
1112
|
-
contentType: mime,
|
|
1113
|
-
}
|
|
1114
|
-
),
|
|
1115
|
-
});
|
|
1116
|
-
return {
|
|
1117
|
-
mode: 'upload-video',
|
|
1118
|
-
mime: VIDEO_MIME,
|
|
1119
|
-
bytes: Number(uploadResult?.sizeBytes) || 0,
|
|
1120
|
-
};
|
|
1089
|
+
presign: async ({ workspaceId, path: filePath, size, mime, sha256 }) => {
|
|
1090
|
+
return api('POST', '/storage/presign', { workspaceId, path: filePath, size, mime, sha256 });
|
|
1091
|
+
},
|
|
1092
|
+
confirmUpload: async ({ workspaceId, path: filePath, objectKey }) => {
|
|
1093
|
+
await api('POST', '/storage/confirm', { workspaceId, path: filePath, objectKey });
|
|
1121
1094
|
},
|
|
1122
1095
|
});
|
|
1123
1096
|
|
|
1124
|
-
const finalMime = result?.mime ??
|
|
1097
|
+
const finalMime = result?.mime ?? 'application/octet-stream';
|
|
1125
1098
|
const finalBytes = Number.isFinite(result?.bytes) ? result.bytes : 0;
|
|
1126
1099
|
const sizeText = finalBytes > 0 ? formatBytes(finalBytes) : 'unknown size';
|
|
1100
|
+
const asyncNote = result?.async ? ' (uploading in background)' : '';
|
|
1127
1101
|
return {
|
|
1128
1102
|
content: [{
|
|
1129
1103
|
type: 'text',
|
|
1130
|
-
text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})`,
|
|
1104
|
+
text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})${asyncNote}`,
|
|
1131
1105
|
}],
|
|
1132
1106
|
};
|
|
1133
1107
|
});
|
|
@@ -229,37 +229,6 @@ export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, j
|
|
|
229
229
|
return res.json();
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
export async function streamPublishJobVideo({
|
|
233
|
-
serverUrl,
|
|
234
|
-
machineApiKey,
|
|
235
|
-
jobId,
|
|
236
|
-
videoId,
|
|
237
|
-
localPath,
|
|
238
|
-
writeBinaryFile = writeFileAtomically,
|
|
239
|
-
}) {
|
|
240
|
-
const normalizedJobId = String(jobId ?? '').trim();
|
|
241
|
-
if (!normalizedJobId) {
|
|
242
|
-
throw new Error('publish job id is required to fetch video files');
|
|
243
|
-
}
|
|
244
|
-
const normalizedVideoId = String(videoId ?? '').trim();
|
|
245
|
-
if (!normalizedVideoId) {
|
|
246
|
-
throw new Error('video id is required to fetch video files');
|
|
247
|
-
}
|
|
248
|
-
const url = `${String(serverUrl).replace(/\/$/, '')}/internal/agent/publish-jobs/${encodeURIComponent(normalizedJobId)}/video-files/${encodeURIComponent(normalizedVideoId)}`;
|
|
249
|
-
const res = await fetch(url, {
|
|
250
|
-
headers: {
|
|
251
|
-
Authorization: `Bearer ${machineApiKey}`,
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
if (!res.ok) {
|
|
255
|
-
const text = await res.text().catch(() => '');
|
|
256
|
-
throw new Error(`publish-jobs video-files GET failed (${res.status}): ${text}`);
|
|
257
|
-
}
|
|
258
|
-
const data = Buffer.from(await res.arrayBuffer());
|
|
259
|
-
writeBinaryFile(localPath, data);
|
|
260
|
-
return localPath;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
232
|
export function workspacePathFromMediaPath(filePath, workspaceId) {
|
|
264
233
|
if (!filePath) return null;
|
|
265
234
|
|
|
@@ -294,7 +263,6 @@ export async function materializeWorkspaceMedia({
|
|
|
294
263
|
machineApiKey,
|
|
295
264
|
jobId,
|
|
296
265
|
fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
|
|
297
|
-
streamVideoFile = streamPublishJobVideo,
|
|
298
266
|
writeBinaryFile = writeFileAtomically,
|
|
299
267
|
}) {
|
|
300
268
|
if (!filePath) return filePath;
|
|
@@ -317,20 +285,33 @@ export async function materializeWorkspaceMedia({
|
|
|
317
285
|
jobId,
|
|
318
286
|
relPath: workspacePath.relPath,
|
|
319
287
|
});
|
|
320
|
-
if (isWorkspaceCacheValid(localPath, data)) return localPath;
|
|
321
288
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
289
|
+
// New: signed URL from object storage
|
|
290
|
+
if (data?.signedUrl) {
|
|
291
|
+
// Use sha256-based cache key in tmp/cache to avoid duplicate downloads
|
|
292
|
+
const cacheDir = path.join(workspaceRootDir, 'tmp', 'cache');
|
|
293
|
+
const ext = path.extname(workspacePath.relPath) || '';
|
|
294
|
+
const cacheKey = data.object_hash ?? data.size_bytes?.toString() ?? 'nokey';
|
|
295
|
+
const cachePath = path.join(cacheDir, `${cacheKey}${ext}`);
|
|
296
|
+
|
|
297
|
+
if (existsSync(cachePath)) return cachePath;
|
|
298
|
+
|
|
299
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
300
|
+
const res = await fetch(data.signedUrl);
|
|
301
|
+
if (!res.ok) throw new Error(`Failed to download from signed URL: ${res.status}`);
|
|
302
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
303
|
+
writeFileAtomically(cachePath, buf);
|
|
304
|
+
return cachePath;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Legacy: text content in DB
|
|
308
|
+
if (data?.content != null) {
|
|
309
|
+
if (isWorkspaceCacheValid(localPath, data)) return localPath;
|
|
331
310
|
writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
|
|
311
|
+
return localPath;
|
|
332
312
|
}
|
|
333
|
-
|
|
313
|
+
|
|
314
|
+
throw new Error(`No content or signedUrl in workspace file response for: ${filePath}`);
|
|
334
315
|
}
|
|
335
316
|
|
|
336
317
|
async function materializeMedia({
|
|
@@ -1,36 +1,89 @@
|
|
|
1
1
|
import path, { extname } from 'node:path';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, createReadStream } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
export const WORKSPACE_BINARY_MIME = {
|
|
5
|
-
'.
|
|
6
|
-
'.
|
|
6
|
+
'.mp4': 'video/mp4',
|
|
7
|
+
'.png': 'image/png',
|
|
8
|
+
'.jpg': 'image/jpeg',
|
|
7
9
|
'.jpeg': 'image/jpeg',
|
|
8
10
|
'.webp': 'image/webp',
|
|
9
|
-
'.gif':
|
|
10
|
-
'.pdf':
|
|
11
|
+
'.gif': 'image/gif',
|
|
12
|
+
'.pdf': 'application/pdf',
|
|
13
|
+
'.mp3': 'audio/mpeg',
|
|
14
|
+
'.wav': 'audio/wav',
|
|
15
|
+
'.m4a': 'audio/mp4',
|
|
11
16
|
};
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
const TEXT_EXTS = new Set([
|
|
19
|
+
'.md', '.txt', '.json', '.js', '.ts', '.html', '.css',
|
|
20
|
+
'.csv', '.xml', '.yaml', '.yml', '.sh',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
|
|
15
24
|
|
|
16
25
|
export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
|
|
17
26
|
const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
|
|
18
27
|
const ext = extname(target).toLowerCase();
|
|
19
28
|
const filename = path.basename(target || localPath);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
|
|
30
|
+
const isText = TEXT_EXTS.has(ext);
|
|
31
|
+
return { ext, filename, mime, isText };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sha256ofBuffer(buf) {
|
|
35
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Upload a local file to the workspace via the new presign → PUT → confirm flow.
|
|
40
|
+
* For large files (>= 5MB), presign returns immediately (DB status = uploading)
|
|
41
|
+
* and the PUT + confirm happen in the background.
|
|
42
|
+
*/
|
|
43
|
+
async function uploadBinaryFile({
|
|
44
|
+
localPath,
|
|
45
|
+
workspacePath,
|
|
46
|
+
workspaceId,
|
|
47
|
+
mime,
|
|
48
|
+
presign, // async fn({ workspaceId, path, size, mime, sha256 }) → { uploadUrl, objectKey, alreadyExists }
|
|
49
|
+
confirmUpload, // async fn({ workspaceId, path, objectKey }) → void
|
|
50
|
+
}) {
|
|
51
|
+
const buf = readFileSync(localPath);
|
|
52
|
+
const size = buf.length;
|
|
53
|
+
const sha256 = sha256ofBuffer(buf);
|
|
54
|
+
|
|
55
|
+
const { uploadUrl, objectKey, alreadyExists } = await presign({
|
|
56
|
+
workspaceId,
|
|
57
|
+
path: workspacePath,
|
|
58
|
+
size,
|
|
59
|
+
mime,
|
|
60
|
+
sha256,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (alreadyExists) {
|
|
64
|
+
return { mode: 'object-storage', mime, bytes: size, objectKey, deduped: true };
|
|
27
65
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
|
|
67
|
+
const isLarge = size >= LARGE_FILE_THRESHOLD;
|
|
68
|
+
|
|
69
|
+
const doPut = async () => {
|
|
70
|
+
const resp = await fetch(uploadUrl, {
|
|
71
|
+
method: 'PUT',
|
|
72
|
+
body: buf,
|
|
73
|
+
headers: { 'Content-Type': mime, 'Content-Length': String(size) },
|
|
74
|
+
});
|
|
75
|
+
if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
|
|
76
|
+
await confirmUpload({ workspaceId, path: workspacePath, objectKey });
|
|
33
77
|
};
|
|
78
|
+
|
|
79
|
+
if (isLarge) {
|
|
80
|
+
// Fire-and-forget for large files; server already wrote `uploading` status
|
|
81
|
+
doPut().catch(err => console.error('[workspace-upload] large file background upload failed:', err?.message));
|
|
82
|
+
return { mode: 'object-storage', mime, bytes: size, objectKey, async: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await doPut();
|
|
86
|
+
return { mode: 'object-storage', mime, bytes: size, objectKey, async: false };
|
|
34
87
|
}
|
|
35
88
|
|
|
36
89
|
export async function writeLocalFileToWorkspace({
|
|
@@ -38,34 +91,33 @@ export async function writeLocalFileToWorkspace({
|
|
|
38
91
|
workspacePath,
|
|
39
92
|
workspaceId,
|
|
40
93
|
readFileSyncFn = readFileSync,
|
|
41
|
-
uploadWorkspaceMemory,
|
|
42
|
-
|
|
94
|
+
uploadWorkspaceMemory, // legacy text path
|
|
95
|
+
presign,
|
|
96
|
+
confirmUpload,
|
|
43
97
|
}) {
|
|
44
98
|
const plan = resolveWorkspaceFileUploadPlan({ localPath, workspacePath });
|
|
45
|
-
|
|
46
|
-
|
|
99
|
+
|
|
100
|
+
if (!plan.isText) {
|
|
101
|
+
return uploadBinaryFile({
|
|
47
102
|
localPath,
|
|
48
103
|
workspacePath,
|
|
49
104
|
workspaceId,
|
|
50
|
-
filename: plan.filename,
|
|
51
105
|
mime: plan.mime,
|
|
52
|
-
|
|
106
|
+
presign,
|
|
107
|
+
confirmUpload,
|
|
53
108
|
});
|
|
54
109
|
}
|
|
55
110
|
|
|
111
|
+
// Text files: existing path via workspace-memory
|
|
56
112
|
const buf = readFileSyncFn(localPath);
|
|
57
|
-
const content =
|
|
113
|
+
const content = buf.toString('utf8');
|
|
58
114
|
await uploadWorkspaceMemory({
|
|
59
115
|
workspacePath,
|
|
60
116
|
workspaceId,
|
|
61
117
|
content,
|
|
62
118
|
mime: plan.mime,
|
|
63
119
|
bytes: buf.length,
|
|
64
|
-
mode:
|
|
120
|
+
mode: 'workspace-memory',
|
|
65
121
|
});
|
|
66
|
-
return {
|
|
67
|
-
mode: plan.mode,
|
|
68
|
-
mime: plan.mime,
|
|
69
|
-
bytes: buf.length,
|
|
70
|
-
};
|
|
122
|
+
return { mode: 'workspace-memory', mime: plan.mime, bytes: buf.length };
|
|
71
123
|
}
|