@memtensor/memos-cloud-openclaw-plugin 0.1.9 → 0.1.10-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/README.md +9 -2
- package/README_ZH.md +9 -2
- package/clawdbot.plugin.json +1 -1
- package/index.js +11 -15
- package/lib/memos-cloud-api.js +130 -6
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/test/query-strip.test.mjs +143 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Make sure it’s enabled in `~/.openclaw/openclaw.json`:
|
|
|
44
44
|
},
|
|
45
45
|
"load": {
|
|
46
46
|
"paths": [
|
|
47
|
-
"C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin
|
|
47
|
+
"C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin"
|
|
48
48
|
]
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -106,6 +106,11 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
106
106
|
- `MEMOS_RECALL_FILTER_CANDIDATE_LIMIT` (default: `30` per category)
|
|
107
107
|
- `MEMOS_RECALL_FILTER_MAX_ITEM_CHARS` (default: `500`)
|
|
108
108
|
- `MEMOS_RECALL_FILTER_FAIL_OPEN` (default: `true`; fallback to unfiltered recall on failure)
|
|
109
|
+
- `MEMOS_CAPTURE_STRATEGY` (default: `last_turn`)
|
|
110
|
+
- `MEMOS_ASYNC_MODE` (default: `true`; non-blocking memory addition)
|
|
111
|
+
- `MEMOS_THROTTLE_MS` (default: `0`; throttle memory requests)
|
|
112
|
+
- `MEMOS_INCLUDE_ASSISTANT` (default: `true`; include assistant messages in memory)
|
|
113
|
+
- `MEMOS_MAX_MESSAGE_CHARS` (default: `20000`; max characters for message history)
|
|
109
114
|
|
|
110
115
|
## Optional Plugin Config
|
|
111
116
|
In `plugins.entries.memos-cloud-openclaw-plugin.config`:
|
|
@@ -145,7 +150,9 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
|
|
|
145
150
|
"recallFilterRetries": 0,
|
|
146
151
|
"recallFilterCandidateLimit": 30,
|
|
147
152
|
"recallFilterMaxItemChars": 500,
|
|
148
|
-
"recallFilterFailOpen": true
|
|
153
|
+
"recallFilterFailOpen": true,
|
|
154
|
+
"throttleMs": 0,
|
|
155
|
+
"maxMessageChars": 20000
|
|
149
156
|
}
|
|
150
157
|
```
|
|
151
158
|
|
package/README_ZH.md
CHANGED
|
@@ -46,7 +46,7 @@ openclaw gateway restart
|
|
|
46
46
|
},
|
|
47
47
|
"load": {
|
|
48
48
|
"paths": [
|
|
49
|
-
"C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin
|
|
49
|
+
"C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin"
|
|
50
50
|
]
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -108,6 +108,11 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
108
108
|
- `MEMOS_RECALL_FILTER_CANDIDATE_LIMIT`(默认每类 `30` 条)
|
|
109
109
|
- `MEMOS_RECALL_FILTER_MAX_ITEM_CHARS`(默认 `500`)
|
|
110
110
|
- `MEMOS_RECALL_FILTER_FAIL_OPEN`(默认 `true`;筛选失败时回退为“不过滤”)
|
|
111
|
+
- `MEMOS_CAPTURE_STRATEGY`(默认 `last_turn`;记忆捕获策略)
|
|
112
|
+
- `MEMOS_ASYNC_MODE`(默认 `true`;异步模式添加记忆)
|
|
113
|
+
- `MEMOS_THROTTLE_MS`(默认 `0`;请求节流时间,单位毫秒)
|
|
114
|
+
- `MEMOS_INCLUDE_ASSISTANT`(默认 `true`;记忆是否包含助手回复)
|
|
115
|
+
- `MEMOS_MAX_MESSAGE_CHARS`(默认 `20000`;单条记忆最大字符数限制)
|
|
111
116
|
|
|
112
117
|
## 可选插件配置
|
|
113
118
|
在 `plugins.entries.memos-cloud-openclaw-plugin.config` 中设置:
|
|
@@ -145,7 +150,9 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
145
150
|
"recallFilterRetries": 0,
|
|
146
151
|
"recallFilterCandidateLimit": 30,
|
|
147
152
|
"recallFilterMaxItemChars": 500,
|
|
148
|
-
"recallFilterFailOpen": true
|
|
153
|
+
"recallFilterFailOpen": true,
|
|
154
|
+
"throttleMs": 0,
|
|
155
|
+
"maxMessageChars": 20000
|
|
149
156
|
}
|
|
150
157
|
```
|
|
151
158
|
|
package/clawdbot.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.10-beta.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
extractResultData,
|
|
6
6
|
extractText,
|
|
7
7
|
formatRecallHookResult,
|
|
8
|
-
USER_QUERY_MARKER,
|
|
9
8
|
searchMemory,
|
|
9
|
+
stripOpenClawInjectedPrefix,
|
|
10
10
|
} from "./lib/memos-cloud-api.js";
|
|
11
11
|
import { startUpdateChecker } from "./lib/check-update.js";
|
|
12
12
|
let lastCaptureTime = 0;
|
|
@@ -32,14 +32,7 @@ function warnMissingApiKey(log, context) {
|
|
|
32
32
|
].join("\n"),
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
function stripPrependedPrompt(content) {
|
|
37
|
-
if (!content) return content;
|
|
38
|
-
const idx = content.lastIndexOf(USER_QUERY_MARKER);
|
|
39
|
-
if (idx === -1) return content;
|
|
40
|
-
return content.slice(idx + USER_QUERY_MARKER.length).trimStart();
|
|
41
|
-
}
|
|
42
|
-
|
|
35
|
+
console.log('11111111111111111111111111111111')
|
|
43
36
|
function getCounterSuffix(sessionKey) {
|
|
44
37
|
if (!sessionKey) return "";
|
|
45
38
|
const current = conversationCounters.get(sessionKey) ?? 0;
|
|
@@ -73,7 +66,8 @@ function resolveConversationId(cfg, ctx) {
|
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
function buildSearchPayload(cfg, prompt, ctx) {
|
|
76
|
-
const
|
|
69
|
+
const cleanPrompt = stripOpenClawInjectedPrefix(prompt);
|
|
70
|
+
const queryRaw = `${cfg.queryPrefix || ""}${cleanPrompt}`;
|
|
77
71
|
const query =
|
|
78
72
|
Number.isFinite(cfg.maxQueryChars) && cfg.maxQueryChars > 0
|
|
79
73
|
? queryRaw.slice(0, cfg.maxQueryChars)
|
|
@@ -162,7 +156,7 @@ function pickLastTurnMessages(messages, cfg) {
|
|
|
162
156
|
for (const msg of slice) {
|
|
163
157
|
if (!msg || !msg.role) continue;
|
|
164
158
|
if (msg.role === "user") {
|
|
165
|
-
const content =
|
|
159
|
+
const content = stripOpenClawInjectedPrefix(extractText(msg.content));
|
|
166
160
|
if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
|
|
167
161
|
continue;
|
|
168
162
|
}
|
|
@@ -180,7 +174,7 @@ function pickFullSessionMessages(messages, cfg) {
|
|
|
180
174
|
for (const msg of messages) {
|
|
181
175
|
if (!msg || !msg.role) continue;
|
|
182
176
|
if (msg.role === "user") {
|
|
183
|
-
const content =
|
|
177
|
+
const content = stripOpenClawInjectedPrefix(extractText(msg.content));
|
|
184
178
|
if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
|
|
185
179
|
}
|
|
186
180
|
if (msg.role === "assistant" && cfg.includeAssistant) {
|
|
@@ -426,18 +420,19 @@ export default {
|
|
|
426
420
|
|
|
427
421
|
api.on("before_agent_start", async (event, ctx) => {
|
|
428
422
|
if (!cfg.recallEnabled) return;
|
|
429
|
-
|
|
423
|
+
const userPrompt = stripOpenClawInjectedPrefix(event?.prompt || "");
|
|
424
|
+
if (!userPrompt || userPrompt.length < 3) return;
|
|
430
425
|
if (!cfg.apiKey) {
|
|
431
426
|
warnMissingApiKey(log, "recall");
|
|
432
427
|
return;
|
|
433
428
|
}
|
|
434
429
|
|
|
435
430
|
try {
|
|
436
|
-
const payload = buildSearchPayload(cfg,
|
|
431
|
+
const payload = buildSearchPayload(cfg, userPrompt, ctx);
|
|
437
432
|
const result = await searchMemory(cfg, payload);
|
|
438
433
|
const resultData = extractResultData(result);
|
|
439
434
|
if (!resultData) return;
|
|
440
|
-
const filteredData = await maybeFilterRecallData(cfg, resultData,
|
|
435
|
+
const filteredData = await maybeFilterRecallData(cfg, resultData, userPrompt, log);
|
|
441
436
|
const hookResult = formatRecallHookResult({ data: filteredData }, {
|
|
442
437
|
wrapTagBlocks: true,
|
|
443
438
|
relativity: payload.relativity,
|
|
@@ -453,6 +448,7 @@ export default {
|
|
|
453
448
|
|
|
454
449
|
api.on("agent_end", async (event, ctx) => {
|
|
455
450
|
if (!cfg.addEnabled) return;
|
|
451
|
+
console.log('222222222222222222222\n22222222222', cfg.addEnabled)
|
|
456
452
|
if (!event?.success || !event?.messages?.length) return;
|
|
457
453
|
if (!cfg.apiKey) {
|
|
458
454
|
warnMissingApiKey(log, "add");
|
package/lib/memos-cloud-api.js
CHANGED
|
@@ -5,6 +5,18 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_BASE_URL = "https://memos.memtensor.cn/api/openmem/v1";
|
|
7
7
|
export const USER_QUERY_MARKER = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
|
|
8
|
+
const INBOUND_META_SENTINELS = [
|
|
9
|
+
"Conversation info (untrusted metadata):",
|
|
10
|
+
"Sender (untrusted metadata):",
|
|
11
|
+
"Thread starter (untrusted, for context):",
|
|
12
|
+
"Replied message (untrusted, for context):",
|
|
13
|
+
"Forwarded message context (untrusted metadata):",
|
|
14
|
+
"Chat history since last reply (untrusted, for context):",
|
|
15
|
+
];
|
|
16
|
+
const LEADING_TIMESTAMP_ENVELOPE_PATTERNS = [
|
|
17
|
+
/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?:\s+GMT[+-]\d{1,2})?\]\s*/,
|
|
18
|
+
/^\[\d{1,2}:\d{2}\s*(?:AM|PM)\s+on\s+\d{1,2}\s+[A-Za-z]+,\s+\d{4}\]:\s*/i,
|
|
19
|
+
];
|
|
8
20
|
const ENV_SOURCES = [
|
|
9
21
|
{ name: "openclaw", path: join(homedir(), ".openclaw", ".env") },
|
|
10
22
|
{ name: "moltbot", path: join(homedir(), ".moltbot", ".env") },
|
|
@@ -161,6 +173,14 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
161
173
|
cfg.recallFilterFailOpen,
|
|
162
174
|
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_FAIL_OPEN"), true),
|
|
163
175
|
);
|
|
176
|
+
const captureStrategy = cfg.captureStrategy ?? (loadEnvVar("MEMOS_CAPTURE_STRATEGY") || "last_turn");
|
|
177
|
+
const asyncMode = cfg.asyncMode ?? parseBool(loadEnvVar("MEMOS_ASYNC_MODE"), true);
|
|
178
|
+
const throttleMs = cfg.throttleMs ?? parseNumber(loadEnvVar("MEMOS_THROTTLE_MS"), 0);
|
|
179
|
+
const includeAssistant =
|
|
180
|
+
cfg.includeAssistant === undefined
|
|
181
|
+
? parseBool(loadEnvVar("MEMOS_INCLUDE_ASSISTANT"), true)
|
|
182
|
+
: cfg.includeAssistant !== false;
|
|
183
|
+
const maxMessageChars = cfg.maxMessageChars ?? parseNumber(loadEnvVar("MEMOS_MAX_MESSAGE_CHARS"), 20000);
|
|
164
184
|
|
|
165
185
|
return {
|
|
166
186
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
@@ -177,10 +197,10 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
177
197
|
maxQueryChars: cfg.maxQueryChars ?? 0,
|
|
178
198
|
recallEnabled: cfg.recallEnabled !== false,
|
|
179
199
|
addEnabled: cfg.addEnabled !== false,
|
|
180
|
-
captureStrategy
|
|
181
|
-
maxMessageChars
|
|
200
|
+
captureStrategy,
|
|
201
|
+
maxMessageChars,
|
|
182
202
|
maxItemChars: cfg.maxItemChars ?? 8000,
|
|
183
|
-
includeAssistant
|
|
203
|
+
includeAssistant,
|
|
184
204
|
memoryLimitNumber: cfg.memoryLimitNumber ?? 9,
|
|
185
205
|
preferenceLimitNumber: cfg.preferenceLimitNumber ?? 6,
|
|
186
206
|
includePreference: cfg.includePreference !== false,
|
|
@@ -198,7 +218,7 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
198
218
|
appId: cfg.appId,
|
|
199
219
|
allowPublic: cfg.allowPublic ?? false,
|
|
200
220
|
allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? [],
|
|
201
|
-
asyncMode
|
|
221
|
+
asyncMode,
|
|
202
222
|
multiAgentMode,
|
|
203
223
|
recallFilterEnabled,
|
|
204
224
|
recallFilterBaseUrl:
|
|
@@ -217,7 +237,7 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
217
237
|
recallFilterFailOpen,
|
|
218
238
|
timeoutMs: cfg.timeoutMs ?? 5000,
|
|
219
239
|
retries: cfg.retries ?? 1,
|
|
220
|
-
throttleMs
|
|
240
|
+
throttleMs,
|
|
221
241
|
};
|
|
222
242
|
}
|
|
223
243
|
|
|
@@ -267,7 +287,111 @@ export async function searchMemory(cfg, payload) {
|
|
|
267
287
|
}
|
|
268
288
|
|
|
269
289
|
export async function addMessage(cfg, payload) {
|
|
270
|
-
|
|
290
|
+
let finalPayload = payload;
|
|
291
|
+
try {
|
|
292
|
+
finalPayload = sanitizeAddMessagePayload(payload);
|
|
293
|
+
} catch {
|
|
294
|
+
// Fail open: if sanitization throws unexpectedly, send original payload.
|
|
295
|
+
finalPayload = payload;
|
|
296
|
+
}
|
|
297
|
+
return callApi(cfg, "/add/message", finalPayload);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isInboundMetaSentinelLine(line) {
|
|
301
|
+
const trimmed = line.trim();
|
|
302
|
+
return INBOUND_META_SENTINELS.some((sentinel) => sentinel === trimmed);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function stripLeadingInboundMetadata(text) {
|
|
306
|
+
if (!text || typeof text !== "string") return "";
|
|
307
|
+
const lines = text.split(/\r?\n/);
|
|
308
|
+
let index = 0;
|
|
309
|
+
let strippedAny = false;
|
|
310
|
+
|
|
311
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
312
|
+
index += 1;
|
|
313
|
+
}
|
|
314
|
+
if (index >= lines.length || !isInboundMetaSentinelLine(lines[index])) {
|
|
315
|
+
return text;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
while (index < lines.length) {
|
|
319
|
+
if (!isInboundMetaSentinelLine(lines[index])) break;
|
|
320
|
+
const blockStart = index;
|
|
321
|
+
index += 1;
|
|
322
|
+
if (index >= lines.length || lines[index].trim() !== "```json") {
|
|
323
|
+
return strippedAny ? lines.slice(blockStart).join("\n") : text;
|
|
324
|
+
}
|
|
325
|
+
index += 1;
|
|
326
|
+
while (index < lines.length && lines[index].trim() !== "```") {
|
|
327
|
+
index += 1;
|
|
328
|
+
}
|
|
329
|
+
if (index >= lines.length) {
|
|
330
|
+
return strippedAny ? lines.slice(blockStart).join("\n") : text;
|
|
331
|
+
}
|
|
332
|
+
index += 1;
|
|
333
|
+
strippedAny = true;
|
|
334
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
335
|
+
index += 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return lines.slice(index).join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function stripLeadingTimestampEnvelope(text) {
|
|
343
|
+
if (!text || typeof text !== "string") return "";
|
|
344
|
+
for (const pattern of LEADING_TIMESTAMP_ENVELOPE_PATTERNS) {
|
|
345
|
+
if (!pattern.test(text)) continue;
|
|
346
|
+
return text.replace(pattern, "").trimStart();
|
|
347
|
+
}
|
|
348
|
+
return text;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function stripFeishuInjectedPrompt(text) {
|
|
352
|
+
if (!text || typeof text !== "string") return text;
|
|
353
|
+
// Check for the Feishu System header
|
|
354
|
+
if (!/^System: \[.*?\] Feishu\[.*?\]/.test(text)) {
|
|
355
|
+
return text;
|
|
356
|
+
}
|
|
357
|
+
// Remove only the first injected Feishu prompt prefix.
|
|
358
|
+
// Any later "[message_id] ou_xxx:" pattern should be treated as user query content.
|
|
359
|
+
const leadingInjectedPattern = /^[\s\S]*?\[message_id: [^\]]+\]\s+ou_[a-z0-9]+:\s*/;
|
|
360
|
+
if (leadingInjectedPattern.test(text)) {
|
|
361
|
+
return text.replace(leadingInjectedPattern, "").trim();
|
|
362
|
+
}
|
|
363
|
+
return text;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function sanitizeAddMessagePayload(payload) {
|
|
367
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
368
|
+
const nextPayload = { ...payload };
|
|
369
|
+
if (typeof nextPayload.query === "string") {
|
|
370
|
+
nextPayload.query = stripOpenClawInjectedPrefix(nextPayload.query);
|
|
371
|
+
}
|
|
372
|
+
if (Array.isArray(nextPayload.messages)) {
|
|
373
|
+
nextPayload.messages = nextPayload.messages.map((msg) => {
|
|
374
|
+
if (!msg || typeof msg !== "object") return msg;
|
|
375
|
+
if (msg.role !== "user" || typeof msg.content !== "string") return msg;
|
|
376
|
+
return {
|
|
377
|
+
...msg,
|
|
378
|
+
content: stripOpenClawInjectedPrefix(msg.content),
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return nextPayload;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function stripOpenClawInjectedPrefix(text) {
|
|
386
|
+
if (!text || typeof text !== "string") return "";
|
|
387
|
+
const cleanedText = stripFeishuInjectedPrompt(text);
|
|
388
|
+
const markerIndex = cleanedText.lastIndexOf(USER_QUERY_MARKER);
|
|
389
|
+
const withoutRecallPrefix =
|
|
390
|
+
markerIndex === -1
|
|
391
|
+
? cleanedText
|
|
392
|
+
: cleanedText.slice(markerIndex + USER_QUERY_MARKER.length);
|
|
393
|
+
const withoutInboundMetadata = stripLeadingInboundMetadata(withoutRecallPrefix).trimStart();
|
|
394
|
+
return stripLeadingTimestampEnvelope(withoutInboundMetadata);
|
|
271
395
|
}
|
|
272
396
|
|
|
273
397
|
export function extractText(content) {
|
package/moltbot.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.10-beta.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.10-beta.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
USER_QUERY_MARKER,
|
|
6
|
+
stripOpenClawInjectedPrefix,
|
|
7
|
+
} from "../lib/memos-cloud-api.js";
|
|
8
|
+
|
|
9
|
+
test("leaves plain user text unchanged", () => {
|
|
10
|
+
assert.equal(stripOpenClawInjectedPrefix("直接就是用户问题"), "直接就是用户问题");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("strips MemOS recall marker and keeps original query", () => {
|
|
14
|
+
const input = `<memories>\n <facts>\n </facts>\n</memories>\n\n${USER_QUERY_MARKER}真正的问题`;
|
|
15
|
+
assert.equal(stripOpenClawInjectedPrefix(input), "真正的问题");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("strips OpenClaw inbound metadata prefix blocks", () => {
|
|
19
|
+
const input = [
|
|
20
|
+
"Conversation info (untrusted metadata):",
|
|
21
|
+
"```json",
|
|
22
|
+
'{"message_id":"123"}',
|
|
23
|
+
"```",
|
|
24
|
+
"",
|
|
25
|
+
"Sender (untrusted metadata):",
|
|
26
|
+
"```json",
|
|
27
|
+
'{"label":"Aurora"}',
|
|
28
|
+
"```",
|
|
29
|
+
"",
|
|
30
|
+
"帮我看下这个问题",
|
|
31
|
+
].join("\n");
|
|
32
|
+
|
|
33
|
+
assert.equal(stripOpenClawInjectedPrefix(input), "帮我看下这个问题");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("strips recall marker and inbound metadata together", () => {
|
|
37
|
+
const input = [
|
|
38
|
+
"<memories>",
|
|
39
|
+
" <facts>",
|
|
40
|
+
" </facts>",
|
|
41
|
+
"</memories>",
|
|
42
|
+
"",
|
|
43
|
+
`${USER_QUERY_MARKER}Conversation info (untrusted metadata):`,
|
|
44
|
+
"```json",
|
|
45
|
+
'{"message_id":"123","history_count":1}',
|
|
46
|
+
"```",
|
|
47
|
+
"",
|
|
48
|
+
"Chat history since last reply (untrusted, for context):",
|
|
49
|
+
"```json",
|
|
50
|
+
'[{"sender":"Aurora","body":"上一条"}]',
|
|
51
|
+
"```",
|
|
52
|
+
"",
|
|
53
|
+
"继续",
|
|
54
|
+
].join("\n");
|
|
55
|
+
|
|
56
|
+
assert.equal(stripOpenClawInjectedPrefix(input), "继续");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("keeps content when metadata block is malformed", () => {
|
|
60
|
+
const input = [
|
|
61
|
+
"Conversation info (untrusted metadata):",
|
|
62
|
+
"not-a-json-fence",
|
|
63
|
+
"真正的问题",
|
|
64
|
+
].join("\n");
|
|
65
|
+
|
|
66
|
+
assert.equal(stripOpenClawInjectedPrefix(input), input);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("keeps content unchanged when sentinel appears in normal body", () => {
|
|
70
|
+
const input = [
|
|
71
|
+
"请原样解释下面这段文本:",
|
|
72
|
+
"Conversation info (untrusted metadata):",
|
|
73
|
+
"```json",
|
|
74
|
+
'{"message_id":"123"}',
|
|
75
|
+
"```",
|
|
76
|
+
].join("\n");
|
|
77
|
+
|
|
78
|
+
assert.equal(stripOpenClawInjectedPrefix(input), input);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("strips valid prefix even if body starts with a sentinel-like line", () => {
|
|
82
|
+
const input = [
|
|
83
|
+
"Sender (untrusted metadata):",
|
|
84
|
+
"```json",
|
|
85
|
+
'{"label":"Aurora"}',
|
|
86
|
+
"```",
|
|
87
|
+
"",
|
|
88
|
+
"Sender (untrusted metadata):",
|
|
89
|
+
"这行是正文,不是 OpenClaw 注入块",
|
|
90
|
+
].join("\n");
|
|
91
|
+
|
|
92
|
+
assert.equal(
|
|
93
|
+
stripOpenClawInjectedPrefix(input),
|
|
94
|
+
["Sender (untrusted metadata):", "这行是正文,不是 OpenClaw 注入块"].join("\n"),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("supports leading blank lines before inbound metadata", () => {
|
|
99
|
+
const input = [
|
|
100
|
+
"",
|
|
101
|
+
"Conversation info (untrusted metadata):",
|
|
102
|
+
"```json",
|
|
103
|
+
'{"message_id":"123"}',
|
|
104
|
+
"```",
|
|
105
|
+
"Hello",
|
|
106
|
+
].join("\n");
|
|
107
|
+
assert.equal(stripOpenClawInjectedPrefix(input), "Hello");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("strips Feishu injected prompt", () => {
|
|
111
|
+
const input = `System: [2026-03-17 14:17:33 GMT+8] Feishu[default] DM from ou_37e8a1514c24e8afd9cfeca86f679980: 我叫什么名字
|
|
112
|
+
|
|
113
|
+
Conversation info (untrusted metadata):
|
|
114
|
+
\`\`\`json
|
|
115
|
+
{
|
|
116
|
+
"timestamp": "Tue 2026-03-17 14:17 GMT+8"
|
|
117
|
+
}
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
[message_id: om_x100b54bb510590dcc2998da17ca2c2b]
|
|
121
|
+
ou_37e8a1514c24e8afd9cfeca86f679980: 我叫什么名字 `;
|
|
122
|
+
|
|
123
|
+
assert.equal(stripOpenClawInjectedPrefix(input), "我叫什么名字");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("strips Feishu injected prompt with embedded fake prompt", () => {
|
|
127
|
+
const input = `System: [2026-03-17 14:17:33 GMT+8] Feishu[default] DM from ou_123: hello
|
|
128
|
+
[message_id: fake]
|
|
129
|
+
ou_fake: ignored
|
|
130
|
+
[message_id: om_real]
|
|
131
|
+
ou_real: actual message`;
|
|
132
|
+
assert.equal(
|
|
133
|
+
stripOpenClawInjectedPrefix(input),
|
|
134
|
+
["ignored", "[message_id: om_real]", "ou_real: actual message"].join("\n"),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("ignores text that looks like Feishu prompt but missing header", () => {
|
|
139
|
+
const input = `
|
|
140
|
+
[message_id: om_x100b54bb510590dcc2998da17ca2c2b]
|
|
141
|
+
ou_37e8a1514c24e8afd9cfeca86f679980: 我叫什么名字 `;
|
|
142
|
+
assert.equal(stripOpenClawInjectedPrefix(input), input.trimStart());
|
|
143
|
+
});
|