@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 +90 -44
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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"]
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
562
|
+
files.push({ name: rel, contentText: await fs.promises.readFile(full, "utf8") });
|
|
524
563
|
} else {
|
|
525
|
-
files.push({ name: rel, contentBase64: fs.
|
|
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.
|
|
1225
|
+
body.contentText = await fs.promises.readFile(fileCandidate.path, "utf8");
|
|
1187
1226
|
} else {
|
|
1188
|
-
body.contentBase64 = fs.
|
|
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.
|
|
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") {
|
package/openclaw-plugin.json
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.2.2",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED