memorylake-openclaw 1.0.2-beta.5 → 1.0.2-beta.6
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.ts +46 -4
- package/package.json +1 -1
- package/skills/memorylake-upload/scripts/upload.mjs +26 -11
package/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import fsPromises from "node:fs/promises";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import got from "got";
|
|
18
|
+
import { pipeline } from "node:stream/promises";
|
|
18
19
|
import { Type } from "@sinclair/typebox";
|
|
19
20
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
20
21
|
import { loadCoreAgentDeps } from "./core-bridge";
|
|
@@ -1329,35 +1330,76 @@ const memoryPlugin = {
|
|
|
1329
1330
|
{ name: "document_search" },
|
|
1330
1331
|
);
|
|
1331
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* Try to extract a usable filename from a pre-signed download URL.
|
|
1335
|
+
* Returns null if the URL doesn't contain a recognizable filename.
|
|
1336
|
+
*/
|
|
1337
|
+
function fileNameFromUrl(urlStr: string): string | null {
|
|
1338
|
+
try {
|
|
1339
|
+
const p = new URL(urlStr).pathname;
|
|
1340
|
+
const base = path.posix.basename(p);
|
|
1341
|
+
if (base && /\.\w{1,10}$/.test(base)) return decodeURIComponent(base);
|
|
1342
|
+
} catch { /* ignore */ }
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1332
1346
|
api.registerTool(
|
|
1333
1347
|
(ctx) => ({
|
|
1334
1348
|
name: "document_download",
|
|
1335
1349
|
label: "Document Download",
|
|
1336
1350
|
description:
|
|
1337
|
-
"Download a document (image, PDF, etc.) from MemoryLake
|
|
1351
|
+
"Download a document (image, PDF, etc.) from MemoryLake to local disk. After calling this tool, you MUST call the `message` tool with action='send' and media=<the returned local file path> to deliver the file to the user.",
|
|
1338
1352
|
parameters: Type.Object({
|
|
1339
1353
|
documentId: Type.String({
|
|
1340
1354
|
description:
|
|
1341
1355
|
"The document ID to download (from document_search results or document listing)",
|
|
1342
1356
|
}),
|
|
1357
|
+
fileName: Type.Optional(
|
|
1358
|
+
Type.String({
|
|
1359
|
+
description:
|
|
1360
|
+
"Original file name for saving locally (e.g. 'report.pdf'). Falls back to the name in the download URL or the document ID.",
|
|
1361
|
+
}),
|
|
1362
|
+
),
|
|
1343
1363
|
}),
|
|
1344
1364
|
async execute(_toolCallId, params) {
|
|
1345
1365
|
const effectiveCfg = resolveConfig(ctx);
|
|
1346
1366
|
const effectiveProvider = getProvider(effectiveCfg);
|
|
1347
|
-
const { documentId } = params as {
|
|
1367
|
+
const { documentId, fileName } = params as {
|
|
1368
|
+
documentId: string;
|
|
1369
|
+
fileName?: string;
|
|
1370
|
+
};
|
|
1348
1371
|
|
|
1349
1372
|
try {
|
|
1373
|
+
// 1. Get pre-signed download URL
|
|
1350
1374
|
const downloadUrl =
|
|
1351
1375
|
await effectiveProvider.getDocumentDownloadUrl(documentId);
|
|
1352
1376
|
|
|
1377
|
+
// 2. Determine local save directory (cross-platform)
|
|
1378
|
+
const workspaceDir = (ctx as any)?.workspaceDir;
|
|
1379
|
+
const downloadDir = workspaceDir
|
|
1380
|
+
? path.join(workspaceDir, ".memorylake", "downloads")
|
|
1381
|
+
: path.join(os.tmpdir(), "memorylake-downloads");
|
|
1382
|
+
fs.mkdirSync(downloadDir, { recursive: true });
|
|
1383
|
+
|
|
1384
|
+
// 3. Determine filename: explicit param > URL-derived > documentId
|
|
1385
|
+
const finalName =
|
|
1386
|
+
fileName || fileNameFromUrl(downloadUrl) || documentId;
|
|
1387
|
+
const localPath = path.join(downloadDir, finalName);
|
|
1388
|
+
|
|
1389
|
+
// 4. Stream download to local file
|
|
1390
|
+
await pipeline(
|
|
1391
|
+
got.stream(downloadUrl),
|
|
1392
|
+
fs.createWriteStream(localPath),
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1353
1395
|
return {
|
|
1354
1396
|
content: [
|
|
1355
1397
|
{
|
|
1356
1398
|
type: "text",
|
|
1357
|
-
text: `Document ${documentId}
|
|
1399
|
+
text: `Document ${documentId} downloaded to:\n${localPath}\n\nYou MUST now call the message tool with action="send" and media set to this local path to deliver the file to the user.`,
|
|
1358
1400
|
},
|
|
1359
1401
|
],
|
|
1360
|
-
details: { documentId },
|
|
1402
|
+
details: { documentId, localPath },
|
|
1361
1403
|
};
|
|
1362
1404
|
} catch (err) {
|
|
1363
1405
|
return {
|
package/package.json
CHANGED
|
@@ -369,8 +369,13 @@ function collectFiles(dir, _counter = { value: 0 }) {
|
|
|
369
369
|
return files;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/** Max number of concurrent uploads */
|
|
373
|
+
const UPLOAD_CONCURRENCY = 10;
|
|
374
|
+
|
|
372
375
|
/**
|
|
373
|
-
* Upload multiple files with
|
|
376
|
+
* Upload multiple files with a sliding-window concurrency pool.
|
|
377
|
+
* Up to UPLOAD_CONCURRENCY uploads run simultaneously; as soon as one
|
|
378
|
+
* finishes the next file starts immediately — no waiting for a full batch.
|
|
374
379
|
* @param {Array<string>} files - absolute paths of files to upload
|
|
375
380
|
* @param {string} label - label for the summary line (e.g. "Archive" / "Directory")
|
|
376
381
|
*/
|
|
@@ -378,21 +383,31 @@ async function uploadMany({ host, apiKey, projectId, files, label }) {
|
|
|
378
383
|
const results = [];
|
|
379
384
|
let success = 0;
|
|
380
385
|
let failed = 0;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
let nextIndex = 0;
|
|
387
|
+
|
|
388
|
+
function runNext() {
|
|
389
|
+
if (nextIndex >= files.length) return null;
|
|
390
|
+
const i = nextIndex++;
|
|
391
|
+
const f = files[i];
|
|
392
|
+
return upload({
|
|
393
|
+
host, apiKey, projectId,
|
|
394
|
+
filePath: f,
|
|
395
|
+
fileName: path.basename(f),
|
|
396
|
+
}).then(doc => {
|
|
389
397
|
results.push(doc);
|
|
390
398
|
success++;
|
|
391
|
-
}
|
|
399
|
+
}).catch(err => {
|
|
392
400
|
console.error(`Failed to upload ${path.basename(f)}: ${err.message}`);
|
|
393
401
|
failed++;
|
|
394
|
-
}
|
|
402
|
+
}).then(() => runNext()); // slot freed → pick up next file immediately
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Kick off the initial pool of workers
|
|
406
|
+
const workers = [];
|
|
407
|
+
for (let w = 0; w < Math.min(UPLOAD_CONCURRENCY, files.length); w++) {
|
|
408
|
+
workers.push(runNext());
|
|
395
409
|
}
|
|
410
|
+
await Promise.all(workers);
|
|
396
411
|
|
|
397
412
|
console.log(`\n${label} upload complete: ${success} succeeded, ${failed} failed.`);
|
|
398
413
|
return results;
|