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.
Files changed (3) hide show
  1. package/identity.ts +43 -30
  2. package/index.ts +121 -30
  3. package/package.json +1 -1
package/identity.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * 身份解析模块
3
3
  *
4
- * sessionKey 中解析出当前会话的身份类型和场景。
5
- * sessionKey 格式参考 V1.4 文档和 memory-privacy/identity.ts
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
- * 从 sessionKey 解析身份 + 场景
39
+ * 从 LLM 工具参数解析身份 + 场景
30
40
  *
31
- * sessionKey 末段格式:
32
- * owner/peer: user_{userId}_lobster_{lobsterId}_release_{version}
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
- * owner(私聊自己的 agent)→ scene = "private_own"
37
- * peer (私聊别人的 agent)→ scene = "private_other"
38
- * group(群聊中 agent 回复)→ scene = "group"
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 resolveIdentity(sessionKey: string | undefined): ResolvedIdentity {
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
- // ── 正常模式:从 sessionKey 解析 ──────────────────────
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
- const identity = resolveIdentity(ctx.sessionKey);
159
- const agentId = extractAgentId(ctx.sessionKey);
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: { query?: string; maxResults?: number; minScore?: number }
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.tools.createMemorySearchTool({
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 identity = resolveIdentity(ctx.sessionKey);
261
- const agentId = extractAgentId(ctx.sessionKey);
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: { path?: string; from?: number; lines?: number }
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
- // MEMORY 路径 Gateway(带 ACL)
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
- api.runtime.tools.registerMemoryCli(program);
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
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-search-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "files": [