@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.14.12",
3
+ "version": "0.14.14",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 || existsSync(filePath)) return filePath;
296
+ if (!filePath) return filePath;
187
297
 
188
298
  const workspacePath = workspacePathFromMediaPath(filePath, workspaceId);
189
- if (!workspacePath?.workspaceId || !workspacePath.relPath) return filePath;
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
- mkdirSync(path.dirname(localPath), { recursive: true });
206
- writeFileSync(localPath, decodeWorkspaceContent(data.content));
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,