@dai_ming/plugin-deliverables 1.2.0 → 1.2.2

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/index.js CHANGED
@@ -287,6 +287,7 @@ function deliverableTypeForPath(filePath, isDirectory) {
287
287
 
288
288
  function isAllowedWorkspacePath(candidatePath) {
289
289
  const normalized = normalizeSlash(path.resolve(candidatePath));
290
+ // 老布局:根目录直接挂 workspace(保持向后兼容)。
290
291
  if (
291
292
  normalized.indexOf("/data/workspace-") === 0 ||
292
293
  normalized.indexOf("/home/node/.openclaw/workspace") === 0
@@ -294,7 +295,13 @@ function isAllowedWorkspacePath(candidatePath) {
294
295
  return true;
295
296
  }
296
297
  const openclawRoot = normalizeSlash(path.resolve(__dirname, "..", ".."));
297
- return normalized.indexOf(openclawRoot + "/workspace") === 0;
298
+ if (normalized.indexOf(openclawRoot + "/workspace") === 0) {
299
+ return true;
300
+ }
301
+ // 新布局(多租户)及通用情况:绝对路径中任意位置含有 `xxx/workspace-*`
302
+ // 目录段都允许,覆盖 /data/tenants/<user>/workspace-<lobster>、
303
+ // /home/node/.openclaw/<user>/workspace-<lobster> 等。
304
+ return /\/workspace-[^/]+(?:\/|$)/.test(normalized);
298
305
  }
299
306
 
300
307
  function shouldIgnorePath(candidatePath) {
@@ -308,42 +315,74 @@ function shouldIgnorePath(candidatePath) {
308
315
  return IGNORED_FILE_NAMES.has(path.basename(normalized));
309
316
  }
310
317
 
311
- function safeStat(candidatePath) {
318
+ async function safeStat(candidatePath) {
312
319
  try {
313
- return fs.statSync(candidatePath);
320
+ return await fs.promises.stat(candidatePath);
314
321
  } catch (_err) {
315
322
  return null;
316
323
  }
317
324
  }
318
325
 
319
- function workspaceRoots() {
326
+ async function workspaceRoots() {
320
327
  const roots = [];
321
- ["/home/node/.openclaw/workspace-main", "/home/node/.openclaw/workspace"].forEach((root) => {
322
- if (safeStat(root)) {
328
+ for (const root of ["/home/node/.openclaw/workspace-main", "/home/node/.openclaw/workspace"]) {
329
+ if (await safeStat(root)) {
323
330
  roots.push(root);
324
331
  }
325
- });
332
+ }
326
333
  try {
327
- for (const entry of fs.readdirSync("/data")) {
334
+ for (const entry of await fs.promises.readdir("/data")) {
328
335
  if (!entry || entry.indexOf("workspace-") !== 0) {
329
336
  continue;
330
337
  }
331
338
  const root = path.join("/data", entry);
332
- if (safeStat(root)) {
339
+ if (await safeStat(root)) {
333
340
  roots.push(root);
334
341
  }
335
342
  }
336
343
  } catch (_err) {
337
344
  // /data is not guaranteed in non-pod test environments.
338
345
  }
346
+ // 多租户布局:枚举 <base>/<tenant-or-user>/workspace-* 下的 workspace 根目录,
347
+ // 覆盖 /data/tenants/<user>/workspace-<lobster>、
348
+ // /home/node/.openclaw/<user>/workspace-<lobster> 等。
349
+ for (const base of ["/data/tenants", "/home/node/.openclaw"]) {
350
+ let tenants;
351
+ try {
352
+ tenants = await fs.promises.readdir(base);
353
+ } catch (_err) {
354
+ continue;
355
+ }
356
+ for (const tenant of tenants) {
357
+ if (!tenant || tenant.indexOf("workspace") === 0) {
358
+ continue;
359
+ }
360
+ const tenantDir = path.join(base, tenant);
361
+ let children;
362
+ try {
363
+ children = await fs.promises.readdir(tenantDir);
364
+ } catch (_err) {
365
+ continue;
366
+ }
367
+ for (const child of children) {
368
+ if (!child || child.indexOf("workspace-") !== 0) {
369
+ continue;
370
+ }
371
+ const root = path.join(tenantDir, child);
372
+ if ((await safeStat(root)) && roots.indexOf(root) === -1) {
373
+ roots.push(root);
374
+ }
375
+ }
376
+ }
377
+ }
339
378
  const openclawRoot = path.resolve(__dirname, "..", "..");
340
379
  try {
341
- for (const entry of fs.readdirSync(openclawRoot)) {
380
+ for (const entry of await fs.promises.readdir(openclawRoot)) {
342
381
  if (!entry || entry.indexOf("workspace") !== 0) {
343
382
  continue;
344
383
  }
345
384
  const root = path.join(openclawRoot, entry);
346
- if (safeStat(root) && roots.indexOf(root) === -1) {
385
+ if ((await safeStat(root)) && roots.indexOf(root) === -1) {
347
386
  roots.push(root);
348
387
  }
349
388
  }
@@ -364,12 +403,12 @@ function resolveCandidatePath(rawPath) {
364
403
  return cleaned;
365
404
  }
366
405
 
367
- function findFileByRelativeOrBaseName(rawRef) {
406
+ async function findFileByRelativeOrBaseName(rawRef) {
368
407
  const cleaned = stripTrailingPathPunctuation(trimString(rawRef)).replace(/^\.\//, "");
369
408
  if (!cleaned || isURLLike(cleaned) || !hasFileExtension(cleaned)) {
370
409
  return "";
371
410
  }
372
- const roots = workspaceRoots();
411
+ const roots = await workspaceRoots();
373
412
  const directCandidates = [];
374
413
  for (const root of roots) {
375
414
  directCandidates.push(path.join(root, cleaned));
@@ -378,7 +417,7 @@ function findFileByRelativeOrBaseName(rawRef) {
378
417
  directCandidates.push(path.join(root, "output", path.basename(cleaned)));
379
418
  }
380
419
  for (const candidate of directCandidates) {
381
- const stat = safeStat(candidate);
420
+ const stat = await safeStat(candidate);
382
421
  if (stat && (stat.isFile() || stat.isDirectory()) && isAllowedWorkspacePath(candidate) && !shouldIgnorePath(candidate)) {
383
422
  return path.resolve(candidate);
384
423
  }
@@ -387,13 +426,13 @@ function findFileByRelativeOrBaseName(rawRef) {
387
426
  const basename = path.basename(cleaned);
388
427
  const found = [];
389
428
  let visited = 0;
390
- function scan(dir, depth) {
429
+ async function scan(dir, depth) {
391
430
  if (depth < 0 || visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || shouldIgnorePath(dir)) {
392
431
  return;
393
432
  }
394
433
  let entries;
395
434
  try {
396
- entries = fs.readdirSync(dir, { withFileTypes: true });
435
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
397
436
  } catch (_err) {
398
437
  return;
399
438
  }
@@ -407,11 +446,11 @@ function findFileByRelativeOrBaseName(rawRef) {
407
446
  continue;
408
447
  }
409
448
  if (entry.isDirectory()) {
410
- scan(full, depth - 1);
449
+ await scan(full, depth - 1);
411
450
  continue;
412
451
  }
413
452
  if (entry.isFile() && entry.name === basename) {
414
- const stat = safeStat(full);
453
+ const stat = await safeStat(full);
415
454
  if (stat) {
416
455
  found.push({ path: full, mtimeMs: stat.mtimeMs });
417
456
  }
@@ -419,7 +458,7 @@ function findFileByRelativeOrBaseName(rawRef) {
419
458
  }
420
459
  }
421
460
  for (const root of roots) {
422
- scan(root, AUTO_UPLOAD_SCAN_DEPTH);
461
+ await scan(root, AUTO_UPLOAD_SCAN_DEPTH);
423
462
  }
424
463
  found.sort((a, b) => b.mtimeMs - a.mtimeMs);
425
464
  return found.length > 0 ? path.resolve(found[0].path) : "";
@@ -457,16 +496,16 @@ function extractFileReferencesFromText(text) {
457
496
  return refs;
458
497
  }
459
498
 
460
- function resolveFileReference(ref) {
499
+ async function resolveFileReference(ref) {
461
500
  if (!ref || !ref.value) {
462
501
  return null;
463
502
  }
464
503
  const candidate = resolveCandidatePath(ref.value);
465
- const resolved = path.isAbsolute(candidate) ? candidate : findFileByRelativeOrBaseName(candidate);
504
+ const resolved = path.isAbsolute(candidate) ? candidate : await findFileByRelativeOrBaseName(candidate);
466
505
  if (!resolved) {
467
506
  return null;
468
507
  }
469
- const stat = safeStat(resolved);
508
+ const stat = await safeStat(resolved);
470
509
  if (!stat || (!stat.isFile() && !stat.isDirectory())) {
471
510
  return null;
472
511
  }
@@ -483,17 +522,17 @@ function resolveFileReference(ref) {
483
522
  };
484
523
  }
485
524
 
486
- function collectDirectoryFiles(dirPath) {
525
+ async function collectDirectoryFiles(dirPath) {
487
526
  const files = [];
488
527
  let total = 0;
489
528
  let visited = 0;
490
- function scan(current, depth) {
529
+ async function scan(current, depth) {
491
530
  if (depth < 0 || visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || shouldIgnorePath(current)) {
492
531
  return;
493
532
  }
494
533
  let entries;
495
534
  try {
496
- entries = fs.readdirSync(current, { withFileTypes: true });
535
+ entries = await fs.promises.readdir(current, { withFileTypes: true });
497
536
  } catch (_err) {
498
537
  return;
499
538
  }
@@ -507,26 +546,26 @@ function collectDirectoryFiles(dirPath) {
507
546
  continue;
508
547
  }
509
548
  if (entry.isDirectory()) {
510
- scan(full, depth - 1);
549
+ await scan(full, depth - 1);
511
550
  continue;
512
551
  }
513
552
  if (!entry.isFile()) {
514
553
  continue;
515
554
  }
516
- const stat = safeStat(full);
555
+ const stat = await safeStat(full);
517
556
  if (!stat || total + stat.size > AUTO_UPLOAD_MAX_BYTES) {
518
557
  continue;
519
558
  }
520
559
  total += stat.size;
521
560
  const rel = normalizeSlash(path.relative(dirPath, full));
522
561
  if (isTextLikeFile(full)) {
523
- files.push({ name: rel, contentText: fs.readFileSync(full, "utf8") });
562
+ files.push({ name: rel, contentText: await fs.promises.readFile(full, "utf8") });
524
563
  } else {
525
- files.push({ name: rel, contentBase64: fs.readFileSync(full).toString("base64") });
564
+ files.push({ name: rel, contentBase64: (await fs.promises.readFile(full)).toString("base64") });
526
565
  }
527
566
  }
528
567
  }
529
- scan(dirPath, AUTO_UPLOAD_SCAN_DEPTH);
568
+ await scan(dirPath, AUTO_UPLOAD_SCAN_DEPTH);
530
569
  return files;
531
570
  }
532
571
 
@@ -689,7 +728,7 @@ function rememberPendingFile(event) {
689
728
  });
690
729
  }
691
730
 
692
- function pendingCandidatesForPayload(body) {
731
+ async function pendingCandidatesForPayload(body) {
693
732
  const resourceId = payloadResourceId(body);
694
733
  if (!resourceId) {
695
734
  return [];
@@ -702,7 +741,7 @@ function pendingCandidatesForPayload(body) {
702
741
  continue;
703
742
  }
704
743
  const ref = { raw: entry.path, value: entry.path, kind: "pending" };
705
- const resolved = resolveFileReference(ref);
744
+ const resolved = await resolveFileReference(ref);
706
745
  if (resolved) {
707
746
  out.push(resolved);
708
747
  }
@@ -1155,11 +1194,11 @@ function normalizeNativeToolFiles(files) {
1155
1194
  }));
1156
1195
  }
1157
1196
 
1158
- function buildNativeUploadRequestBody(args) {
1197
+ async function buildNativeUploadRequestBody(args) {
1159
1198
  const rawFilePath = trimString(args && args.file_path);
1160
1199
  let fileCandidate = null;
1161
1200
  if (rawFilePath) {
1162
- fileCandidate = resolveFileReference({ raw: rawFilePath, value: rawFilePath, kind: "tool" });
1201
+ fileCandidate = await resolveFileReference({ raw: rawFilePath, value: rawFilePath, kind: "tool" });
1163
1202
  if (!fileCandidate) {
1164
1203
  throw new Error(`file_path is not readable or not under an OpenClaw workspace: ${rawFilePath}`);
1165
1204
  }
@@ -1168,7 +1207,7 @@ function buildNativeUploadRequestBody(args) {
1168
1207
  const body = {};
1169
1208
 
1170
1209
  if (fileCandidate) {
1171
- const stat = safeStat(fileCandidate.path);
1210
+ const stat = await safeStat(fileCandidate.path);
1172
1211
  const isDirectory = !!(stat && stat.isDirectory());
1173
1212
  const requestedType = trimString(args.type);
1174
1213
  body.type =
@@ -1177,15 +1216,15 @@ function buildNativeUploadRequestBody(args) {
1177
1216
  : requestedType || deliverableTypeForPath(fileCandidate.path, isDirectory);
1178
1217
  body.fileName = normalizeDeliverableFileName(args, fileCandidate.fileName);
1179
1218
  if (isDirectory) {
1180
- const files = collectDirectoryFiles(fileCandidate.path);
1219
+ const files = await collectDirectoryFiles(fileCandidate.path);
1181
1220
  if (files.length === 0) {
1182
1221
  throw new Error(`directory has no uploadable files: ${fileCandidate.fileName}`);
1183
1222
  }
1184
1223
  body.files = files;
1185
1224
  } else if (isTextLikeFile(fileCandidate.path)) {
1186
- body.contentText = fs.readFileSync(fileCandidate.path, "utf8");
1225
+ body.contentText = await fs.promises.readFile(fileCandidate.path, "utf8");
1187
1226
  } else {
1188
- body.contentBase64 = fs.readFileSync(fileCandidate.path).toString("base64");
1227
+ body.contentBase64 = (await fs.promises.readFile(fileCandidate.path)).toString("base64");
1189
1228
  }
1190
1229
  return { body, isDirectory };
1191
1230
  }
@@ -1223,7 +1262,7 @@ function buildNativeUploadRequestBody(args) {
1223
1262
  }
1224
1263
 
1225
1264
  async function uploadDeliverable(args) {
1226
- const { body, isDirectory } = buildNativeUploadRequestBody(args || {});
1265
+ const { body, isDirectory } = await buildNativeUploadRequestBody(args || {});
1227
1266
  const userId = payloadUserId(args) || "default";
1228
1267
  const groupId = payloadGroupId(args) || DEFAULT_GROUP_ID;
1229
1268
  const groupName = extractStringField(args || {}, ["group_name", "groupName", "group_subject", "groupSubject"]);
@@ -1274,7 +1313,7 @@ async function uploadCandidate(candidate, body) {
1274
1313
  return cached.result;
1275
1314
  }
1276
1315
 
1277
- const stat = safeStat(candidate.path);
1316
+ const stat = await safeStat(candidate.path);
1278
1317
  if (!stat) {
1279
1318
  throw new Error(`file no longer exists: ${candidate.fileName}`);
1280
1319
  }
@@ -1286,7 +1325,7 @@ async function uploadCandidate(candidate, body) {
1286
1325
 
1287
1326
  let fileBuffers;
1288
1327
  if (stat.isDirectory()) {
1289
- const files = collectDirectoryFiles(candidate.path);
1328
+ const files = await collectDirectoryFiles(candidate.path);
1290
1329
  if (files.length === 0) {
1291
1330
  throw new Error(`directory has no uploadable files: ${candidate.fileName}`);
1292
1331
  }
@@ -1298,7 +1337,7 @@ async function uploadCandidate(candidate, body) {
1298
1337
  if (stat.size > AUTO_UPLOAD_MAX_BYTES) {
1299
1338
  throw new Error(`file exceeds auto-upload limit: ${candidate.fileName}`);
1300
1339
  }
1301
- fileBuffers = [{ fileName, content: fs.readFileSync(candidate.path) }];
1340
+ fileBuffers = [{ fileName, content: await fs.promises.readFile(candidate.path) }];
1302
1341
  }
1303
1342
 
1304
1343
  const response = await kbMultipartUpload(fileBuffers, {
@@ -1397,8 +1436,8 @@ async function autoUploadAndRewritePayload(body, api) {
1397
1436
  return body;
1398
1437
  }
1399
1438
  const refs = extractFileReferencesFromText(body.content);
1400
- const explicitCandidates = refs.map(resolveFileReference).filter(Boolean);
1401
- const candidates = dedupeCandidates(explicitCandidates.concat(pendingCandidatesForPayload(body)));
1439
+ const explicitCandidates = (await Promise.all(refs.map(resolveFileReference))).filter(Boolean);
1440
+ const candidates = dedupeCandidates(explicitCandidates.concat(await pendingCandidatesForPayload(body)));
1402
1441
  if (candidates.length === 0) {
1403
1442
  return body;
1404
1443
  }
@@ -2294,6 +2333,13 @@ const plugin = {
2294
2333
  name: "Deliverables",
2295
2334
  description: "Upload-first runtime guard for generated file deliverables.",
2296
2335
  register(api) {
2336
+ // Warm the config cache once at startup so the synchronous read in
2337
+ // loadDeliverableConfig() never lands on the message hot path.
2338
+ try {
2339
+ loadDeliverableConfig();
2340
+ } catch (err) {
2341
+ api.logger.warn?.(`[plugin-deliverables] config preload failed: ${err.message}`);
2342
+ }
2297
2343
  installPalzFetchPatch(api);
2298
2344
 
2299
2345
  if (api && typeof api.registerTool === "function") {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "npm_package": "@dai_ming/plugin-deliverables",
5
5
  "description": "Deliverables plugin: native upload tool + skill + AGENTS rules for AI-generated file uploads",
6
6
  "skills": {
@@ -2,7 +2,7 @@
2
2
  "id": "plugin-deliverables",
3
3
  "name": "Deliverables",
4
4
  "description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
5
- "version": "1.2.0",
5
+ "version": "1.2.2",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dai_ming/plugin-deliverables",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "OpenClaw deliverables native plugin — upload AI-generated files to OSS and return shareable preview/download links",
5
5
  "keywords": [
6
6
  "openclaw",