memory-search-plugin 0.2.0 → 0.4.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/identity.ts +43 -30
- package/index.ts +121 -30
- package/package.json +1 -1
package/identity.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 身份解析模块
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 方案 4:从 LLM 传入的工具参数解析身份信息。
|
|
5
|
+
* LLM 从 system prompt 中的 UntrustedContext 提取字段并填入工具参数。
|
|
6
|
+
*
|
|
7
|
+
* owner 判断:通过环境变量 OPENCLAW_GATEWAY_TOKEN 获取 owner_id,
|
|
8
|
+
* 与 sender_id 比对(与旧版逻辑一致)。
|
|
6
9
|
*
|
|
7
10
|
* 输出:
|
|
8
11
|
* - scene: 对应后端 SearchRequest.scene(private_own / private_other / group)
|
|
@@ -25,22 +28,26 @@ export interface ResolvedIdentity {
|
|
|
25
28
|
group_id: string | null;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/** LLM 传入的身份相关参数(来自 UntrustedContext) */
|
|
32
|
+
export interface IdentityParams {
|
|
33
|
+
sender_id?: string;
|
|
34
|
+
conversation_type?: string; // "direct" | "group"
|
|
35
|
+
group_id?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
|
-
* 从
|
|
39
|
+
* 从 LLM 工具参数解析身份 + 场景
|
|
30
40
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* group: user_{userId}_lobster_{lobsterId}_group_{groupId}_release_{version}
|
|
41
|
+
* owner 判断逻辑与旧版一致:通过环境变量 OPENCLAW_GATEWAY_TOKEN 获取 owner_id,
|
|
42
|
+
* 再与 sender_id 比对。
|
|
34
43
|
*
|
|
35
44
|
* 场景映射:
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
45
|
+
* conversation_type="group" → scene = "group"
|
|
46
|
+
* conversation_type="direct" + isOwner(sender_id) → scene = "private_own"
|
|
47
|
+
* conversation_type="direct" + !isOwner → scene = "private_other"
|
|
39
48
|
*/
|
|
40
|
-
export function
|
|
49
|
+
export function resolveIdentityFromParams(params: IdentityParams): ResolvedIdentity {
|
|
41
50
|
// ── 测试模式:环境变量覆盖 ──────────────────────────
|
|
42
|
-
// 设置 MEMORY_GATEWAY_TEST_SCENE 即可跳过 sessionKey 解析,
|
|
43
|
-
// 方便在没有真实 IM 会话时测试插件。
|
|
44
51
|
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE as Scene | undefined;
|
|
45
52
|
if (testScene) {
|
|
46
53
|
const result: ResolvedIdentity = {
|
|
@@ -52,23 +59,39 @@ export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentit
|
|
|
52
59
|
return result;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
const userId = params.sender_id?.trim() || "unknown";
|
|
63
|
+
const convType = (params.conversation_type || "direct").trim().toLowerCase();
|
|
64
|
+
|
|
65
|
+
// 1. 群聊
|
|
66
|
+
if (convType === "group") {
|
|
67
|
+
return {
|
|
68
|
+
scene: "group",
|
|
69
|
+
user_id: userId,
|
|
70
|
+
group_id: params.group_id?.trim() || null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. 私聊:通过环境变量比对 owner_id 判断 owner/peer
|
|
75
|
+
if (isOwner(userId)) {
|
|
76
|
+
return { scene: "private_own", user_id: userId, group_id: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { scene: "private_other", user_id: userId, group_id: null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 向后兼容:保留旧函数签名,sessionKey 仍可用时作为 fallback ──
|
|
83
|
+
|
|
84
|
+
export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentity {
|
|
56
85
|
const sk = sessionKey ?? "";
|
|
57
86
|
const sessionPart = sk.split(":").pop() || "";
|
|
58
87
|
|
|
59
|
-
// 1. 群聊: 包含 _group_
|
|
60
88
|
const groupMatch = sessionPart.match(
|
|
61
89
|
/user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/
|
|
62
90
|
);
|
|
63
91
|
if (groupMatch) {
|
|
64
|
-
return {
|
|
65
|
-
scene: "group",
|
|
66
|
-
user_id: groupMatch[1],
|
|
67
|
-
group_id: groupMatch[3],
|
|
68
|
-
};
|
|
92
|
+
return { scene: "group", user_id: groupMatch[1], group_id: groupMatch[3] };
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
// 2. 私聊 (owner 或 peer)
|
|
72
95
|
const peerMatch = sessionPart.match(
|
|
73
96
|
/user_(.+?)_lobster_(.+?)_release_(.+)$/
|
|
74
97
|
);
|
|
@@ -80,15 +103,9 @@ export function resolveIdentity(sessionKey: string | undefined): ResolvedIdentit
|
|
|
80
103
|
return { scene: "private_other", user_id: userId, group_id: null };
|
|
81
104
|
}
|
|
82
105
|
|
|
83
|
-
// 3. 兜底: 安全起见当作 peer(private_other)
|
|
84
106
|
return { scene: "private_other", user_id: "unknown", group_id: null };
|
|
85
107
|
}
|
|
86
108
|
|
|
87
|
-
/**
|
|
88
|
-
* 判断是否是 owner
|
|
89
|
-
* 通过环境变量 OPENCLAW_GATEWAY_TOKEN 判断
|
|
90
|
-
* token 格式: oc-user-{ownerUserId}
|
|
91
|
-
*/
|
|
92
109
|
function isOwner(userId: string): boolean {
|
|
93
110
|
const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
94
111
|
if (!token) return false;
|
|
@@ -96,10 +113,6 @@ function isOwner(userId: string): boolean {
|
|
|
96
113
|
return ownerUserId !== "" && ownerUserId === userId;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
/**
|
|
100
|
-
* 从 sessionKey 提取 agent_id
|
|
101
|
-
* sessionKey 格式: agent:{agentId}:palz:...
|
|
102
|
-
*/
|
|
103
116
|
export function extractAgentId(sessionKey: string | undefined): string {
|
|
104
117
|
const parts = (sessionKey || "").split(":");
|
|
105
118
|
return parts.length >= 2 ? parts[1] : "main";
|
package/index.ts
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
28
28
|
import { createGatewayClient } from "./gateway-client";
|
|
29
|
-
import { resolveIdentity, extractAgentId } from "./identity";
|
|
29
|
+
import { resolveIdentityFromParams, resolveIdentity, extractAgentId } from "./identity";
|
|
30
|
+
import type { IdentityParams } from "./identity";
|
|
30
31
|
import type { MemorySearchResult } from "./gateway-client";
|
|
31
32
|
|
|
32
33
|
// ── Prompt Section ──────────────────────────────────────
|
|
@@ -64,6 +65,14 @@ const buildPromptSection = ({
|
|
|
64
65
|
|
|
65
66
|
const lines = ["## Memory Recall", toolGuidance];
|
|
66
67
|
|
|
68
|
+
lines.push(
|
|
69
|
+
"**Context extraction:** When calling memory_search or memory_get, " +
|
|
70
|
+
"you MUST fill in the agent_id, sender_id, conversation_type, and group_id parameters " +
|
|
71
|
+
"by extracting them from the UntrustedContext section in this conversation. " +
|
|
72
|
+
"Look for lines like `agent_id: xxx`, `sender_id: xxx`, `conversation_type: xxx`, `group_id: xxx`. " +
|
|
73
|
+
"If a field is not present, omit it."
|
|
74
|
+
);
|
|
75
|
+
|
|
67
76
|
if (citationsMode === "off") {
|
|
68
77
|
lines.push(
|
|
69
78
|
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."
|
|
@@ -78,7 +87,7 @@ const buildPromptSection = ({
|
|
|
78
87
|
return lines;
|
|
79
88
|
};
|
|
80
89
|
|
|
81
|
-
// ── 路径判断
|
|
90
|
+
// ── 路径判断 ────────────────────────────────────────────
|
|
82
91
|
|
|
83
92
|
/**
|
|
84
93
|
* 判断是否是 MEMORY 路径
|
|
@@ -155,13 +164,15 @@ export default definePluginEntry({
|
|
|
155
164
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
156
165
|
api.registerTool(
|
|
157
166
|
(ctx: any) => {
|
|
158
|
-
|
|
159
|
-
const
|
|
167
|
+
// fallback: 如果 sessionKey 仍可用,保留兼容
|
|
168
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
169
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
160
170
|
|
|
161
171
|
return {
|
|
162
172
|
name: "memory_search",
|
|
163
173
|
description:
|
|
164
|
-
"Semantically search MEMORY.md and memory/**/*.md for relevant memories."
|
|
174
|
+
"Semantically search MEMORY.md and memory/**/*.md for relevant memories. " +
|
|
175
|
+
"You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
165
176
|
parameters: {
|
|
166
177
|
type: "object" as const,
|
|
167
178
|
properties: {
|
|
@@ -169,6 +180,22 @@ export default definePluginEntry({
|
|
|
169
180
|
type: "string" as const,
|
|
170
181
|
description: "The search query",
|
|
171
182
|
},
|
|
183
|
+
agent_id: {
|
|
184
|
+
type: "string" as const,
|
|
185
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')",
|
|
186
|
+
},
|
|
187
|
+
sender_id: {
|
|
188
|
+
type: "string" as const,
|
|
189
|
+
description: "The sender_id from UntrustedContext",
|
|
190
|
+
},
|
|
191
|
+
conversation_type: {
|
|
192
|
+
type: "string" as const,
|
|
193
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'",
|
|
194
|
+
},
|
|
195
|
+
group_id: {
|
|
196
|
+
type: "string" as const,
|
|
197
|
+
description: "The group_id from UntrustedContext (only present in group chats)",
|
|
198
|
+
},
|
|
172
199
|
maxResults: {
|
|
173
200
|
type: "number" as const,
|
|
174
201
|
description: "Maximum number of results to return",
|
|
@@ -183,7 +210,15 @@ export default definePluginEntry({
|
|
|
183
210
|
|
|
184
211
|
async execute(
|
|
185
212
|
toolCallId: string,
|
|
186
|
-
params: {
|
|
213
|
+
params: {
|
|
214
|
+
query?: string;
|
|
215
|
+
agent_id?: string;
|
|
216
|
+
sender_id?: string;
|
|
217
|
+
conversation_type?: string;
|
|
218
|
+
group_id?: string;
|
|
219
|
+
maxResults?: number;
|
|
220
|
+
minScore?: number;
|
|
221
|
+
}
|
|
187
222
|
) {
|
|
188
223
|
const query = params.query || "";
|
|
189
224
|
if (!query) {
|
|
@@ -192,8 +227,23 @@ export default definePluginEntry({
|
|
|
192
227
|
};
|
|
193
228
|
}
|
|
194
229
|
|
|
230
|
+
// 优先用 LLM 传参,fallback 到 sessionKey 解析
|
|
231
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
232
|
+
const identity = (params.sender_id || params.conversation_type || params.group_id)
|
|
233
|
+
? resolveIdentityFromParams({
|
|
234
|
+
sender_id: params.sender_id,
|
|
235
|
+
conversation_type: params.conversation_type,
|
|
236
|
+
group_id: params.group_id,
|
|
237
|
+
})
|
|
238
|
+
: (fallbackIdentity || resolveIdentityFromParams({}));
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
`[memory-search-plugin] search: agent_id=${agentId} scene=${identity.scene} ` +
|
|
242
|
+
`user_id=${identity.user_id} group_id=${identity.group_id} ` +
|
|
243
|
+
`(source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
244
|
+
);
|
|
245
|
+
|
|
195
246
|
try {
|
|
196
|
-
// 按后端 SearchRequest 格式发送请求
|
|
197
247
|
const data = await gateway.callGatewaySearch({
|
|
198
248
|
agent_id: agentId,
|
|
199
249
|
user_id: identity.user_id,
|
|
@@ -218,10 +268,9 @@ export default definePluginEntry({
|
|
|
218
268
|
} catch (err: any) {
|
|
219
269
|
console.error("[memory-search-plugin] search failed:", err.message);
|
|
220
270
|
|
|
221
|
-
// ── 降级:尝试用原始的 memory_search ──
|
|
222
271
|
try {
|
|
223
272
|
const fallbackTool =
|
|
224
|
-
api.runtime
|
|
273
|
+
api.runtime?.tools?.createMemorySearchTool({
|
|
225
274
|
config: ctx.config,
|
|
226
275
|
agentSessionKey: ctx.sessionKey,
|
|
227
276
|
});
|
|
@@ -229,10 +278,10 @@ export default definePluginEntry({
|
|
|
229
278
|
console.warn(
|
|
230
279
|
"[memory-search-plugin] falling back to local memory_search"
|
|
231
280
|
);
|
|
232
|
-
return await fallbackTool.execute(toolCallId, params);
|
|
281
|
+
return await fallbackTool.execute(toolCallId, { query, maxResults: params.maxResults, minScore: params.minScore });
|
|
233
282
|
}
|
|
234
283
|
} catch {
|
|
235
|
-
//
|
|
284
|
+
// 降级也失败
|
|
236
285
|
}
|
|
237
286
|
|
|
238
287
|
return {
|
|
@@ -257,20 +306,15 @@ export default definePluginEntry({
|
|
|
257
306
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
258
307
|
api.registerTool(
|
|
259
308
|
(ctx: any) => {
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
// 获取原始 memory_get 实现,用于非 MEMORY 路径的 passthrough
|
|
264
|
-
const originalGetTool = api.runtime.tools.createMemoryGetTool({
|
|
265
|
-
config: ctx.config,
|
|
266
|
-
agentSessionKey: ctx.sessionKey,
|
|
267
|
-
});
|
|
309
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
310
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
268
311
|
|
|
269
312
|
return {
|
|
270
313
|
name: "memory_get",
|
|
271
314
|
description:
|
|
272
315
|
"Read a memory file or workspace file. " +
|
|
273
|
-
"Supports reading MEMORY.md, memory/**/*.md, and other workspace files."
|
|
316
|
+
"Supports reading MEMORY.md, memory/**/*.md, and other workspace files. " +
|
|
317
|
+
"You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
274
318
|
parameters: {
|
|
275
319
|
type: "object" as const,
|
|
276
320
|
properties: {
|
|
@@ -279,6 +323,22 @@ export default definePluginEntry({
|
|
|
279
323
|
description:
|
|
280
324
|
"Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)",
|
|
281
325
|
},
|
|
326
|
+
agent_id: {
|
|
327
|
+
type: "string" as const,
|
|
328
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')",
|
|
329
|
+
},
|
|
330
|
+
sender_id: {
|
|
331
|
+
type: "string" as const,
|
|
332
|
+
description: "The sender_id from UntrustedContext",
|
|
333
|
+
},
|
|
334
|
+
conversation_type: {
|
|
335
|
+
type: "string" as const,
|
|
336
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'",
|
|
337
|
+
},
|
|
338
|
+
group_id: {
|
|
339
|
+
type: "string" as const,
|
|
340
|
+
description: "The group_id from UntrustedContext (only present in group chats)",
|
|
341
|
+
},
|
|
282
342
|
from: {
|
|
283
343
|
type: "number" as const,
|
|
284
344
|
description: "Starting line number (1-indexed)",
|
|
@@ -293,7 +353,15 @@ export default definePluginEntry({
|
|
|
293
353
|
|
|
294
354
|
async execute(
|
|
295
355
|
toolCallId: string,
|
|
296
|
-
params: {
|
|
356
|
+
params: {
|
|
357
|
+
path?: string;
|
|
358
|
+
agent_id?: string;
|
|
359
|
+
sender_id?: string;
|
|
360
|
+
conversation_type?: string;
|
|
361
|
+
group_id?: string;
|
|
362
|
+
from?: number;
|
|
363
|
+
lines?: number;
|
|
364
|
+
}
|
|
297
365
|
) {
|
|
298
366
|
const path = params.path || "";
|
|
299
367
|
if (!path) {
|
|
@@ -302,11 +370,34 @@ export default definePluginEntry({
|
|
|
302
370
|
};
|
|
303
371
|
}
|
|
304
372
|
|
|
373
|
+
// 懒加载原始 memory_get 实现,用于非 MEMORY 路径的 passthrough
|
|
374
|
+
let originalGetTool: any = null;
|
|
375
|
+
try {
|
|
376
|
+
originalGetTool = api.runtime?.tools?.createMemoryGetTool({
|
|
377
|
+
config: ctx.config,
|
|
378
|
+
agentSessionKey: ctx.sessionKey,
|
|
379
|
+
});
|
|
380
|
+
} catch {
|
|
381
|
+
// memory-core 被替换后 api.runtime.tools 不可用,忽略
|
|
382
|
+
}
|
|
383
|
+
|
|
305
384
|
// ── 路径分流 ──
|
|
306
385
|
if (isMemoryPath(path)) {
|
|
307
|
-
|
|
386
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
387
|
+
const identity = (params.sender_id || params.conversation_type || params.group_id)
|
|
388
|
+
? resolveIdentityFromParams({
|
|
389
|
+
sender_id: params.sender_id,
|
|
390
|
+
conversation_type: params.conversation_type,
|
|
391
|
+
group_id: params.group_id,
|
|
392
|
+
})
|
|
393
|
+
: (fallbackIdentity || resolveIdentityFromParams({}));
|
|
394
|
+
|
|
395
|
+
console.log(
|
|
396
|
+
`[memory-search-plugin] get: path=${path} agent_id=${agentId} scene=${identity.scene} ` +
|
|
397
|
+
`user_id=${identity.user_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
398
|
+
);
|
|
399
|
+
|
|
308
400
|
try {
|
|
309
|
-
// 按后端 GetRequest 格式发送请求
|
|
310
401
|
const data = await gateway.callGatewayGet({
|
|
311
402
|
agent_id: agentId,
|
|
312
403
|
user_id: identity.user_id,
|
|
@@ -324,7 +415,6 @@ export default definePluginEntry({
|
|
|
324
415
|
} catch (err: any) {
|
|
325
416
|
console.error("[memory-search-plugin] get failed:", err.message);
|
|
326
417
|
|
|
327
|
-
// 403 明确拒绝 → 不降级,直接返回拒绝
|
|
328
418
|
if (err.message?.includes('403')) {
|
|
329
419
|
return {
|
|
330
420
|
content: [
|
|
@@ -336,13 +426,12 @@ export default definePluginEntry({
|
|
|
336
426
|
};
|
|
337
427
|
}
|
|
338
428
|
|
|
339
|
-
// 其他错误(网络不通等)→ 降级到本地文件读取
|
|
340
429
|
if (originalGetTool) {
|
|
341
430
|
console.warn(
|
|
342
431
|
"[memory-search-plugin] falling back to local memory_get for:",
|
|
343
432
|
path
|
|
344
433
|
);
|
|
345
|
-
return await originalGetTool.execute(toolCallId, params);
|
|
434
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
346
435
|
}
|
|
347
436
|
|
|
348
437
|
return {
|
|
@@ -356,10 +445,8 @@ export default definePluginEntry({
|
|
|
356
445
|
};
|
|
357
446
|
}
|
|
358
447
|
} else {
|
|
359
|
-
// 非 MEMORY 路径 → passthrough 给原始实现
|
|
360
|
-
// 这样 SOUL.md、rules.md 等文件的读取行为完全不变
|
|
361
448
|
if (originalGetTool) {
|
|
362
|
-
return await originalGetTool.execute(toolCallId, params);
|
|
449
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
363
450
|
}
|
|
364
451
|
|
|
365
452
|
return {
|
|
@@ -382,7 +469,11 @@ export default definePluginEntry({
|
|
|
382
469
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
383
470
|
api.registerCli(
|
|
384
471
|
({ program }: any) => {
|
|
385
|
-
|
|
472
|
+
try {
|
|
473
|
+
api.runtime?.tools?.registerMemoryCli(program);
|
|
474
|
+
} catch {
|
|
475
|
+
// memory-core 被替换后 api.runtime.tools 不可用,忽略
|
|
476
|
+
}
|
|
386
477
|
},
|
|
387
478
|
{ commands: ["memory"] }
|
|
388
479
|
);
|