@humanlikememory/human-like-mem 0.3.10 → 0.3.12
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 +475 -127
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -6
package/index.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* @license Apache-2.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const PLUGIN_VERSION = "0.3.4";
|
|
10
|
-
const USER_QUERY_MARKER = "--- User Query ---";
|
|
9
|
+
const PLUGIN_VERSION = "0.3.4";
|
|
10
|
+
const USER_QUERY_MARKER = "--- User Query ---";
|
|
11
|
+
const CACHE_DEDUP_WINDOW_MS = 4000;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Session cache for tracking conversation history
|
|
@@ -114,31 +115,101 @@ function extractFeishuTailBlock(text) {
|
|
|
114
115
|
/**
|
|
115
116
|
* Whether text is only transport metadata rather than a real utterance.
|
|
116
117
|
*/
|
|
117
|
-
function isMetadataOnlyText(text) {
|
|
118
|
-
if (!text) return true;
|
|
119
|
-
const value = String(text).trim();
|
|
120
|
-
if (!value) return true;
|
|
121
|
-
if (/^\[message_id:\s*[^\]]+\]$/i.test(value)) return true;
|
|
122
|
-
if (/^\[\[reply_to[^\]]*\]\]$/i.test(value)) return true;
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
118
|
+
function isMetadataOnlyText(text) {
|
|
119
|
+
if (!text) return true;
|
|
120
|
+
const value = String(text).trim();
|
|
121
|
+
if (!value) return true;
|
|
122
|
+
if (/^\[message_id:\s*[^\]]+\]$/i.test(value)) return true;
|
|
123
|
+
if (/^\[\[reply_to[^\]]*\]\]$/i.test(value)) return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remove injected relay metadata blocks/lines from user content.
|
|
129
|
+
*/
|
|
130
|
+
function stripInjectedMetadata(text) {
|
|
131
|
+
if (!text || typeof text !== "string") return "";
|
|
132
|
+
|
|
133
|
+
let result = text.replace(/\r\n/g, "\n");
|
|
134
|
+
|
|
135
|
+
// Remove labeled untrusted metadata JSON blocks.
|
|
136
|
+
// Examples:
|
|
137
|
+
// Conversation info (untrusted metadata): ```json ... ```
|
|
138
|
+
// Sender (untrusted metadata): ```json ... ```
|
|
139
|
+
result = result.replace(
|
|
140
|
+
/(^|\n)[^\n]*\(untrusted metadata(?:,\s*for context)?\)\s*:\s*```json[\s\S]*?```(?=\n|$)/gi,
|
|
141
|
+
"\n"
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Remove transport/system hint lines that are not user utterances.
|
|
145
|
+
result = result.replace(/^\[System:[^\n]*\]\s*$/gim, "");
|
|
146
|
+
result = result.replace(/^\[message_id:\s*[^\]]+\]\s*$/gim, "");
|
|
147
|
+
|
|
148
|
+
// Collapse extra blank lines.
|
|
149
|
+
result = result.replace(/\n{3,}/g, "\n\n").trim();
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Keep the latest meaningful line from cleaned metadata-injected payload.
|
|
155
|
+
*/
|
|
156
|
+
function extractLatestUtteranceFromCleanText(text) {
|
|
157
|
+
if (!text || typeof text !== "string") return "";
|
|
158
|
+
const lines = text
|
|
159
|
+
.split("\n")
|
|
160
|
+
.map((line) => line.trim())
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.filter((line) => !isMetadataOnlyText(line));
|
|
163
|
+
if (lines.length === 0) return "";
|
|
164
|
+
return lines[lines.length - 1];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function escapeRegex(text) {
|
|
168
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Prefix utterance with parsed sender name when available.
|
|
173
|
+
* This preserves who said it in multi-user group contexts.
|
|
174
|
+
*/
|
|
175
|
+
function attachSenderPrefix(utterance, senderName) {
|
|
176
|
+
const content = String(utterance || "").trim();
|
|
177
|
+
if (!content) return "";
|
|
178
|
+
const sender = String(senderName || "").trim();
|
|
179
|
+
if (!sender) return content;
|
|
180
|
+
|
|
181
|
+
const senderRegex = new RegExp(`^${escapeRegex(sender)}\\s*[::]`);
|
|
182
|
+
if (senderRegex.test(content)) return content;
|
|
183
|
+
return `${sender}: ${content}`;
|
|
184
|
+
}
|
|
125
185
|
|
|
126
186
|
/**
|
|
127
187
|
* Normalize user message text before caching/storing.
|
|
128
188
|
* For channel-formatted payloads (e.g. Feishu), keep only the actual user utterance
|
|
129
189
|
* and drop prepended system transcript lines.
|
|
130
190
|
*/
|
|
131
|
-
function normalizeUserMessageContent(content) {
|
|
132
|
-
const text = stripPrependedPrompt(content);
|
|
133
|
-
if (!text) return "";
|
|
134
|
-
|
|
135
|
-
const normalized = String(text).replace(/\r\n/g, "\n").trim();
|
|
136
|
-
if (!normalized) return "";
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
191
|
+
function normalizeUserMessageContent(content) {
|
|
192
|
+
const text = stripPrependedPrompt(content);
|
|
193
|
+
if (!text) return "";
|
|
194
|
+
|
|
195
|
+
const normalized = String(text).replace(/\r\n/g, "\n").trim();
|
|
196
|
+
if (!normalized) return "";
|
|
197
|
+
|
|
198
|
+
const parsedIdentity = parseIdentityFromUntrustedMetadata(normalized);
|
|
199
|
+
|
|
200
|
+
// New relay payload format: strip injected metadata blocks first.
|
|
201
|
+
const strippedMetadata = stripInjectedMetadata(normalized);
|
|
202
|
+
if (strippedMetadata && strippedMetadata !== normalized) {
|
|
203
|
+
const latestUtterance = extractLatestUtteranceFromCleanText(strippedMetadata);
|
|
204
|
+
if (latestUtterance && !isMetadataOnlyText(latestUtterance)) {
|
|
205
|
+
return attachSenderPrefix(latestUtterance, parsedIdentity?.userName);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Feishu: preserve the final traceable tail block (contains platform user id/message_id).
|
|
210
|
+
const feishuTailBlock = extractFeishuTailBlock(normalized);
|
|
211
|
+
if (feishuTailBlock && !isMetadataOnlyText(feishuTailBlock)) {
|
|
212
|
+
return feishuTailBlock;
|
|
142
213
|
}
|
|
143
214
|
|
|
144
215
|
// Generic channel relays: fallback to latest "System: ...: <message>" line.
|
|
@@ -160,16 +231,18 @@ function normalizeUserMessageContent(content) {
|
|
|
160
231
|
|
|
161
232
|
// Discord-style channel-formatted payload:
|
|
162
233
|
// [from: username (id)] actual message
|
|
163
|
-
const discordTail = normalized.match(
|
|
164
|
-
/\[from:\s*[^\(\]\n]+?\s*\(\d{6,}\)\]\s*([\s\S]*?)$/i
|
|
165
|
-
);
|
|
166
|
-
if (discordTail && discordTail[1]) {
|
|
167
|
-
const candidate = discordTail[1].trim();
|
|
168
|
-
if (!isMetadataOnlyText(candidate)) return candidate;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return isMetadataOnlyText(normalized)
|
|
172
|
-
|
|
234
|
+
const discordTail = normalized.match(
|
|
235
|
+
/\[from:\s*[^\(\]\n]+?\s*\(\d{6,}\)\]\s*([\s\S]*?)$/i
|
|
236
|
+
);
|
|
237
|
+
if (discordTail && discordTail[1]) {
|
|
238
|
+
const candidate = discordTail[1].trim();
|
|
239
|
+
if (!isMetadataOnlyText(candidate)) return candidate;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return isMetadataOnlyText(normalized)
|
|
243
|
+
? ""
|
|
244
|
+
: attachSenderPrefix(normalized, parsedIdentity?.userName);
|
|
245
|
+
}
|
|
173
246
|
|
|
174
247
|
/**
|
|
175
248
|
* Normalize assistant message text before caching/storing.
|
|
@@ -183,12 +256,139 @@ function normalizeAssistantMessageContent(content) {
|
|
|
183
256
|
return normalized;
|
|
184
257
|
}
|
|
185
258
|
|
|
186
|
-
/**
|
|
187
|
-
* Parse
|
|
188
|
-
*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Parse JSON objects from markdown code fences after a specific label.
|
|
261
|
+
* Example:
|
|
262
|
+
* Sender (untrusted metadata):
|
|
263
|
+
* ```json
|
|
264
|
+
* { "id": "123" }
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
function parseJsonFencesAfterLabel(text, labelPattern) {
|
|
268
|
+
if (!text || typeof text !== "string") return [];
|
|
269
|
+
|
|
270
|
+
const results = [];
|
|
271
|
+
const regex = new RegExp(`${labelPattern}\\s*:\\s*\`\`\`json\\s*([\\s\\S]*?)\\s*\`\`\``, "gi");
|
|
272
|
+
let match;
|
|
273
|
+
while ((match = regex.exec(text)) !== null) {
|
|
274
|
+
if (!match[1]) continue;
|
|
275
|
+
try {
|
|
276
|
+
const parsed = JSON.parse(match[1]);
|
|
277
|
+
if (parsed && typeof parsed === "object") {
|
|
278
|
+
results.push(parsed);
|
|
279
|
+
}
|
|
280
|
+
} catch (_) {
|
|
281
|
+
// Ignore malformed JSON blocks and continue matching others.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return results;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Normalize potential user id values into non-empty strings.
|
|
289
|
+
*/
|
|
290
|
+
function normalizeParsedUserId(value) {
|
|
291
|
+
if (value === undefined || value === null) return "";
|
|
292
|
+
const normalized = String(value).trim();
|
|
293
|
+
return normalized || "";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Extract user id from a display label like:
|
|
298
|
+
* "用户250389 (ou_xxx)" / "name (123456789)".
|
|
299
|
+
*/
|
|
300
|
+
function extractUserIdFromLabel(label) {
|
|
301
|
+
const text = normalizeParsedUserId(label);
|
|
302
|
+
if (!text) return "";
|
|
303
|
+
const match = text.match(/\(\s*((?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)$/i);
|
|
304
|
+
return match?.[1] ? match[1] : "";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Extract transport message id from current relay payload.
|
|
309
|
+
*/
|
|
310
|
+
function extractRelayMessageId(text) {
|
|
311
|
+
if (!text || typeof text !== "string") return "";
|
|
312
|
+
|
|
313
|
+
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
314
|
+
text,
|
|
315
|
+
"Conversation info \\(untrusted metadata\\)"
|
|
316
|
+
);
|
|
317
|
+
for (let i = conversationMetaList.length - 1; i >= 0; i--) {
|
|
318
|
+
const conversationMeta = conversationMetaList[i];
|
|
319
|
+
const messageId =
|
|
320
|
+
normalizeParsedUserId(conversationMeta?.message_id) ||
|
|
321
|
+
normalizeParsedUserId(conversationMeta?.messageId);
|
|
322
|
+
if (messageId) return messageId;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const messageIdMatch = text.match(/\[message_id:\s*([^\]\n]+)\]/i);
|
|
326
|
+
if (messageIdMatch?.[1]) return messageIdMatch[1].trim();
|
|
327
|
+
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Parse identity from new "untrusted metadata" JSON blocks.
|
|
333
|
+
* Priority:
|
|
334
|
+
* 1) Sender (untrusted metadata).id
|
|
335
|
+
* 2) Conversation info (untrusted metadata).sender_id
|
|
336
|
+
*/
|
|
337
|
+
function parseIdentityFromUntrustedMetadata(text) {
|
|
338
|
+
if (!text || typeof text !== "string") return null;
|
|
339
|
+
|
|
340
|
+
const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
|
|
341
|
+
for (let i = senderMetaList.length - 1; i >= 0; i--) {
|
|
342
|
+
const senderMeta = senderMetaList[i];
|
|
343
|
+
const userId =
|
|
344
|
+
normalizeParsedUserId(senderMeta?.id) ||
|
|
345
|
+
extractUserIdFromLabel(senderMeta?.label);
|
|
346
|
+
if (userId) {
|
|
347
|
+
return {
|
|
348
|
+
platform: null,
|
|
349
|
+
userId,
|
|
350
|
+
userName: senderMeta?.name || senderMeta?.username || senderMeta?.label || null,
|
|
351
|
+
source: "sender-untrusted-metadata-json",
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
357
|
+
text,
|
|
358
|
+
"Conversation info \\(untrusted metadata\\)"
|
|
359
|
+
);
|
|
360
|
+
for (let i = conversationMetaList.length - 1; i >= 0; i--) {
|
|
361
|
+
const conversationMeta = conversationMetaList[i];
|
|
362
|
+
const userId =
|
|
363
|
+
normalizeParsedUserId(conversationMeta?.sender_id) ||
|
|
364
|
+
normalizeParsedUserId(conversationMeta?.senderId);
|
|
365
|
+
if (userId) {
|
|
366
|
+
return {
|
|
367
|
+
platform: null,
|
|
368
|
+
userId,
|
|
369
|
+
userName: conversationMeta?.sender || null,
|
|
370
|
+
source: "conversation-untrusted-metadata-json",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parse platform identity hints from channel-formatted message text.
|
|
380
|
+
* Returns null when no platform-specific id can be extracted.
|
|
381
|
+
*/
|
|
382
|
+
function parsePlatformIdentity(text) {
|
|
383
|
+
if (!text || typeof text !== "string") return null;
|
|
384
|
+
|
|
385
|
+
// New Discord format first:
|
|
386
|
+
// Sender (untrusted metadata): ```json { "id": "116..." } ```
|
|
387
|
+
// Conversation info (untrusted metadata): ```json { "sender_id": "116..." } ```
|
|
388
|
+
const metadataIdentity = parseIdentityFromUntrustedMetadata(text);
|
|
389
|
+
if (metadataIdentity?.userId) {
|
|
390
|
+
return metadataIdentity;
|
|
391
|
+
}
|
|
192
392
|
|
|
193
393
|
// Discord example:
|
|
194
394
|
// [from: huang yongqing (1470374017541079042)]
|
|
@@ -220,18 +420,39 @@ function parsePlatformIdentity(text) {
|
|
|
220
420
|
/**
|
|
221
421
|
* Parse all platform user ids from a text blob.
|
|
222
422
|
*/
|
|
223
|
-
function parseAllPlatformUserIds(text) {
|
|
224
|
-
if (!text || typeof text !== "string") return [];
|
|
225
|
-
|
|
226
|
-
const ids = [];
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
423
|
+
function parseAllPlatformUserIds(text) {
|
|
424
|
+
if (!text || typeof text !== "string") return [];
|
|
425
|
+
|
|
426
|
+
const ids = [];
|
|
427
|
+
let match;
|
|
428
|
+
|
|
429
|
+
// New format: "Sender (untrusted metadata)" JSON blocks.
|
|
430
|
+
const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
|
|
431
|
+
for (const senderMeta of senderMetaList) {
|
|
432
|
+
const parsedId =
|
|
433
|
+
normalizeParsedUserId(senderMeta?.id) ||
|
|
434
|
+
extractUserIdFromLabel(senderMeta?.label);
|
|
435
|
+
if (parsedId) ids.push(parsedId);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// New format: "Conversation info (untrusted metadata)" JSON blocks.
|
|
439
|
+
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
440
|
+
text,
|
|
441
|
+
"Conversation info \\(untrusted metadata\\)"
|
|
442
|
+
);
|
|
443
|
+
for (const conversationMeta of conversationMetaList) {
|
|
444
|
+
const parsedId =
|
|
445
|
+
normalizeParsedUserId(conversationMeta?.sender_id) ||
|
|
446
|
+
normalizeParsedUserId(conversationMeta?.senderId);
|
|
447
|
+
if (parsedId) ids.push(parsedId);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Discord example:
|
|
451
|
+
// [from: huang yongqing (1470374017541079042)]
|
|
452
|
+
const discordRegex = /\[from:\s*[^\(\]\n]+?\s*\((\d{6,})\)\]/gi;
|
|
453
|
+
while ((match = discordRegex.exec(text)) !== null) {
|
|
454
|
+
if (match[1]) ids.push(match[1]);
|
|
455
|
+
}
|
|
235
456
|
|
|
236
457
|
// Feishu example:
|
|
237
458
|
// [Feishu ...:ou_17b624... Wed ...] username: message
|
|
@@ -246,8 +467,8 @@ function parseAllPlatformUserIds(text) {
|
|
|
246
467
|
/**
|
|
247
468
|
* Collect distinct user ids parsed from all messages.
|
|
248
469
|
*/
|
|
249
|
-
function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
|
|
250
|
-
const unique = new Set();
|
|
470
|
+
function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
|
|
471
|
+
const unique = new Set();
|
|
251
472
|
|
|
252
473
|
if (Array.isArray(messages)) {
|
|
253
474
|
for (const msg of messages) {
|
|
@@ -263,12 +484,96 @@ function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
|
|
|
263
484
|
unique.add(fallbackUserId);
|
|
264
485
|
}
|
|
265
486
|
|
|
266
|
-
return Array.from(unique);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
487
|
+
return Array.from(unique);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function extractUserNameFromLabel(label) {
|
|
491
|
+
const text = normalizeParsedUserId(label);
|
|
492
|
+
if (!text) return "";
|
|
493
|
+
const match = text.match(
|
|
494
|
+
/^(.*?)\s*\(\s*(?:(?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)\s*$/i
|
|
495
|
+
);
|
|
496
|
+
return match?.[1] ? match[1].trim() : text;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Parse name-id pairs from supported relay payload formats.
|
|
501
|
+
*/
|
|
502
|
+
function parseNameIdPairsFromText(text) {
|
|
503
|
+
if (!text || typeof text !== "string") return [];
|
|
504
|
+
const pairs = [];
|
|
505
|
+
|
|
506
|
+
// New format: Sender (untrusted metadata)
|
|
507
|
+
const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
|
|
508
|
+
for (const senderMeta of senderMetaList) {
|
|
509
|
+
const id =
|
|
510
|
+
normalizeParsedUserId(senderMeta?.id) ||
|
|
511
|
+
extractUserIdFromLabel(senderMeta?.label);
|
|
512
|
+
const name =
|
|
513
|
+
normalizeParsedUserId(senderMeta?.name) ||
|
|
514
|
+
normalizeParsedUserId(senderMeta?.username) ||
|
|
515
|
+
extractUserNameFromLabel(senderMeta?.label);
|
|
516
|
+
if (name && id) pairs.push([name, id]);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// New format: Conversation info (untrusted metadata)
|
|
520
|
+
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
521
|
+
text,
|
|
522
|
+
"Conversation info \\(untrusted metadata\\)"
|
|
523
|
+
);
|
|
524
|
+
for (const conversationMeta of conversationMetaList) {
|
|
525
|
+
const id =
|
|
526
|
+
normalizeParsedUserId(conversationMeta?.sender_id) ||
|
|
527
|
+
normalizeParsedUserId(conversationMeta?.senderId);
|
|
528
|
+
const name = normalizeParsedUserId(conversationMeta?.sender);
|
|
529
|
+
if (name && id) pairs.push([name, id]);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Old Discord: [from: name (id)]
|
|
533
|
+
const discordRegex = /\[from:\s*([^\(\]\n]+?)\s*\((\d{6,})\)\]/gi;
|
|
534
|
+
let match;
|
|
535
|
+
while ((match = discordRegex.exec(text)) !== null) {
|
|
536
|
+
const name = normalizeParsedUserId(match[1]);
|
|
537
|
+
const id = normalizeParsedUserId(match[2]);
|
|
538
|
+
if (name && id) pairs.push([name, id]);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Old Feishu: [Feishu ...:ou_xxx ...] name:
|
|
542
|
+
const feishuRegex = /\[Feishu[^\]]*:((?:ou|on|u)_[A-Za-z0-9]+)[^\]]*\]\s*([^:\n]+):/gi;
|
|
543
|
+
while ((match = feishuRegex.exec(text)) !== null) {
|
|
544
|
+
const id = normalizeParsedUserId(match[1]);
|
|
545
|
+
const name = normalizeParsedUserId(match[2]);
|
|
546
|
+
if (name && id) pairs.push([name, id]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return pairs;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Collect distinct name->id mapping from all messages.
|
|
554
|
+
*/
|
|
555
|
+
function collectNameIdMapFromMessages(messages) {
|
|
556
|
+
const map = {};
|
|
557
|
+
|
|
558
|
+
if (!Array.isArray(messages)) return map;
|
|
559
|
+
|
|
560
|
+
for (const msg of messages) {
|
|
561
|
+
if (!msg) continue;
|
|
562
|
+
const sourceText = msg.rawContent !== undefined ? msg.rawContent : msg.content;
|
|
563
|
+
const text = typeof sourceText === "string" ? sourceText : extractText(sourceText);
|
|
564
|
+
const pairs = parseNameIdPairsFromText(text);
|
|
565
|
+
for (const [name, id] of pairs) {
|
|
566
|
+
if (!name || !id) continue;
|
|
567
|
+
map[name] = id;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return map;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get latest user message text from cached messages.
|
|
576
|
+
*/
|
|
272
577
|
function getLatestUserMessageText(messages) {
|
|
273
578
|
if (!Array.isArray(messages)) return "";
|
|
274
579
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -284,9 +589,9 @@ function getLatestUserMessageText(messages) {
|
|
|
284
589
|
* Resolve request identity with fallback:
|
|
285
590
|
* platform user id -> configured user id -> "openclaw-user"
|
|
286
591
|
*/
|
|
287
|
-
function resolveRequestIdentity(promptText, cfg
|
|
288
|
-
const parsed = parsePlatformIdentity(promptText);
|
|
289
|
-
if (parsed?.userId) {
|
|
592
|
+
function resolveRequestIdentity(promptText, cfg) {
|
|
593
|
+
const parsed = parsePlatformIdentity(promptText);
|
|
594
|
+
if (parsed?.userId) {
|
|
290
595
|
return {
|
|
291
596
|
userId: parsed.userId,
|
|
292
597
|
userName: parsed.userName || null,
|
|
@@ -295,7 +600,7 @@ function resolveRequestIdentity(promptText, cfg, ctx) {
|
|
|
295
600
|
};
|
|
296
601
|
}
|
|
297
602
|
|
|
298
|
-
if (cfg?.configuredUserId) {
|
|
603
|
+
if (cfg?.configuredUserId) {
|
|
299
604
|
return {
|
|
300
605
|
userId: cfg.configuredUserId,
|
|
301
606
|
userName: null,
|
|
@@ -304,17 +609,8 @@ function resolveRequestIdentity(promptText, cfg, ctx) {
|
|
|
304
609
|
};
|
|
305
610
|
}
|
|
306
611
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
userId: ctx.userId,
|
|
310
|
-
userName: null,
|
|
311
|
-
platform: null,
|
|
312
|
-
source: "ctx-user-id",
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
userId: "openclaw-user",
|
|
612
|
+
return {
|
|
613
|
+
userId: "openclaw-user",
|
|
318
614
|
userName: null,
|
|
319
615
|
platform: null,
|
|
320
616
|
source: "default-user-id",
|
|
@@ -507,7 +803,7 @@ async function retrieveMemory(prompt, cfg, ctx, log) {
|
|
|
507
803
|
if (log?.info) {
|
|
508
804
|
log.info(`[Memory Plugin] Recall request URL: ${url}`);
|
|
509
805
|
}
|
|
510
|
-
const identity = resolveRequestIdentity(prompt, cfg
|
|
806
|
+
const identity = resolveRequestIdentity(prompt, cfg);
|
|
511
807
|
const userId = sanitizeUserId(identity.userId);
|
|
512
808
|
const payload = {
|
|
513
809
|
query: prompt,
|
|
@@ -566,18 +862,25 @@ async function addMemory(messages, cfg, ctx, log) {
|
|
|
566
862
|
}
|
|
567
863
|
const sessionId = resolveSessionId(ctx, null) || `session-${Date.now()}`;
|
|
568
864
|
const latestUserText = getLatestUserMessageText(messages);
|
|
569
|
-
const identity = resolveRequestIdentity(latestUserText, cfg
|
|
865
|
+
const identity = resolveRequestIdentity(latestUserText, cfg);
|
|
570
866
|
const userId = sanitizeUserId(identity.userId);
|
|
571
|
-
const metadataUserIds = (() => {
|
|
572
|
-
const parsed = collectUniqueUserIdsFromMessages(messages, null)
|
|
573
|
-
.map((id) => sanitizeUserId(id))
|
|
574
|
-
.filter(Boolean);
|
|
575
|
-
if (parsed.length > 0) {
|
|
576
|
-
return Array.from(new Set(parsed));
|
|
577
|
-
}
|
|
578
|
-
return [userId];
|
|
579
|
-
})();
|
|
580
|
-
const
|
|
867
|
+
const metadataUserIds = (() => {
|
|
868
|
+
const parsed = collectUniqueUserIdsFromMessages(messages, null)
|
|
869
|
+
.map((id) => sanitizeUserId(id))
|
|
870
|
+
.filter(Boolean);
|
|
871
|
+
if (parsed.length > 0) {
|
|
872
|
+
return Array.from(new Set(parsed));
|
|
873
|
+
}
|
|
874
|
+
return [userId];
|
|
875
|
+
})();
|
|
876
|
+
const nameIdMap = (() => {
|
|
877
|
+
const parsed = collectNameIdMapFromMessages(messages);
|
|
878
|
+
if (identity?.userName && userId && !parsed[identity.userName]) {
|
|
879
|
+
parsed[identity.userName] = userId;
|
|
880
|
+
}
|
|
881
|
+
return parsed;
|
|
882
|
+
})();
|
|
883
|
+
const agentId = cfg.agentId || ctx?.agentId || "main";
|
|
581
884
|
|
|
582
885
|
const payload = {
|
|
583
886
|
user_id: userId,
|
|
@@ -591,21 +894,22 @@ async function addMemory(messages, cfg, ctx, log) {
|
|
|
591
894
|
async_mode: true,
|
|
592
895
|
custom_workflows: {
|
|
593
896
|
stream_params: {
|
|
594
|
-
metadata: JSON.stringify({
|
|
595
|
-
user_ids: metadataUserIds,
|
|
596
|
-
agent_ids: [agentId],
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
897
|
+
metadata: JSON.stringify({
|
|
898
|
+
user_ids: metadataUserIds,
|
|
899
|
+
agent_ids: [agentId],
|
|
900
|
+
name_id_map: nameIdMap,
|
|
901
|
+
session_id: sessionId,
|
|
902
|
+
scenario: cfg.scenario || "openclaw-plugin",
|
|
903
|
+
}),
|
|
904
|
+
},
|
|
905
|
+
},
|
|
602
906
|
};
|
|
603
907
|
|
|
604
908
|
if (log?.debug) {
|
|
605
|
-
log.debug(
|
|
606
|
-
`[Memory Plugin] add/message payload: user_id=${userId}, agent_id=${agentId}, conversation_id=${sessionId}, metadata.user_ids=${JSON.stringify(metadataUserIds)}, metadata.agent_ids=${JSON.stringify([agentId])}`
|
|
607
|
-
);
|
|
608
|
-
}
|
|
909
|
+
log.debug(
|
|
910
|
+
`[Memory Plugin] add/message payload: user_id=${userId}, agent_id=${agentId}, conversation_id=${sessionId}, metadata.user_ids=${JSON.stringify(metadataUserIds)}, metadata.agent_ids=${JSON.stringify([agentId])}, metadata.name_id_map=${JSON.stringify(nameIdMap)}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
609
913
|
|
|
610
914
|
try {
|
|
611
915
|
const result = await httpRequest(url, {
|
|
@@ -850,18 +1154,44 @@ function getSessionCache(sessionId) {
|
|
|
850
1154
|
/**
|
|
851
1155
|
* Add message to session cache
|
|
852
1156
|
*/
|
|
853
|
-
function addToSessionCache(sessionId, message) {
|
|
854
|
-
const cache = getSessionCache(sessionId);
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1157
|
+
function addToSessionCache(sessionId, message) {
|
|
1158
|
+
const cache = getSessionCache(sessionId);
|
|
1159
|
+
const now = Date.now();
|
|
1160
|
+
const incoming = {
|
|
1161
|
+
...message,
|
|
1162
|
+
cachedAt: now,
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const prev = cache.messages.length > 0 ? cache.messages[cache.messages.length - 1] : null;
|
|
1166
|
+
if (prev) {
|
|
1167
|
+
const sameRole = prev.role === incoming.role;
|
|
1168
|
+
const sameMessageId =
|
|
1169
|
+
incoming.messageId &&
|
|
1170
|
+
prev.messageId &&
|
|
1171
|
+
String(incoming.messageId) === String(prev.messageId);
|
|
1172
|
+
const sameContent =
|
|
1173
|
+
sameRole &&
|
|
1174
|
+
String(prev.content || "") === String(incoming.content || "") &&
|
|
1175
|
+
String(prev.rawContent || "") === String(incoming.rawContent || "");
|
|
1176
|
+
const withinWindow = now - (prev.cachedAt || 0) <= CACHE_DEDUP_WINDOW_MS;
|
|
1177
|
+
|
|
1178
|
+
// Dedup duplicate hook triggers for the same relay message.
|
|
1179
|
+
if ((sameRole && sameMessageId) || (sameContent && withinWindow)) {
|
|
1180
|
+
cache.lastActivity = now;
|
|
1181
|
+
return cache;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
cache.messages.push(incoming);
|
|
1186
|
+
cache.lastActivity = Date.now();
|
|
1187
|
+
|
|
1188
|
+
// Count turns (user message after assistant = new turn)
|
|
1189
|
+
if (incoming.role === "user" && cache.messages.length > 1) {
|
|
1190
|
+
const prevMsg = cache.messages[cache.messages.length - 2];
|
|
1191
|
+
if (prevMsg && prevMsg.role === "assistant") {
|
|
1192
|
+
cache.turnCount++;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
865
1195
|
|
|
866
1196
|
return cache;
|
|
867
1197
|
}
|
|
@@ -1018,12 +1348,18 @@ function registerPlugin(api) {
|
|
|
1018
1348
|
return;
|
|
1019
1349
|
}
|
|
1020
1350
|
|
|
1021
|
-
const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
|
|
1022
|
-
const userContent = normalizeUserMessageContent(prompt);
|
|
1023
|
-
const rawUserContent = stripPrependedPrompt(prompt);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1351
|
+
const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
|
|
1352
|
+
const userContent = normalizeUserMessageContent(prompt);
|
|
1353
|
+
const rawUserContent = stripPrependedPrompt(prompt);
|
|
1354
|
+
const messageId = extractRelayMessageId(rawUserContent || prompt);
|
|
1355
|
+
if (userContent) {
|
|
1356
|
+
addToSessionCache(sessionId, {
|
|
1357
|
+
role: "user",
|
|
1358
|
+
content: userContent,
|
|
1359
|
+
rawContent: rawUserContent,
|
|
1360
|
+
messageId: messageId || undefined,
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1027
1363
|
|
|
1028
1364
|
try {
|
|
1029
1365
|
const memories = await retrieveMemory(prompt, cfg, ctx, log);
|
|
@@ -1150,12 +1486,18 @@ function createHooksPlugin(config) {
|
|
|
1150
1486
|
return;
|
|
1151
1487
|
}
|
|
1152
1488
|
|
|
1153
|
-
const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
|
|
1154
|
-
const userContent = normalizeUserMessageContent(prompt);
|
|
1155
|
-
const rawUserContent = stripPrependedPrompt(prompt);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1489
|
+
const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
|
|
1490
|
+
const userContent = normalizeUserMessageContent(prompt);
|
|
1491
|
+
const rawUserContent = stripPrependedPrompt(prompt);
|
|
1492
|
+
const messageId = extractRelayMessageId(rawUserContent || prompt);
|
|
1493
|
+
if (userContent) {
|
|
1494
|
+
addToSessionCache(sessionId, {
|
|
1495
|
+
role: "user",
|
|
1496
|
+
content: userContent,
|
|
1497
|
+
rawContent: rawUserContent,
|
|
1498
|
+
messageId: messageId || undefined,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1159
1501
|
|
|
1160
1502
|
try {
|
|
1161
1503
|
const memories = await retrieveMemory(prompt, cfg, ctx, log);
|
|
@@ -1223,13 +1565,19 @@ function createHooksPlugin(config) {
|
|
|
1223
1565
|
const content = msg.role === "user"
|
|
1224
1566
|
? normalizeUserMessageContent(msg.content)
|
|
1225
1567
|
: normalizeAssistantMessageContent(msg.content);
|
|
1226
|
-
const rawSource = msg.role === "user"
|
|
1227
|
-
? stripPrependedPrompt(msg.content)
|
|
1228
|
-
: extractText(msg.content);
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1568
|
+
const rawSource = msg.role === "user"
|
|
1569
|
+
? stripPrependedPrompt(msg.content)
|
|
1570
|
+
: extractText(msg.content);
|
|
1571
|
+
const messageId = msg.role === "user" ? extractRelayMessageId(rawSource || "") : "";
|
|
1572
|
+
if (content) {
|
|
1573
|
+
addToSessionCache(sessionId, {
|
|
1574
|
+
role: msg.role,
|
|
1575
|
+
content,
|
|
1576
|
+
rawContent: rawSource || undefined,
|
|
1577
|
+
messageId: messageId || undefined,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1233
1581
|
} else {
|
|
1234
1582
|
// Only add last assistant message
|
|
1235
1583
|
for (let i = event.messages.length - 1; i >= 0; i--) {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanlikememory/human-like-mem",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Long-term memory plugin for OpenClaw - AI Social Memory Integration",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -50,8 +50,7 @@
|
|
|
50
50
|
"jest": "^29.7.0",
|
|
51
51
|
"prettier": "^3.2.0"
|
|
52
52
|
},
|
|
53
|
-
"publishConfig": {
|
|
54
|
-
"access": "public"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|