memory-search-plugin 0.2.0 → 0.3.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 +103 -21
- 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."
|
|
@@ -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,7 +268,6 @@ 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
273
|
api.runtime.tools.createMemorySearchTool({
|
|
@@ -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,8 +306,8 @@ export default definePluginEntry({
|
|
|
257
306
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
258
307
|
api.registerTool(
|
|
259
308
|
(ctx: any) => {
|
|
260
|
-
const
|
|
261
|
-
const
|
|
309
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
310
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
262
311
|
|
|
263
312
|
// 获取原始 memory_get 实现,用于非 MEMORY 路径的 passthrough
|
|
264
313
|
const originalGetTool = api.runtime.tools.createMemoryGetTool({
|
|
@@ -270,7 +319,8 @@ export default definePluginEntry({
|
|
|
270
319
|
name: "memory_get",
|
|
271
320
|
description:
|
|
272
321
|
"Read a memory file or workspace file. " +
|
|
273
|
-
"Supports reading MEMORY.md, memory/**/*.md, and other workspace files."
|
|
322
|
+
"Supports reading MEMORY.md, memory/**/*.md, and other workspace files. " +
|
|
323
|
+
"You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
274
324
|
parameters: {
|
|
275
325
|
type: "object" as const,
|
|
276
326
|
properties: {
|
|
@@ -279,6 +329,22 @@ export default definePluginEntry({
|
|
|
279
329
|
description:
|
|
280
330
|
"Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)",
|
|
281
331
|
},
|
|
332
|
+
agent_id: {
|
|
333
|
+
type: "string" as const,
|
|
334
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')",
|
|
335
|
+
},
|
|
336
|
+
sender_id: {
|
|
337
|
+
type: "string" as const,
|
|
338
|
+
description: "The sender_id from UntrustedContext",
|
|
339
|
+
},
|
|
340
|
+
conversation_type: {
|
|
341
|
+
type: "string" as const,
|
|
342
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'",
|
|
343
|
+
},
|
|
344
|
+
group_id: {
|
|
345
|
+
type: "string" as const,
|
|
346
|
+
description: "The group_id from UntrustedContext (only present in group chats)",
|
|
347
|
+
},
|
|
282
348
|
from: {
|
|
283
349
|
type: "number" as const,
|
|
284
350
|
description: "Starting line number (1-indexed)",
|
|
@@ -293,7 +359,15 @@ export default definePluginEntry({
|
|
|
293
359
|
|
|
294
360
|
async execute(
|
|
295
361
|
toolCallId: string,
|
|
296
|
-
params: {
|
|
362
|
+
params: {
|
|
363
|
+
path?: string;
|
|
364
|
+
agent_id?: string;
|
|
365
|
+
sender_id?: string;
|
|
366
|
+
conversation_type?: string;
|
|
367
|
+
group_id?: string;
|
|
368
|
+
from?: number;
|
|
369
|
+
lines?: number;
|
|
370
|
+
}
|
|
297
371
|
) {
|
|
298
372
|
const path = params.path || "";
|
|
299
373
|
if (!path) {
|
|
@@ -304,9 +378,21 @@ export default definePluginEntry({
|
|
|
304
378
|
|
|
305
379
|
// ── 路径分流 ──
|
|
306
380
|
if (isMemoryPath(path)) {
|
|
307
|
-
|
|
381
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
382
|
+
const identity = (params.sender_id || params.conversation_type || params.group_id)
|
|
383
|
+
? resolveIdentityFromParams({
|
|
384
|
+
sender_id: params.sender_id,
|
|
385
|
+
conversation_type: params.conversation_type,
|
|
386
|
+
group_id: params.group_id,
|
|
387
|
+
})
|
|
388
|
+
: (fallbackIdentity || resolveIdentityFromParams({}));
|
|
389
|
+
|
|
390
|
+
console.log(
|
|
391
|
+
`[memory-search-plugin] get: path=${path} agent_id=${agentId} scene=${identity.scene} ` +
|
|
392
|
+
`user_id=${identity.user_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
393
|
+
);
|
|
394
|
+
|
|
308
395
|
try {
|
|
309
|
-
// 按后端 GetRequest 格式发送请求
|
|
310
396
|
const data = await gateway.callGatewayGet({
|
|
311
397
|
agent_id: agentId,
|
|
312
398
|
user_id: identity.user_id,
|
|
@@ -324,7 +410,6 @@ export default definePluginEntry({
|
|
|
324
410
|
} catch (err: any) {
|
|
325
411
|
console.error("[memory-search-plugin] get failed:", err.message);
|
|
326
412
|
|
|
327
|
-
// 403 明确拒绝 → 不降级,直接返回拒绝
|
|
328
413
|
if (err.message?.includes('403')) {
|
|
329
414
|
return {
|
|
330
415
|
content: [
|
|
@@ -336,13 +421,12 @@ export default definePluginEntry({
|
|
|
336
421
|
};
|
|
337
422
|
}
|
|
338
423
|
|
|
339
|
-
// 其他错误(网络不通等)→ 降级到本地文件读取
|
|
340
424
|
if (originalGetTool) {
|
|
341
425
|
console.warn(
|
|
342
426
|
"[memory-search-plugin] falling back to local memory_get for:",
|
|
343
427
|
path
|
|
344
428
|
);
|
|
345
|
-
return await originalGetTool.execute(toolCallId, params);
|
|
429
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
346
430
|
}
|
|
347
431
|
|
|
348
432
|
return {
|
|
@@ -356,10 +440,8 @@ export default definePluginEntry({
|
|
|
356
440
|
};
|
|
357
441
|
}
|
|
358
442
|
} else {
|
|
359
|
-
// 非 MEMORY 路径 → passthrough 给原始实现
|
|
360
|
-
// 这样 SOUL.md、rules.md 等文件的读取行为完全不变
|
|
361
443
|
if (originalGetTool) {
|
|
362
|
-
return await originalGetTool.execute(toolCallId, params);
|
|
444
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
363
445
|
}
|
|
364
446
|
|
|
365
447
|
return {
|