memory-search-plugin 0.5.0 → 0.6.0
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/gateway-client.ts +0 -42
- package/index.js +413 -0
- package/index.ts +2 -127
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
package/gateway-client.ts
CHANGED
|
@@ -57,31 +57,6 @@ export interface MemoryGetResponse {
|
|
|
57
57
|
path: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export interface MemoryRawSnapshotsRequest {
|
|
61
|
-
release_name: string;
|
|
62
|
-
start_time: string; // ISO 8601
|
|
63
|
-
end_time: string; // ISO 8601
|
|
64
|
-
path_prefix?: string; // e.g. "memory/"
|
|
65
|
-
page?: number;
|
|
66
|
-
page_size?: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface MemoryRawSnapshot {
|
|
70
|
-
id: string;
|
|
71
|
-
path: string;
|
|
72
|
-
content: string;
|
|
73
|
-
release_name: string;
|
|
74
|
-
synced_at: string;
|
|
75
|
-
[key: string]: unknown;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface MemoryRawSnapshotsResponse {
|
|
79
|
-
records: MemoryRawSnapshot[];
|
|
80
|
-
total: number;
|
|
81
|
-
page: number;
|
|
82
|
-
page_size: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
60
|
// ── Gateway Client ─────────────────────────────────────
|
|
86
61
|
|
|
87
62
|
export interface GatewayClientConfig {
|
|
@@ -92,7 +67,6 @@ export interface GatewayClientConfig {
|
|
|
92
67
|
export interface GatewayClient {
|
|
93
68
|
callGatewaySearch(options: MemorySearchRequest): Promise<MemorySearchResponse>;
|
|
94
69
|
callGatewayGet(options: MemoryGetRequest): Promise<MemoryGetResponse>;
|
|
95
|
-
callGatewayRawSnapshots(options: MemoryRawSnapshotsRequest): Promise<MemoryRawSnapshotsResponse>;
|
|
96
70
|
}
|
|
97
71
|
|
|
98
72
|
export function createGatewayClient(config: GatewayClientConfig): GatewayClient {
|
|
@@ -135,21 +109,5 @@ export function createGatewayClient(config: GatewayClientConfig): GatewayClient
|
|
|
135
109
|
}
|
|
136
110
|
return response.json() as Promise<MemoryGetResponse>;
|
|
137
111
|
},
|
|
138
|
-
|
|
139
|
-
async callGatewayRawSnapshots(options) {
|
|
140
|
-
const url = `${baseUrl}/api/memory/raw_snapshots`;
|
|
141
|
-
const response = await fetch(url, {
|
|
142
|
-
method: "POST",
|
|
143
|
-
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
144
|
-
body: JSON.stringify(options),
|
|
145
|
-
});
|
|
146
|
-
if (!response.ok) {
|
|
147
|
-
const errorBody = await response.text().catch(() => "");
|
|
148
|
-
throw new Error(
|
|
149
|
-
`Gateway raw_snapshots failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
return response.json() as Promise<MemoryRawSnapshotsResponse>;
|
|
153
|
-
},
|
|
154
112
|
};
|
|
155
113
|
}
|
package/index.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
|
|
4
|
+
// gateway-client.ts
|
|
5
|
+
function createGatewayClient(config) {
|
|
6
|
+
const baseUrl = config.gatewayUrl.replace(/\/+$/, "");
|
|
7
|
+
const token = config.gatewayToken || "";
|
|
8
|
+
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
|
|
9
|
+
return {
|
|
10
|
+
async callGatewaySearch(options) {
|
|
11
|
+
const url = `${baseUrl}/api/memory/search`;
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
15
|
+
body: JSON.stringify(options)
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
const errorBody = await response.text().catch(() => "");
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Gateway search failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return response.json();
|
|
24
|
+
},
|
|
25
|
+
async callGatewayGet(options) {
|
|
26
|
+
const url = `${baseUrl}/api/memory/get`;
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
30
|
+
body: JSON.stringify(options)
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorBody = await response.text().catch(() => "");
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Gateway get failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return response.json();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// identity.ts
|
|
44
|
+
function resolveIdentityFromParams(params) {
|
|
45
|
+
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE;
|
|
46
|
+
if (testScene) {
|
|
47
|
+
const result = {
|
|
48
|
+
scene: testScene,
|
|
49
|
+
user_id: process.env.MEMORY_GATEWAY_TEST_USER_ID || "owner_A",
|
|
50
|
+
group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null
|
|
51
|
+
};
|
|
52
|
+
console.log("[memory-search-plugin] TEST MODE identity:", JSON.stringify(result));
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const userId = params.sender_id?.trim() || "unknown";
|
|
56
|
+
const convType = (params.conversation_type || "direct").trim().toLowerCase();
|
|
57
|
+
if (convType === "group") {
|
|
58
|
+
return {
|
|
59
|
+
scene: "group",
|
|
60
|
+
user_id: userId,
|
|
61
|
+
group_id: params.group_id?.trim() || null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (isOwner(userId)) {
|
|
65
|
+
return { scene: "private_own", user_id: userId, group_id: null };
|
|
66
|
+
}
|
|
67
|
+
return { scene: "private_other", user_id: userId, group_id: null };
|
|
68
|
+
}
|
|
69
|
+
function resolveIdentity(sessionKey) {
|
|
70
|
+
const sk = sessionKey ?? "";
|
|
71
|
+
const sessionPart = sk.split(":").pop() || "";
|
|
72
|
+
const groupMatch = sessionPart.match(
|
|
73
|
+
/user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/
|
|
74
|
+
);
|
|
75
|
+
if (groupMatch) {
|
|
76
|
+
return { scene: "group", user_id: groupMatch[1], group_id: groupMatch[3] };
|
|
77
|
+
}
|
|
78
|
+
const peerMatch = sessionPart.match(
|
|
79
|
+
/user_(.+?)_lobster_(.+?)_release_(.+)$/
|
|
80
|
+
);
|
|
81
|
+
if (peerMatch) {
|
|
82
|
+
const userId = peerMatch[1];
|
|
83
|
+
if (isOwner(userId)) {
|
|
84
|
+
return { scene: "private_own", user_id: userId, group_id: null };
|
|
85
|
+
}
|
|
86
|
+
return { scene: "private_other", user_id: userId, group_id: null };
|
|
87
|
+
}
|
|
88
|
+
return { scene: "private_other", user_id: "unknown", group_id: null };
|
|
89
|
+
}
|
|
90
|
+
function isOwner(userId) {
|
|
91
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
92
|
+
if (!token) return false;
|
|
93
|
+
const ownerUserId = token.split("-").pop() || "";
|
|
94
|
+
return ownerUserId !== "" && ownerUserId === userId;
|
|
95
|
+
}
|
|
96
|
+
function extractAgentId(sessionKey) {
|
|
97
|
+
const parts = (sessionKey || "").split(":");
|
|
98
|
+
return parts.length >= 2 ? parts[1] : "main";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// index.ts
|
|
102
|
+
var buildPromptSection = ({
|
|
103
|
+
availableTools,
|
|
104
|
+
citationsMode
|
|
105
|
+
}) => {
|
|
106
|
+
const hasMemorySearch = availableTools.has("memory_search");
|
|
107
|
+
const hasMemoryGet = availableTools.has("memory_get");
|
|
108
|
+
if (!hasMemorySearch && !hasMemoryGet) return [];
|
|
109
|
+
let toolGuidance;
|
|
110
|
+
if (hasMemorySearch && hasMemoryGet) {
|
|
111
|
+
toolGuidance = "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.";
|
|
112
|
+
} else if (hasMemorySearch) {
|
|
113
|
+
toolGuidance = "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md and answer from the matching results. If low confidence after search, say you checked.";
|
|
114
|
+
} else {
|
|
115
|
+
toolGuidance = "Before answering anything about prior work, decisions, dates, people, preferences, or todos that already point to a specific memory file or note: run memory_get to pull only the needed lines. If low confidence after reading them, say you checked.";
|
|
116
|
+
}
|
|
117
|
+
const lines = ["## Memory Recall", toolGuidance];
|
|
118
|
+
lines.push(
|
|
119
|
+
"**Context extraction:** When calling memory_search or memory_get, you MUST fill in the agent_id, sender_id, conversation_type, and group_id parameters by extracting them from the UntrustedContext section in this conversation. Look for lines like `agent_id: xxx`, `sender_id: xxx`, `conversation_type: xxx`, `group_id: xxx`. If a field is not present, omit it."
|
|
120
|
+
);
|
|
121
|
+
if (citationsMode === "off") {
|
|
122
|
+
lines.push(
|
|
123
|
+
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(
|
|
127
|
+
"Citations: include Source: <path#line> when it helps the user verify memory snippets."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
return lines;
|
|
132
|
+
};
|
|
133
|
+
function isMemoryPath(path) {
|
|
134
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
135
|
+
return normalized === "MEMORY.md" || normalized.startsWith("memory/") || normalized.endsWith("/MEMORY.md") || normalized.includes("/memory/");
|
|
136
|
+
}
|
|
137
|
+
function formatSearchResults(results) {
|
|
138
|
+
if (results.length === 0) {
|
|
139
|
+
return "No relevant memories found.";
|
|
140
|
+
}
|
|
141
|
+
return results.map((r, i) => {
|
|
142
|
+
let sourceDesc = r.memory_type;
|
|
143
|
+
if (r.memory_type === "peer" && r.peer_id) {
|
|
144
|
+
sourceDesc = `peer/${r.peer_id}`;
|
|
145
|
+
} else if (r.memory_type === "group" && r.group_id) {
|
|
146
|
+
sourceDesc = `group/${r.group_id}`;
|
|
147
|
+
} else if (r.memory_type === "knowledge" && r.kb_id) {
|
|
148
|
+
sourceDesc = `knowledge/${r.kb_id}`;
|
|
149
|
+
}
|
|
150
|
+
const filePart = r.source_file ? ` | ${r.source_file}` : "";
|
|
151
|
+
const header = `[${i + 1}] (${sourceDesc}, score: ${r.score.toFixed(2)}${filePart})`;
|
|
152
|
+
return `${header}
|
|
153
|
+
${r.content}`;
|
|
154
|
+
}).join("\n\n");
|
|
155
|
+
}
|
|
156
|
+
var index_default = definePluginEntry({
|
|
157
|
+
id: "memory-search-plugin",
|
|
158
|
+
name: "Memory Search Plugin",
|
|
159
|
+
description: "Routes memory to external Memory Search Gateway with ACL",
|
|
160
|
+
kind: "memory",
|
|
161
|
+
register(api) {
|
|
162
|
+
const gatewayUrl = api.pluginConfig?.gatewayUrl || "http://localhost:18790";
|
|
163
|
+
const gatewayToken = api.pluginConfig?.gatewayToken || "";
|
|
164
|
+
console.log("[memory-search-plugin] gatewayUrl:", gatewayUrl);
|
|
165
|
+
const gateway = createGatewayClient({ gatewayUrl, gatewayToken });
|
|
166
|
+
api.registerMemoryPromptSection(buildPromptSection);
|
|
167
|
+
api.registerTool(
|
|
168
|
+
(ctx) => {
|
|
169
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
170
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
171
|
+
return {
|
|
172
|
+
name: "memory_search",
|
|
173
|
+
description: "Semantically search MEMORY.md and memory/**/*.md for relevant memories. You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
174
|
+
parameters: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
query: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "The search query"
|
|
180
|
+
},
|
|
181
|
+
agent_id: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
|
|
184
|
+
},
|
|
185
|
+
sender_id: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "The sender_id from UntrustedContext"
|
|
188
|
+
},
|
|
189
|
+
conversation_type: {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'"
|
|
192
|
+
},
|
|
193
|
+
group_id: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description: "The group_id from UntrustedContext (only present in group chats)"
|
|
196
|
+
},
|
|
197
|
+
maxResults: {
|
|
198
|
+
type: "number",
|
|
199
|
+
description: "Maximum number of results to return"
|
|
200
|
+
},
|
|
201
|
+
minScore: {
|
|
202
|
+
type: "number",
|
|
203
|
+
description: "Minimum similarity score threshold"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
required: ["query"]
|
|
207
|
+
},
|
|
208
|
+
async execute(toolCallId, params) {
|
|
209
|
+
const query = params.query || "";
|
|
210
|
+
if (!query) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: "No query provided." }]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
216
|
+
const identity = params.sender_id || params.conversation_type || params.group_id ? resolveIdentityFromParams({
|
|
217
|
+
sender_id: params.sender_id,
|
|
218
|
+
conversation_type: params.conversation_type,
|
|
219
|
+
group_id: params.group_id
|
|
220
|
+
}) : fallbackIdentity || resolveIdentityFromParams({});
|
|
221
|
+
console.log(
|
|
222
|
+
`[memory-search-plugin] search: agent_id=${agentId} scene=${identity.scene} user_id=${identity.user_id} group_id=${identity.group_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
223
|
+
);
|
|
224
|
+
try {
|
|
225
|
+
const data = await gateway.callGatewaySearch({
|
|
226
|
+
agent_id: agentId,
|
|
227
|
+
user_id: identity.user_id,
|
|
228
|
+
query,
|
|
229
|
+
scene: identity.scene,
|
|
230
|
+
group_id: identity.group_id,
|
|
231
|
+
limit: params.maxResults || 20,
|
|
232
|
+
threshold: params.minScore || 0.3
|
|
233
|
+
});
|
|
234
|
+
const text = formatSearchResults(data.results);
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text }],
|
|
237
|
+
details: {
|
|
238
|
+
results: data.results,
|
|
239
|
+
total: data.total,
|
|
240
|
+
scene: data.scene,
|
|
241
|
+
steps: data.steps
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error("[memory-search-plugin] search failed:", err.message);
|
|
246
|
+
try {
|
|
247
|
+
const fallbackTool = api.runtime?.tools?.createMemorySearchTool({
|
|
248
|
+
config: ctx.config,
|
|
249
|
+
agentSessionKey: ctx.sessionKey
|
|
250
|
+
});
|
|
251
|
+
if (fallbackTool) {
|
|
252
|
+
console.warn(
|
|
253
|
+
"[memory-search-plugin] falling back to local memory_search"
|
|
254
|
+
);
|
|
255
|
+
return await fallbackTool.execute(toolCallId, { query, maxResults: params.maxResults, minScore: params.minScore });
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: "Memory search temporarily unavailable."
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
{ names: ["memory_search"] }
|
|
272
|
+
);
|
|
273
|
+
api.registerTool(
|
|
274
|
+
(ctx) => {
|
|
275
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
276
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
277
|
+
return {
|
|
278
|
+
name: "memory_get",
|
|
279
|
+
description: "Read a memory file or workspace file. Supports reading MEMORY.md, memory/**/*.md, and other workspace files. You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
280
|
+
parameters: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
path: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)"
|
|
286
|
+
},
|
|
287
|
+
agent_id: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
|
|
290
|
+
},
|
|
291
|
+
sender_id: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "The sender_id from UntrustedContext"
|
|
294
|
+
},
|
|
295
|
+
conversation_type: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'"
|
|
298
|
+
},
|
|
299
|
+
group_id: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "The group_id from UntrustedContext (only present in group chats)"
|
|
302
|
+
},
|
|
303
|
+
from: {
|
|
304
|
+
type: "number",
|
|
305
|
+
description: "Starting line number (1-indexed)"
|
|
306
|
+
},
|
|
307
|
+
lines: {
|
|
308
|
+
type: "number",
|
|
309
|
+
description: "Number of lines to read"
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
required: ["path"]
|
|
313
|
+
},
|
|
314
|
+
async execute(toolCallId, params) {
|
|
315
|
+
const path = params.path || "";
|
|
316
|
+
if (!path) {
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: "No path provided." }]
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
let originalGetTool = null;
|
|
322
|
+
try {
|
|
323
|
+
originalGetTool = api.runtime?.tools?.createMemoryGetTool({
|
|
324
|
+
config: ctx.config,
|
|
325
|
+
agentSessionKey: ctx.sessionKey
|
|
326
|
+
});
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
if (isMemoryPath(path)) {
|
|
330
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
331
|
+
const identity = params.sender_id || params.conversation_type || params.group_id ? resolveIdentityFromParams({
|
|
332
|
+
sender_id: params.sender_id,
|
|
333
|
+
conversation_type: params.conversation_type,
|
|
334
|
+
group_id: params.group_id
|
|
335
|
+
}) : fallbackIdentity || resolveIdentityFromParams({});
|
|
336
|
+
console.log(
|
|
337
|
+
`[memory-search-plugin] get: path=${path} agent_id=${agentId} scene=${identity.scene} user_id=${identity.user_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
338
|
+
);
|
|
339
|
+
try {
|
|
340
|
+
const data = await gateway.callGatewayGet({
|
|
341
|
+
agent_id: agentId,
|
|
342
|
+
user_id: identity.user_id,
|
|
343
|
+
path,
|
|
344
|
+
scene: identity.scene,
|
|
345
|
+
group_id: identity.group_id,
|
|
346
|
+
from: params.from,
|
|
347
|
+
lines: params.lines
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text", text: data.text }],
|
|
351
|
+
details: { path: data.path, text: data.text }
|
|
352
|
+
};
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error("[memory-search-plugin] get failed:", err.message);
|
|
355
|
+
if (err.message?.includes("403")) {
|
|
356
|
+
return {
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: "text",
|
|
360
|
+
text: `Access denied: ${path}`
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (originalGetTool) {
|
|
366
|
+
console.warn(
|
|
367
|
+
"[memory-search-plugin] falling back to local memory_get for:",
|
|
368
|
+
path
|
|
369
|
+
);
|
|
370
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: `Failed to read ${path}: Gateway unavailable.`
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
details: { path, text: "", error: err.message }
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
if (originalGetTool) {
|
|
384
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: `Cannot read ${path}: memory_get backend not available.`
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
},
|
|
398
|
+
{ names: ["memory_get"] }
|
|
399
|
+
);
|
|
400
|
+
api.registerCli(
|
|
401
|
+
({ program }) => {
|
|
402
|
+
try {
|
|
403
|
+
api.runtime?.tools?.registerMemoryCli(program);
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{ commands: ["memory"] }
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
export {
|
|
412
|
+
index_default as default
|
|
413
|
+
};
|
package/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
|
28
28
|
import { createGatewayClient } from "./gateway-client";
|
|
29
29
|
import { resolveIdentityFromParams, resolveIdentity, extractAgentId } from "./identity";
|
|
30
30
|
import type { IdentityParams } from "./identity";
|
|
31
|
-
import type { MemorySearchResult
|
|
31
|
+
import type { MemorySearchResult } from "./gateway-client";
|
|
32
32
|
|
|
33
33
|
// ── Prompt Section ──────────────────────────────────────
|
|
34
34
|
// 告诉 LLM 如何使用 memory 工具(和 memory-core 的逻辑一致)
|
|
@@ -136,23 +136,6 @@ function formatSearchResults(results: MemorySearchResult[]): string {
|
|
|
136
136
|
.join("\n\n");
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/**
|
|
140
|
-
* 将 raw_snapshots 查询结果格式化为可读文本
|
|
141
|
-
* 包含 path、synced_at、release_name 和内容摘要
|
|
142
|
-
*/
|
|
143
|
-
function formatRawSnapshots(records: MemoryRawSnapshot[]): string {
|
|
144
|
-
if (records.length === 0) {
|
|
145
|
-
return "No matching raw snapshot records found.";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return records
|
|
149
|
-
.map((r, i) => {
|
|
150
|
-
const header = `[${i + 1}] path: ${r.path} | release: ${r.release_name} | synced_at: ${r.synced_at}`;
|
|
151
|
-
return `${header}\n${r.content}`;
|
|
152
|
-
})
|
|
153
|
-
.join("\n\n");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
139
|
// ── 插件定义 ────────────────────────────────────────────
|
|
157
140
|
|
|
158
141
|
export default definePluginEntry({
|
|
@@ -482,115 +465,7 @@ export default definePluginEntry({
|
|
|
482
465
|
);
|
|
483
466
|
|
|
484
467
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
485
|
-
// 4.
|
|
486
|
-
// 按 synced_at 时间段 + path 为 memory/xxx + release_name 过滤
|
|
487
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
488
|
-
api.registerTool(
|
|
489
|
-
(_ctx: any) => {
|
|
490
|
-
return {
|
|
491
|
-
name: "memory_raw_snapshots_query",
|
|
492
|
-
description:
|
|
493
|
-
"Query raw memory snapshot records from the memory_raw_snapshots table. " +
|
|
494
|
-
"Filters by synced_at time range, release_name, and only returns records whose path starts with 'memory/' " +
|
|
495
|
-
"(excluding MEMORY.md, USER.md, etc.).",
|
|
496
|
-
parameters: {
|
|
497
|
-
type: "object" as const,
|
|
498
|
-
properties: {
|
|
499
|
-
release_name: {
|
|
500
|
-
type: "string" as const,
|
|
501
|
-
description: "The release_name to filter by (e.g. 'lobster_xxx')",
|
|
502
|
-
},
|
|
503
|
-
start_time: {
|
|
504
|
-
type: "string" as const,
|
|
505
|
-
description: "Start time in ISO 8601 format (e.g. '2025-05-01T00:00:00Z')",
|
|
506
|
-
},
|
|
507
|
-
end_time: {
|
|
508
|
-
type: "string" as const,
|
|
509
|
-
description: "End time in ISO 8601 format (e.g. '2025-05-07T23:59:59Z')",
|
|
510
|
-
},
|
|
511
|
-
page: {
|
|
512
|
-
type: "number" as const,
|
|
513
|
-
description: "Page number for pagination (default 1)",
|
|
514
|
-
},
|
|
515
|
-
page_size: {
|
|
516
|
-
type: "number" as const,
|
|
517
|
-
description: "Number of records per page (default 50, max 200)",
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
required: ["release_name", "start_time", "end_time"],
|
|
521
|
-
},
|
|
522
|
-
|
|
523
|
-
async execute(
|
|
524
|
-
_toolCallId: string,
|
|
525
|
-
params: {
|
|
526
|
-
release_name?: string;
|
|
527
|
-
start_time?: string;
|
|
528
|
-
end_time?: string;
|
|
529
|
-
page?: number;
|
|
530
|
-
page_size?: number;
|
|
531
|
-
}
|
|
532
|
-
) {
|
|
533
|
-
const releaseName = params.release_name?.trim() || "";
|
|
534
|
-
const startTime = params.start_time?.trim() || "";
|
|
535
|
-
const endTime = params.end_time?.trim() || "";
|
|
536
|
-
|
|
537
|
-
if (!releaseName || !startTime || !endTime) {
|
|
538
|
-
return {
|
|
539
|
-
content: [
|
|
540
|
-
{
|
|
541
|
-
type: "text" as const,
|
|
542
|
-
text: "Missing required parameters: release_name, start_time, and end_time are all required.",
|
|
543
|
-
},
|
|
544
|
-
],
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
console.log(
|
|
549
|
-
`[memory-search-plugin] raw_snapshots_query: release_name=${releaseName} ` +
|
|
550
|
-
`start_time=${startTime} end_time=${endTime} page=${params.page || 1} page_size=${params.page_size || 50}`
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
const data = await gateway.callGatewayRawSnapshots({
|
|
555
|
-
release_name: releaseName,
|
|
556
|
-
start_time: startTime,
|
|
557
|
-
end_time: endTime,
|
|
558
|
-
path_prefix: "memory/",
|
|
559
|
-
page: params.page || 1,
|
|
560
|
-
page_size: params.page_size || 50,
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
const text = formatRawSnapshots(data.records);
|
|
564
|
-
const summary = `Found ${data.total} record(s) (page ${data.page}, page_size ${data.page_size}).\n\n${text}`;
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
content: [{ type: "text" as const, text: summary }],
|
|
568
|
-
details: {
|
|
569
|
-
records: data.records,
|
|
570
|
-
total: data.total,
|
|
571
|
-
page: data.page,
|
|
572
|
-
page_size: data.page_size,
|
|
573
|
-
},
|
|
574
|
-
};
|
|
575
|
-
} catch (err: any) {
|
|
576
|
-
console.error("[memory-search-plugin] raw_snapshots_query failed:", err.message);
|
|
577
|
-
return {
|
|
578
|
-
content: [
|
|
579
|
-
{
|
|
580
|
-
type: "text" as const,
|
|
581
|
-
text: `Raw snapshots query failed: ${err.message}`,
|
|
582
|
-
},
|
|
583
|
-
],
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
},
|
|
587
|
-
};
|
|
588
|
-
},
|
|
589
|
-
{ names: ["memory_raw_snapshots_query"] }
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
593
|
-
// 5. 保留 CLI 命令(和 memory-core 一致)
|
|
468
|
+
// 4. 保留 CLI 命令(和 memory-core 一致)
|
|
594
469
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
595
470
|
api.registerCli(
|
|
596
471
|
({ program }: any) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Memory Search Plugin",
|
|
4
4
|
"description": "Routes memory_search to external Memory Search Gateway with ACL, memory_get with path routing",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-search-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "index.
|
|
5
|
+
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
|
+
"*.js",
|
|
7
8
|
"*.ts",
|
|
8
9
|
"openclaw.plugin.json",
|
|
9
10
|
"!*.test.ts"
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
},
|
|
15
16
|
"openclaw": {
|
|
16
17
|
"extensions": [
|
|
17
|
-
"./index.
|
|
18
|
+
"./index.js"
|
|
18
19
|
]
|
|
19
20
|
}
|
|
20
21
|
}
|