@lightcone-ai/daemon 0.14.12 → 0.14.14
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/publish-job-runner.js +130 -7
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync } from 'fs';
|
|
1
|
+
import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync, renameSync, rmSync, readdirSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getSession, closeSession } from '../mcp-servers/publisher/chrome-pool.js';
|
|
4
4
|
import { XhsAdapter } from '../mcp-servers/publisher/adapters/xhs.js';
|
|
@@ -26,6 +26,7 @@ const DEFAULT_MEDIA_LIMITS = {
|
|
|
26
26
|
douyin: { maxImages: 35, imageExts: ['.jpg', '.jpeg', '.png', '.webp'], videoExts: ['.mp4', '.mov', '.webm'] },
|
|
27
27
|
kuaishou: { maxImages: 12, imageExts: ['.jpg', '.jpeg', '.png'], videoExts: ['.mp4', '.mov'] },
|
|
28
28
|
};
|
|
29
|
+
const PARTIAL_FILE_RE = /\.partial-\d+-[a-z0-9]+$/;
|
|
29
30
|
|
|
30
31
|
function normalizeText(value) {
|
|
31
32
|
const normalized = String(value ?? '').trim();
|
|
@@ -130,6 +131,82 @@ function decodeWorkspaceContent(content) {
|
|
|
130
131
|
return Buffer.from(decodeURIComponent(body), 'utf8');
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function buildPartialFilePath(localPath) {
|
|
135
|
+
return `${localPath}.partial-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeExpectedSizeBytes(value) {
|
|
139
|
+
if (value == null) return null;
|
|
140
|
+
const size = Number(value);
|
|
141
|
+
if (!Number.isInteger(size) || size < 0) return null;
|
|
142
|
+
return size;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isWorkspaceCacheValid(localPath, expected = {}) {
|
|
146
|
+
if (!existsSync(localPath)) return false;
|
|
147
|
+
let stat = null;
|
|
148
|
+
try {
|
|
149
|
+
stat = statSync(localPath);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (!stat.isFile()) return false;
|
|
154
|
+
const expectedSize = normalizeExpectedSizeBytes(expected?.size_bytes);
|
|
155
|
+
if (expectedSize != null && stat.size !== expectedSize) return false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function writeFileAtomically(localPath, data, {
|
|
160
|
+
mkdirRecursive = mkdirSync,
|
|
161
|
+
writeFile = writeFileSync,
|
|
162
|
+
renameFile = renameSync,
|
|
163
|
+
removeFile = rmSync,
|
|
164
|
+
} = {}) {
|
|
165
|
+
mkdirRecursive(path.dirname(localPath), { recursive: true });
|
|
166
|
+
const tmpPath = buildPartialFilePath(localPath);
|
|
167
|
+
try {
|
|
168
|
+
writeFile(tmpPath, data);
|
|
169
|
+
renameFile(tmpPath, localPath);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
try { removeFile(tmpPath, { force: true }); } catch {}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return localPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function removePartialFilesRecursively(rootDir) {
|
|
178
|
+
const stack = [rootDir];
|
|
179
|
+
while (stack.length > 0) {
|
|
180
|
+
const current = stack.pop();
|
|
181
|
+
let entries = null;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const entryPath = path.join(current, entry.name);
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
stack.push(entryPath);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!entry.isFile()) continue;
|
|
194
|
+
if (!PARTIAL_FILE_RE.test(entry.name)) continue;
|
|
195
|
+
try { rmSync(entryPath, { force: true }); } catch {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function cleanupWorkspacePartialFiles(workspaceRootDir) {
|
|
201
|
+
const normalizedRoot = String(workspaceRootDir ?? '').trim();
|
|
202
|
+
if (!normalizedRoot) return;
|
|
203
|
+
const scanRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(normalizedRoot, dir));
|
|
204
|
+
for (const root of scanRoots) {
|
|
205
|
+
if (!existsSync(root)) continue;
|
|
206
|
+
removePartialFilesRecursively(root);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
133
210
|
export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, jobId, relPath }) {
|
|
134
211
|
const normalizedJobId = String(jobId ?? '').trim();
|
|
135
212
|
if (!normalizedJobId) {
|
|
@@ -148,6 +225,37 @@ export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, j
|
|
|
148
225
|
return res.json();
|
|
149
226
|
}
|
|
150
227
|
|
|
228
|
+
export async function streamPublishJobVideo({
|
|
229
|
+
serverUrl,
|
|
230
|
+
machineApiKey,
|
|
231
|
+
jobId,
|
|
232
|
+
videoId,
|
|
233
|
+
localPath,
|
|
234
|
+
writeBinaryFile = writeFileAtomically,
|
|
235
|
+
}) {
|
|
236
|
+
const normalizedJobId = String(jobId ?? '').trim();
|
|
237
|
+
if (!normalizedJobId) {
|
|
238
|
+
throw new Error('publish job id is required to fetch video files');
|
|
239
|
+
}
|
|
240
|
+
const normalizedVideoId = String(videoId ?? '').trim();
|
|
241
|
+
if (!normalizedVideoId) {
|
|
242
|
+
throw new Error('video id is required to fetch video files');
|
|
243
|
+
}
|
|
244
|
+
const url = `${String(serverUrl).replace(/\/$/, '')}/internal/agent/publish-jobs/${encodeURIComponent(normalizedJobId)}/video-files/${encodeURIComponent(normalizedVideoId)}`;
|
|
245
|
+
const res = await fetch(url, {
|
|
246
|
+
headers: {
|
|
247
|
+
Authorization: `Bearer ${machineApiKey}`,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
if (!res.ok) {
|
|
251
|
+
const text = await res.text().catch(() => '');
|
|
252
|
+
throw new Error(`publish-jobs video-files GET failed (${res.status}): ${text}`);
|
|
253
|
+
}
|
|
254
|
+
const data = Buffer.from(await res.arrayBuffer());
|
|
255
|
+
writeBinaryFile(localPath, data);
|
|
256
|
+
return localPath;
|
|
257
|
+
}
|
|
258
|
+
|
|
151
259
|
export function workspacePathFromMediaPath(filePath, workspaceId) {
|
|
152
260
|
if (!filePath) return null;
|
|
153
261
|
|
|
@@ -182,11 +290,16 @@ export async function materializeWorkspaceMedia({
|
|
|
182
290
|
machineApiKey,
|
|
183
291
|
jobId,
|
|
184
292
|
fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
|
|
293
|
+
streamVideoFile = streamPublishJobVideo,
|
|
294
|
+
writeBinaryFile = writeFileAtomically,
|
|
185
295
|
}) {
|
|
186
|
-
if (!filePath
|
|
296
|
+
if (!filePath) return filePath;
|
|
187
297
|
|
|
188
298
|
const workspacePath = workspacePathFromMediaPath(filePath, workspaceId);
|
|
189
|
-
if (!workspacePath?.workspaceId || !workspacePath.relPath)
|
|
299
|
+
if (!workspacePath?.workspaceId || !workspacePath.relPath) {
|
|
300
|
+
if (existsSync(filePath)) return filePath;
|
|
301
|
+
return filePath;
|
|
302
|
+
}
|
|
190
303
|
|
|
191
304
|
const localPath = path.resolve(workspaceRootDir, workspacePath.relPath);
|
|
192
305
|
const allowedRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(workspaceRootDir, dir));
|
|
@@ -194,16 +307,25 @@ export async function materializeWorkspaceMedia({
|
|
|
194
307
|
throw new Error(`workspace media path is outside allowed workspace directories: ${filePath}`);
|
|
195
308
|
}
|
|
196
309
|
|
|
197
|
-
if (existsSync(localPath)) return localPath;
|
|
198
|
-
|
|
199
310
|
const data = await fetchWorkspaceFile({
|
|
200
311
|
serverUrl,
|
|
201
312
|
machineApiKey,
|
|
202
313
|
jobId,
|
|
203
314
|
relPath: workspacePath.relPath,
|
|
204
315
|
});
|
|
205
|
-
|
|
206
|
-
|
|
316
|
+
if (isWorkspaceCacheValid(localPath, data)) return localPath;
|
|
317
|
+
|
|
318
|
+
if (data?.videoId) {
|
|
319
|
+
await streamVideoFile({
|
|
320
|
+
serverUrl,
|
|
321
|
+
machineApiKey,
|
|
322
|
+
jobId,
|
|
323
|
+
videoId: data.videoId,
|
|
324
|
+
localPath,
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
|
|
328
|
+
}
|
|
207
329
|
return localPath;
|
|
208
330
|
}
|
|
209
331
|
|
|
@@ -217,6 +339,7 @@ async function materializeMedia({
|
|
|
217
339
|
machineApiKey,
|
|
218
340
|
jobId,
|
|
219
341
|
}) {
|
|
342
|
+
cleanupWorkspacePartialFiles(workspaceRootDir);
|
|
220
343
|
return {
|
|
221
344
|
images: await Promise.all(images.map(filePath => materializeWorkspaceMedia({
|
|
222
345
|
filePath,
|