@humanlikememory/human-like-mem 0.3.13 → 0.3.14
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 +1 -1
- package/README_ZH.md +1 -1
- package/index.js +65 -764
- package/openclaw.plugin.json +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
|
|
|
35
35
|
## Optional Environment Variables
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
|
|
38
|
+
export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
|
|
39
39
|
export HUMAN_LIKE_MEM_USER_ID="your-user-id"
|
|
40
40
|
export HUMAN_LIKE_MEM_AGENT_ID="main"
|
|
41
41
|
export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
|
package/README_ZH.md
CHANGED
|
@@ -35,7 +35,7 @@ export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
|
|
|
35
35
|
## 可选环境变量
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
|
|
38
|
+
export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
|
|
39
39
|
export HUMAN_LIKE_MEM_USER_ID="your-user-id"
|
|
40
40
|
export HUMAN_LIKE_MEM_AGENT_ID="main"
|
|
41
41
|
export HUMAN_LIKE_MEM_LIMIT_NUMBER="6"
|
package/index.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
const PLUGIN_VERSION = "0.3.4";
|
|
10
10
|
const USER_QUERY_MARKER = "--- User Query ---";
|
|
11
|
-
const CACHE_DEDUP_WINDOW_MS = 4000;
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Session cache for tracking conversation history
|
|
@@ -36,7 +35,7 @@ function warnMissingApiKey(log) {
|
|
|
36
35
|
Get your API key from: https://human-like.me
|
|
37
36
|
Then set:
|
|
38
37
|
export HUMAN_LIKE_MEM_API_KEY="mp_xxxxxx"
|
|
39
|
-
export HUMAN_LIKE_MEM_BASE_URL="https://human-like.me"
|
|
38
|
+
export HUMAN_LIKE_MEM_BASE_URL="https://plugin.human-like.me"
|
|
40
39
|
`;
|
|
41
40
|
if (log?.warn) {
|
|
42
41
|
log.warn(msg);
|
|
@@ -60,127 +59,6 @@ function extractText(content) {
|
|
|
60
59
|
return "";
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
function isToolCallBlock(block) {
|
|
64
|
-
if (!block || typeof block !== "object") return false;
|
|
65
|
-
const type = String(block.type || "").trim().toLowerCase();
|
|
66
|
-
return type === "toolcall" || type === "tool_call";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeToolCallBlock(block) {
|
|
70
|
-
if (!isToolCallBlock(block)) return null;
|
|
71
|
-
|
|
72
|
-
const name =
|
|
73
|
-
block.function?.name ||
|
|
74
|
-
block.toolName ||
|
|
75
|
-
block.name ||
|
|
76
|
-
"unknown";
|
|
77
|
-
const args =
|
|
78
|
-
block.function?.arguments ??
|
|
79
|
-
block.arguments ??
|
|
80
|
-
block.args ??
|
|
81
|
-
block.input ??
|
|
82
|
-
{};
|
|
83
|
-
const callId =
|
|
84
|
-
block.id ||
|
|
85
|
-
block.callId ||
|
|
86
|
-
block.toolCallId ||
|
|
87
|
-
block.tool_call_id ||
|
|
88
|
-
null;
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
id: callId,
|
|
92
|
-
name,
|
|
93
|
-
arguments: args,
|
|
94
|
-
function: {
|
|
95
|
-
name,
|
|
96
|
-
arguments: args,
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function extractToolCallsFromContent(content) {
|
|
102
|
-
if (!Array.isArray(content)) return [];
|
|
103
|
-
|
|
104
|
-
return content
|
|
105
|
-
.map((block) => normalizeToolCallBlock(block))
|
|
106
|
-
.filter(Boolean);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function getMessageToolCalls(msg) {
|
|
110
|
-
if (!msg || typeof msg !== "object") return [];
|
|
111
|
-
if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
112
|
-
return msg.tool_calls;
|
|
113
|
-
}
|
|
114
|
-
return extractToolCallsFromContent(msg.content);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function getToolResultCallId(msg) {
|
|
118
|
-
if (!msg || typeof msg !== "object") return null;
|
|
119
|
-
return (
|
|
120
|
-
msg.tool_call_id ||
|
|
121
|
-
msg.toolCallId ||
|
|
122
|
-
msg.call_id ||
|
|
123
|
-
msg.callId ||
|
|
124
|
-
null
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function getToolResultName(msg) {
|
|
129
|
-
if (!msg || typeof msg !== "object") return undefined;
|
|
130
|
-
return msg.name || msg.toolName || msg.tool_name || undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function normalizeCacheRole(role) {
|
|
134
|
-
if (role === "toolResult") return "tool";
|
|
135
|
-
return role;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isAssistantLikeRole(role) {
|
|
139
|
-
return role === "assistant" || role === "tool" || role === "toolResult";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function buildCacheMessageFromTranscript(msg) {
|
|
143
|
-
if (!msg || msg.role === "system") return null;
|
|
144
|
-
|
|
145
|
-
const normalizedRole = normalizeCacheRole(msg.role);
|
|
146
|
-
let content = "";
|
|
147
|
-
let rawSource = msg.content;
|
|
148
|
-
let messageId = "";
|
|
149
|
-
|
|
150
|
-
if (msg.role === "user") {
|
|
151
|
-
content = normalizeUserMessageContent(msg.content);
|
|
152
|
-
rawSource = stripPrependedPrompt(msg.content);
|
|
153
|
-
messageId = extractRelayMessageId(rawSource || "");
|
|
154
|
-
} else if (msg.role === "assistant") {
|
|
155
|
-
content = normalizeAssistantMessageContent(msg.content);
|
|
156
|
-
rawSource = extractText(msg.content);
|
|
157
|
-
} else if (normalizedRole === "tool") {
|
|
158
|
-
content = extractText(msg.content);
|
|
159
|
-
rawSource = content;
|
|
160
|
-
} else {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const toolCalls = getMessageToolCalls(msg);
|
|
165
|
-
const toolCallId = getToolResultCallId(msg);
|
|
166
|
-
const toolName = getToolResultName(msg);
|
|
167
|
-
const hasToolContext =
|
|
168
|
-
(normalizedRole === "assistant" && toolCalls.length > 0) ||
|
|
169
|
-
(normalizedRole === "tool" && (!!toolCallId || !!toolName));
|
|
170
|
-
|
|
171
|
-
if (!content && !hasToolContext) return null;
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
role: normalizedRole,
|
|
175
|
-
content: content || "",
|
|
176
|
-
rawContent: rawSource || undefined,
|
|
177
|
-
messageId: messageId || undefined,
|
|
178
|
-
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
179
|
-
tool_call_id: toolCallId || undefined,
|
|
180
|
-
name: toolName,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
62
|
/**
|
|
185
63
|
* Strip prepended prompt markers from message content
|
|
186
64
|
*/
|
|
@@ -245,65 +123,6 @@ function isMetadataOnlyText(text) {
|
|
|
245
123
|
return false;
|
|
246
124
|
}
|
|
247
125
|
|
|
248
|
-
/**
|
|
249
|
-
* Remove injected relay metadata blocks/lines from user content.
|
|
250
|
-
*/
|
|
251
|
-
function stripInjectedMetadata(text) {
|
|
252
|
-
if (!text || typeof text !== "string") return "";
|
|
253
|
-
|
|
254
|
-
let result = text.replace(/\r\n/g, "\n");
|
|
255
|
-
|
|
256
|
-
// Remove labeled untrusted metadata JSON blocks.
|
|
257
|
-
// Examples:
|
|
258
|
-
// Conversation info (untrusted metadata): ```json ... ```
|
|
259
|
-
// Sender (untrusted metadata): ```json ... ```
|
|
260
|
-
result = result.replace(
|
|
261
|
-
/(^|\n)[^\n]*\(untrusted metadata(?:,\s*for context)?\)\s*:\s*```json[\s\S]*?```(?=\n|$)/gi,
|
|
262
|
-
"\n"
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// Remove transport/system hint lines that are not user utterances.
|
|
266
|
-
result = result.replace(/^\[System:[^\n]*\]\s*$/gim, "");
|
|
267
|
-
result = result.replace(/^\[message_id:\s*[^\]]+\]\s*$/gim, "");
|
|
268
|
-
|
|
269
|
-
// Collapse extra blank lines.
|
|
270
|
-
result = result.replace(/\n{3,}/g, "\n\n").trim();
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Keep the latest meaningful line from cleaned metadata-injected payload.
|
|
276
|
-
*/
|
|
277
|
-
function extractLatestUtteranceFromCleanText(text) {
|
|
278
|
-
if (!text || typeof text !== "string") return "";
|
|
279
|
-
const lines = text
|
|
280
|
-
.split("\n")
|
|
281
|
-
.map((line) => line.trim())
|
|
282
|
-
.filter(Boolean)
|
|
283
|
-
.filter((line) => !isMetadataOnlyText(line));
|
|
284
|
-
if (lines.length === 0) return "";
|
|
285
|
-
return lines[lines.length - 1];
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function escapeRegex(text) {
|
|
289
|
-
return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Prefix utterance with parsed sender name when available.
|
|
294
|
-
* This preserves who said it in multi-user group contexts.
|
|
295
|
-
*/
|
|
296
|
-
function attachSenderPrefix(utterance, senderName) {
|
|
297
|
-
const content = String(utterance || "").trim();
|
|
298
|
-
if (!content) return "";
|
|
299
|
-
const sender = String(senderName || "").trim();
|
|
300
|
-
if (!sender) return content;
|
|
301
|
-
|
|
302
|
-
const senderRegex = new RegExp(`^${escapeRegex(sender)}\\s*[::]`);
|
|
303
|
-
if (senderRegex.test(content)) return content;
|
|
304
|
-
return `${sender}: ${content}`;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
126
|
/**
|
|
308
127
|
* Normalize user message text before caching/storing.
|
|
309
128
|
* For channel-formatted payloads (e.g. Feishu), keep only the actual user utterance
|
|
@@ -316,17 +135,6 @@ function normalizeUserMessageContent(content) {
|
|
|
316
135
|
const normalized = String(text).replace(/\r\n/g, "\n").trim();
|
|
317
136
|
if (!normalized) return "";
|
|
318
137
|
|
|
319
|
-
const parsedIdentity = parseIdentityFromUntrustedMetadata(normalized);
|
|
320
|
-
|
|
321
|
-
// New relay payload format: strip injected metadata blocks first.
|
|
322
|
-
const strippedMetadata = stripInjectedMetadata(normalized);
|
|
323
|
-
if (strippedMetadata && strippedMetadata !== normalized) {
|
|
324
|
-
const latestUtterance = extractLatestUtteranceFromCleanText(strippedMetadata);
|
|
325
|
-
if (latestUtterance && !isMetadataOnlyText(latestUtterance)) {
|
|
326
|
-
return attachSenderPrefix(latestUtterance, parsedIdentity?.userName);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
138
|
// Feishu: preserve the final traceable tail block (contains platform user id/message_id).
|
|
331
139
|
const feishuTailBlock = extractFeishuTailBlock(normalized);
|
|
332
140
|
if (feishuTailBlock && !isMetadataOnlyText(feishuTailBlock)) {
|
|
@@ -360,9 +168,7 @@ function normalizeUserMessageContent(content) {
|
|
|
360
168
|
if (!isMetadataOnlyText(candidate)) return candidate;
|
|
361
169
|
}
|
|
362
170
|
|
|
363
|
-
return isMetadataOnlyText(normalized)
|
|
364
|
-
? ""
|
|
365
|
-
: attachSenderPrefix(normalized, parsedIdentity?.userName);
|
|
171
|
+
return isMetadataOnlyText(normalized) ? "" : normalized;
|
|
366
172
|
}
|
|
367
173
|
|
|
368
174
|
/**
|
|
@@ -377,125 +183,6 @@ function normalizeAssistantMessageContent(content) {
|
|
|
377
183
|
return normalized;
|
|
378
184
|
}
|
|
379
185
|
|
|
380
|
-
/**
|
|
381
|
-
* Parse JSON objects from markdown code fences after a specific label.
|
|
382
|
-
* Example:
|
|
383
|
-
* Sender (untrusted metadata):
|
|
384
|
-
* ```json
|
|
385
|
-
* { "id": "123" }
|
|
386
|
-
* ```
|
|
387
|
-
*/
|
|
388
|
-
function parseJsonFencesAfterLabel(text, labelPattern) {
|
|
389
|
-
if (!text || typeof text !== "string") return [];
|
|
390
|
-
|
|
391
|
-
const results = [];
|
|
392
|
-
const regex = new RegExp(`${labelPattern}\\s*:\\s*\`\`\`json\\s*([\\s\\S]*?)\\s*\`\`\``, "gi");
|
|
393
|
-
let match;
|
|
394
|
-
while ((match = regex.exec(text)) !== null) {
|
|
395
|
-
if (!match[1]) continue;
|
|
396
|
-
try {
|
|
397
|
-
const parsed = JSON.parse(match[1]);
|
|
398
|
-
if (parsed && typeof parsed === "object") {
|
|
399
|
-
results.push(parsed);
|
|
400
|
-
}
|
|
401
|
-
} catch (_) {
|
|
402
|
-
// Ignore malformed JSON blocks and continue matching others.
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return results;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Normalize potential user id values into non-empty strings.
|
|
410
|
-
*/
|
|
411
|
-
function normalizeParsedUserId(value) {
|
|
412
|
-
if (value === undefined || value === null) return "";
|
|
413
|
-
const normalized = String(value).trim();
|
|
414
|
-
return normalized || "";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Extract user id from a display label like:
|
|
419
|
-
* "用户250389 (ou_xxx)" / "name (123456789)".
|
|
420
|
-
*/
|
|
421
|
-
function extractUserIdFromLabel(label) {
|
|
422
|
-
const text = normalizeParsedUserId(label);
|
|
423
|
-
if (!text) return "";
|
|
424
|
-
const match = text.match(/\(\s*((?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)$/i);
|
|
425
|
-
return match?.[1] ? match[1] : "";
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Extract transport message id from current relay payload.
|
|
430
|
-
*/
|
|
431
|
-
function extractRelayMessageId(text) {
|
|
432
|
-
if (!text || typeof text !== "string") return "";
|
|
433
|
-
|
|
434
|
-
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
435
|
-
text,
|
|
436
|
-
"Conversation info \\(untrusted metadata\\)"
|
|
437
|
-
);
|
|
438
|
-
for (let i = conversationMetaList.length - 1; i >= 0; i--) {
|
|
439
|
-
const conversationMeta = conversationMetaList[i];
|
|
440
|
-
const messageId =
|
|
441
|
-
normalizeParsedUserId(conversationMeta?.message_id) ||
|
|
442
|
-
normalizeParsedUserId(conversationMeta?.messageId);
|
|
443
|
-
if (messageId) return messageId;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const messageIdMatch = text.match(/\[message_id:\s*([^\]\n]+)\]/i);
|
|
447
|
-
if (messageIdMatch?.[1]) return messageIdMatch[1].trim();
|
|
448
|
-
|
|
449
|
-
return "";
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Parse identity from new "untrusted metadata" JSON blocks.
|
|
454
|
-
* Priority:
|
|
455
|
-
* 1) Sender (untrusted metadata).id
|
|
456
|
-
* 2) Conversation info (untrusted metadata).sender_id
|
|
457
|
-
*/
|
|
458
|
-
function parseIdentityFromUntrustedMetadata(text) {
|
|
459
|
-
if (!text || typeof text !== "string") return null;
|
|
460
|
-
|
|
461
|
-
const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
|
|
462
|
-
for (let i = senderMetaList.length - 1; i >= 0; i--) {
|
|
463
|
-
const senderMeta = senderMetaList[i];
|
|
464
|
-
const userId =
|
|
465
|
-
normalizeParsedUserId(senderMeta?.id) ||
|
|
466
|
-
extractUserIdFromLabel(senderMeta?.label);
|
|
467
|
-
if (userId) {
|
|
468
|
-
return {
|
|
469
|
-
platform: null,
|
|
470
|
-
userId,
|
|
471
|
-
userName: senderMeta?.name || senderMeta?.username || senderMeta?.label || null,
|
|
472
|
-
source: "sender-untrusted-metadata-json",
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
478
|
-
text,
|
|
479
|
-
"Conversation info \\(untrusted metadata\\)"
|
|
480
|
-
);
|
|
481
|
-
for (let i = conversationMetaList.length - 1; i >= 0; i--) {
|
|
482
|
-
const conversationMeta = conversationMetaList[i];
|
|
483
|
-
const userId =
|
|
484
|
-
normalizeParsedUserId(conversationMeta?.sender_id) ||
|
|
485
|
-
normalizeParsedUserId(conversationMeta?.senderId);
|
|
486
|
-
if (userId) {
|
|
487
|
-
return {
|
|
488
|
-
platform: null,
|
|
489
|
-
userId,
|
|
490
|
-
userName: conversationMeta?.sender || null,
|
|
491
|
-
source: "conversation-untrusted-metadata-json",
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
186
|
/**
|
|
500
187
|
* Parse platform identity hints from channel-formatted message text.
|
|
501
188
|
* Returns null when no platform-specific id can be extracted.
|
|
@@ -503,14 +190,6 @@ function parseIdentityFromUntrustedMetadata(text) {
|
|
|
503
190
|
function parsePlatformIdentity(text) {
|
|
504
191
|
if (!text || typeof text !== "string") return null;
|
|
505
192
|
|
|
506
|
-
// New Discord format first:
|
|
507
|
-
// Sender (untrusted metadata): ```json { "id": "116..." } ```
|
|
508
|
-
// Conversation info (untrusted metadata): ```json { "sender_id": "116..." } ```
|
|
509
|
-
const metadataIdentity = parseIdentityFromUntrustedMetadata(text);
|
|
510
|
-
if (metadataIdentity?.userId) {
|
|
511
|
-
return metadataIdentity;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
193
|
// Discord example:
|
|
515
194
|
// [from: huang yongqing (1470374017541079042)]
|
|
516
195
|
const discordFrom = text.match(/\[from:\s*([^\(\]\n]+?)\s*\((\d{6,})\)\]/i);
|
|
@@ -545,32 +224,11 @@ function parseAllPlatformUserIds(text) {
|
|
|
545
224
|
if (!text || typeof text !== "string") return [];
|
|
546
225
|
|
|
547
226
|
const ids = [];
|
|
548
|
-
let match;
|
|
549
|
-
|
|
550
|
-
// New format: "Sender (untrusted metadata)" JSON blocks.
|
|
551
|
-
const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
|
|
552
|
-
for (const senderMeta of senderMetaList) {
|
|
553
|
-
const parsedId =
|
|
554
|
-
normalizeParsedUserId(senderMeta?.id) ||
|
|
555
|
-
extractUserIdFromLabel(senderMeta?.label);
|
|
556
|
-
if (parsedId) ids.push(parsedId);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// New format: "Conversation info (untrusted metadata)" JSON blocks.
|
|
560
|
-
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
561
|
-
text,
|
|
562
|
-
"Conversation info \\(untrusted metadata\\)"
|
|
563
|
-
);
|
|
564
|
-
for (const conversationMeta of conversationMetaList) {
|
|
565
|
-
const parsedId =
|
|
566
|
-
normalizeParsedUserId(conversationMeta?.sender_id) ||
|
|
567
|
-
normalizeParsedUserId(conversationMeta?.senderId);
|
|
568
|
-
if (parsedId) ids.push(parsedId);
|
|
569
|
-
}
|
|
570
227
|
|
|
571
228
|
// Discord example:
|
|
572
229
|
// [from: huang yongqing (1470374017541079042)]
|
|
573
230
|
const discordRegex = /\[from:\s*[^\(\]\n]+?\s*\((\d{6,})\)\]/gi;
|
|
231
|
+
let match;
|
|
574
232
|
while ((match = discordRegex.exec(text)) !== null) {
|
|
575
233
|
if (match[1]) ids.push(match[1]);
|
|
576
234
|
}
|
|
@@ -608,90 +266,6 @@ function collectUniqueUserIdsFromMessages(messages, fallbackUserId) {
|
|
|
608
266
|
return Array.from(unique);
|
|
609
267
|
}
|
|
610
268
|
|
|
611
|
-
function extractUserNameFromLabel(label) {
|
|
612
|
-
const text = normalizeParsedUserId(label);
|
|
613
|
-
if (!text) return "";
|
|
614
|
-
const match = text.match(
|
|
615
|
-
/^(.*?)\s*\(\s*(?:(?:ou|on|u)_[A-Za-z0-9]+|\d{6,})\s*\)\s*$/i
|
|
616
|
-
);
|
|
617
|
-
return match?.[1] ? match[1].trim() : text;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Parse name-id pairs from supported relay payload formats.
|
|
622
|
-
*/
|
|
623
|
-
function parseNameIdPairsFromText(text) {
|
|
624
|
-
if (!text || typeof text !== "string") return [];
|
|
625
|
-
const pairs = [];
|
|
626
|
-
|
|
627
|
-
// New format: Sender (untrusted metadata)
|
|
628
|
-
const senderMetaList = parseJsonFencesAfterLabel(text, "Sender \\(untrusted metadata\\)");
|
|
629
|
-
for (const senderMeta of senderMetaList) {
|
|
630
|
-
const id =
|
|
631
|
-
normalizeParsedUserId(senderMeta?.id) ||
|
|
632
|
-
extractUserIdFromLabel(senderMeta?.label);
|
|
633
|
-
const name =
|
|
634
|
-
normalizeParsedUserId(senderMeta?.name) ||
|
|
635
|
-
normalizeParsedUserId(senderMeta?.username) ||
|
|
636
|
-
extractUserNameFromLabel(senderMeta?.label);
|
|
637
|
-
if (name && id) pairs.push([name, id]);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// New format: Conversation info (untrusted metadata)
|
|
641
|
-
const conversationMetaList = parseJsonFencesAfterLabel(
|
|
642
|
-
text,
|
|
643
|
-
"Conversation info \\(untrusted metadata\\)"
|
|
644
|
-
);
|
|
645
|
-
for (const conversationMeta of conversationMetaList) {
|
|
646
|
-
const id =
|
|
647
|
-
normalizeParsedUserId(conversationMeta?.sender_id) ||
|
|
648
|
-
normalizeParsedUserId(conversationMeta?.senderId);
|
|
649
|
-
const name = normalizeParsedUserId(conversationMeta?.sender);
|
|
650
|
-
if (name && id) pairs.push([name, id]);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Old Discord: [from: name (id)]
|
|
654
|
-
const discordRegex = /\[from:\s*([^\(\]\n]+?)\s*\((\d{6,})\)\]/gi;
|
|
655
|
-
let match;
|
|
656
|
-
while ((match = discordRegex.exec(text)) !== null) {
|
|
657
|
-
const name = normalizeParsedUserId(match[1]);
|
|
658
|
-
const id = normalizeParsedUserId(match[2]);
|
|
659
|
-
if (name && id) pairs.push([name, id]);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Old Feishu: [Feishu ...:ou_xxx ...] name:
|
|
663
|
-
const feishuRegex = /\[Feishu[^\]]*:((?:ou|on|u)_[A-Za-z0-9]+)[^\]]*\]\s*([^:\n]+):/gi;
|
|
664
|
-
while ((match = feishuRegex.exec(text)) !== null) {
|
|
665
|
-
const id = normalizeParsedUserId(match[1]);
|
|
666
|
-
const name = normalizeParsedUserId(match[2]);
|
|
667
|
-
if (name && id) pairs.push([name, id]);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
return pairs;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Collect distinct name->id mapping from all messages.
|
|
675
|
-
*/
|
|
676
|
-
function collectNameIdMapFromMessages(messages) {
|
|
677
|
-
const map = {};
|
|
678
|
-
|
|
679
|
-
if (!Array.isArray(messages)) return map;
|
|
680
|
-
|
|
681
|
-
for (const msg of messages) {
|
|
682
|
-
if (!msg) continue;
|
|
683
|
-
const sourceText = msg.rawContent !== undefined ? msg.rawContent : msg.content;
|
|
684
|
-
const text = typeof sourceText === "string" ? sourceText : extractText(sourceText);
|
|
685
|
-
const pairs = parseNameIdPairsFromText(text);
|
|
686
|
-
for (const [name, id] of pairs) {
|
|
687
|
-
if (!name || !id) continue;
|
|
688
|
-
map[name] = id;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return map;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
269
|
/**
|
|
696
270
|
* Get latest user message text from cached messages.
|
|
697
271
|
*/
|
|
@@ -710,7 +284,7 @@ function getLatestUserMessageText(messages) {
|
|
|
710
284
|
* Resolve request identity with fallback:
|
|
711
285
|
* platform user id -> configured user id -> "openclaw-user"
|
|
712
286
|
*/
|
|
713
|
-
function resolveRequestIdentity(promptText, cfg) {
|
|
287
|
+
function resolveRequestIdentity(promptText, cfg, ctx) {
|
|
714
288
|
const parsed = parsePlatformIdentity(promptText);
|
|
715
289
|
if (parsed?.userId) {
|
|
716
290
|
return {
|
|
@@ -730,6 +304,15 @@ function resolveRequestIdentity(promptText, cfg) {
|
|
|
730
304
|
};
|
|
731
305
|
}
|
|
732
306
|
|
|
307
|
+
if (ctx?.userId) {
|
|
308
|
+
return {
|
|
309
|
+
userId: ctx.userId,
|
|
310
|
+
userName: null,
|
|
311
|
+
platform: null,
|
|
312
|
+
source: "ctx-user-id",
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
733
316
|
return {
|
|
734
317
|
userId: "openclaw-user",
|
|
735
318
|
userName: null,
|
|
@@ -924,7 +507,7 @@ async function retrieveMemory(prompt, cfg, ctx, log) {
|
|
|
924
507
|
if (log?.info) {
|
|
925
508
|
log.info(`[Memory Plugin] Recall request URL: ${url}`);
|
|
926
509
|
}
|
|
927
|
-
const identity = resolveRequestIdentity(prompt, cfg);
|
|
510
|
+
const identity = resolveRequestIdentity(prompt, cfg, ctx);
|
|
928
511
|
const userId = sanitizeUserId(identity.userId);
|
|
929
512
|
const payload = {
|
|
930
513
|
query: prompt,
|
|
@@ -971,7 +554,7 @@ async function retrieveMemory(prompt, cfg, ctx, log) {
|
|
|
971
554
|
/**
|
|
972
555
|
* Add memories to the API
|
|
973
556
|
*/
|
|
974
|
-
async function addMemory(messages, cfg, ctx, log
|
|
557
|
+
async function addMemory(messages, cfg, ctx, log) {
|
|
975
558
|
const baseUrl = cfg.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL;
|
|
976
559
|
const apiKey = cfg.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY;
|
|
977
560
|
|
|
@@ -981,9 +564,9 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
|
|
|
981
564
|
if (log?.info) {
|
|
982
565
|
log.info(`[Memory Plugin] Add-memory request URL: ${url}`);
|
|
983
566
|
}
|
|
984
|
-
const sessionId =
|
|
567
|
+
const sessionId = resolveSessionId(ctx, null) || `session-${Date.now()}`;
|
|
985
568
|
const latestUserText = getLatestUserMessageText(messages);
|
|
986
|
-
const identity = resolveRequestIdentity(latestUserText, cfg);
|
|
569
|
+
const identity = resolveRequestIdentity(latestUserText, cfg, ctx);
|
|
987
570
|
const userId = sanitizeUserId(identity.userId);
|
|
988
571
|
const metadataUserIds = (() => {
|
|
989
572
|
const parsed = collectUniqueUserIdsFromMessages(messages, null)
|
|
@@ -994,13 +577,6 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
|
|
|
994
577
|
}
|
|
995
578
|
return [userId];
|
|
996
579
|
})();
|
|
997
|
-
const nameIdMap = (() => {
|
|
998
|
-
const parsed = collectNameIdMapFromMessages(messages);
|
|
999
|
-
if (identity?.userName && userId && !parsed[identity.userName]) {
|
|
1000
|
-
parsed[identity.userName] = userId;
|
|
1001
|
-
}
|
|
1002
|
-
return parsed;
|
|
1003
|
-
})();
|
|
1004
580
|
const agentId = cfg.agentId || ctx?.agentId || "main";
|
|
1005
581
|
|
|
1006
582
|
const payload = {
|
|
@@ -1018,7 +594,6 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
|
|
|
1018
594
|
metadata: JSON.stringify({
|
|
1019
595
|
user_ids: metadataUserIds,
|
|
1020
596
|
agent_ids: [agentId],
|
|
1021
|
-
name_id_map: nameIdMap,
|
|
1022
597
|
session_id: sessionId,
|
|
1023
598
|
scenario: cfg.scenario || "openclaw-plugin",
|
|
1024
599
|
}),
|
|
@@ -1028,7 +603,7 @@ async function addMemory(messages, cfg, ctx, log, explicitSessionId) {
|
|
|
1028
603
|
|
|
1029
604
|
if (log?.debug) {
|
|
1030
605
|
log.debug(
|
|
1031
|
-
`[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])}
|
|
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])}`
|
|
1032
607
|
);
|
|
1033
608
|
}
|
|
1034
609
|
|
|
@@ -1178,8 +753,8 @@ function formatMemoriesForContext(memories, options = {}) {
|
|
|
1178
753
|
* @param {number} maxTurns - Maximum number of turns to extract
|
|
1179
754
|
* @returns {Array} Recent messages
|
|
1180
755
|
*/
|
|
1181
|
-
function pickRecentMessages(messages, maxTurns = 10) {
|
|
1182
|
-
if (!messages || messages.length === 0) return [];
|
|
756
|
+
function pickRecentMessages(messages, maxTurns = 10) {
|
|
757
|
+
if (!messages || messages.length === 0) return [];
|
|
1183
758
|
|
|
1184
759
|
const result = [];
|
|
1185
760
|
let turnCount = 0;
|
|
@@ -1194,7 +769,7 @@ function pickRecentMessages(messages, maxTurns = 10) {
|
|
|
1194
769
|
if (role === "system") continue;
|
|
1195
770
|
|
|
1196
771
|
// Count a turn when we see a user message after an assistant message
|
|
1197
|
-
if (role === "user" &&
|
|
772
|
+
if (role === "user" && lastRole === "assistant") {
|
|
1198
773
|
turnCount++;
|
|
1199
774
|
}
|
|
1200
775
|
|
|
@@ -1220,230 +795,9 @@ function pickRecentMessages(messages, maxTurns = 10) {
|
|
|
1220
795
|
lastRole = role;
|
|
1221
796
|
}
|
|
1222
797
|
|
|
1223
|
-
return result;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function buildV2ConversationMessage(msg) {
|
|
1227
|
-
if (!msg) return null;
|
|
1228
|
-
|
|
1229
|
-
const role = normalizeCacheRole(msg.role);
|
|
1230
|
-
if (role === "system") return null;
|
|
1231
|
-
if (role !== "user" && role !== "assistant" && role !== "tool") return null;
|
|
1232
|
-
|
|
1233
|
-
const rawSource = msg.rawContent !== undefined ? msg.rawContent : msg.content;
|
|
1234
|
-
const rawContent = typeof rawSource === "string" ? rawSource : extractText(rawSource);
|
|
1235
|
-
const content =
|
|
1236
|
-
typeof msg.content === "string"
|
|
1237
|
-
? msg.content
|
|
1238
|
-
: extractText(msg.content);
|
|
1239
|
-
const toolCalls = role === "assistant" ? getMessageToolCalls(msg) : [];
|
|
1240
|
-
const toolCallId = role === "tool" ? getToolResultCallId(msg) : null;
|
|
1241
|
-
const toolName = role === "tool" ? getToolResultName(msg) : undefined;
|
|
1242
|
-
const hasToolContext =
|
|
1243
|
-
(role === "assistant" && toolCalls.length > 0) ||
|
|
1244
|
-
(role === "tool" && (!!toolCallId || !!toolName));
|
|
1245
|
-
|
|
1246
|
-
if (!content && !hasToolContext) return null;
|
|
1247
|
-
|
|
1248
|
-
const result = {
|
|
1249
|
-
role,
|
|
1250
|
-
content: content || "",
|
|
1251
|
-
rawContent: rawContent || undefined,
|
|
1252
|
-
};
|
|
1253
|
-
|
|
1254
|
-
if (toolCalls.length > 0) result.tool_calls = toolCalls;
|
|
1255
|
-
if (toolCallId) result.tool_call_id = toolCallId;
|
|
1256
|
-
if (toolName) result.name = toolName;
|
|
1257
|
-
|
|
1258
|
-
return result;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function pickRecentContextMessagesV2(messages, maxTurns = 10) {
|
|
1262
|
-
if (!messages || messages.length === 0) return [];
|
|
1263
|
-
|
|
1264
|
-
const result = [];
|
|
1265
|
-
let turnCount = 0;
|
|
1266
|
-
let lastRole = null;
|
|
1267
|
-
|
|
1268
|
-
for (let i = messages.length - 1; i >= 0 && turnCount < maxTurns; i--) {
|
|
1269
|
-
const msg = messages[i];
|
|
1270
|
-
const role = normalizeCacheRole(msg?.role);
|
|
1271
|
-
|
|
1272
|
-
if (role === "system") continue;
|
|
1273
|
-
|
|
1274
|
-
if (role === "user" && isAssistantLikeRole(lastRole)) {
|
|
1275
|
-
turnCount++;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
if (turnCount >= maxTurns) break;
|
|
1279
|
-
|
|
1280
|
-
const contextMessage = buildV2ConversationMessage(msg);
|
|
1281
|
-
if (contextMessage) {
|
|
1282
|
-
result.unshift(contextMessage);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
lastRole = role;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
return result;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function stripToolMessagesForV1(messages) {
|
|
1292
|
-
if (!Array.isArray(messages)) return [];
|
|
1293
|
-
|
|
1294
|
-
return messages
|
|
1295
|
-
.filter((msg) => msg && (msg.role === "user" || msg.role === "assistant"))
|
|
1296
|
-
.filter((msg) => String(msg.content || "").trim())
|
|
1297
|
-
.map((msg) => ({
|
|
1298
|
-
role: msg.role,
|
|
1299
|
-
content: msg.content,
|
|
1300
|
-
rawContent: msg.rawContent,
|
|
1301
|
-
}));
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
/**
|
|
1305
|
-
* Extract tool calls from cached messages for the v2 context protocol.
|
|
1306
|
-
* @param {Array} messages - All cached messages
|
|
1307
|
-
* @returns {Array} Tool call records
|
|
1308
|
-
*/
|
|
1309
|
-
function extractToolCalls(messages) {
|
|
1310
|
-
if (!messages || messages.length === 0) return [];
|
|
1311
|
-
|
|
1312
|
-
const calls = [];
|
|
1313
|
-
for (const msg of messages) {
|
|
1314
|
-
if (msg.role === "assistant") {
|
|
1315
|
-
const toolCalls = getMessageToolCalls(msg);
|
|
1316
|
-
for (const tc of toolCalls) {
|
|
1317
|
-
calls.push({
|
|
1318
|
-
tool_name: tc.function?.name || tc.name || "unknown",
|
|
1319
|
-
arguments: tc.function?.arguments || tc.arguments || {},
|
|
1320
|
-
call_id: tc.id || null,
|
|
1321
|
-
result: null,
|
|
1322
|
-
success: null,
|
|
1323
|
-
duration_ms: null,
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
if (msg.role === "tool") {
|
|
1329
|
-
const toolCallId = getToolResultCallId(msg);
|
|
1330
|
-
const toolName = getToolResultName(msg);
|
|
1331
|
-
const match = calls.find((call) =>
|
|
1332
|
-
(toolCallId && call.call_id === toolCallId) ||
|
|
1333
|
-
(!toolCallId && toolName && call.tool_name === toolName && call.result == null)
|
|
1334
|
-
);
|
|
1335
|
-
if (match) {
|
|
1336
|
-
const resultText = extractText(msg.rawContent !== undefined ? msg.rawContent : msg.content);
|
|
1337
|
-
match.result = truncate(resultText, 2000);
|
|
1338
|
-
match.success = !isErrorResult(resultText);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
return calls;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
/**
|
|
1347
|
-
* Check if text looks like an error result
|
|
1348
|
-
* @param {string} text - Result text
|
|
1349
|
-
* @returns {boolean}
|
|
1350
|
-
*/
|
|
1351
|
-
function isErrorResult(text) {
|
|
1352
|
-
if (!text) return false;
|
|
1353
|
-
const lower = String(text).toLowerCase();
|
|
1354
|
-
return lower.includes("error") || lower.includes("exception") ||
|
|
1355
|
-
lower.includes("failed") || lower.includes("traceback") ||
|
|
1356
|
-
lower.includes("enoent") || lower.includes("permission denied");
|
|
798
|
+
return result;
|
|
1357
799
|
}
|
|
1358
800
|
|
|
1359
|
-
/**
|
|
1360
|
-
* Collect context blocks from messages (v2 protocol)
|
|
1361
|
-
* @param {Array} messages - All cached messages
|
|
1362
|
-
* @param {Object} cfg - Configuration
|
|
1363
|
-
* @returns {Array} Context blocks
|
|
1364
|
-
*/
|
|
1365
|
-
function collectContextBlocks(messages, cfg) {
|
|
1366
|
-
const blocks = [];
|
|
1367
|
-
|
|
1368
|
-
const conversationMsgs =
|
|
1369
|
-
cfg.captureToolCalls === false
|
|
1370
|
-
? pickRecentMessages(messages, cfg.maxTurnsToStore || 10)
|
|
1371
|
-
: pickRecentContextMessagesV2(messages, cfg.maxTurnsToStore || 10);
|
|
1372
|
-
if (conversationMsgs.length > 0) {
|
|
1373
|
-
blocks.push({
|
|
1374
|
-
type: "conversation",
|
|
1375
|
-
data: { messages: conversationMsgs },
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
return blocks;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
/**
|
|
1383
|
-
* Add context via v2 protocol
|
|
1384
|
-
*/
|
|
1385
|
-
async function addContextV2(contextBlocks, cfg, ctx, log, sessionId) {
|
|
1386
|
-
const baseUrl = cfg.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL;
|
|
1387
|
-
const apiKey = cfg.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY;
|
|
1388
|
-
|
|
1389
|
-
if (!apiKey) return;
|
|
1390
|
-
|
|
1391
|
-
const url = `${baseUrl}/api/plugin/v2/add/context`;
|
|
1392
|
-
const latestUserText = getLatestUserMessageText(
|
|
1393
|
-
contextBlocks.find((block) => block.type === "conversation")?.data?.messages || []
|
|
1394
|
-
);
|
|
1395
|
-
const identity = resolveRequestIdentity(latestUserText, cfg);
|
|
1396
|
-
const userId = sanitizeUserId(identity.userId);
|
|
1397
|
-
const agentId = cfg.agentId || ctx?.agentId || "main";
|
|
1398
|
-
|
|
1399
|
-
const payload = {
|
|
1400
|
-
user_id: userId,
|
|
1401
|
-
conversation_id: sessionId,
|
|
1402
|
-
agent_id: agentId,
|
|
1403
|
-
tags: cfg.tags || ["openclaw"],
|
|
1404
|
-
async_mode: true,
|
|
1405
|
-
protocol_version: "2.0",
|
|
1406
|
-
context_blocks: contextBlocks,
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
if (log?.debug) {
|
|
1410
|
-
log.debug(
|
|
1411
|
-
`[Memory Plugin] v2 add/context: blocks=${contextBlocks.length}, types=${contextBlocks.map((block) => block.type).join(",")}`
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
try {
|
|
1416
|
-
const result = await httpRequest(url, {
|
|
1417
|
-
method: "POST",
|
|
1418
|
-
headers: {
|
|
1419
|
-
"Content-Type": "application/json",
|
|
1420
|
-
"x-api-key": apiKey,
|
|
1421
|
-
"x-request-id": ctx?.requestId || `openclaw-${Date.now()}`,
|
|
1422
|
-
"x-plugin-version": PLUGIN_VERSION,
|
|
1423
|
-
"x-client-type": "plugin",
|
|
1424
|
-
},
|
|
1425
|
-
body: JSON.stringify(payload),
|
|
1426
|
-
}, cfg, log);
|
|
1427
|
-
|
|
1428
|
-
const memoryCount = result?.memories_count || 0;
|
|
1429
|
-
if (log?.info) {
|
|
1430
|
-
log.info(
|
|
1431
|
-
`[Memory Plugin] v2 add/context success: ${memoryCount} memories from ${result?.blocks_processed || 0} blocks`
|
|
1432
|
-
);
|
|
1433
|
-
}
|
|
1434
|
-
return result;
|
|
1435
|
-
} catch (error) {
|
|
1436
|
-
if (log?.warn) {
|
|
1437
|
-
log.warn(`[Memory Plugin] v2 add/context failed, falling back to v1: ${error.message}`);
|
|
1438
|
-
}
|
|
1439
|
-
const conversationBlock = contextBlocks.find((block) => block.type === "conversation");
|
|
1440
|
-
if (conversationBlock) {
|
|
1441
|
-
return await addMemory(stripToolMessagesForV1(conversationBlock.data.messages), cfg, ctx, log, sessionId);
|
|
1442
|
-
}
|
|
1443
|
-
throw error;
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
801
|
/**
|
|
1448
802
|
* Check if conversation has enough turns to be worth storing
|
|
1449
803
|
* @param {Array} messages - Messages to check
|
|
@@ -1463,7 +817,7 @@ function isConversationWorthStoring(messages, cfg) {
|
|
|
1463
817
|
if (msg.role === "system") continue;
|
|
1464
818
|
|
|
1465
819
|
if (msg.role === "user") {
|
|
1466
|
-
if (
|
|
820
|
+
if (lastRole === "assistant") {
|
|
1467
821
|
turns++;
|
|
1468
822
|
}
|
|
1469
823
|
}
|
|
@@ -1498,39 +852,13 @@ function getSessionCache(sessionId) {
|
|
|
1498
852
|
*/
|
|
1499
853
|
function addToSessionCache(sessionId, message) {
|
|
1500
854
|
const cache = getSessionCache(sessionId);
|
|
1501
|
-
|
|
1502
|
-
const incoming = {
|
|
1503
|
-
...message,
|
|
1504
|
-
cachedAt: now,
|
|
1505
|
-
};
|
|
1506
|
-
|
|
1507
|
-
const prev = cache.messages.length > 0 ? cache.messages[cache.messages.length - 1] : null;
|
|
1508
|
-
if (prev) {
|
|
1509
|
-
const sameRole = prev.role === incoming.role;
|
|
1510
|
-
const sameMessageId =
|
|
1511
|
-
incoming.messageId &&
|
|
1512
|
-
prev.messageId &&
|
|
1513
|
-
String(incoming.messageId) === String(prev.messageId);
|
|
1514
|
-
const sameContent =
|
|
1515
|
-
sameRole &&
|
|
1516
|
-
String(prev.content || "") === String(incoming.content || "") &&
|
|
1517
|
-
String(prev.rawContent || "") === String(incoming.rawContent || "");
|
|
1518
|
-
const withinWindow = now - (prev.cachedAt || 0) <= CACHE_DEDUP_WINDOW_MS;
|
|
1519
|
-
|
|
1520
|
-
// Dedup duplicate hook triggers for the same relay message.
|
|
1521
|
-
if ((sameRole && sameMessageId) || (sameContent && withinWindow)) {
|
|
1522
|
-
cache.lastActivity = now;
|
|
1523
|
-
return cache;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
cache.messages.push(incoming);
|
|
855
|
+
cache.messages.push(message);
|
|
1528
856
|
cache.lastActivity = Date.now();
|
|
1529
857
|
|
|
1530
858
|
// Count turns (user message after assistant = new turn)
|
|
1531
|
-
if (
|
|
859
|
+
if (message.role === "user" && cache.messages.length > 1) {
|
|
1532
860
|
const prevMsg = cache.messages[cache.messages.length - 2];
|
|
1533
|
-
if (prevMsg &&
|
|
861
|
+
if (prevMsg && prevMsg.role === "assistant") {
|
|
1534
862
|
cache.turnCount++;
|
|
1535
863
|
}
|
|
1536
864
|
}
|
|
@@ -1642,15 +970,6 @@ async function flushSession(sessionId, cfg, ctx, log) {
|
|
|
1642
970
|
if (log?.info) {
|
|
1643
971
|
log.info(`[Memory Plugin] Flushing session ${sessionId}: ${messagesToSave.length} messages, ${cache.turnCount} turns`);
|
|
1644
972
|
}
|
|
1645
|
-
|
|
1646
|
-
if (cfg.useV2Protocol !== false) {
|
|
1647
|
-
const contextBlocks = collectContextBlocks(cache.messages, cfg);
|
|
1648
|
-
if (contextBlocks.length > 0) {
|
|
1649
|
-
await addContextV2(contextBlocks, cfg, ctx, log, sessionId);
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
973
|
await addMemory(messagesToSave, cfg, ctx, log, sessionId);
|
|
1655
974
|
} catch (error) {
|
|
1656
975
|
if (log?.warn) {
|
|
@@ -1702,14 +1021,8 @@ function registerPlugin(api) {
|
|
|
1702
1021
|
const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
|
|
1703
1022
|
const userContent = normalizeUserMessageContent(prompt);
|
|
1704
1023
|
const rawUserContent = stripPrependedPrompt(prompt);
|
|
1705
|
-
const messageId = extractRelayMessageId(rawUserContent || prompt);
|
|
1706
1024
|
if (userContent) {
|
|
1707
|
-
addToSessionCache(sessionId, {
|
|
1708
|
-
role: "user",
|
|
1709
|
-
content: userContent,
|
|
1710
|
-
rawContent: rawUserContent,
|
|
1711
|
-
messageId: messageId || undefined,
|
|
1712
|
-
});
|
|
1025
|
+
addToSessionCache(sessionId, { role: "user", content: userContent, rawContent: rawUserContent });
|
|
1713
1026
|
}
|
|
1714
1027
|
|
|
1715
1028
|
try {
|
|
@@ -1757,28 +1070,22 @@ function registerPlugin(api) {
|
|
|
1757
1070
|
return;
|
|
1758
1071
|
}
|
|
1759
1072
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const cacheMessage = buildCacheMessageFromTranscript(event.messages[i]);
|
|
1768
|
-
if (cacheMessage) pending.unshift(cacheMessage);
|
|
1073
|
+
const assistantContent = event?.response || event?.result;
|
|
1074
|
+
if (assistantContent) {
|
|
1075
|
+
const content = normalizeAssistantMessageContent(assistantContent);
|
|
1076
|
+
const rawContent = extractText(assistantContent);
|
|
1077
|
+
if (content) {
|
|
1078
|
+
addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
|
|
1769
1079
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
});
|
|
1780
|
-
if (cacheMessage) {
|
|
1781
|
-
addToSessionCache(sessionId, cacheMessage);
|
|
1080
|
+
} else if (event?.messages?.length) {
|
|
1081
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
1082
|
+
if (event.messages[i].role === "assistant") {
|
|
1083
|
+
const content = normalizeAssistantMessageContent(event.messages[i].content);
|
|
1084
|
+
const rawContent = extractText(event.messages[i].content);
|
|
1085
|
+
if (content) {
|
|
1086
|
+
addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
|
|
1087
|
+
}
|
|
1088
|
+
break;
|
|
1782
1089
|
}
|
|
1783
1090
|
}
|
|
1784
1091
|
}
|
|
@@ -1846,14 +1153,8 @@ function createHooksPlugin(config) {
|
|
|
1846
1153
|
const sessionId = resolveSessionId(ctx, event) || `session-${Date.now()}`;
|
|
1847
1154
|
const userContent = normalizeUserMessageContent(prompt);
|
|
1848
1155
|
const rawUserContent = stripPrependedPrompt(prompt);
|
|
1849
|
-
const messageId = extractRelayMessageId(rawUserContent || prompt);
|
|
1850
1156
|
if (userContent) {
|
|
1851
|
-
addToSessionCache(sessionId, {
|
|
1852
|
-
role: "user",
|
|
1853
|
-
content: userContent,
|
|
1854
|
-
rawContent: rawUserContent,
|
|
1855
|
-
messageId: messageId || undefined,
|
|
1856
|
-
});
|
|
1157
|
+
addToSessionCache(sessionId, { role: "user", content: userContent, rawContent: rawUserContent });
|
|
1857
1158
|
}
|
|
1858
1159
|
|
|
1859
1160
|
try {
|
|
@@ -1918,37 +1219,37 @@ function createHooksPlugin(config) {
|
|
|
1918
1219
|
const cache = getSessionCache(sessionId);
|
|
1919
1220
|
if (cache.messages.length === 0) {
|
|
1920
1221
|
for (const msg of event.messages) {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1222
|
+
if (msg.role === "system") continue;
|
|
1223
|
+
const content = msg.role === "user"
|
|
1224
|
+
? normalizeUserMessageContent(msg.content)
|
|
1225
|
+
: normalizeAssistantMessageContent(msg.content);
|
|
1226
|
+
const rawSource = msg.role === "user"
|
|
1227
|
+
? stripPrependedPrompt(msg.content)
|
|
1228
|
+
: extractText(msg.content);
|
|
1229
|
+
if (content) {
|
|
1230
|
+
addToSessionCache(sessionId, { role: msg.role, content, rawContent: rawSource || undefined });
|
|
1924
1231
|
}
|
|
1925
1232
|
}
|
|
1926
1233
|
} else {
|
|
1927
|
-
//
|
|
1928
|
-
const pending = [];
|
|
1234
|
+
// Only add last assistant message
|
|
1929
1235
|
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1236
|
+
if (event.messages[i].role === "assistant") {
|
|
1237
|
+
const content = normalizeAssistantMessageContent(event.messages[i].content);
|
|
1238
|
+
const rawContent = extractText(event.messages[i].content);
|
|
1239
|
+
if (content) {
|
|
1240
|
+
addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
|
|
1241
|
+
}
|
|
1242
|
+
break;
|
|
1937
1243
|
}
|
|
1938
1244
|
}
|
|
1939
|
-
for (const cacheMessage of pending) {
|
|
1940
|
-
addToSessionCache(sessionId, cacheMessage);
|
|
1941
|
-
}
|
|
1942
1245
|
}
|
|
1943
1246
|
} else {
|
|
1944
1247
|
const assistantContent = event?.response || event?.result;
|
|
1945
1248
|
if (assistantContent) {
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
if (cacheMessage) {
|
|
1951
|
-
addToSessionCache(sessionId, cacheMessage);
|
|
1249
|
+
const content = normalizeAssistantMessageContent(assistantContent);
|
|
1250
|
+
const rawContent = extractText(assistantContent);
|
|
1251
|
+
if (content) {
|
|
1252
|
+
addToSessionCache(sessionId, { role: "assistant", content, rawContent: rawContent || undefined });
|
|
1952
1253
|
}
|
|
1953
1254
|
}
|
|
1954
1255
|
}
|
|
@@ -2010,7 +1311,7 @@ function buildConfig(config) {
|
|
|
2010
1311
|
const configuredUserId = config?.userId || process.env.HUMAN_LIKE_MEM_USER_ID;
|
|
2011
1312
|
|
|
2012
1313
|
return {
|
|
2013
|
-
baseUrl: config?.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL || "https://human-like.me",
|
|
1314
|
+
baseUrl: config?.baseUrl || process.env.HUMAN_LIKE_MEM_BASE_URL || "https://plugin.human-like.me",
|
|
2014
1315
|
apiKey: config?.apiKey || process.env.HUMAN_LIKE_MEM_API_KEY,
|
|
2015
1316
|
configuredUserId: configuredUserId,
|
|
2016
1317
|
userId: configuredUserId || "openclaw-user",
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "human-like-mem",
|
|
4
4
|
"name": "Human-Like Memory Plugin",
|
|
5
5
|
"description": "Long-term memory plugin with automatic recall and storage.",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.14",
|
|
7
7
|
"kind": "lifecycle",
|
|
8
8
|
"main": "./index.js",
|
|
9
9
|
"author": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"baseUrl": {
|
|
26
26
|
"type": "string",
|
|
27
27
|
"description": "Memory API base URL",
|
|
28
|
-
"default": "https://human-like.me"
|
|
28
|
+
"default": "https://plugin.human-like.me"
|
|
29
29
|
},
|
|
30
30
|
"userId": {
|
|
31
31
|
"type": "string",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"baseUrl": {
|
|
91
91
|
"type": "string",
|
|
92
92
|
"description": "Memory API base URL",
|
|
93
|
-
"default": "https://human-like.me",
|
|
93
|
+
"default": "https://plugin.human-like.me",
|
|
94
94
|
"env": "HUMAN_LIKE_MEM_BASE_URL"
|
|
95
95
|
},
|
|
96
96
|
"userId": {
|
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.14",
|
|
4
4
|
"description": "Long-term memory plugin for OpenClaw - AI Social Memory Integration",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
55
|
+
}
|
|
56
|
+
}
|