@memtensor/memos-cloud-openclaw-plugin 0.1.7 → 0.1.8-beta.1
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/LICENSE +201 -201
- package/README.md +155 -145
- package/README_ZH.md +161 -151
- package/clawdbot.plugin.json +164 -160
- package/index.js +285 -259
- package/lib/memos-cloud-api.js +449 -443
- package/moltbot.plugin.json +164 -160
- package/openclaw.plugin.json +164 -160
- package/package.json +46 -46
- package/scripts/sync-version.js +45 -45
package/lib/memos-cloud-api.js
CHANGED
|
@@ -1,443 +1,449 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_BASE_URL = "https://memos.memtensor.cn/api/openmem/v1";
|
|
7
|
-
export const USER_QUERY_MARKER = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
|
|
8
|
-
const ENV_SOURCES = [
|
|
9
|
-
{ name: "openclaw", path: join(homedir(), ".openclaw", ".env") },
|
|
10
|
-
{ name: "moltbot", path: join(homedir(), ".moltbot", ".env") },
|
|
11
|
-
{ name: "clawdbot", path: join(homedir(), ".clawdbot", ".env") },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
let envFilesLoaded = false;
|
|
15
|
-
const envFileContents = new Map();
|
|
16
|
-
const envFileValues = new Map();
|
|
17
|
-
|
|
18
|
-
function stripQuotes(value) {
|
|
19
|
-
if (!value) return value;
|
|
20
|
-
const trimmed = value.trim();
|
|
21
|
-
if (
|
|
22
|
-
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
23
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
24
|
-
) {
|
|
25
|
-
return trimmed.slice(1, -1);
|
|
26
|
-
}
|
|
27
|
-
return trimmed;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function extractResultData(result) {
|
|
31
|
-
if (!result || typeof result !== "object") return null;
|
|
32
|
-
return result.data ?? result.data?.data ?? result.data?.result ?? null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function pad2(value) {
|
|
36
|
-
return String(value).padStart(2, "0");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function formatTime(value) {
|
|
40
|
-
if (value === undefined || value === null || value === "") return "";
|
|
41
|
-
if (typeof value === "number") {
|
|
42
|
-
const date = new Date(value);
|
|
43
|
-
if (Number.isNaN(date.getTime())) return "";
|
|
44
|
-
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(
|
|
45
|
-
date.getHours(),
|
|
46
|
-
)}:${pad2(date.getMinutes())}`;
|
|
47
|
-
}
|
|
48
|
-
if (typeof value === "string") {
|
|
49
|
-
const trimmed = value.trim();
|
|
50
|
-
if (!trimmed) return "";
|
|
51
|
-
if (/^\d+$/.test(trimmed)) return formatTime(Number(trimmed));
|
|
52
|
-
return trimmed;
|
|
53
|
-
}
|
|
54
|
-
return "";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseEnvFile(content) {
|
|
58
|
-
const values = new Map();
|
|
59
|
-
for (const line of content.split(/\r?\n/)) {
|
|
60
|
-
const trimmed = line.trim();
|
|
61
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
62
|
-
const idx = trimmed.indexOf("=");
|
|
63
|
-
if (idx <= 0) continue;
|
|
64
|
-
const key = trimmed.slice(0, idx).trim();
|
|
65
|
-
const rawValue = trimmed.slice(idx + 1);
|
|
66
|
-
if (!key) continue;
|
|
67
|
-
values.set(key, stripQuotes(rawValue));
|
|
68
|
-
}
|
|
69
|
-
return values;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function loadEnvFiles() {
|
|
73
|
-
if (envFilesLoaded) return;
|
|
74
|
-
envFilesLoaded = true;
|
|
75
|
-
for (const source of ENV_SOURCES) {
|
|
76
|
-
try {
|
|
77
|
-
const content = readFileSync(source.path, "utf-8");
|
|
78
|
-
envFileContents.set(source.name, content);
|
|
79
|
-
envFileValues.set(source.name, parseEnvFile(content));
|
|
80
|
-
} catch {
|
|
81
|
-
// ignore missing files
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function loadEnvFromFiles(name) {
|
|
87
|
-
for (const source of ENV_SOURCES) {
|
|
88
|
-
const values = envFileValues.get(source.name);
|
|
89
|
-
if (!values) continue;
|
|
90
|
-
if (values.has(name)) return values.get(name);
|
|
91
|
-
}
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function loadEnvVar(name) {
|
|
96
|
-
loadEnvFiles();
|
|
97
|
-
const fromFiles = loadEnvFromFiles(name);
|
|
98
|
-
if (fromFiles !== undefined) return fromFiles;
|
|
99
|
-
if (envFileContents.size === 0) return process.env[name];
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function getEnvFileStatus() {
|
|
104
|
-
loadEnvFiles();
|
|
105
|
-
const sources = ENV_SOURCES.filter((source) => envFileContents.has(source.name));
|
|
106
|
-
return {
|
|
107
|
-
found: sources.length > 0,
|
|
108
|
-
sources: sources.map((source) => source.name),
|
|
109
|
-
paths: sources.map((source) => source.path),
|
|
110
|
-
searchPaths: ENV_SOURCES.map((source) => source.path),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function parseBool(value, fallback) {
|
|
115
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
116
|
-
if (typeof value === "boolean") return value;
|
|
117
|
-
const normalized = String(value).trim().toLowerCase();
|
|
118
|
-
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
119
|
-
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
120
|
-
return fallback;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function buildConfig(pluginConfig = {}) {
|
|
124
|
-
const cfg = pluginConfig ?? {};
|
|
125
|
-
|
|
126
|
-
const baseUrl = cfg.baseUrl || loadEnvVar("MEMOS_BASE_URL") || DEFAULT_BASE_URL;
|
|
127
|
-
const apiKey = cfg.apiKey || loadEnvVar("MEMOS_API_KEY") || "";
|
|
128
|
-
const userId = cfg.userId || loadEnvVar("MEMOS_USER_ID") || "openclaw-user";
|
|
129
|
-
const conversationId = cfg.conversationId || loadEnvVar("MEMOS_CONVERSATION_ID") || "";
|
|
130
|
-
|
|
131
|
-
const recallGlobal = parseBool(
|
|
132
|
-
cfg.recallGlobal,
|
|
133
|
-
parseBool(loadEnvVar("MEMOS_RECALL_GLOBAL"), true),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const conversationIdPrefix = cfg.conversationIdPrefix ?? loadEnvVar("MEMOS_CONVERSATION_PREFIX") ?? "";
|
|
137
|
-
const conversationIdSuffix = cfg.conversationIdSuffix ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX") ?? "";
|
|
138
|
-
const conversationSuffixMode =
|
|
139
|
-
cfg.conversationSuffixMode ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX_MODE") ?? "none";
|
|
140
|
-
const resetOnNew = parseBool(
|
|
141
|
-
cfg.resetOnNew,
|
|
142
|
-
parseBool(loadEnvVar("MEMOS_CONVERSATION_RESET_ON_NEW"), true),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return [
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"",
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
"",
|
|
345
|
-
"
|
|
346
|
-
"",
|
|
347
|
-
|
|
348
|
-
"
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
"",
|
|
352
|
-
|
|
353
|
-
"",
|
|
354
|
-
"
|
|
355
|
-
"",
|
|
356
|
-
"
|
|
357
|
-
"",
|
|
358
|
-
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"",
|
|
364
|
-
"
|
|
365
|
-
"*
|
|
366
|
-
"* If
|
|
367
|
-
"",
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"",
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"",
|
|
375
|
-
"
|
|
376
|
-
"",
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BASE_URL = "https://memos.memtensor.cn/api/openmem/v1";
|
|
7
|
+
export const USER_QUERY_MARKER = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
|
|
8
|
+
const ENV_SOURCES = [
|
|
9
|
+
{ name: "openclaw", path: join(homedir(), ".openclaw", ".env") },
|
|
10
|
+
{ name: "moltbot", path: join(homedir(), ".moltbot", ".env") },
|
|
11
|
+
{ name: "clawdbot", path: join(homedir(), ".clawdbot", ".env") },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
let envFilesLoaded = false;
|
|
15
|
+
const envFileContents = new Map();
|
|
16
|
+
const envFileValues = new Map();
|
|
17
|
+
|
|
18
|
+
function stripQuotes(value) {
|
|
19
|
+
if (!value) return value;
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (
|
|
22
|
+
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
23
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
24
|
+
) {
|
|
25
|
+
return trimmed.slice(1, -1);
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractResultData(result) {
|
|
31
|
+
if (!result || typeof result !== "object") return null;
|
|
32
|
+
return result.data ?? result.data?.data ?? result.data?.result ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pad2(value) {
|
|
36
|
+
return String(value).padStart(2, "0");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatTime(value) {
|
|
40
|
+
if (value === undefined || value === null || value === "") return "";
|
|
41
|
+
if (typeof value === "number") {
|
|
42
|
+
const date = new Date(value);
|
|
43
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
44
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(
|
|
45
|
+
date.getHours(),
|
|
46
|
+
)}:${pad2(date.getMinutes())}`;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (!trimmed) return "";
|
|
51
|
+
if (/^\d+$/.test(trimmed)) return formatTime(Number(trimmed));
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseEnvFile(content) {
|
|
58
|
+
const values = new Map();
|
|
59
|
+
for (const line of content.split(/\r?\n/)) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
62
|
+
const idx = trimmed.indexOf("=");
|
|
63
|
+
if (idx <= 0) continue;
|
|
64
|
+
const key = trimmed.slice(0, idx).trim();
|
|
65
|
+
const rawValue = trimmed.slice(idx + 1);
|
|
66
|
+
if (!key) continue;
|
|
67
|
+
values.set(key, stripQuotes(rawValue));
|
|
68
|
+
}
|
|
69
|
+
return values;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadEnvFiles() {
|
|
73
|
+
if (envFilesLoaded) return;
|
|
74
|
+
envFilesLoaded = true;
|
|
75
|
+
for (const source of ENV_SOURCES) {
|
|
76
|
+
try {
|
|
77
|
+
const content = readFileSync(source.path, "utf-8");
|
|
78
|
+
envFileContents.set(source.name, content);
|
|
79
|
+
envFileValues.set(source.name, parseEnvFile(content));
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore missing files
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadEnvFromFiles(name) {
|
|
87
|
+
for (const source of ENV_SOURCES) {
|
|
88
|
+
const values = envFileValues.get(source.name);
|
|
89
|
+
if (!values) continue;
|
|
90
|
+
if (values.has(name)) return values.get(name);
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function loadEnvVar(name) {
|
|
96
|
+
loadEnvFiles();
|
|
97
|
+
const fromFiles = loadEnvFromFiles(name);
|
|
98
|
+
if (fromFiles !== undefined) return fromFiles;
|
|
99
|
+
if (envFileContents.size === 0) return process.env[name];
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getEnvFileStatus() {
|
|
104
|
+
loadEnvFiles();
|
|
105
|
+
const sources = ENV_SOURCES.filter((source) => envFileContents.has(source.name));
|
|
106
|
+
return {
|
|
107
|
+
found: sources.length > 0,
|
|
108
|
+
sources: sources.map((source) => source.name),
|
|
109
|
+
paths: sources.map((source) => source.path),
|
|
110
|
+
searchPaths: ENV_SOURCES.map((source) => source.path),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseBool(value, fallback) {
|
|
115
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
116
|
+
if (typeof value === "boolean") return value;
|
|
117
|
+
const normalized = String(value).trim().toLowerCase();
|
|
118
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
119
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildConfig(pluginConfig = {}) {
|
|
124
|
+
const cfg = pluginConfig ?? {};
|
|
125
|
+
|
|
126
|
+
const baseUrl = cfg.baseUrl || loadEnvVar("MEMOS_BASE_URL") || DEFAULT_BASE_URL;
|
|
127
|
+
const apiKey = cfg.apiKey || loadEnvVar("MEMOS_API_KEY") || "";
|
|
128
|
+
const userId = cfg.userId || loadEnvVar("MEMOS_USER_ID") || "openclaw-user";
|
|
129
|
+
const conversationId = cfg.conversationId || loadEnvVar("MEMOS_CONVERSATION_ID") || "";
|
|
130
|
+
|
|
131
|
+
const recallGlobal = parseBool(
|
|
132
|
+
cfg.recallGlobal,
|
|
133
|
+
parseBool(loadEnvVar("MEMOS_RECALL_GLOBAL"), true),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const conversationIdPrefix = cfg.conversationIdPrefix ?? loadEnvVar("MEMOS_CONVERSATION_PREFIX") ?? "";
|
|
137
|
+
const conversationIdSuffix = cfg.conversationIdSuffix ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX") ?? "";
|
|
138
|
+
const conversationSuffixMode =
|
|
139
|
+
cfg.conversationSuffixMode ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX_MODE") ?? "none";
|
|
140
|
+
const resetOnNew = parseBool(
|
|
141
|
+
cfg.resetOnNew,
|
|
142
|
+
parseBool(loadEnvVar("MEMOS_CONVERSATION_RESET_ON_NEW"), true),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const multiAgentMode = parseBool(
|
|
146
|
+
cfg.multiAgentMode,
|
|
147
|
+
parseBool(loadEnvVar("MEMOS_MULTI_AGENT_MODE"), false),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
152
|
+
apiKey,
|
|
153
|
+
userId,
|
|
154
|
+
conversationId,
|
|
155
|
+
conversationIdPrefix,
|
|
156
|
+
conversationIdSuffix,
|
|
157
|
+
conversationSuffixMode,
|
|
158
|
+
recallGlobal,
|
|
159
|
+
resetOnNew,
|
|
160
|
+
envFileStatus: getEnvFileStatus(),
|
|
161
|
+
queryPrefix: cfg.queryPrefix ?? "",
|
|
162
|
+
maxQueryChars: cfg.maxQueryChars ?? 0,
|
|
163
|
+
recallEnabled: cfg.recallEnabled !== false,
|
|
164
|
+
addEnabled: cfg.addEnabled !== false,
|
|
165
|
+
captureStrategy: cfg.captureStrategy ?? "last_turn",
|
|
166
|
+
maxMessageChars: cfg.maxMessageChars ?? 20000,
|
|
167
|
+
includeAssistant: cfg.includeAssistant !== false,
|
|
168
|
+
memoryLimitNumber: cfg.memoryLimitNumber ?? 9,
|
|
169
|
+
preferenceLimitNumber: cfg.preferenceLimitNumber ?? 6,
|
|
170
|
+
includePreference: cfg.includePreference !== false,
|
|
171
|
+
includeToolMemory: cfg.includeToolMemory === true,
|
|
172
|
+
toolMemoryLimitNumber: cfg.toolMemoryLimitNumber ?? 6,
|
|
173
|
+
relativity: cfg.relativity ?? ((() => {
|
|
174
|
+
const v = loadEnvVar("MEMOS_RELATIVITY");
|
|
175
|
+
return v ? parseFloat(v) : 0.45;
|
|
176
|
+
})()),
|
|
177
|
+
filter: cfg.filter,
|
|
178
|
+
knowledgebaseIds: cfg.knowledgebaseIds ?? [],
|
|
179
|
+
tags: cfg.tags ?? ["openclaw"],
|
|
180
|
+
info: cfg.info ?? {},
|
|
181
|
+
agentId: cfg.agentId,
|
|
182
|
+
appId: cfg.appId,
|
|
183
|
+
allowPublic: cfg.allowPublic ?? false,
|
|
184
|
+
allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? [],
|
|
185
|
+
asyncMode: cfg.asyncMode ?? true,
|
|
186
|
+
multiAgentMode,
|
|
187
|
+
timeoutMs: cfg.timeoutMs ?? 5000,
|
|
188
|
+
retries: cfg.retries ?? 1,
|
|
189
|
+
throttleMs: cfg.throttleMs ?? 0,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function callApi({ baseUrl, apiKey, timeoutMs = 5000, retries = 1 }, path, body) {
|
|
194
|
+
if (!apiKey) {
|
|
195
|
+
throw new Error("Missing MEMOS API key (Token auth)");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const headers = {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
Authorization: `Token ${apiKey}`,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
let lastError;
|
|
204
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
205
|
+
try {
|
|
206
|
+
const controller = new AbortController();
|
|
207
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
208
|
+
|
|
209
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers,
|
|
212
|
+
body: JSON.stringify(body),
|
|
213
|
+
signal: controller.signal,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
throw new Error(`HTTP ${res.status}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return await res.json();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
lastError = err;
|
|
225
|
+
if (attempt < retries) {
|
|
226
|
+
await delay(100 * (attempt + 1));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw lastError;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function searchMemory(cfg, payload) {
|
|
235
|
+
return callApi(cfg, "/search/memory", payload);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function addMessage(cfg, payload) {
|
|
239
|
+
return callApi(cfg, "/add/message", payload);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function extractText(content) {
|
|
243
|
+
if (!content) return "";
|
|
244
|
+
if (typeof content === "string") return content;
|
|
245
|
+
if (Array.isArray(content)) {
|
|
246
|
+
return content
|
|
247
|
+
.filter((block) => block && typeof block === "object" && block.type === "text")
|
|
248
|
+
.map((block) => block.text)
|
|
249
|
+
.join(" ");
|
|
250
|
+
}
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizePreferenceType(value) {
|
|
255
|
+
if (!value) return "";
|
|
256
|
+
const normalized = String(value).trim().toLowerCase();
|
|
257
|
+
if (!normalized) return "";
|
|
258
|
+
if (normalized.includes("explicit")) return "Explicit Preference";
|
|
259
|
+
if (normalized.includes("implicit")) return "Implicit Preference";
|
|
260
|
+
return String(value)
|
|
261
|
+
.replace(/[_-]+/g, " ")
|
|
262
|
+
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sanitizeInlineText(text) {
|
|
266
|
+
if (text === undefined || text === null) return "";
|
|
267
|
+
return String(text).replace(/\r?\n+/g, " ").trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatMemoryLine(item, text, options = {}) {
|
|
271
|
+
const cleaned = sanitizeInlineText(text);
|
|
272
|
+
if (!cleaned) return "";
|
|
273
|
+
const maxChars = options.maxItemChars;
|
|
274
|
+
const truncated = truncate(cleaned, maxChars);
|
|
275
|
+
const time = formatTime(item?.create_time);
|
|
276
|
+
if (time) return ` -[${time}] ${truncated}`;
|
|
277
|
+
return ` - ${truncated}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function formatPreferenceLine(item, text, options = {}) {
|
|
281
|
+
const cleaned = sanitizeInlineText(text);
|
|
282
|
+
if (!cleaned) return "";
|
|
283
|
+
const maxChars = options.maxItemChars;
|
|
284
|
+
const truncated = truncate(cleaned, maxChars);
|
|
285
|
+
const time = formatTime(item?.create_time);
|
|
286
|
+
const type = normalizePreferenceType(item?.preference_type);
|
|
287
|
+
const typeLabel = type ? ` [${type}]` : "";
|
|
288
|
+
if (time) return ` -[${time}]${typeLabel} ${truncated}`;
|
|
289
|
+
return ` -${typeLabel} ${truncated}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function wrapCodeBlock(lines, options = {}) {
|
|
293
|
+
if (!options.wrapTagBlocks) return lines;
|
|
294
|
+
return ["```text", ...lines, "```"];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildPromptFromData(data, options = {}) {
|
|
298
|
+
const now = options.currentTime ?? Date.now();
|
|
299
|
+
const nowText = formatTime(now) || formatTime(Date.now()) || "";
|
|
300
|
+
const memoryList = data?.memory_detail_list ?? [];
|
|
301
|
+
const preferenceList = data?.preference_detail_list ?? [];
|
|
302
|
+
|
|
303
|
+
const memoryLines = memoryList
|
|
304
|
+
.filter((item) => {
|
|
305
|
+
const score = item?.relativity ?? 1;
|
|
306
|
+
const threshold = options.relativity ?? 0;
|
|
307
|
+
return score > threshold;
|
|
308
|
+
})
|
|
309
|
+
.map((item) => {
|
|
310
|
+
const text = item?.memory_value || item?.memory_key || "";
|
|
311
|
+
return formatMemoryLine(item, text, options);
|
|
312
|
+
})
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
|
|
315
|
+
const preferenceLines = preferenceList
|
|
316
|
+
.filter((item) => {
|
|
317
|
+
const score = item?.relativity ?? 1;
|
|
318
|
+
const threshold = options.relativity ?? 0;
|
|
319
|
+
return score > threshold;
|
|
320
|
+
})
|
|
321
|
+
.map((item) => {
|
|
322
|
+
const text = item?.preference || "";
|
|
323
|
+
return formatPreferenceLine(item, text, options);
|
|
324
|
+
})
|
|
325
|
+
.filter(Boolean);
|
|
326
|
+
|
|
327
|
+
const hasContent = memoryLines.length > 0 || preferenceLines.length > 0;
|
|
328
|
+
|
|
329
|
+
if (!hasContent) return "";
|
|
330
|
+
|
|
331
|
+
const memoriesBlock = [
|
|
332
|
+
"<memories>",
|
|
333
|
+
" <facts>",
|
|
334
|
+
...memoryLines,
|
|
335
|
+
" </facts>",
|
|
336
|
+
" <preferences>",
|
|
337
|
+
...preferenceLines,
|
|
338
|
+
" </preferences>",
|
|
339
|
+
"</memories>",
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
const lines = [
|
|
343
|
+
"# Role",
|
|
344
|
+
"",
|
|
345
|
+
"You are an intelligent assistant with long-term memory capabilities (MemOS Assistant). Your goal is to combine retrieved memory fragments to provide highly personalized, accurate, and logically rigorous responses.",
|
|
346
|
+
"",
|
|
347
|
+
"# System Context",
|
|
348
|
+
"",
|
|
349
|
+
`* Current Time: ${nowText} (Use this as the baseline for freshness checks)`,
|
|
350
|
+
"",
|
|
351
|
+
"# Memory Data",
|
|
352
|
+
"",
|
|
353
|
+
'Below is the information retrieved by MemOS, categorized into "Facts" and "Preferences".',
|
|
354
|
+
"* **Facts**: May include user attributes, historical conversations, or third-party details.",
|
|
355
|
+
"* **Special Note**: Content tagged with '[assistant观点]' or '[模型总结]' represents **past AI inference**, **not** direct user statements.",
|
|
356
|
+
"* **Preferences**: The user's explicit or implicit requirements on response style, format, or reasoning.",
|
|
357
|
+
"",
|
|
358
|
+
...wrapCodeBlock(memoriesBlock, options),
|
|
359
|
+
"",
|
|
360
|
+
"# Critical Protocol: Memory Safety",
|
|
361
|
+
"",
|
|
362
|
+
"Retrieved memories may contain **AI speculation**, **irrelevant noise**, or **wrong subject attribution**. You must strictly apply the **Four-Step Verdict**. If any step fails, **discard the memory**:",
|
|
363
|
+
"",
|
|
364
|
+
"1. **Source Verification**:",
|
|
365
|
+
"* **Core**: Distinguish direct user statements from AI inference.",
|
|
366
|
+
"* If a memory has tags like '[assistant观点]' or '[模型总结]', treat it as a **hypothesis**, not a user-grounded fact.",
|
|
367
|
+
"* *Counterexample*: If memory says '[assistant观点] User loves mangoes' but the user never said that, do not assume it as fact.",
|
|
368
|
+
"* **Principle: AI summaries are reference-only and have much lower authority than direct user statements.**",
|
|
369
|
+
"",
|
|
370
|
+
"2. **Attribution Check**:",
|
|
371
|
+
"* Is the subject in memory definitely the user?",
|
|
372
|
+
"* If the memory describes a **third party** (e.g., candidate, interviewee, fictional character, case data), never attribute it to the user.",
|
|
373
|
+
"",
|
|
374
|
+
"3. **Strong Relevance Check**:",
|
|
375
|
+
"* Does the memory directly help answer the current 'Original Query'?",
|
|
376
|
+
"* If it is only a keyword overlap with different context, ignore it.",
|
|
377
|
+
"",
|
|
378
|
+
"4. **Freshness Check**:",
|
|
379
|
+
"* If memory conflicts with the user's latest intent, prioritize the current 'Original Query' as the highest source of truth.",
|
|
380
|
+
"",
|
|
381
|
+
"# Instructions",
|
|
382
|
+
"",
|
|
383
|
+
"1. **Review**: Read '<facts>' first and apply the Four-Step Verdict to remove noise and unreliable AI inference.",
|
|
384
|
+
"2. **Execute**:",
|
|
385
|
+
" - Use only memories that pass filtering as context.",
|
|
386
|
+
" - Strictly follow style requirements from '<preferences>'.",
|
|
387
|
+
"3. **Output**: Answer directly. Never mention internal terms such as \"memory store\", \"retrieval\", or \"AI opinions\".",
|
|
388
|
+
"4. **Attention**: Additional memory context is already provided. Do not read from or write to local `MEMORY.md` or `memory/*` files for reference, as they may be outdated or irrelevant to the current query.",
|
|
389
|
+
USER_QUERY_MARKER,
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
return lines.join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function formatContextBlock(result, options = {}) {
|
|
396
|
+
const data = extractResultData(result);
|
|
397
|
+
if (!data) return "";
|
|
398
|
+
|
|
399
|
+
const memoryList = data.memory_detail_list ?? [];
|
|
400
|
+
const prefList = data.preference_detail_list ?? [];
|
|
401
|
+
const toolList = data.tool_memory_detail_list ?? [];
|
|
402
|
+
const preferenceNote = data.preference_note;
|
|
403
|
+
|
|
404
|
+
const lines = [];
|
|
405
|
+
if (memoryList.length > 0) {
|
|
406
|
+
lines.push("Facts:");
|
|
407
|
+
for (const item of memoryList) {
|
|
408
|
+
const text = item?.memory_value || item?.memory_key || "";
|
|
409
|
+
if (!text) continue;
|
|
410
|
+
lines.push(`- ${truncate(text, options.maxItemChars)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (prefList.length > 0) {
|
|
415
|
+
lines.push("Preferences:");
|
|
416
|
+
for (const item of prefList) {
|
|
417
|
+
const pref = item?.preference || "";
|
|
418
|
+
const type = item?.preference_type ? `(${item.preference_type}) ` : "";
|
|
419
|
+
if (!pref) continue;
|
|
420
|
+
lines.push(`- ${type}${truncate(pref, options.maxItemChars)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (toolList.length > 0) {
|
|
425
|
+
lines.push("Tool Memories:");
|
|
426
|
+
for (const item of toolList) {
|
|
427
|
+
const value = item?.tool_value || "";
|
|
428
|
+
if (!value) continue;
|
|
429
|
+
lines.push(`- ${truncate(value, options.maxItemChars)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (preferenceNote) {
|
|
434
|
+
lines.push(`Preference Note: ${truncate(preferenceNote, options.maxItemChars)}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return lines.length > 0 ? lines.join("\n") : "";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function formatPromptBlock(result, options = {}) {
|
|
441
|
+
const data = extractResultData(result);
|
|
442
|
+
if (!data) return "";
|
|
443
|
+
return buildPromptFromData(data, options);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function truncate(text, maxLen) {
|
|
447
|
+
if (!text) return "";
|
|
448
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
449
|
+
}
|