@lightcone-ai/daemon 0.14.13 → 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.13",
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,7 +225,14 @@ export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, j
148
225
  return res.json();
149
226
  }
150
227
 
151
- export async function streamPublishJobVideo({ serverUrl, machineApiKey, jobId, videoId, localPath }) {
228
+ export async function streamPublishJobVideo({
229
+ serverUrl,
230
+ machineApiKey,
231
+ jobId,
232
+ videoId,
233
+ localPath,
234
+ writeBinaryFile = writeFileAtomically,
235
+ }) {
152
236
  const normalizedJobId = String(jobId ?? '').trim();
153
237
  if (!normalizedJobId) {
154
238
  throw new Error('publish job id is required to fetch video files');
@@ -168,8 +252,7 @@ export async function streamPublishJobVideo({ serverUrl, machineApiKey, jobId, v
168
252
  throw new Error(`publish-jobs video-files GET failed (${res.status}): ${text}`);
169
253
  }
170
254
  const data = Buffer.from(await res.arrayBuffer());
171
- mkdirSync(path.dirname(localPath), { recursive: true });
172
- writeFileSync(localPath, data);
255
+ writeBinaryFile(localPath, data);
173
256
  return localPath;
174
257
  }
175
258
 
@@ -208,11 +291,15 @@ export async function materializeWorkspaceMedia({
208
291
  jobId,
209
292
  fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
210
293
  streamVideoFile = streamPublishJobVideo,
294
+ writeBinaryFile = writeFileAtomically,
211
295
  }) {
212
- if (!filePath || existsSync(filePath)) return filePath;
296
+ if (!filePath) return filePath;
213
297
 
214
298
  const workspacePath = workspacePathFromMediaPath(filePath, workspaceId);
215
- if (!workspacePath?.workspaceId || !workspacePath.relPath) return filePath;
299
+ if (!workspacePath?.workspaceId || !workspacePath.relPath) {
300
+ if (existsSync(filePath)) return filePath;
301
+ return filePath;
302
+ }
216
303
 
217
304
  const localPath = path.resolve(workspaceRootDir, workspacePath.relPath);
218
305
  const allowedRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(workspaceRootDir, dir));
@@ -220,14 +307,14 @@ export async function materializeWorkspaceMedia({
220
307
  throw new Error(`workspace media path is outside allowed workspace directories: ${filePath}`);
221
308
  }
222
309
 
223
- if (existsSync(localPath)) return localPath;
224
-
225
310
  const data = await fetchWorkspaceFile({
226
311
  serverUrl,
227
312
  machineApiKey,
228
313
  jobId,
229
314
  relPath: workspacePath.relPath,
230
315
  });
316
+ if (isWorkspaceCacheValid(localPath, data)) return localPath;
317
+
231
318
  if (data?.videoId) {
232
319
  await streamVideoFile({
233
320
  serverUrl,
@@ -237,8 +324,7 @@ export async function materializeWorkspaceMedia({
237
324
  localPath,
238
325
  });
239
326
  } else {
240
- mkdirSync(path.dirname(localPath), { recursive: true });
241
- writeFileSync(localPath, decodeWorkspaceContent(data.content));
327
+ writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
242
328
  }
243
329
  return localPath;
244
330
  }
@@ -253,6 +339,7 @@ async function materializeMedia({
253
339
  machineApiKey,
254
340
  jobId,
255
341
  }) {
342
+ cleanupWorkspacePartialFiles(workspaceRootDir);
256
343
  return {
257
344
  images: await Promise.all(images.map(filePath => materializeWorkspaceMedia({
258
345
  filePath,