memorylake-openclaw 0.0.10 → 0.0.11
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/README.md +4 -1
- package/docs/openclaw.mdx +5 -3
- package/index.ts +115 -3
- package/openclaw.plugin.json +7 -0
- package/package.json +1 -1
- package/skills/memorylake-upload/scripts/upload.mjs +5 -1
- package/test/path_reg.test.mjs +197 -0
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ Your agent forgets everything between sessions. This plugin fixes that. It watch
|
|
|
14
14
|
|
|
15
15
|
**Auto-Capture** — After the agent responds, the plugin sends the exchange to MemoryLake. MemoryLake decides what's worth keeping — new facts get stored, stale ones updated, duplicates merged.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
**Auto-Upload** — When a user sends a file (image, document, etc.), the plugin uploads it to MemoryLake as a project document.
|
|
18
|
+
|
|
19
|
+
Auto-Recall, Auto-Capture, and Auto-Upload run silently by default.
|
|
18
20
|
|
|
19
21
|
## Setup
|
|
20
22
|
|
|
@@ -68,6 +70,7 @@ openclaw memorylake stats
|
|
|
68
70
|
| `host` | `string` | `https://app.memorylake.ai` | MemoryLake server endpoint URL |
|
|
69
71
|
| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
|
|
70
72
|
| `autoCapture` | `boolean` | `true` | Store facts after each turn |
|
|
73
|
+
| `autoUpload` | `boolean` | `true` | Auto-upload inbound files to MemoryLake |
|
|
71
74
|
| `topK` | `number` | `5` | Max memories per recall |
|
|
72
75
|
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
|
73
76
|
| `rerank` | `boolean` | `true` | Rerank search results |
|
package/docs/openclaw.mdx
CHANGED
|
@@ -13,9 +13,10 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents
|
|
|
13
13
|
The plugin provides:
|
|
14
14
|
1. **Auto-Recall** — Before the agent responds, memories and relevant document excerpts matching the current message are injected into context
|
|
15
15
|
2. **Auto-Capture** — After the agent responds, the exchange is sent to MemoryLake which decides what's worth keeping
|
|
16
|
-
3. **
|
|
16
|
+
3. **Auto-Upload** — When a user sends a file, the plugin uploads it to MemoryLake as a project document asynchronously
|
|
17
|
+
4. **Agent Tools** — Seven tools for memory, document, and optional web search operations during conversations
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Auto-Recall, Auto-Capture, and Auto-Upload run silently by default.
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -73,6 +74,7 @@ openclaw memorylake stats
|
|
|
73
74
|
| `host` | `string` | `https://app.memorylake.ai` | MemoryLake server endpoint URL |
|
|
74
75
|
| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
|
|
75
76
|
| `autoCapture` | `boolean` | `true` | Store facts after each turn |
|
|
77
|
+
| `autoUpload` | `boolean` | `true` | Auto-upload inbound files to MemoryLake |
|
|
76
78
|
| `topK` | `number` | `5` | Max memories per recall |
|
|
77
79
|
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
|
78
80
|
| `rerank` | `boolean` | `true` | Rerank search results for better relevance |
|
|
@@ -86,7 +88,7 @@ openclaw memorylake stats
|
|
|
86
88
|
## Key Features
|
|
87
89
|
|
|
88
90
|
1. **Zero Configuration** — Auto-recall and auto-capture work out of the box with no prompting required
|
|
89
|
-
2. **Async Processing** — Memory extraction
|
|
91
|
+
2. **Async Processing** — Memory extraction and file uploads run asynchronously without blocking the agent
|
|
90
92
|
3. **Session Tracking** — Conversations are tagged with `chat_session_id` for traceability
|
|
91
93
|
4. **Rich Tool Suite** — Seven agent tools for memory, document, and optional web search operations when needed
|
|
92
94
|
|
package/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ type MemoryLakeConfig = {
|
|
|
28
28
|
userId: string;
|
|
29
29
|
autoCapture: boolean;
|
|
30
30
|
autoRecall: boolean;
|
|
31
|
+
autoUpload: boolean;
|
|
31
32
|
searchThreshold: number;
|
|
32
33
|
topK: number;
|
|
33
34
|
rerank: boolean;
|
|
@@ -204,7 +205,7 @@ class PlatformProvider implements MemoryLakeProvider {
|
|
|
204
205
|
constructor(host: string, apiKey: string, projectId: string) {
|
|
205
206
|
this.basePath = `openapi/memorylake/api/v2/projects/${projectId}/memories`;
|
|
206
207
|
this.docSearchPath = `openapi/memorylake/api/v1/projects/${projectId}/documents/search`;
|
|
207
|
-
this.webSearchPath = "openapi/memorylake/api/v1/search";
|
|
208
|
+
this.webSearchPath = "openapi/memorylake/api/v1/search";
|
|
208
209
|
this.http = got.extend({
|
|
209
210
|
prefixUrl: host,
|
|
210
211
|
headers: {
|
|
@@ -310,6 +311,7 @@ class PlatformProvider implements MemoryLakeProvider {
|
|
|
310
311
|
.json<WebSearchResponse>();
|
|
311
312
|
return normalizeWebSearchResponse(resp);
|
|
312
313
|
}
|
|
314
|
+
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
// ============================================================================
|
|
@@ -427,6 +429,7 @@ const ALLOWED_KEYS = [
|
|
|
427
429
|
"userId",
|
|
428
430
|
"autoCapture",
|
|
429
431
|
"autoRecall",
|
|
432
|
+
"autoUpload",
|
|
430
433
|
"searchThreshold",
|
|
431
434
|
"topK",
|
|
432
435
|
"rerank",
|
|
@@ -493,6 +496,7 @@ const memoryLakeConfigSchema = {
|
|
|
493
496
|
userId: DEFAULT_USER_ID,
|
|
494
497
|
autoCapture: cfg.autoCapture !== false,
|
|
495
498
|
autoRecall: cfg.autoRecall !== false,
|
|
499
|
+
autoUpload: cfg.autoUpload !== false,
|
|
496
500
|
searchThreshold:
|
|
497
501
|
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.3,
|
|
498
502
|
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
|
@@ -582,7 +586,7 @@ const memoryPlugin = {
|
|
|
582
586
|
let currentSessionId: string | undefined;
|
|
583
587
|
|
|
584
588
|
api.logger.info(
|
|
585
|
-
`memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
|
589
|
+
`memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, autoUpload: ${cfg.autoUpload})`,
|
|
586
590
|
);
|
|
587
591
|
|
|
588
592
|
// Helper: build add options
|
|
@@ -1191,6 +1195,114 @@ const memoryPlugin = {
|
|
|
1191
1195
|
// Lifecycle Hooks
|
|
1192
1196
|
// ========================================================================
|
|
1193
1197
|
|
|
1198
|
+
// --- Auto-upload helpers ---
|
|
1199
|
+
const UPLOADED_RECORD_FILE = "uploaded.json";
|
|
1200
|
+
|
|
1201
|
+
type UploadedRecord = Record<string, { mtimeMs: number }>;
|
|
1202
|
+
|
|
1203
|
+
function getUploadedRecord(workspaceDir: string): UploadedRecord {
|
|
1204
|
+
const filePath = path.join(workspaceDir, ".memorylake", UPLOADED_RECORD_FILE);
|
|
1205
|
+
try {
|
|
1206
|
+
if (fs.existsSync(filePath)) {
|
|
1207
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1208
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
1209
|
+
}
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
api.logger.warn(`memorylake-openclaw: failed to read uploaded record: ${String(err)}`);
|
|
1212
|
+
}
|
|
1213
|
+
return {};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function saveUploadedRecord(workspaceDir: string, record: UploadedRecord): void {
|
|
1217
|
+
const dirPath = path.join(workspaceDir, ".memorylake");
|
|
1218
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
1219
|
+
fs.writeFileSync(
|
|
1220
|
+
path.join(dirPath, UPLOADED_RECORD_FILE),
|
|
1221
|
+
JSON.stringify(record, null, 2),
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function needsUpload(record: UploadedRecord, filePath: string): fs.Stats | null {
|
|
1226
|
+
if (!fs.existsSync(filePath)) return null;
|
|
1227
|
+
const stat = fs.statSync(filePath);
|
|
1228
|
+
const prev = record[filePath];
|
|
1229
|
+
return (!prev || prev.mtimeMs !== stat.mtimeMs) ? stat : null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function extractInboundPaths(prompt: string): string[] {
|
|
1233
|
+
// Path must contain /media/inbound/ (or \media\inbound\)
|
|
1234
|
+
// Filename must end with .<ext>, ext = alphanumeric, 1-6 chars
|
|
1235
|
+
const sep = '[/\\\\]';
|
|
1236
|
+
const regex = new RegExp(
|
|
1237
|
+
`(?:[A-Za-z]:${sep}|/)\\S*?media${sep}inbound${sep}.+?\\.[a-zA-Z0-9]{1,6}(?=[^a-zA-Z0-9]|$)`,
|
|
1238
|
+
"g",
|
|
1239
|
+
);
|
|
1240
|
+
const matches = prompt.match(regex) || [];
|
|
1241
|
+
return [...new Set(matches)];
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Auto-upload: upload inbound files to MemoryLake before prompt build
|
|
1245
|
+
if (cfg.autoUpload) {
|
|
1246
|
+
// Lazy-load upload function from upload.mjs
|
|
1247
|
+
let uploadFn: ((opts: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>) | undefined;
|
|
1248
|
+
|
|
1249
|
+
api.on("before_prompt_build", (event, ctx) => {
|
|
1250
|
+
const workspaceDir = (ctx as any)?.workspaceDir;
|
|
1251
|
+
if (!workspaceDir || !event.prompt) return;
|
|
1252
|
+
|
|
1253
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
1254
|
+
const paths = extractInboundPaths(event.prompt);
|
|
1255
|
+
if (paths.length === 0) return;
|
|
1256
|
+
|
|
1257
|
+
const record = getUploadedRecord(workspaceDir);
|
|
1258
|
+
const filesToUpload: { filePath: string; stat: fs.Stats }[] = [];
|
|
1259
|
+
for (const p of paths) {
|
|
1260
|
+
const stat = needsUpload(record, p);
|
|
1261
|
+
if (stat) filesToUpload.push({ filePath: p, stat });
|
|
1262
|
+
}
|
|
1263
|
+
if (filesToUpload.length === 0) return;
|
|
1264
|
+
|
|
1265
|
+
// Fire-and-forget: upload asynchronously without blocking
|
|
1266
|
+
(async () => {
|
|
1267
|
+
// Lazy import upload.mjs
|
|
1268
|
+
if (!uploadFn) {
|
|
1269
|
+
const uploadModule = await import(
|
|
1270
|
+
/* webpackIgnore: true */
|
|
1271
|
+
new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
|
|
1272
|
+
);
|
|
1273
|
+
uploadFn = uploadModule.upload;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
for (const { filePath, stat } of filesToUpload) {
|
|
1277
|
+
try {
|
|
1278
|
+
await uploadFn!({
|
|
1279
|
+
host: effectiveCfg.host,
|
|
1280
|
+
apiKey: effectiveCfg.apiKey,
|
|
1281
|
+
projectId: effectiveCfg.projectId,
|
|
1282
|
+
filePath,
|
|
1283
|
+
fileName: path.basename(filePath),
|
|
1284
|
+
});
|
|
1285
|
+
// Save record only after successful upload to avoid race on crash
|
|
1286
|
+
const current = getUploadedRecord(workspaceDir);
|
|
1287
|
+
current[filePath] = { mtimeMs: stat.mtimeMs };
|
|
1288
|
+
saveUploadedRecord(workspaceDir, current);
|
|
1289
|
+
api.logger.info(
|
|
1290
|
+
`memorylake-openclaw: auto-uploaded ${path.basename(filePath)}`,
|
|
1291
|
+
);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
api.logger.warn(
|
|
1294
|
+
`memorylake-openclaw: auto-upload failed for ${filePath}: ${String(err)}`,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
})().catch((err) => {
|
|
1299
|
+
api.logger.warn(
|
|
1300
|
+
`memorylake-openclaw: auto-upload unexpected error: ${String(err)}`,
|
|
1301
|
+
);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1194
1306
|
// Auto-recall: inject relevant memories and documents before agent starts
|
|
1195
1307
|
if (cfg.autoRecall) {
|
|
1196
1308
|
api.on("before_agent_start", async (event, ctx) => {
|
|
@@ -1337,7 +1449,7 @@ const memoryPlugin = {
|
|
|
1337
1449
|
id: "memorylake-openclaw",
|
|
1338
1450
|
start: () => {
|
|
1339
1451
|
api.logger.info(
|
|
1340
|
-
`memorylake-openclaw: initialized (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
|
1452
|
+
`memorylake-openclaw: initialized (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, autoUpload: ${cfg.autoUpload})`,
|
|
1341
1453
|
);
|
|
1342
1454
|
},
|
|
1343
1455
|
stop: () => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"label": "Auto-Recall",
|
|
23
23
|
"help": "Automatically inject relevant memories before each agent turn"
|
|
24
24
|
},
|
|
25
|
+
"autoUpload": {
|
|
26
|
+
"label": "Auto-Upload",
|
|
27
|
+
"help": "Automatically upload files from media/inbound to MemoryLake"
|
|
28
|
+
},
|
|
25
29
|
"searchThreshold": {
|
|
26
30
|
"label": "Search Threshold",
|
|
27
31
|
"placeholder": "0.3",
|
|
@@ -76,6 +80,9 @@
|
|
|
76
80
|
"autoRecall": {
|
|
77
81
|
"type": "boolean"
|
|
78
82
|
},
|
|
83
|
+
"autoUpload": {
|
|
84
|
+
"type": "boolean"
|
|
85
|
+
},
|
|
79
86
|
"searchThreshold": {
|
|
80
87
|
"type": "number"
|
|
81
88
|
},
|
package/package.json
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
// Extract the regex logic inline (same as index.ts extractInboundPaths)
|
|
5
|
+
function extractInboundPaths(prompt) {
|
|
6
|
+
// Path must contain /media/inbound/ (or \media\inbound\)
|
|
7
|
+
// Filename must end with .<ext>, ext = alphanumeric, 1-6 chars
|
|
8
|
+
const sep = '[/\\\\]';
|
|
9
|
+
const regex = new RegExp(
|
|
10
|
+
`(?:[A-Za-z]:${sep}|/)\\S*?media${sep}inbound${sep}.+?\\.[a-zA-Z0-9]{1,6}(?=[^a-zA-Z0-9]|$)`,
|
|
11
|
+
"g",
|
|
12
|
+
);
|
|
13
|
+
const matches = prompt.match(regex) || [];
|
|
14
|
+
return [...new Set(matches)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("extractInboundPaths", () => {
|
|
18
|
+
// ==================== Should match ====================
|
|
19
|
+
|
|
20
|
+
it("Unix: basic inbound path", () => {
|
|
21
|
+
const prompt = "请看这个文件 /Users/henry/.openclaw/media/inbound/abc.png";
|
|
22
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
23
|
+
"/Users/henry/.openclaw/media/inbound/abc.png",
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("Unix: UUID filename", () => {
|
|
28
|
+
const prompt =
|
|
29
|
+
"[media attached: /Users/henry/.openclaw/media/inbound/69b064ed-7afa-4184-b81d-aacbc34c7d95.png (image/png)]";
|
|
30
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
31
|
+
"/Users/henry/.openclaw/media/inbound/69b064ed-7afa-4184-b81d-aacbc34c7d95.png",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("Unix: timestamp-prefixed filename", () => {
|
|
36
|
+
const prompt =
|
|
37
|
+
"文件已保存: /Users/henry/.openclaw/workspace/media/inbound/1774440188532-b204bb126d0b41bf8b97275d0871728c_1.doc,请基于文件名和上下文回答";
|
|
38
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
39
|
+
"/Users/henry/.openclaw/workspace/media/inbound/1774440188532-b204bb126d0b41bf8b97275d0871728c_1.doc",
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("Unix: Chinese filename", () => {
|
|
44
|
+
const prompt =
|
|
45
|
+
"上传 /Users/henry/.openclaw/workspace/media/inbound/1773887151830-男士护肤品推广.xlsx 到项目";
|
|
46
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
47
|
+
"/Users/henry/.openclaw/workspace/media/inbound/1773887151830-男士护肤品推广.xlsx",
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Unix: subdirectory under inbound", () => {
|
|
52
|
+
const prompt = "file at /data/media/inbound/subdir/nested/report.pdf end";
|
|
53
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
54
|
+
"/data/media/inbound/subdir/nested/report.pdf",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("Unix: multiple paths in one prompt", () => {
|
|
59
|
+
const prompt =
|
|
60
|
+
"files: /a/media/inbound/one.txt and /b/media/inbound/two.pdf here";
|
|
61
|
+
const result = extractInboundPaths(prompt);
|
|
62
|
+
assert.equal(result.length, 2);
|
|
63
|
+
assert.ok(result.includes("/a/media/inbound/one.txt"));
|
|
64
|
+
assert.ok(result.includes("/b/media/inbound/two.pdf"));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("Unix: dedup same path appearing twice", () => {
|
|
68
|
+
const prompt =
|
|
69
|
+
"/x/media/inbound/f.txt and again /x/media/inbound/f.txt here";
|
|
70
|
+
assert.deepEqual(extractInboundPaths(prompt), ["/x/media/inbound/f.txt"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("Windows: backslash path with drive letter", () => {
|
|
74
|
+
const prompt =
|
|
75
|
+
'file at C:\\Users\\hello\\.openclaw\\media\\inbound\\report.docx end';
|
|
76
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
77
|
+
"C:\\Users\\hello\\.openclaw\\media\\inbound\\report.docx",
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("Windows: Chinese username and filename", () => {
|
|
82
|
+
const prompt =
|
|
83
|
+
'C:\\Users\\你好\\.openclaw\\media\\inbound\\sds\\中文的.odf 这个文件';
|
|
84
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
85
|
+
"C:\\Users\\你好\\.openclaw\\media\\inbound\\sds\\中文的.odf",
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("Windows: mixed separators (forward slash after drive)", () => {
|
|
90
|
+
const prompt = "C:/Users/test/media/inbound/file.txt done";
|
|
91
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
92
|
+
"C:/Users/test/media/inbound/file.txt",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("Unix: path with dot-prefixed directory", () => {
|
|
97
|
+
const prompt = "/home/user/.config/media/inbound/data.csv end";
|
|
98
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
99
|
+
"/home/user/.config/media/inbound/data.csv",
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ==================== Should NOT match ====================
|
|
104
|
+
|
|
105
|
+
it("no match: no media/inbound in path", () => {
|
|
106
|
+
const prompt = "/Users/henry/Documents/report.pdf";
|
|
107
|
+
assert.deepEqual(extractInboundPaths(prompt), []);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("no match: media/outbound (not inbound)", () => {
|
|
111
|
+
const prompt = "/Users/henry/media/outbound/file.txt";
|
|
112
|
+
assert.deepEqual(extractInboundPaths(prompt), []);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("no match: bare text media/inbound without leading path", () => {
|
|
116
|
+
const prompt = "check media/inbound/file.txt please";
|
|
117
|
+
assert.deepEqual(extractInboundPaths(prompt), []);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("no match: empty prompt", () => {
|
|
121
|
+
assert.deepEqual(extractInboundPaths(""), []);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("no match: path-like text without separator before media", () => {
|
|
125
|
+
const prompt = "wordmedia/inbound/file.txt";
|
|
126
|
+
assert.deepEqual(extractInboundPaths(prompt), []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ==================== Edge cases ====================
|
|
130
|
+
|
|
131
|
+
it("stops at Chinese comma after path", () => {
|
|
132
|
+
const prompt =
|
|
133
|
+
"文件已保存: /a/media/inbound/file.doc,请回答";
|
|
134
|
+
const result = extractInboundPaths(prompt);
|
|
135
|
+
assert.equal(result.length, 1);
|
|
136
|
+
// Should NOT include the Chinese comma or text after it
|
|
137
|
+
assert.ok(!result[0].includes(","));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("stops at Chinese enumeration comma (、) after path", () => {
|
|
141
|
+
const prompt =
|
|
142
|
+
"上传 /a/media/inbound/one.txt、/a/media/inbound/two.txt 到项目";
|
|
143
|
+
const result = extractInboundPaths(prompt);
|
|
144
|
+
assert.equal(result.length, 2);
|
|
145
|
+
assert.ok(!result[0].includes("、"));
|
|
146
|
+
assert.ok(!result[1].includes("、"));
|
|
147
|
+
assert.ok(result.includes("/a/media/inbound/one.txt"));
|
|
148
|
+
assert.ok(result.includes("/a/media/inbound/two.txt"));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("stops at parenthesis after path", () => {
|
|
152
|
+
const prompt =
|
|
153
|
+
"[media attached: /a/media/inbound/img.png (image/png) | /a/media/inbound/img.png]";
|
|
154
|
+
const result = extractInboundPaths(prompt);
|
|
155
|
+
// Should capture the path but not "(image/png)"
|
|
156
|
+
assert.ok(result.length >= 1);
|
|
157
|
+
assert.ok(result.some((p) => p === "/a/media/inbound/img.png"));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("stops at double quote", () => {
|
|
161
|
+
const prompt = 'path is "/a/media/inbound/file.txt" here';
|
|
162
|
+
const result = extractInboundPaths(prompt);
|
|
163
|
+
assert.equal(result.length, 1);
|
|
164
|
+
assert.ok(!result[0].includes('"'));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("path inside brackets", () => {
|
|
168
|
+
const prompt = "[/a/media/inbound/file.txt]";
|
|
169
|
+
const result = extractInboundPaths(prompt);
|
|
170
|
+
assert.equal(result.length, 1);
|
|
171
|
+
assert.equal(result[0], "/a/media/inbound/file.txt");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("filename with parentheses (browser duplicate)", () => {
|
|
175
|
+
const prompt =
|
|
176
|
+
"C:\\Users\\test\\.openclaw\\workspace\\media\\inbound\\米家吸尘器2(1)-1774512141374.pdf 这个文件";
|
|
177
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
178
|
+
"C:\\Users\\test\\.openclaw\\workspace\\media\\inbound\\米家吸尘器2(1)-1774512141374.pdf",
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("filename with spaces", () => {
|
|
183
|
+
const prompt =
|
|
184
|
+
"C:\\Users\\test\\.openclaw\\workspace\\media\\inbound\\Contemporary Report-1774522842609.pdf 请分析";
|
|
185
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
186
|
+
"C:\\Users\\test\\.openclaw\\workspace\\media\\inbound\\Contemporary Report-1774522842609.pdf",
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("Unix: filename with spaces and parens", () => {
|
|
191
|
+
const prompt =
|
|
192
|
+
"看看 /Users/henry/.openclaw/media/inbound/My Document (2).pdf 这个";
|
|
193
|
+
assert.deepEqual(extractInboundPaths(prompt), [
|
|
194
|
+
"/Users/henry/.openclaw/media/inbound/My Document (2).pdf",
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
});
|